Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions docs/migrating-from-feathers-fletching.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ app.service("albums").find(addSkip({}, "myHook"));

## `stashable`

In `feathers-fletching`, `stashable` lazily stashes the pre-mutation state of a record. In `feathers-utils`, [`stashable`](/hooks/stashable.html) eagerly starts the fetch but exposes a memoized function — calling it multiple times only hits the database once.
In `feathers-fletching`, `stashable` lazily stashes the pre-mutation state of a record and exposes it as a memoized function on `context.params.stashed`. In `feathers-utils`, [`stashable`](/hooks/stashable.html) stashes all affected records by their id and stores the result as plain data on `context.params.stash` (`Record<Id, { before, item }>`). Enable `fetchBefore` to capture the pre-mutation state.

```ts
// old
Expand All @@ -346,22 +346,25 @@ const before = await context.params.stashed();
import { stashable } from "feathers-utils/hooks";

app.service("users").hooks({
before: {
patch: [stashable()],
around: {
patch: [stashable(undefined, { fetchBefore: true })],
},
});

// Access in a later hook (before or after):
const before = await context.params.stashed();
// Access in a later (after) hook by id:
const { before, item } = context.params.stash[id];
```

### Key differences

| `feathers-fletching` | `feathers-utils` |
| ------------------------------------------ | -------------------------------------------------------------------- |
| Lazy — only fetches when `stashed()` is called | Eager start — fetch begins immediately, result is memoized |
| `propName` option (default: `'stashed'`) | Same — `propName` option (default: `'stashed'`) |
| `stashFunc` option for custom fetch | Same — `stashFunc` option for custom fetch |
| `feathers-fletching` | `feathers-utils` |
| ----------------------------------------------- | ---------------------------------------------------------------------------- |
| Lazy — only fetches when `stashed()` is called | Captures all affected records; opt into the before-state with `fetchBefore` |
| `context.params.stashed()` returns the record | `context.params.stash` holds `Record<Id, { before, item }>` |
| `propName` option (default: `'stashed'`) | `name` option (default: `'stash'`) |
| `stashFunc` option for custom fetch | `params` / `skipHooks` / `deleteParams` options to control the (re)fetch |

> The low-level [`stash`](/hooks/stashable.html) util (`stash(context, options?)`) lets you do the same imperatively inside your own hooks.

## `joinQuery`

Expand Down
49 changes: 43 additions & 6 deletions docs/migrating-from-feathers-hooks-common.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ import { some } from "feathers-utils/predicates";

## `stashBefore`

The `stashBefore` hook has been renamed to [`stashable`](/hooks/stashable.html). Instead of eagerly fetching and storing the result directly on `context.params.before`, it now exposes a memoized function that returns a promise. The fetch starts immediately but multiple calls to `stashed()` only hit the database once.
The `stashBefore` hook has been merged into [`stashable`](/hooks/stashable.html). Instead of storing the pre-mutation record directly on `context.params.before`, `stashable` captures all affected records by their id on `context.params.stash` (`Record<Id, { before, item }>`). Enable `fetchBefore` to capture the pre-mutation state.

```ts
// old
Expand All @@ -383,16 +383,53 @@ const before = context.params.before;
import { stashable } from "feathers-utils/hooks";

app.service("users").hooks({
before: {
patch: [stashable()],
around: {
patch: [stashable(undefined, { fetchBefore: true })],
},
});

// Access by id in a later (after) hook:
const { before } = context.params.stash[id];
```

The default property name changed from `before` to `stash`, and the stored shape is now `Record<Id, { before, item }>`. Use the `name` option to store it elsewhere.

## `changesById`

The `changesById` hook has been merged into [`stashable`](/hooks/stashable.html). It tracks the `{ before, item }` of all affected records by their id and passes them to an optional callback (the callback is now the first argument). The result is stored on `context.params.stash` instead of `context.params.changesById`.

```ts
// old
import { changesById } from "feathers-hooks-common";

app.service("users").hooks({
after: {
all: [
changesById((context, changes) => {
// react to the changes
}),
],
},
});

// Access via memoized function:
const before = await context.params.stashed();
// new
import { stashable } from "feathers-utils/hooks";

app.service("users").hooks({
around: {
all: [
stashable((changes, context) => {
for (const id in changes) {
const { before, item } = changes[id];
// react to the change
}
}, { fetchBefore: true }),
],
},
});
```

The default property name changed from `before` to `stashed`. You can restore the old name with `stashable({ propName: 'before' })`.
Enable `fetchBefore` to include the pre-mutation state. The same result is always available on `context.params.stash`, and the low-level [`stash`](/hooks/stashable.html) util lets you compute it imperatively.

## `traverse`

Expand Down
17 changes: 2 additions & 15 deletions src/hooks/cache/cache-utils.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
import { sortQueryProperties } from '../../utils/sort-query-properties/sort-query-properties.util.js'
import { stableStringify } from '../../utils/stable-stringify/stable-stringify.util.js'

export { sortQueryProperties }

export const stableStringify = (obj: Record<string, any>) => {
// Canonicalize the whole params object once (recursive key-sort + operator-array
// sort). The JSON.stringify pass then only needs to reject non-JSON values
// instead of re-sorting and re-allocating every node.
const normalized = sortQueryProperties(obj as any)

return JSON.stringify(normalized, (_key, value) => {
if (typeof value === 'function') {
throw new Error('Cannot stringify non JSON value')
}
return value
})
}
export { sortQueryProperties, stableStringify }
61 changes: 59 additions & 2 deletions src/hooks/stashable/stashable.hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,64 @@
title: stashable
category: hooks
hook:
type: ["before", "around"]
method: ["update", "patch", "remove"]
type: ["before", "after", "around"]
method: ["create", "update", "patch", "remove"]
multi: true
---

The `stashable` hook stashes all affected records by their id for a `create`, `update`, `patch` or `remove` call and (optionally) passes them to a callback. For every affected id it provides the state `before` the mutation and the resulting `item` after it.

It runs in `before` + `after` (or a single `around`) and stores the result at `context.params[name]` (default `stash`) as a `Record<Id, { before, item }>`.

## Example

```ts
// users.hooks.ts
import { stashable } from "feathers-utils/hooks";

export default {
around: {
all: [
stashable(
(stash, context) => {
for (const id in stash) {
const { before, item } = stash[id];
// react to the change
}
},
{ fetchBefore: true },
),
],
},
};
```

The callback is optional — without it, the result is still available on `context.params.stash`:

```ts
const { before, item } = context.params.stash[id];
```

## Options

| Property | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fetchBefore` | Whether to fetch the items before the mutation (for `update`, `patch`, `remove`). Disabled by default for performance.<br>**Type:** `boolean` - _Default:_ `false` |
| `params` | Manipulate the params used for (re)fetching the items if needed.<br>**Type:** `(params: Params, context: HookContext) => Promisable<Params \| null>` - _Default:_ `undefined` |
| `skipHooks` | Use `_find`/`_get` instead of `find`/`get` for (re)fetching, bypassing the service hooks.<br>**Type:** `boolean` - _Default:_ `false` |
| `deleteParams` | Keys to delete from the params before (re)fetching.<br>**Type:** `string[]` - _Default:_ `[]` |
| `name` | The property on `context.params` to store the stash at.<br>**Type:** `string \| string[]` - _Default:_ `"stash"` |

## `stash` util

`stashable` is built on top of the `stash` util. Use it to stash imperatively inside your own hooks — it is phase-aware and writes to `context.params.stash`:

```ts
import { stash } from "feathers-utils";

// in a before hook
await stash(context, { fetchBefore: true });

// in an after hook
const changes = await stash(context, { fetchBefore: true });
```
Loading
Loading