@marktiderman/genesis-core 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +112 -0
  2. package/dist/chunk-ADD2RIGA.js +108 -0
  3. package/dist/chunk-ADD2RIGA.js.map +1 -0
  4. package/dist/{chunk-3JSFTLDV.js → chunk-CKGGE6WA.js} +4 -4
  5. package/dist/{chunk-3JSFTLDV.js.map → chunk-CKGGE6WA.js.map} +1 -1
  6. package/dist/{chunk-6Q477CHB.js → chunk-KV6KH3L2.js} +6 -3
  7. package/dist/chunk-KV6KH3L2.js.map +1 -0
  8. package/dist/chunk-KXVHDK3Q.js +71 -0
  9. package/dist/chunk-KXVHDK3Q.js.map +1 -0
  10. package/dist/{chunk-F3BE7NGM.js → chunk-OBUBP4BY.js} +83 -47
  11. package/dist/chunk-OBUBP4BY.js.map +1 -0
  12. package/dist/chunk-V3GDAELU.js +3 -0
  13. package/dist/{chunk-HT43MPKW.js.map → chunk-V3GDAELU.js.map} +1 -1
  14. package/dist/{chunk-E5VYFAQO.js → chunk-Y475Z7BM.js} +202 -11
  15. package/dist/chunk-Y475Z7BM.js.map +1 -0
  16. package/dist/{chunk-ZK5C3OOT.js → chunk-ZKMAU6NN.js} +12 -3
  17. package/dist/chunk-ZKMAU6NN.js.map +1 -0
  18. package/dist/hooks/index.d.ts +4 -2
  19. package/dist/hooks/index.js +7 -5
  20. package/dist/hooks/mutation-mode.d.ts +2 -2
  21. package/dist/hooks/mutation-mode.js +1 -1
  22. package/dist/hooks/related-keys.d.ts +2 -2
  23. package/dist/hooks/use-capabilities.d.ts +1 -1
  24. package/dist/hooks/use-infinite-resource.d.ts +50 -0
  25. package/dist/hooks/use-infinite-resource.js +6 -0
  26. package/dist/hooks/use-infinite-resource.js.map +1 -0
  27. package/dist/hooks/use-live.d.ts +27 -0
  28. package/dist/hooks/use-live.js +5 -0
  29. package/dist/hooks/use-live.js.map +1 -0
  30. package/dist/hooks/use-one.d.ts +9 -2
  31. package/dist/hooks/use-one.js +2 -1
  32. package/dist/hooks/use-resource-form.d.ts +2 -2
  33. package/dist/hooks/use-resource-form.js +4 -3
  34. package/dist/hooks/use-resource.d.ts +14 -2
  35. package/dist/hooks/use-resource.js +3 -2
  36. package/dist/index.d.ts +6 -4
  37. package/dist/index.js +9 -7
  38. package/dist/provider/index.d.ts +54 -6
  39. package/dist/provider/index.js +1 -1
  40. package/dist/{read-outcome-kgNhfuST.d.ts → read-outcome-C_9HbXvb.d.ts} +1 -1
  41. package/dist/schema/index.d.ts +4 -4
  42. package/dist/{types-5cuflvha.d.ts → types-C7FJtFXV.d.ts} +12 -0
  43. package/dist/{types-DDOmG9hj.d.ts → types-CS9UpxQS.d.ts} +62 -4
  44. package/package.json +1 -1
  45. package/dist/chunk-6Q477CHB.js.map +0 -1
  46. package/dist/chunk-E5VYFAQO.js.map +0 -1
  47. package/dist/chunk-F3BE7NGM.js.map +0 -1
  48. package/dist/chunk-HT43MPKW.js +0 -3
  49. package/dist/chunk-ZK5C3OOT.js.map +0 -1
@@ -0,0 +1,27 @@
1
+ import { QueryKey } from '@tanstack/react-query';
2
+ import { o as SubscribeParams, l as ResourceChangeEvent } from '../types-CS9UpxQS.js';
3
+ import '../types-C7FJtFXV.js';
4
+ import '../form-types.js';
5
+
6
+ /** How long a burst of change events is gathered into one refetch. */
7
+ declare const DEFAULT_LIVE_DEBOUNCE_MS = 250;
8
+ /** `live` as an object, to tune it. `live: true` takes the defaults. */
9
+ interface LiveOptions {
10
+ /**
11
+ * Events arriving within this many milliseconds of each other cause one
12
+ * refetch, not one each. Default {@link DEFAULT_LIVE_DEBOUNCE_MS}. A
13
+ * steady stream still refetches at least every `4 × debounceMs`.
14
+ */
15
+ debounceMs?: number;
16
+ }
17
+ /**
18
+ * Subscribes while mounted and `enabled`, and invalidates `queryKey` (a
19
+ * prefix) once per burst of events that `accept` lets through. While the
20
+ * page is hidden the queries are only marked stale; they refetch when it is
21
+ * shown again (TanStack Query's `refetchOnWindowFocus`) or next used.
22
+ *
23
+ * @internal Used by `useResource` and `useOne`'s `live` option.
24
+ */
25
+ declare function useLiveInvalidation(resource: string, live: boolean | LiveOptions | undefined, enabled: boolean, params: SubscribeParams, queryKey: QueryKey, accept?: (event: ResourceChangeEvent) => boolean): void;
26
+
27
+ export { DEFAULT_LIVE_DEBOUNCE_MS, type LiveOptions, useLiveInvalidation };
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_LIVE_DEBOUNCE_MS, useLiveInvalidation } from '../chunk-KXVHDK3Q.js';
2
+ import '../chunk-GGSXSI6Z.js';
3
+ import '../chunk-ZYJUEJ4Z.js';
4
+ //# sourceMappingURL=use-live.js.map
5
+ //# sourceMappingURL=use-live.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"use-live.js"}
@@ -1,6 +1,7 @@
1
1
  import { UseQueryResult } from '@tanstack/react-query';
2
- import { B as BaseRecord, I as Identifier } from '../types-DDOmG9hj.js';
3
- import '../types-5cuflvha.js';
2
+ import { B as BaseRecord, I as Identifier } from '../types-CS9UpxQS.js';
3
+ import { LiveOptions } from './use-live.js';
4
+ import '../types-C7FJtFXV.js';
4
5
  import '../form-types.js';
5
6
 
6
7
  interface UseOneOptions {
@@ -8,6 +9,12 @@ interface UseOneOptions {
8
9
  select?: string;
9
10
  /** Defaults to `!!id` — won't fetch when id is falsy. */
10
11
  enabled?: boolean;
12
+ /**
13
+ * Keep the record current while mounted: refetch it when the provider
14
+ * reports a change to it (`DataProvider.subscribe`). See
15
+ * `UseResourceOptions.live`. Off by default.
16
+ */
17
+ live?: boolean | LiveOptions;
11
18
  }
12
19
  /**
13
20
  * Fetch a single record by id, powered by TanStack Query.
@@ -1,4 +1,5 @@
1
- export { useOne } from '../chunk-ZK5C3OOT.js';
1
+ export { useOne } from '../chunk-ZKMAU6NN.js';
2
+ import '../chunk-KXVHDK3Q.js';
2
3
  import '../chunk-GGSXSI6Z.js';
3
4
  import '../chunk-ZYJUEJ4Z.js';
4
5
  //# sourceMappingURL=use-one.js.map
@@ -1,8 +1,8 @@
1
1
  import { UseFormReturn } from 'react-hook-form';
2
2
  import { ResourceFormFieldDef } from '../form-types.js';
3
- import { j as InvalidationKey } from '../types-DDOmG9hj.js';
3
+ import { j as InvalidationKey } from '../types-CS9UpxQS.js';
4
4
  import { MutationModeOptions } from './mutation-mode.js';
5
- import '../types-5cuflvha.js';
5
+ import '../types-C7FJtFXV.js';
6
6
  import '@tanstack/react-query';
7
7
 
8
8
  interface UseResourceFormOptions extends MutationModeOptions {
@@ -1,11 +1,12 @@
1
- export { buildArrayItemDefaults, useResourceForm } from '../chunk-3JSFTLDV.js';
1
+ export { buildArrayItemDefaults, useResourceForm } from '../chunk-CKGGE6WA.js';
2
+ import '../chunk-ZKMAU6NN.js';
2
3
  import '../chunk-XQO4OH2N.js';
3
4
  import '../chunk-5NHR6ZXS.js';
4
5
  import '../chunk-RYEXWQFD.js';
5
- import '../chunk-F3BE7NGM.js';
6
+ import '../chunk-OBUBP4BY.js';
6
7
  import '../chunk-7NUL32W7.js';
7
8
  import '../chunk-BQOJX67X.js';
8
- import '../chunk-ZK5C3OOT.js';
9
+ import '../chunk-KXVHDK3Q.js';
9
10
  import '../chunk-GGSXSI6Z.js';
10
11
  import '../chunk-ZYJUEJ4Z.js';
11
12
  //# sourceMappingURL=use-resource-form.js.map
@@ -1,7 +1,8 @@
1
1
  import { UseQueryResult, UseMutationResult } from '@tanstack/react-query';
2
- import { m as SortParam, F as FilterParam, j as InvalidationKey, k as ListResult, L as ListParams, I as Identifier, B as BaseRecord } from '../types-DDOmG9hj.js';
2
+ import { n as SortParam, F as FilterParam, j as InvalidationKey, k as ListResult, L as ListParams, I as Identifier, B as BaseRecord } from '../types-CS9UpxQS.js';
3
3
  import { MutationModeOptions } from './mutation-mode.js';
4
- import '../types-5cuflvha.js';
4
+ import { LiveOptions } from './use-live.js';
5
+ import '../types-C7FJtFXV.js';
5
6
  import '../form-types.js';
6
7
 
7
8
  interface UseResourceOptions<T> extends MutationModeOptions {
@@ -40,6 +41,17 @@ interface UseResourceOptions<T> extends MutationModeOptions {
40
41
  * rather than flashing a skeleton. Default false.
41
42
  */
42
43
  keepPreviousData?: boolean;
44
+ /**
45
+ * Keep the list current while mounted: subscribe to the provider's change
46
+ * events (`DataProvider.subscribe`) and refetch this resource's cached
47
+ * reads (lists and records) when it reports one, once per burst of
48
+ * events. Events are hints: rows always come from the provider's normal
49
+ * read. Pending optimistic and undoable writes stay applied over the
50
+ * refetch. Needs a provider that serves `subscribe` for the resource
51
+ * (`capabilities(resource).realtime`); otherwise it does nothing. Off by
52
+ * default.
53
+ */
54
+ live?: boolean | LiveOptions;
43
55
  /**
44
56
  * After the write commits, with the provider's row. In `optimistic` and
45
57
  * `undoable` modes that is after `create` / `update` / `remove` have
@@ -1,7 +1,8 @@
1
- export { useResource } from '../chunk-6Q477CHB.js';
2
- import '../chunk-F3BE7NGM.js';
1
+ export { useResource } from '../chunk-KV6KH3L2.js';
2
+ import '../chunk-OBUBP4BY.js';
3
3
  import '../chunk-7NUL32W7.js';
4
4
  import '../chunk-BQOJX67X.js';
5
+ import '../chunk-KXVHDK3Q.js';
5
6
  import '../chunk-GGSXSI6Z.js';
6
7
  import '../chunk-ZYJUEJ4Z.js';
7
8
  //# sourceMappingURL=use-resource.js.map
package/dist/index.d.ts CHANGED
@@ -1,17 +1,19 @@
1
- export { A as ArrayFilter, B as BaseRecord, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, D as DataProvider, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, I as Identifier, i as ImportParams, j as InvalidationKey, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as RowFailure, S as ScalarFilter, m as SortParam, U as UpdateParams, n as UpdateResult, W as WriteEvent } from './types-DDOmG9hj.js';
2
- export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from './read-outcome-kgNhfuST.js';
3
- export { DataProviderRoot, DataProviderRootProps, GenesisProvider, GenesisProviderProps, MockProviderOptions, PostgrestFieldErrors, SupabaseClient, SupabaseClientInput, SupabaseProviderOptions, SupabaseResourceConfig, SupabaseWriteContext, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode } from './provider/index.js';
1
+ export { A as ArrayFilter, B as BaseRecord, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, D as DataProvider, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, I as Identifier, i as ImportParams, j as InvalidationKey, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as ResourceChangeEvent, m as RowFailure, S as ScalarFilter, n as SortParam, o as SubscribeParams, U as UpdateParams, p as UpdateResult, W as WriteEvent } from './types-CS9UpxQS.js';
2
+ export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from './read-outcome-C_9HbXvb.js';
3
+ export { DataProviderRoot, DataProviderRootProps, GenesisProvider, GenesisProviderProps, MockProviderOptions, PostgrestFieldErrors, SupabaseClient, SupabaseClientInput, SupabaseProviderOptions, SupabaseRealtimeChannel, SupabaseRealtimePayload, SupabaseResourceConfig, SupabaseWriteContext, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode } from './provider/index.js';
4
4
  export { S as StorageAdapter } from './types-DKcxHXxq.js';
5
5
  export { createLocalStorageAdapter, createMemoryStorageAdapter, createNoopStorageAdapter } from './storage/index.js';
6
6
  export { UseResourceOptions, UseResourceReturn, useResource } from './hooks/use-resource.js';
7
7
  export { UseOneOptions, useOne } from './hooks/use-one.js';
8
+ export { InfinitePageParam, UseInfiniteResourceOptions, UseInfiniteResourceReturn, useInfiniteResource } from './hooks/use-infinite-resource.js';
8
9
  export { useCapabilities } from './hooks/use-capabilities.js';
10
+ export { DEFAULT_LIVE_DEBOUNCE_MS, LiveOptions } from './hooks/use-live.js';
9
11
  export { UseResourceFormOptions, UseResourceFormReturn, buildArrayItemDefaults, useResourceForm } from './hooks/use-resource-form.js';
10
12
  export { UseWizardReturn, WizardStep, useWizard } from './hooks/use-wizard.js';
11
13
  export { DEFAULT_UNDO_DELAY_MS, MutationMode, MutationModeOptions, UndoableWrite, pendingWriteCount } from './hooks/mutation-mode.js';
12
14
  export { DEFAULT_UNSAVED_CHANGES_MESSAGE, UseUnsavedChangesGuardOptions, UseUnsavedChangesGuardReturn, useUnsavedChangesGuard } from './hooks/use-unsaved-changes-guard.js';
13
15
  export { FieldType, ResourceFormFieldDef, SelectOption, FieldType as WidgetHint } from './form-types.js';
14
- export { A as AuditEntry, D as DataRow, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, e as ResourceContract, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from './types-5cuflvha.js';
16
+ export { A as AuditEntry, D as DataRow, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, e as ResourceContract, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from './types-C7FJtFXV.js';
15
17
  export { decodeRecordKey, encodeRecordKey, keyFields } from './schema/index.js';
16
18
  export { titleCase } from './utils.js';
17
19
  import 'react';
package/dist/index.js CHANGED
@@ -1,19 +1,21 @@
1
- export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from './chunk-E5VYFAQO.js';
1
+ import './chunk-XCX2TRBL.js';
2
+ export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from './chunk-Y475Z7BM.js';
2
3
  export { decodeRecordKey, encodeRecordKey, keyFields } from './chunk-KV2HW4G4.js';
3
4
  export { DataReadError, isDataReadError, readFailureMessage } from './chunk-HUNGCTRA.js';
4
- import './chunk-XCX2TRBL.js';
5
5
  import './chunk-65SXNHWE.js';
6
- import './chunk-HT43MPKW.js';
7
- export { buildArrayItemDefaults, useResourceForm } from './chunk-3JSFTLDV.js';
8
- export { useResource } from './chunk-6Q477CHB.js';
6
+ import './chunk-V3GDAELU.js';
7
+ export { buildArrayItemDefaults, useResourceForm } from './chunk-CKGGE6WA.js';
8
+ export { useOne } from './chunk-ZKMAU6NN.js';
9
+ export { useResource } from './chunk-KV6KH3L2.js';
9
10
  export { DEFAULT_UNSAVED_CHANGES_MESSAGE, useUnsavedChangesGuard } from './chunk-XQO4OH2N.js';
10
11
  export { useWizard } from './chunk-7KOLYF27.js';
11
12
  export { DataWriteError, isDataWriteError } from './chunk-5NHR6ZXS.js';
12
13
  export { titleCase } from './chunk-RYEXWQFD.js';
13
- export { DEFAULT_UNDO_DELAY_MS, pendingWriteCount } from './chunk-F3BE7NGM.js';
14
+ export { DEFAULT_UNDO_DELAY_MS, pendingWriteCount } from './chunk-OBUBP4BY.js';
14
15
  import './chunk-7NUL32W7.js';
15
16
  export { useCapabilities } from './chunk-BQOJX67X.js';
16
- export { useOne } from './chunk-ZK5C3OOT.js';
17
+ export { useInfiniteResource } from './chunk-ADD2RIGA.js';
18
+ export { DEFAULT_LIVE_DEBOUNCE_MS } from './chunk-KXVHDK3Q.js';
17
19
  export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage } from './chunk-GGSXSI6Z.js';
18
20
  export { createLocalStorageAdapter, createMemoryStorageAdapter, createNoopStorageAdapter } from './chunk-ZYJUEJ4Z.js';
19
21
  //# sourceMappingURL=index.js.map
@@ -1,12 +1,12 @@
1
- import { D as DataProvider, I as Identifier, B as BaseRecord, j as InvalidationKey, W as WriteEvent } from '../types-DDOmG9hj.js';
2
- export { A as ArrayFilter, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, i as ImportParams, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as RowFailure, S as ScalarFilter, m as SortParam, U as UpdateParams, n as UpdateResult } from '../types-DDOmG9hj.js';
3
- import { D as DataReadError, R as ReadOutcome, b as DataWriteError, W as WriteOutcome } from '../read-outcome-kgNhfuST.js';
4
- export { a as DataReadErrorOptions, c as DataWriteErrorOptions, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-kgNhfuST.js';
1
+ import { D as DataProvider, I as Identifier, B as BaseRecord, j as InvalidationKey, W as WriteEvent } from '../types-CS9UpxQS.js';
2
+ export { A as ArrayFilter, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, i as ImportParams, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as ResourceChangeEvent, m as RowFailure, S as ScalarFilter, n as SortParam, o as SubscribeParams, U as UpdateParams, p as UpdateResult } from '../types-CS9UpxQS.js';
3
+ import { D as DataReadError, R as ReadOutcome, b as DataWriteError, W as WriteOutcome } from '../read-outcome-C_9HbXvb.js';
4
+ export { a as DataReadErrorOptions, c as DataWriteErrorOptions, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-C_9HbXvb.js';
5
5
  import * as react from 'react';
6
6
  import { ReactNode } from 'react';
7
7
  import { QueryClient } from '@tanstack/react-query';
8
8
  import { S as StorageAdapter } from '../types-DKcxHXxq.js';
9
- import '../types-5cuflvha.js';
9
+ import '../types-C7FJtFXV.js';
10
10
  import '../form-types.js';
11
11
 
12
12
  interface MockProviderOptions {
@@ -37,6 +37,7 @@ interface SupabaseQueryBuilder {
37
37
  or(filters: string): SupabaseQueryBuilder;
38
38
  order(column: string, options?: {
39
39
  ascending?: boolean;
40
+ nullsFirst?: boolean;
40
41
  }): SupabaseQueryBuilder;
41
42
  range(from: number, to: number): SupabaseQueryBuilder;
42
43
  single(): SupabaseQueryBuilder;
@@ -55,6 +56,35 @@ interface SupabaseQueryBuilder {
55
56
  */
56
57
  interface SupabaseClient {
57
58
  from(table: string): SupabaseQueryBuilder;
59
+ /** Supabase Realtime; needed only for `subscribe` (see `SupabaseProviderOptions.realtime`). */
60
+ channel?(name: string): SupabaseRealtimeChannel;
61
+ /** Releases a channel made by `channel()`. */
62
+ removeChannel?(channel: SupabaseRealtimeChannel): unknown;
63
+ }
64
+ /**
65
+ * One `postgres_changes` message, as Supabase Realtime sends it. Only the
66
+ * fields the provider reads; a real payload has more.
67
+ */
68
+ interface SupabaseRealtimePayload {
69
+ eventType: "INSERT" | "UPDATE" | "DELETE";
70
+ /** The row after an insert or update (empty for a delete). */
71
+ new?: Record<string, unknown>;
72
+ /** For a delete or update: the old row's key (all of it with `REPLICA IDENTITY FULL`). */
73
+ old?: Record<string, unknown>;
74
+ }
75
+ /**
76
+ * The part of a Supabase `RealtimeChannel` the provider uses. A real
77
+ * `@supabase/supabase-js` channel satisfies it.
78
+ */
79
+ interface SupabaseRealtimeChannel {
80
+ on(type: "postgres_changes", filter: {
81
+ event: "*" | "INSERT" | "UPDATE" | "DELETE";
82
+ schema: string;
83
+ table: string;
84
+ filter?: string;
85
+ }, callback: (payload: SupabaseRealtimePayload) => void): SupabaseRealtimeChannel;
86
+ subscribe(callback?: (status: string, error?: unknown) => void): SupabaseRealtimeChannel;
87
+ unsubscribe?(): unknown;
58
88
  }
59
89
  /**
60
90
  * What `createSupabaseProvider` actually requires from its `client` argument
@@ -72,6 +102,9 @@ interface SupabaseClient {
72
102
  */
73
103
  interface SupabaseClientInput {
74
104
  from(table: string): unknown;
105
+ /** Supabase Realtime, for `subscribe`. Any real client's `channel` fits. */
106
+ channel?(name: string): unknown;
107
+ removeChannel?(channel: unknown): unknown;
75
108
  }
76
109
  /** What a write hook is told about the write it is part of. */
77
110
  interface SupabaseWriteContext {
@@ -138,6 +171,14 @@ interface SupabaseResourceConfig {
138
171
  * `useResource` and `useResourceForm` invalidate after their writes.
139
172
  */
140
173
  invalidates?: InvalidationKey[] | ((event: WriteEvent) => InvalidationKey[]);
174
+ /**
175
+ * Serve `subscribe` for this resource through Supabase Realtime
176
+ * (`postgres_changes`), so `live` lists and records refresh when the table
177
+ * changes. The table must be in the `supabase_realtime` publication, with
178
+ * RLS on. `false` turns it off even when `SupabaseProviderOptions.realtime`
179
+ * covers the resource.
180
+ */
181
+ realtime?: boolean;
141
182
  }
142
183
  /** Options for `createSupabaseProvider`. All optional; none changes reads. */
143
184
  interface SupabaseProviderOptions {
@@ -175,6 +216,13 @@ interface SupabaseProviderOptions {
175
216
  resource: string;
176
217
  operation: "getList" | "getOne" | "getMany";
177
218
  }) => void | Promise<void>;
219
+ /**
220
+ * Which resources `subscribe` serves through Supabase Realtime: `true` for
221
+ * all, a list of names, or a test. Off by default; a resource's own
222
+ * `resources[name].realtime` wins. Needs a client with `channel()` (any
223
+ * `@supabase/supabase-js` client).
224
+ */
225
+ realtime?: boolean | readonly string[] | ((resource: string) => boolean);
178
226
  }
179
227
  /**
180
228
  * The Supabase data provider. Reads and writes go through the client you
@@ -310,4 +358,4 @@ type GenesisProviderProps = {
310
358
  */
311
359
  declare function GenesisProvider({ children, provider, mock, storage, queryClient: queryClientProp, }: GenesisProviderProps): react.JSX.Element;
312
360
 
313
- export { BaseRecord, DataProvider, DataProviderRoot, type DataProviderRootProps, DataReadError, DataWriteError, GenesisProvider, type GenesisProviderProps, Identifier, InvalidationKey, type MockProviderOptions, type PostgrestFieldErrors, ReadOutcome, type SupabaseClient, type SupabaseClientInput, type SupabaseProviderOptions, type SupabaseResourceConfig, type SupabaseWriteContext, WriteEvent, WriteOutcome, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode };
361
+ export { BaseRecord, DataProvider, DataProviderRoot, type DataProviderRootProps, DataReadError, DataWriteError, GenesisProvider, type GenesisProviderProps, Identifier, InvalidationKey, type MockProviderOptions, type PostgrestFieldErrors, ReadOutcome, type SupabaseClient, type SupabaseClientInput, type SupabaseProviderOptions, type SupabaseRealtimeChannel, type SupabaseRealtimePayload, type SupabaseResourceConfig, type SupabaseWriteContext, WriteEvent, WriteOutcome, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode };
@@ -1,4 +1,4 @@
1
- export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from '../chunk-E5VYFAQO.js';
1
+ export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from '../chunk-Y475Z7BM.js';
2
2
  export { DataReadError, isDataReadError, readFailureMessage } from '../chunk-HUNGCTRA.js';
3
3
  export { DataWriteError, isDataWriteError } from '../chunk-5NHR6ZXS.js';
4
4
  import '../chunk-RYEXWQFD.js';
@@ -1,4 +1,4 @@
1
- import { D as DataRow } from './types-5cuflvha.js';
1
+ import { D as DataRow } from './types-C7FJtFXV.js';
2
2
 
3
3
  /**
4
4
  * - `conflict`: someone else changed the row since it was read. `current`
@@ -1,7 +1,7 @@
1
- import { e as ResourceContract, D as DataRow } from '../types-5cuflvha.js';
2
- export { A as AuditEntry, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from '../types-5cuflvha.js';
3
- import { I as Identifier } from '../types-DDOmG9hj.js';
4
- export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-kgNhfuST.js';
1
+ import { e as ResourceContract, D as DataRow } from '../types-C7FJtFXV.js';
2
+ export { A as AuditEntry, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from '../types-C7FJtFXV.js';
3
+ import { I as Identifier } from '../types-CS9UpxQS.js';
4
+ export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-C_9HbXvb.js';
5
5
  export { FieldType as WidgetHint } from '../form-types.js';
6
6
 
7
7
  /** The key of a contract as a list, whichever form `key` was declared in. */
@@ -258,6 +258,18 @@ interface ProviderCapabilities {
258
258
  * "Leave site?" prompt.
259
259
  */
260
260
  readonly keepaliveWrites?: boolean;
261
+ /**
262
+ * `getList` serves cursor (keyset) paging: `pagination.cursor` in,
263
+ * `nextCursor` out. Pages stay correct while rows are added or removed,
264
+ * and a deep page costs no more than the first.
265
+ */
266
+ readonly cursorPagination?: boolean;
267
+ /**
268
+ * `subscribe` reports changes to this resource (asked per resource:
269
+ * `capabilities(resource)`), so `live` lists and records refresh when
270
+ * someone else changes them.
271
+ */
272
+ readonly realtime?: boolean;
261
273
  }
262
274
  /** One audit record, as committed alongside the edit it describes. */
263
275
  interface AuditEntry {
@@ -1,4 +1,4 @@
1
- import { A as AuditEntry, g as ResourceSchema, h as ResourceSummary, P as ProviderCapabilities, R as RecordComment } from './types-5cuflvha.js';
1
+ import { A as AuditEntry, g as ResourceSchema, h as ResourceSummary, P as ProviderCapabilities, R as RecordComment } from './types-C7FJtFXV.js';
2
2
 
3
3
  /** Record identifier — supports both string UUIDs and numeric auto-increment IDs. */
4
4
  type Identifier = string | number;
@@ -7,11 +7,20 @@ type BaseRecord = {
7
7
  id: Identifier;
8
8
  [key: string]: unknown;
9
9
  };
10
- /** Pagination parameters (offset-based, with optional cursor for future use). */
10
+ /**
11
+ * Pagination parameters: offset (`page`) or cursor (keyset).
12
+ *
13
+ * Cursor paging: set `cursor` to `null` for the first page and to the
14
+ * previous page's `ListResult.nextCursor` for each next one. A provider that
15
+ * serves it (`capabilities().cursorPagination`) then ignores `page` and
16
+ * returns `nextCursor`; one that doesn't ignores `cursor` and pages by
17
+ * `page`, returning no `nextCursor`. `useInfiniteResource` handles both.
18
+ */
11
19
  interface PaginationParam {
12
20
  page: number;
13
21
  perPage: number;
14
- cursor?: string;
22
+ /** `null`: the first page, by cursor. A string: the page after that cursor. Opaque. */
23
+ cursor?: string | null;
15
24
  }
16
25
  /** Sort direction for a single field. */
17
26
  interface SortParam {
@@ -67,7 +76,16 @@ interface ListParams {
67
76
  /** Result of `getList`. */
68
77
  interface ListResult<T> {
69
78
  data: T[];
79
+ /**
80
+ * Rows matching the filters and search. In cursor mode a provider may
81
+ * count only on the first page (`cursor: null`) and return `-1` after it.
82
+ */
70
83
  total: number;
84
+ /**
85
+ * Cursor mode only: the cursor for the next page, or `null` when this is
86
+ * the last one. Absent when the provider paged by offset.
87
+ */
88
+ nextCursor?: string | null;
71
89
  }
72
90
  /** Parameters for `getOne`. */
73
91
  interface GetOneParams {
@@ -192,6 +210,34 @@ interface WriteEvent {
192
210
  /** The row as written, when the provider has it (not for a delete). */
193
211
  record?: BaseRecord;
194
212
  }
213
+ /**
214
+ * A change to a resource, as `DataProvider.subscribe` reports it.
215
+ *
216
+ * Treat it as a HINT that cached reads are stale, never as data to show:
217
+ * the Genesis hooks refetch through the provider's normal (RLS-checked)
218
+ * read instead of writing `record` into the cache. `changed` says only that
219
+ * something may have changed (e.g. after a reconnect, when events may have
220
+ * been missed), so every cached read of the resource should be refetched.
221
+ */
222
+ type ResourceChangeEvent = {
223
+ type: "created" | "updated" | "deleted";
224
+ /** The changed record's id, when the provider knows it. */
225
+ id?: Identifier;
226
+ /** The record as the provider saw it, when it sends one. A hint only. */
227
+ record?: BaseRecord;
228
+ } | {
229
+ type: "changed";
230
+ };
231
+ /** What a `subscribe` call watches. Every field narrows; none is required. */
232
+ interface SubscribeParams {
233
+ /** Watch one record only. */
234
+ id?: Identifier;
235
+ /**
236
+ * The list's filters. A provider MAY use them to send fewer events; it
237
+ * may also ignore them and report every change to the resource.
238
+ */
239
+ filters?: FilterParam[];
240
+ }
195
241
  /**
196
242
  * A cache key to invalidate after a write. Shaped like a TanStack Query
197
243
  * `QueryKey` (`readonly unknown[]`), matched by prefix.
@@ -281,6 +327,18 @@ interface DataProvider {
281
327
  * `prepare`). `useResourceForm` applies it in edit mode.
282
328
  */
283
329
  toFormValues?(resource: string, record: Record<string, unknown>): Record<string, unknown>;
330
+ /**
331
+ * Watch a resource for changes made anywhere (other users, other tabs,
332
+ * server jobs). Calls `callback` for each change until the returned
333
+ * function is called, which must release everything the subscription
334
+ * holds (sockets, channels). Report `realtime: true` in `capabilities()`
335
+ * for the resources it serves; for others it may return a no-op.
336
+ *
337
+ * Events are invalidation hints: `useResource({ live: true })` and
338
+ * `useOne(..., { live: true })` refetch on them and never trust their
339
+ * payload.
340
+ */
341
+ subscribe?(resource: string, params: SubscribeParams, callback: (event: ResourceChangeEvent) => void): () => void;
284
342
  /**
285
343
  * Other cache keys a committed write makes stale, beyond the resource's
286
344
  * own `[resource]` keys. `useResource` and `useResourceForm` invalidate
@@ -289,4 +347,4 @@ interface DataProvider {
289
347
  invalidates?(event: WriteEvent): InvalidationKey[];
290
348
  }
291
349
 
292
- export type { ArrayFilter as A, BaseRecord as B, CreateParams as C, DataProvider as D, FilterParam as F, GetManyParams as G, HistoryParams as H, Identifier as I, ListParams as L, NullFilter as N, PaginationParam as P, RangeFilter as R, ScalarFilter as S, UpdateParams as U, WriteEvent as W, BulkChange as a, BulkResult as b, CreateResult as c, DeleteParams as d, DeleteResult as e, GetManyResult as f, GetOneParams as g, GetOneResult as h, ImportParams as i, InvalidationKey as j, ListResult as k, RowFailure as l, SortParam as m, UpdateResult as n };
350
+ export type { ArrayFilter as A, BaseRecord as B, CreateParams as C, DataProvider as D, FilterParam as F, GetManyParams as G, HistoryParams as H, Identifier as I, ListParams as L, NullFilter as N, PaginationParam as P, RangeFilter as R, ScalarFilter as S, UpdateParams as U, WriteEvent as W, BulkChange as a, BulkResult as b, CreateResult as c, DeleteParams as d, DeleteResult as e, GetManyResult as f, GetOneParams as g, GetOneResult as h, ImportParams as i, InvalidationKey as j, ListResult as k, ResourceChangeEvent as l, RowFailure as m, SortParam as n, SubscribeParams as o, UpdateResult as p };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marktiderman/genesis-core",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hooks/use-resource.ts"],"names":["keepPreviousData","keepPreviousPlaceholder"],"mappings":"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","file":"chunk-6Q477CHB.js","sourcesContent":["import { useState, useCallback, useMemo } from \"react\";\nimport {\n keepPreviousData as keepPreviousPlaceholder,\n useQuery,\n useMutation,\n useQueryClient,\n type UseQueryResult,\n type UseMutationResult,\n} from \"@tanstack/react-query\";\nimport { useDataProvider } from \"../provider/context\";\nimport type {\n BaseRecord,\n Identifier,\n SortParam,\n FilterParam,\n ListParams,\n ListResult,\n InvalidationKey,\n WriteEvent,\n} from \"../provider/types\";\nimport { relatedKeys } from \"./related-keys\";\nimport {\n cachedRecord,\n executeWrite,\n invalidateResource,\n type MutationModeOptions,\n} from \"./mutation-mode\";\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface UseResourceOptions<T> extends MutationModeOptions {\n defaultSort?: SortParam;\n /**\n * Re-synced whenever its value changes (deep-equal, order-independent),\n * not just its identity — safe to pass a scope/audience-derived array that\n * changes across renders. This only covers CHANGES on an already-mounted\n * hook, though: if this starts `undefined` while the real value resolves\n * asynchronously, gate `enabled` on that same readiness signal too — the\n * hook cannot tell \"no defaultFilters intended\" apart from \"not resolved\n * yet,\" so an ungated hook's very first fetch can still run unscoped.\n */\n defaultFilters?: FilterParam[];\n defaultPerPage?: number;\n /** The page to start on (from 1), e.g. restored from the URL. Default 1. */\n defaultPage?: number;\n /** The search to start with, e.g. restored from the URL. Default \"\". */\n defaultSearch?: string;\n /** Supabase select/join syntax, e.g. \"*, author:profiles(name)\". */\n select?: string;\n /** Fields to search across when setSearch is called. Required for server-side search. */\n searchFields?: string[];\n /** Disable auto-fetching. */\n enabled?: boolean;\n /**\n * Other query keys to invalidate after a create, update or delete, on top\n * of `[resource]` and the provider's own `invalidates` (e.g. `[[\"me\"]]`\n * when a write changes the signed-in user's roles).\n */\n invalidates?: InvalidationKey[];\n /**\n * While the next page / sort / search is loading, keep returning the\n * previous result (`list.isPlaceholderData` is true meanwhile) instead of\n * dropping to `undefined`. A paged list then stays on screen between pages\n * rather than flashing a skeleton. Default false.\n */\n keepPreviousData?: boolean;\n /**\n * After the write commits, with the provider's row. In `optimistic` and\n * `undoable` modes that is after `create` / `update` / `remove` have\n * already resolved.\n */\n onCreateSuccess?: (data: T) => void;\n onUpdateSuccess?: (data: T) => void;\n onDeleteSuccess?: () => void;\n /**\n * A write failed. In `optimistic` and `undoable` modes this is the only\n * place a failure is reported (the cache has already been put back), so\n * show it to the user here.\n */\n onError?: (error: Error) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Return type\n// ---------------------------------------------------------------------------\n\nexport interface UseResourceReturn<T> {\n /** TanStack Query result for the list query. */\n list: UseQueryResult<ListResult<T>>;\n /** Current list params (pagination, sort, filters, search). */\n params: ListParams;\n setSort: (sort: SortParam | undefined) => void;\n setFilters: (filters: FilterParam[]) => void;\n setPage: (page: number) => void;\n setPerPage: (perPage: number) => void;\n setSearch: (query: string) => void;\n /**\n * Create a new record. Returns the created item. In `optimistic` and\n * `undoable` modes it resolves at once with the values given (no `id` yet;\n * `onCreateSuccess` gets the stored row).\n */\n create: (data: Partial<T>) => Promise<T>;\n /**\n * Update an existing record by id. Returns the updated item. In\n * `optimistic` and `undoable` modes it resolves at once with the cached\n * row merged with `data`.\n */\n update: (id: Identifier, data: Partial<T>) => Promise<T>;\n /** Delete a record by id. Resolves at once in `optimistic` / `undoable` modes. */\n remove: (id: Identifier) => Promise<void>;\n /** True when any mutation is in-flight. */\n isMutating: boolean;\n createMutation: UseMutationResult<T, Error, Partial<T>>;\n updateMutation: UseMutationResult<T, Error, { id: Identifier; data: Partial<T> }>;\n deleteMutation: UseMutationResult<void, Error, Identifier>;\n}\n\n// ---------------------------------------------------------------------------\n// Filter identity — a deep-equality key independent of object-key order, so\n// a caller that rebuilds the same logical filters via a differently-ordered\n// spread (e.g. `{...base, operator: \"eq\"}` vs `{operator: \"eq\", ...base}`)\n// doesn't trip a spurious resync + pagination reset on an unrelated rerender.\n// ---------------------------------------------------------------------------\n\nfunction stableFilterKey(filters: FilterParam[] | undefined): string | undefined {\n if (!filters) return undefined;\n return JSON.stringify(\n filters.map((f) => {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(f).sort()) {\n sorted[key] = (f as unknown as Record<string, unknown>)[key];\n }\n return sorted;\n }),\n );\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useResource<T extends BaseRecord = BaseRecord>(\n resource: string,\n options: UseResourceOptions<T> = {},\n): UseResourceReturn<T> {\n const {\n defaultSort,\n defaultFilters,\n defaultPerPage = 25,\n defaultPage = 1,\n defaultSearch = \"\",\n select,\n searchFields: searchFieldsOption,\n enabled = true,\n invalidates,\n keepPreviousData = false,\n onCreateSuccess,\n onUpdateSuccess,\n onDeleteSuccess,\n onError,\n mutationMode = \"pessimistic\",\n undoDelayMs,\n onUndoable,\n } = options;\n\n const provider = useDataProvider();\n const queryClient = useQueryClient();\n\n // -------------------------------------------------------------------------\n // State\n // -------------------------------------------------------------------------\n\n const [sort, setSortRaw] = useState<SortParam | undefined>(defaultSort);\n const [filters, setFiltersRaw] = useState<FilterParam[]>(defaultFilters ?? []);\n const [page, setPage] = useState(Math.max(1, Math.floor(defaultPage)));\n const [perPage, setPerPageRaw] = useState(defaultPerPage);\n const [searchQuery, setSearchQuery] = useState(defaultSearch);\n\n // `defaultFilters` often resolves asynchronously — a caller's scope/audience\n // (team, project, ...) can still be loading on first render, so the array\n // it passes in changes value (not just identity) after mount, and may later\n // clear back to undefined (e.g. the scope itself is cleared). Re-syncing\n // here, during render rather than in an effect, means `filters` is already\n // correct by the time `params`/`list` below compute for this pass — no\n // stray unscoped fetch on the frame before an effect would have caught up.\n //\n // This only closes the gap for `defaultFilters` CHANGING across renders on\n // an already-mounted, already-enabled hook (e.g. a scope switch). It cannot\n // help the very first render: `filters` still seeds from whatever\n // `defaultFilters` is on mount, and `undefined` here is genuinely ambiguous\n // between \"this caller never uses defaultFilters\" (in which case forcing a\n // fetch to wait would break it forever) and \"the real value hasn't resolved\n // yet.\" A caller in the second case MUST also gate `enabled` on that same\n // readiness signal for its very first fetch to be correctly scoped — this\n // hook has no way to tell the two cases apart on its own.\n const defaultFiltersKey = stableFilterKey(defaultFilters);\n const [syncedFiltersKey, setSyncedFiltersKey] = useState(defaultFiltersKey);\n if (defaultFiltersKey !== syncedFiltersKey) {\n setSyncedFiltersKey(defaultFiltersKey);\n setFiltersRaw(defaultFilters ?? []);\n setPage(1);\n }\n\n const setSort = useCallback((s: SortParam | undefined) => {\n setSortRaw(s);\n setPage(1);\n }, []);\n\n const setFilters = useCallback((f: FilterParam[]) => {\n setFiltersRaw(f);\n setPage(1);\n }, []);\n\n const setPerPage = useCallback((value: number) => {\n setPerPageRaw(value);\n setPage(1);\n }, []);\n\n const setSearch = useCallback((query: string) => {\n setSearchQuery(query);\n setPage(1);\n }, []);\n\n // -------------------------------------------------------------------------\n // Params\n // -------------------------------------------------------------------------\n\n const params = useMemo<ListParams>(() => {\n const p: ListParams = {\n pagination: { page, perPage },\n sort,\n filters: filters.length > 0 ? filters : undefined,\n select,\n };\n if (searchQuery && searchFieldsOption && searchFieldsOption.length > 0) {\n p.search = { query: searchQuery, fields: searchFieldsOption };\n }\n return p;\n }, [page, perPage, sort, filters, select, searchQuery, searchFieldsOption]);\n\n // -------------------------------------------------------------------------\n // List query\n // -------------------------------------------------------------------------\n\n const listQueryKey = useMemo(\n () => [resource, \"list\", params] as const,\n [resource, params],\n );\n\n const list = useQuery<ListResult<T>>({\n queryKey: listQueryKey,\n queryFn: () => provider.getList<T>(resource, params),\n enabled,\n placeholderData: keepPreviousData ? keepPreviousPlaceholder : undefined,\n });\n\n // -------------------------------------------------------------------------\n // Mutations\n // -------------------------------------------------------------------------\n\n const invalidateAfter = useCallback(\n (event: WriteEvent) => {\n // Pending optimistic/undoable patches are re-applied to the refetch.\n void invalidateResource(queryClient, resource);\n for (const queryKey of relatedKeys(provider, event, invalidates)) {\n void queryClient.invalidateQueries({ queryKey: [...queryKey] });\n }\n },\n [queryClient, resource, provider, invalidates],\n );\n\n const defaultOnError = useCallback(\n (error: Error) => {\n console.error(`[useResource] ${resource} mutation failed:`, error.message);\n },\n [resource],\n );\n const reportError = onError ?? defaultOnError;\n\n const modeArgs = {\n queryClient,\n provider,\n resource,\n mode: mutationMode,\n undoDelayMs,\n onUndoable,\n failedLater: reportError,\n };\n\n const createMutation = useMutation<T, Error, Partial<T>>({\n mutationFn: async (data) => {\n const outcome = await executeWrite<T>({\n ...modeArgs,\n kind: \"create\",\n data: data as Record<string, unknown>,\n send: async ({ keepalive }) => {\n const result = await provider.create<T>(resource, { data, ...(keepalive ? { keepalive } : {}) });\n return { record: result.data, audit: result.audit };\n },\n committed: (record) => {\n invalidateAfter({ resource, kind: \"create\", id: record.id, record });\n onCreateSuccess?.(record);\n },\n });\n return outcome.status === \"committed\" ? outcome.record : (data as T);\n },\n onError: reportError,\n });\n\n const updateMutation = useMutation<\n T,\n Error,\n { id: Identifier; data: Partial<T> }\n >({\n mutationFn: async ({ id, data }) => {\n const before = cachedRecord(queryClient, resource, id);\n const outcome = await executeWrite<T>({\n ...modeArgs,\n kind: \"update\",\n id,\n data: data as Record<string, unknown>,\n send: async (ctx) => {\n const result = await provider.update<T>(resource, {\n id,\n data: (ctx.data ?? data) as Partial<T>,\n ...(ctx.keepalive ? { keepalive: true } : {}),\n });\n return { record: result.data, audit: result.audit };\n },\n committed: (record) => {\n invalidateAfter({ resource, kind: \"update\", id, record });\n onUpdateSuccess?.(record);\n },\n });\n return outcome.status === \"committed\"\n ? outcome.record\n : ({ ...(before ?? {}), ...data, id: before?.id ?? id } as T);\n },\n onError: reportError,\n });\n\n const deleteMutation = useMutation<void, Error, Identifier>({\n mutationFn: async (id) => {\n await executeWrite<void>({\n ...modeArgs,\n kind: \"delete\",\n id,\n send: async ({ keepalive }) => {\n const result = await provider.deleteOne(resource, { id, ...(keepalive ? { keepalive } : {}) });\n return { record: undefined, audit: result?.audit };\n },\n committed: () => {\n invalidateAfter({ resource, kind: \"delete\", id });\n onDeleteSuccess?.();\n },\n });\n },\n onError: reportError,\n });\n\n // -------------------------------------------------------------------------\n // Convenience wrappers\n // -------------------------------------------------------------------------\n\n const create = useCallback(\n (data: Partial<T>) => createMutation.mutateAsync(data),\n [createMutation],\n );\n\n const update = useCallback(\n (id: Identifier, data: Partial<T>) =>\n updateMutation.mutateAsync({ id, data }),\n [updateMutation],\n );\n\n const remove = useCallback(\n (id: Identifier) => deleteMutation.mutateAsync(id),\n [deleteMutation],\n );\n\n const isMutating =\n createMutation.isPending ||\n updateMutation.isPending ||\n deleteMutation.isPending;\n\n return {\n list,\n params,\n setSort,\n setFilters,\n setPage,\n setPerPage,\n setSearch,\n create,\n update,\n remove,\n isMutating,\n createMutation,\n updateMutation,\n deleteMutation,\n };\n}\n"]}