@marktiderman/genesis-core 1.3.0 → 1.4.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 (42) hide show
  1. package/README.md +142 -0
  2. package/dist/{chunk-4W4W7F4I.js → chunk-6MBACQYA.js} +6 -3
  3. package/dist/chunk-6MBACQYA.js.map +1 -0
  4. package/dist/chunk-7NUL32W7.js +14 -0
  5. package/dist/chunk-7NUL32W7.js.map +1 -0
  6. package/dist/{chunk-6V2TIN3G.js → chunk-GGSXSI6Z.js} +6 -5
  7. package/dist/chunk-GGSXSI6Z.js.map +1 -0
  8. package/dist/{chunk-5FTUB2M6.js → chunk-IHWWJUEN.js} +47 -15
  9. package/dist/chunk-IHWWJUEN.js.map +1 -0
  10. package/dist/chunk-NW26Z3LZ.js +303 -0
  11. package/dist/chunk-NW26Z3LZ.js.map +1 -0
  12. package/dist/{chunk-QFTMHOEH.js → chunk-P4OIIRQS.js} +22 -13
  13. package/dist/chunk-P4OIIRQS.js.map +1 -0
  14. package/dist/{chunk-3P4PHPMJ.js → chunk-ZK5C3OOT.js} +3 -3
  15. package/dist/{chunk-3P4PHPMJ.js.map → chunk-ZK5C3OOT.js.map} +1 -1
  16. package/dist/form-types.d.ts +6 -0
  17. package/dist/hooks/index.d.ts +1 -1
  18. package/dist/hooks/index.js +6 -5
  19. package/dist/hooks/related-keys.d.ts +11 -0
  20. package/dist/hooks/related-keys.js +3 -0
  21. package/dist/hooks/related-keys.js.map +1 -0
  22. package/dist/hooks/use-one.d.ts +1 -1
  23. package/dist/hooks/use-one.js +2 -2
  24. package/dist/hooks/use-resource-form.d.ts +19 -1
  25. package/dist/hooks/use-resource-form.js +4 -3
  26. package/dist/hooks/use-resource.d.ts +14 -1
  27. package/dist/hooks/use-resource.js +3 -2
  28. package/dist/index.d.ts +3 -3
  29. package/dist/index.js +9 -8
  30. package/dist/provider/index.d.ts +140 -6
  31. package/dist/provider/index.js +3 -3
  32. package/dist/schema/index.d.ts +3 -3
  33. package/dist/schema/index.js +1 -1
  34. package/dist/{types-DUbRb8qV.d.ts → types-zILrlHuV.d.ts} +27 -2
  35. package/dist/{write-outcome-aYToY3dK.d.ts → write-outcome-BE3Tk6bA.d.ts} +10 -1
  36. package/package.json +2 -2
  37. package/dist/chunk-4W4W7F4I.js.map +0 -1
  38. package/dist/chunk-5FTUB2M6.js.map +0 -1
  39. package/dist/chunk-6AA4ISUA.js +0 -125
  40. package/dist/chunk-6AA4ISUA.js.map +0 -1
  41. package/dist/chunk-6V2TIN3G.js.map +0 -1
  42. package/dist/chunk-QFTMHOEH.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { UseFormReturn } from 'react-hook-form';
2
2
  import { ResourceFormFieldDef } from '../form-types.js';
3
+ import { p as InvalidationKey } from '../types-zILrlHuV.js';
3
4
 
4
5
  interface UseResourceFormOptions {
5
6
  resource: string;
@@ -11,14 +12,31 @@ interface UseResourceFormOptions {
11
12
  listData?: Record<string, unknown>[];
12
13
  onSuccess?: (data: Record<string, unknown>) => void;
13
14
  onError?: (error: Error) => void;
14
- /** Pre-fill create form defaults from URL query params. Default true. */
15
+ /**
16
+ * Pre-fill create form defaults from URL query params: a param named like a
17
+ * field (`?status=draft`) becomes that field's starting value. Default true.
18
+ * Pass false for a form that should not read the page's URL, e.g. one
19
+ * opened from a list whose own filters live in the query string.
20
+ */
15
21
  prefillFromUrl?: boolean;
22
+ /**
23
+ * Generate a placeholder ("Enter name", "name@example.com") for a field
24
+ * whose `placeholder` is unset. Default true. A placeholder that only
25
+ * repeats the label adds noise without information, so `ResourceForm`
26
+ * turns this off unless asked.
27
+ */
28
+ autoPlaceholder?: boolean;
16
29
  /** Validation trigger mode. Default "onBlur". */
17
30
  validationMode?: "onBlur" | "onChange" | "onSubmit";
18
31
  /** Enable autosave drafts to localStorage for create mode. */
19
32
  autosave?: boolean | {
20
33
  debounceMs?: number;
21
34
  };
35
+ /**
36
+ * Other query keys to invalidate after a save, on top of `[resource]` and
37
+ * the provider's own `invalidates`.
38
+ */
39
+ invalidates?: InvalidationKey[];
22
40
  }
23
41
  interface UseResourceFormReturn {
24
42
  form: UseFormReturn<Record<string, unknown>>;
@@ -1,7 +1,8 @@
1
- export { buildArrayItemDefaults, useResourceForm } from '../chunk-5FTUB2M6.js';
1
+ export { buildArrayItemDefaults, useResourceForm } from '../chunk-IHWWJUEN.js';
2
2
  import '../chunk-RYEXWQFD.js';
3
- import '../chunk-3P4PHPMJ.js';
4
- import '../chunk-6V2TIN3G.js';
3
+ import '../chunk-ZK5C3OOT.js';
4
+ import '../chunk-7NUL32W7.js';
5
+ import '../chunk-GGSXSI6Z.js';
5
6
  import '../chunk-ZYJUEJ4Z.js';
6
7
  //# sourceMappingURL=use-resource-form.js.map
7
8
  //# sourceMappingURL=use-resource-form.js.map
@@ -1,5 +1,5 @@
1
1
  import { UseQueryResult, UseMutationResult } from '@tanstack/react-query';
2
- import { z as SortParam, k as FilterParam, p as ListResult, L as ListParams, I as Identifier, B as BaseRecord } from '../types-DUbRb8qV.js';
2
+ import { E as SortParam, k as FilterParam, p as InvalidationKey, q as ListResult, L as ListParams, I as Identifier, B as BaseRecord } from '../types-zILrlHuV.js';
3
3
  import '../form-types.js';
4
4
 
5
5
  interface UseResourceOptions<T> {
@@ -21,6 +21,19 @@ interface UseResourceOptions<T> {
21
21
  searchFields?: string[];
22
22
  /** Disable auto-fetching. */
23
23
  enabled?: boolean;
24
+ /**
25
+ * Other query keys to invalidate after a create, update or delete, on top
26
+ * of `[resource]` and the provider's own `invalidates` (e.g. `[["me"]]`
27
+ * when a write changes the signed-in user's roles).
28
+ */
29
+ invalidates?: InvalidationKey[];
30
+ /**
31
+ * While the next page / sort / search is loading, keep returning the
32
+ * previous result (`list.isPlaceholderData` is true meanwhile) instead of
33
+ * dropping to `undefined`. A paged list then stays on screen between pages
34
+ * rather than flashing a skeleton. Default false.
35
+ */
36
+ keepPreviousData?: boolean;
24
37
  onCreateSuccess?: (data: T) => void;
25
38
  onUpdateSuccess?: (data: T) => void;
26
39
  onDeleteSuccess?: () => void;
@@ -1,5 +1,6 @@
1
- export { useResource } from '../chunk-QFTMHOEH.js';
2
- import '../chunk-6V2TIN3G.js';
1
+ export { useResource } from '../chunk-P4OIIRQS.js';
2
+ import '../chunk-7NUL32W7.js';
3
+ import '../chunk-GGSXSI6Z.js';
3
4
  import '../chunk-ZYJUEJ4Z.js';
4
5
  //# sourceMappingURL=use-resource.js.map
5
6
  //# sourceMappingURL=use-resource.js.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { A as ArrayFilter, a as AuditEntry, B as BaseRecord, b as BulkChange, c as BulkResult, C as CreateParams, d as CreateResult, D as DataProvider, e as DataRow, f as DeleteParams, g as DeleteResult, F as FieldPermissions, h as FieldReference, i as FieldSchema, j as FilterOperator, k as FilterParam, G as GetManyParams, l as GetManyResult, m as GetOneParams, n as GetOneResult, H as HistoryParams, I as Identifier, o as ImportParams, L as ListParams, p as ListResult, N as NullFilter, P as PaginationParam, q as ProviderCapabilities, R as RangeFilter, r as RecordComment, s as ResourceAccess, t as ResourceContract, u as ResourceRelation, v as ResourceSchema, w as ResourceSummary, x as RowAction, y as RowFailure, S as ScalarFilter, z as SortParam, E as StorageType, U as UpdateParams, J as UpdateResult, V as VersionField } from './types-DUbRb8qV.js';
2
- export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from './write-outcome-aYToY3dK.js';
3
- export { DataProviderRoot, DataProviderRootProps, GenesisProvider, GenesisProviderProps, MockProviderOptions, SupabaseClient, SupabaseClientInput, createMockProvider, createSupabaseProvider, useDataProvider, useStorage } from './provider/index.js';
1
+ export { A as ArrayFilter, a as AuditEntry, B as BaseRecord, b as BulkChange, c as BulkResult, C as CreateParams, d as CreateResult, D as DataProvider, e as DataRow, f as DeleteParams, g as DeleteResult, F as FieldPermissions, h as FieldReference, i as FieldSchema, j as FilterOperator, k as FilterParam, G as GetManyParams, l as GetManyResult, m as GetOneParams, n as GetOneResult, H as HistoryParams, I as Identifier, o as ImportParams, p as InvalidationKey, L as ListParams, q as ListResult, N as NullFilter, P as PaginationParam, r as ProviderCapabilities, R as RangeFilter, s as RecordComment, t as ResourceAccess, u as ResourceContract, v as ResourceRelation, w as ResourceSchema, x as ResourceSummary, y as RowAction, z as RowFailure, S as ScalarFilter, E as SortParam, J as StorageType, U as UpdateParams, K as UpdateResult, V as VersionField, W as WriteEvent } from './types-zILrlHuV.js';
2
+ export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from './write-outcome-BE3Tk6bA.js';
3
+ export { DataProviderRoot, DataProviderRootProps, GenesisProvider, GenesisProviderProps, MockProviderOptions, SupabaseClient, SupabaseClientInput, SupabaseProviderOptions, SupabaseResourceConfig, SupabaseWriteContext, createMockProvider, createSupabaseProvider, 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';
package/dist/index.js CHANGED
@@ -1,15 +1,16 @@
1
- export { createSupabaseProvider } from './chunk-6AA4ISUA.js';
2
- import './chunk-XCX2TRBL.js';
1
+ export { createSupabaseProvider, toDataWriteError, writeOutcomeFromCode } from './chunk-NW26Z3LZ.js';
3
2
  export { decodeRecordKey, encodeRecordKey, keyFields } from './chunk-KV2HW4G4.js';
4
- export { DataWriteError, isDataWriteError } from './chunk-4W4W7F4I.js';
3
+ export { DataWriteError, isDataWriteError } from './chunk-6MBACQYA.js';
4
+ import './chunk-XCX2TRBL.js';
5
5
  import './chunk-65SXNHWE.js';
6
6
  import './chunk-EL7YGOTY.js';
7
- export { buildArrayItemDefaults, useResourceForm } from './chunk-5FTUB2M6.js';
7
+ export { useWizard } from './chunk-7KOLYF27.js';
8
+ export { buildArrayItemDefaults, useResourceForm } from './chunk-IHWWJUEN.js';
8
9
  export { titleCase } from './chunk-RYEXWQFD.js';
9
- export { useOne } from './chunk-3P4PHPMJ.js';
10
- export { useResource } from './chunk-QFTMHOEH.js';
11
- export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage } from './chunk-6V2TIN3G.js';
10
+ export { useOne } from './chunk-ZK5C3OOT.js';
11
+ export { useResource } from './chunk-P4OIIRQS.js';
12
+ import './chunk-7NUL32W7.js';
13
+ export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage } from './chunk-GGSXSI6Z.js';
12
14
  export { createLocalStorageAdapter, createMemoryStorageAdapter, createNoopStorageAdapter } from './chunk-ZYJUEJ4Z.js';
13
- export { useWizard } from './chunk-7KOLYF27.js';
14
15
  //# sourceMappingURL=index.js.map
15
16
  //# sourceMappingURL=index.js.map
@@ -1,8 +1,10 @@
1
- import { D as DataProvider } from '../types-DUbRb8qV.js';
2
- export { A as ArrayFilter, B as BaseRecord, b as BulkChange, c as BulkResult, C as CreateParams, d as CreateResult, f as DeleteParams, g as DeleteResult, k as FilterParam, G as GetManyParams, l as GetManyResult, m as GetOneParams, n as GetOneResult, H as HistoryParams, I as Identifier, o as ImportParams, L as ListParams, p as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, y as RowFailure, S as ScalarFilter, z as SortParam, U as UpdateParams, J as UpdateResult } from '../types-DUbRb8qV.js';
3
- export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from '../write-outcome-aYToY3dK.js';
1
+ import { D as DataProvider, I as Identifier, B as BaseRecord, p as InvalidationKey, W as WriteEvent } from '../types-zILrlHuV.js';
2
+ export { A as ArrayFilter, b as BulkChange, c as BulkResult, C as CreateParams, d as CreateResult, f as DeleteParams, g as DeleteResult, k as FilterParam, G as GetManyParams, l as GetManyResult, m as GetOneParams, n as GetOneResult, H as HistoryParams, o as ImportParams, L as ListParams, q as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, z as RowFailure, S as ScalarFilter, E as SortParam, U as UpdateParams, K as UpdateResult } from '../types-zILrlHuV.js';
3
+ import { D as DataWriteError, W as WriteOutcome } from '../write-outcome-BE3Tk6bA.js';
4
+ export { a as DataWriteErrorOptions, i as isDataWriteError } from '../write-outcome-BE3Tk6bA.js';
4
5
  import * as react from 'react';
5
6
  import { ReactNode } from 'react';
7
+ import { QueryClient } from '@tanstack/react-query';
6
8
  import { S as StorageAdapter } from '../types-DKcxHXxq.js';
7
9
  import '../form-types.js';
8
10
 
@@ -70,8 +72,124 @@ interface SupabaseClient {
70
72
  interface SupabaseClientInput {
71
73
  from(table: string): unknown;
72
74
  }
73
- declare function createSupabaseProvider(client: SupabaseClientInput): DataProvider;
75
+ /** What a write hook is told about the write it is part of. */
76
+ interface SupabaseWriteContext {
77
+ resource: string;
78
+ action: "create" | "update";
79
+ /** The row's id, for an update. */
80
+ id?: Identifier;
81
+ }
82
+ /** Per-resource rules for `createSupabaseProvider`. Every field is optional. */
83
+ interface SupabaseResourceConfig {
84
+ /** Singular noun for failure messages, e.g. "project". Defaults to "record". */
85
+ noun?: string;
86
+ /**
87
+ * `"bin"` for a table whose delete trigger moves the row to a recycle bin
88
+ * instead of removing it (see `SupabaseProviderOptions.binTables`).
89
+ * `"hard"` forces a plain delete even when `binTables` lists the table.
90
+ */
91
+ deleteMode?: "hard" | "bin";
92
+ /**
93
+ * Turns submitted values into the row to write: trim, drop read-only keys,
94
+ * validate. Runs before every create and update, including the `create`
95
+ * and `update` overrides. Throw (ideally `DataWriteError("invalid", ...)`)
96
+ * to refuse the write; nothing is sent.
97
+ */
98
+ prepare?: (values: Record<string, unknown>, context: SupabaseWriteContext) => Record<string, unknown> | Promise<Record<string, unknown>>;
99
+ /**
100
+ * Creates the row some other way than an insert, e.g. a security-definer
101
+ * RPC. Return the new row, or its id and the provider reads the row back
102
+ * (if the read-back fails, the create is still reported as saved, with
103
+ * just the id, so the user does not retry and make a second one). Throw to
104
+ * fail; a Supabase `{ message, code }` error is mapped like any other.
105
+ */
106
+ create?: (row: Record<string, unknown>) => Promise<BaseRecord | Identifier>;
107
+ /**
108
+ * Updates the row some other way than an update, e.g. an RPC. Return the
109
+ * updated row, or its id (or `true`) and the provider reads it back.
110
+ * Returning nothing (`undefined`/`null`/`false`) means no row changed: the provider then reads the
111
+ * row to say why (`forbidden` or `gone`), exactly as for a plain update,
112
+ * so a function that silently skips a row never reports a save.
113
+ */
114
+ update?: (id: Identifier, row: Record<string, unknown>) => Promise<BaseRecord | Identifier | boolean | null | undefined | void>;
115
+ /** Maps a stored row to edit-form values; see `DataProvider.toFormValues`. */
116
+ toFormValues?: (record: Record<string, unknown>) => Record<string, unknown>;
117
+ /**
118
+ * Other cache keys a committed write to this resource makes stale, e.g.
119
+ * `[["staff"], ["me"]]`. Served through `DataProvider.invalidates`, which
120
+ * `useResource` and `useResourceForm` invalidate after their writes.
121
+ */
122
+ invalidates?: InvalidationKey[] | ((event: WriteEvent) => InvalidationKey[]);
123
+ }
124
+ /** Options for `createSupabaseProvider`. All optional; none changes reads. */
125
+ interface SupabaseProviderOptions {
126
+ /**
127
+ * Recycle-bin tables: their delete trigger turns a delete into a "binned"
128
+ * mark and the binned row drops out of the caller's view (RLS or a view
129
+ * filter). The database reports such a delete as zero rows, so for these
130
+ * the provider checks the row is visible, deletes it, and reads it back:
131
+ * gone from view is success, still visible is `forbidden`. A row that was
132
+ * not visible beforehand is `gone`.
133
+ */
134
+ binTables?: readonly string[] | ((resource: string) => boolean);
135
+ /** Per-resource rules, keyed by resource (table) name. */
136
+ resources?: Record<string, SupabaseResourceConfig>;
137
+ /**
138
+ * Called after every committed create, update and delete, e.g. to
139
+ * invalidate caches the Genesis hooks don't know about. Awaited, but a
140
+ * failure is logged and never turns a committed write into a failed one.
141
+ */
142
+ onWrite?: (event: WriteEvent) => void | Promise<void>;
143
+ }
144
+ /**
145
+ * The Supabase data provider. Reads and writes go through the client you
146
+ * pass, under that client's session and RLS.
147
+ *
148
+ * Writes never report a save that did not happen:
149
+ * - `update` and `deleteOne` ask for the affected rows. Zero rows is a
150
+ * `DataWriteError`: the row is read back, and still visible means
151
+ * `forbidden` (RLS or a trigger refused the change), no longer visible
152
+ * means `gone`.
153
+ * - Database errors on writes are thrown as `DataWriteError` with the
154
+ * outcome their code means (`writeOutcomeFromCode`) and the code kept on
155
+ * `error.code`.
156
+ * - Recycle-bin tables (`binTables`) succeed when the row leaves the
157
+ * caller's view.
158
+ *
159
+ * Reads are unchanged: errors are thrown as an `Error` carrying `code`.
160
+ */
161
+ declare function createSupabaseProvider(client: SupabaseClientInput, options?: SupabaseProviderOptions): DataProvider;
162
+
163
+ /**
164
+ * Maps a Postgres SQLSTATE or PostgREST error code to the write outcome it
165
+ * means. Anything not listed is `error`: a code is only read as a refusal or
166
+ * a bad value when it says so.
167
+ *
168
+ * - `42501` (insufficient privilege, including "new row violates row-level
169
+ * security policy") → `forbidden`
170
+ * - `22***` (data exception: bad type, out of range) and `23***` (integrity
171
+ * constraint: `23505` unique, `23503` foreign key, `23502` not null,
172
+ * `23514` check) → `invalid`
173
+ * - `PGRST301`–`PGRST303` (JWT missing, expired or rejected), `28000`,
174
+ * `28P01` → `unauthenticated`
175
+ * - `PGRST116` (one row was asked for and none came back) → `gone`
176
+ */
177
+ declare function writeOutcomeFromCode(code: string | null | undefined): WriteOutcome;
178
+ /**
179
+ * Turns anything a write threw, or the `{ message, code }` error a Supabase
180
+ * client returned, into a `DataWriteError`. A `DataWriteError` passes through
181
+ * unchanged; anything else keeps its message and `code`, and is kept as the
182
+ * `cause`.
183
+ */
184
+ declare function toDataWriteError(error: unknown): DataWriteError;
74
185
 
186
+ /**
187
+ * Puts a `DataProvider` in context, and nothing else. Mount it inside your
188
+ * app's own `QueryClientProvider` and the Genesis hooks share that cache:
189
+ * the same query keys (`[resource, "list", params]`,
190
+ * `[resource, "detail", id, select]`), so invalidating `[resource]` from app
191
+ * code refreshes Genesis lists too, and vice versa.
192
+ */
75
193
  interface DataProviderRootProps {
76
194
  provider: DataProvider;
77
195
  children: ReactNode;
@@ -87,6 +205,14 @@ type GenesisProviderProps = {
87
205
  mock?: never;
88
206
  /** Platform-specific storage adapter. Defaults to localStorage (SSR-safe). */
89
207
  storage?: StorageAdapter;
208
+ /**
209
+ * The TanStack Query client to use. Without it, GenesisProvider creates
210
+ * its OWN client — a second cache beside any the app already has, so app
211
+ * queries and Genesis queries would not see each other's invalidations.
212
+ * An app with a `QueryClientProvider` should pass its client here, or
213
+ * use `DataProviderRoot` inside that provider instead.
214
+ */
215
+ queryClient?: QueryClient;
90
216
  } | {
91
217
  children: ReactNode;
92
218
  provider?: never;
@@ -97,7 +223,15 @@ type GenesisProviderProps = {
97
223
  };
98
224
  /** Platform-specific storage adapter. Defaults to localStorage (SSR-safe). */
99
225
  storage?: StorageAdapter;
226
+ /** See the `provider` variant. */
227
+ queryClient?: QueryClient;
100
228
  };
101
- declare function GenesisProvider({ children, provider, mock, storage, }: GenesisProviderProps): react.JSX.Element;
229
+ /**
230
+ * Convenience root: a `QueryClientProvider`, storage, and a data provider
231
+ * (yours, or a mock built from datasets). It creates its own `QueryClient`
232
+ * unless you pass `queryClient`; an app that already has one should pass it,
233
+ * or mount `DataProviderRoot` inside its existing `QueryClientProvider`.
234
+ */
235
+ declare function GenesisProvider({ children, provider, mock, storage, queryClient: queryClientProp, }: GenesisProviderProps): react.JSX.Element;
102
236
 
103
- export { DataProvider, DataProviderRoot, type DataProviderRootProps, GenesisProvider, type GenesisProviderProps, type MockProviderOptions, type SupabaseClient, type SupabaseClientInput, createMockProvider, createSupabaseProvider, useDataProvider, useStorage };
237
+ export { BaseRecord, DataProvider, DataProviderRoot, type DataProviderRootProps, DataWriteError, GenesisProvider, type GenesisProviderProps, Identifier, InvalidationKey, type MockProviderOptions, type SupabaseClient, type SupabaseClientInput, type SupabaseProviderOptions, type SupabaseResourceConfig, type SupabaseWriteContext, WriteEvent, WriteOutcome, createMockProvider, createSupabaseProvider, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode };
@@ -1,6 +1,6 @@
1
- export { createSupabaseProvider } from '../chunk-6AA4ISUA.js';
2
- export { DataWriteError, isDataWriteError } from '../chunk-4W4W7F4I.js';
3
- export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage } from '../chunk-6V2TIN3G.js';
1
+ export { createSupabaseProvider, toDataWriteError, writeOutcomeFromCode } from '../chunk-NW26Z3LZ.js';
2
+ export { DataWriteError, isDataWriteError } from '../chunk-6MBACQYA.js';
3
+ export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage } from '../chunk-GGSXSI6Z.js';
4
4
  import '../chunk-ZYJUEJ4Z.js';
5
5
  //# sourceMappingURL=index.js.map
6
6
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
- import { t as ResourceContract, I as Identifier, e as DataRow } from '../types-DUbRb8qV.js';
2
- export { a as AuditEntry, F as FieldPermissions, h as FieldReference, i as FieldSchema, j as FilterOperator, q as ProviderCapabilities, r as RecordComment, s as ResourceAccess, u as ResourceRelation, v as ResourceSchema, w as ResourceSummary, x as RowAction, E as StorageType, V as VersionField } from '../types-DUbRb8qV.js';
3
- export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from '../write-outcome-aYToY3dK.js';
1
+ import { u as ResourceContract, I as Identifier, e as DataRow } from '../types-zILrlHuV.js';
2
+ export { a as AuditEntry, F as FieldPermissions, h as FieldReference, i as FieldSchema, j as FilterOperator, r as ProviderCapabilities, s as RecordComment, t as ResourceAccess, v as ResourceRelation, w as ResourceSchema, x as ResourceSummary, y as RowAction, J as StorageType, V as VersionField } from '../types-zILrlHuV.js';
3
+ export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from '../write-outcome-BE3Tk6bA.js';
4
4
  export { FieldType as WidgetHint } from '../form-types.js';
5
5
 
6
6
  /** The key of a contract as a list, whichever form `key` was declared in. */
@@ -1,4 +1,4 @@
1
1
  export { decodeRecordKey, encodeRecordKey, keyFields } from '../chunk-KV2HW4G4.js';
2
- export { DataWriteError, isDataWriteError } from '../chunk-4W4W7F4I.js';
2
+ export { DataWriteError, isDataWriteError } from '../chunk-6MBACQYA.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -70,7 +70,7 @@ interface FieldSchema {
70
70
  * One declaration of a resource: its fields and its identity.
71
71
  *
72
72
  * `key` names the identity field, or several for a composite key (a junction
73
- * table keyed on `(coach_id, client_id)`). Use {@link keyFields} to read it as
73
+ * table keyed on `(team_id, user_id)`). Use {@link keyFields} to read it as
74
74
  * a list without caring which form it took.
75
75
  */
76
76
  interface ResourceContract<Row extends DataRow = DataRow> {
@@ -433,6 +433,19 @@ interface HistoryParams {
433
433
  /** Most recent first; default and maximum are the provider's. */
434
434
  limit?: number;
435
435
  }
436
+ /** A write that committed, as passed to a provider's `onWrite` and `invalidates`. */
437
+ interface WriteEvent {
438
+ resource: string;
439
+ kind: "create" | "update" | "delete";
440
+ id: Identifier;
441
+ /** The row as written, when the provider has it (not for a delete). */
442
+ record?: BaseRecord;
443
+ }
444
+ /**
445
+ * A cache key to invalidate after a write. Shaped like a TanStack Query
446
+ * `QueryKey` (`readonly unknown[]`), matched by prefix.
447
+ */
448
+ type InvalidationKey = readonly unknown[];
436
449
  interface DataProvider {
437
450
  getList<T extends BaseRecord = BaseRecord>(resource: string, params: ListParams): Promise<ListResult<T>>;
438
451
  getOne<T extends BaseRecord = BaseRecord>(resource: string, params: GetOneParams): Promise<GetOneResult<T>>;
@@ -511,6 +524,18 @@ interface DataProvider {
511
524
  id: Identifier;
512
525
  body: string;
513
526
  }): Promise<RecordComment>;
527
+ /**
528
+ * Maps a stored row to the values an edit form starts from, for a resource
529
+ * whose form shape differs from its table (the reverse of a write's
530
+ * `prepare`). `useResourceForm` applies it in edit mode.
531
+ */
532
+ toFormValues?(resource: string, record: Record<string, unknown>): Record<string, unknown>;
533
+ /**
534
+ * Other cache keys a committed write makes stale, beyond the resource's
535
+ * own `[resource]` keys. `useResource` and `useResourceForm` invalidate
536
+ * them after their writes.
537
+ */
538
+ invalidates?(event: WriteEvent): InvalidationKey[];
514
539
  }
515
540
 
516
- export type { ArrayFilter as A, BaseRecord as B, CreateParams as C, DataProvider as D, StorageType as E, FieldPermissions as F, GetManyParams as G, HistoryParams as H, Identifier as I, UpdateResult as J, ListParams as L, NullFilter as N, PaginationParam as P, RangeFilter as R, ScalarFilter as S, UpdateParams as U, VersionField as V, AuditEntry as a, BulkChange as b, BulkResult as c, CreateResult as d, DataRow as e, DeleteParams as f, DeleteResult as g, FieldReference as h, FieldSchema as i, FilterOperator as j, FilterParam as k, GetManyResult as l, GetOneParams as m, GetOneResult as n, ImportParams as o, ListResult as p, ProviderCapabilities as q, RecordComment as r, ResourceAccess as s, ResourceContract as t, ResourceRelation as u, ResourceSchema as v, ResourceSummary as w, RowAction as x, RowFailure as y, SortParam as z };
541
+ export type { ArrayFilter as A, BaseRecord as B, CreateParams as C, DataProvider as D, SortParam as E, FieldPermissions as F, GetManyParams as G, HistoryParams as H, Identifier as I, StorageType as J, UpdateResult as K, ListParams as L, NullFilter as N, PaginationParam as P, RangeFilter as R, ScalarFilter as S, UpdateParams as U, VersionField as V, WriteEvent as W, AuditEntry as a, BulkChange as b, BulkResult as c, CreateResult as d, DataRow as e, DeleteParams as f, DeleteResult as g, FieldReference as h, FieldSchema as i, FilterOperator as j, FilterParam as k, GetManyResult as l, GetOneParams as m, GetOneResult as n, ImportParams as o, InvalidationKey as p, ListResult as q, ProviderCapabilities as r, RecordComment as s, ResourceAccess as t, ResourceContract as u, ResourceRelation as v, ResourceSchema as w, ResourceSummary as x, RowAction as y, RowFailure as z };
@@ -1,4 +1,4 @@
1
- import { e as DataRow } from './types-DUbRb8qV.js';
1
+ import { e as DataRow } from './types-zILrlHuV.js';
2
2
 
3
3
  /**
4
4
  * - `conflict`: someone else changed the row since it was read. `current`
@@ -21,12 +21,21 @@ interface DataWriteErrorOptions {
21
21
  current?: DataRow;
22
22
  /** The row's current version token, for a `conflict`. */
23
23
  currentVersion?: string | null;
24
+ /**
25
+ * The store's own error code, when it gave one (a Postgres SQLSTATE such as
26
+ * `23505`, or a PostgREST code such as `PGRST301`).
27
+ */
28
+ code?: string;
29
+ /** The underlying error, kept for logging. */
30
+ cause?: unknown;
24
31
  }
25
32
  /** Thrown by a provider when a write did not commit. */
26
33
  declare class DataWriteError extends Error {
27
34
  readonly outcome: WriteOutcome;
28
35
  readonly current?: DataRow;
29
36
  readonly currentVersion?: string | null;
37
+ /** The store's own error code, when it gave one. */
38
+ readonly code?: string;
30
39
  constructor(outcome: WriteOutcome, message: string, options?: DataWriteErrorOptions);
31
40
  }
32
41
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marktiderman/genesis-core",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -62,7 +62,7 @@
62
62
  "react-hook-form": "^7.56.4",
63
63
  "tsup": "^8.3.0",
64
64
  "typescript": "^5.9.3",
65
- "vitest": "^2.1.0",
65
+ "vitest": "^4.1.12",
66
66
  "zod": "^3.25.67"
67
67
  },
68
68
  "scripts": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/schema/write-outcome.ts"],"names":[],"mappings":";AAyCO,IAAM,cAAA,GAAN,cAA6B,KAAA,CAAM;AAAA,EAC/B,OAAA;AAAA,EACA,OAAA;AAAA,EACA,cAAA;AAAA,EAET,WAAA,CAAY,OAAA,EAAuB,OAAA,EAAiB,OAAA,GAAiC,EAAC,EAAG;AACvF,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,gBAAA;AACZ,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AACf,IAAA,IAAA,CAAK,UAAU,OAAA,CAAQ,OAAA;AACvB,IAAA,IAAA,CAAK,iBAAiB,OAAA,CAAQ,cAAA;AAAA,EAChC;AACF;AAMO,SAAS,iBAAiB,KAAA,EAAyC;AACxE,EAAA,OACE,iBAAiB,KAAA,IACjB,KAAA,CAAM,SAAS,gBAAA,IACf,OAAQ,MAAgC,OAAA,KAAY,QAAA;AAExD","file":"chunk-4W4W7F4I.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Why a write did not save (DATA-STUDIO.md \"Write safety\")\n//\n// A write that changes nothing is never reported as \"permission denied\" by\n// assumption. The provider re-reads the row and says what actually happened.\n// ---------------------------------------------------------------------------\n\nimport type { DataRow } from \"./types\";\n\n/**\n * - `conflict`: someone else changed the row since it was read. `current`\n * carries the row as it is now.\n * - `gone`: the row was deleted, or is no longer visible to this user. The\n * two cannot be told apart without a privileged look at the table, which\n * would show this user a fact about a row they cannot see.\n * - `forbidden`: the row is visible and unchanged, but the app or the\n * database does not let this user make this change. Only reported when\n * that is proven, never assumed from \"zero rows changed\".\n * - `invalid`: the store rejected the value (type, constraint, not-null).\n * - `audit_failed`: the audit entry could not be written, so the edit was\n * rolled back with it.\n * - `unauthenticated`: no signed-in user, or the session expired.\n * - `error`: anything else. The edit did not commit.\n */\nexport type WriteOutcome =\n | \"conflict\"\n | \"gone\"\n | \"forbidden\"\n | \"invalid\"\n | \"audit_failed\"\n | \"unauthenticated\"\n | \"error\";\n\nexport interface DataWriteErrorOptions {\n /** The row as it is now, for a `conflict`. Only fields the user may read. */\n current?: DataRow;\n /** The row's current version token, for a `conflict`. */\n currentVersion?: string | null;\n}\n\n/** Thrown by a provider when a write did not commit. */\nexport class DataWriteError extends Error {\n readonly outcome: WriteOutcome;\n readonly current?: DataRow;\n readonly currentVersion?: string | null;\n\n constructor(outcome: WriteOutcome, message: string, options: DataWriteErrorOptions = {}) {\n super(message);\n this.name = \"DataWriteError\";\n this.outcome = outcome;\n this.current = options.current;\n this.currentVersion = options.currentVersion;\n }\n}\n\n/**\n * `instanceof` fails when two copies of this package are installed, so check\n * the shape instead.\n */\nexport function isDataWriteError(error: unknown): error is DataWriteError {\n return (\n error instanceof Error &&\n error.name === \"DataWriteError\" &&\n typeof (error as { outcome?: unknown }).outcome === \"string\"\n );\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hooks/use-resource-form.ts"],"names":[],"mappings":"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","file":"chunk-5FTUB2M6.js","sourcesContent":["import { useMemo, useCallback, useEffect } from \"react\";\nimport { useForm, type UseFormReturn, type Resolver } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport { useOne } from \"./use-one\";\nimport { useDataProvider, useStorage } from \"../provider/context\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { titleCase } from \"../utils\";\nimport type { ResourceFormFieldDef, FieldType } from \"../form-types\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface UseResourceFormOptions {\n resource: string;\n action: \"create\" | \"edit\";\n id?: string;\n initialData?: Record<string, unknown>;\n fields?: ResourceFormFieldDef[];\n /** Full dataset used to auto-detect select fields from unique values. */\n listData?: Record<string, unknown>[];\n onSuccess?: (data: Record<string, unknown>) => void;\n onError?: (error: Error) => void;\n /** Pre-fill create form defaults from URL query params. Default true. */\n prefillFromUrl?: boolean;\n /** Validation trigger mode. Default \"onBlur\". */\n validationMode?: \"onBlur\" | \"onChange\" | \"onSubmit\";\n /** Enable autosave drafts to localStorage for create mode. */\n autosave?: boolean | { debounceMs?: number };\n}\n\nexport interface UseResourceFormReturn {\n form: UseFormReturn<Record<string, unknown>>;\n resolvedFields: ResourceFormFieldDef[];\n onSubmit: (e?: { preventDefault?: () => void }) => Promise<void>;\n /** Raw save handler — use with form.handleSubmit(handleSave, onInvalid). */\n handleSave: (data: Record<string, unknown>) => Promise<void>;\n isSaving: boolean;\n isLoading: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Infer a field type from a value */\nfunction inferFieldType(value: unknown): FieldType {\n if (typeof value === \"boolean\") return \"switch\";\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"string\") {\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(value)) return \"date\";\n if (value.includes(\"@\") && value.includes(\".\")) return \"email\";\n if (/^https?:\\/\\//.test(value)) return \"url\";\n if (value.length > 100) return \"textarea\";\n }\n return \"text\";\n}\n\n/** Try to infer a select field from unique values across the full dataset */\nfunction inferSelectFromData(\n key: string,\n allData: Record<string, unknown>[] | undefined,\n): { type: \"select\"; options: { value: string; label: string }[] } | null {\n if (!allData || allData.length === 0) return null;\n const uniqueValues = new Set<string>();\n for (const row of allData) {\n const v = row[key];\n if (typeof v === \"string\" && v.length > 0) uniqueValues.add(v);\n }\n if (uniqueValues.size > 0 && uniqueValues.size <= 10) {\n return {\n type: \"select\" as const,\n options: Array.from(uniqueValues)\n .sort()\n .map((v) => ({ value: v, label: v })),\n };\n }\n return null;\n}\n\n/** Generate a sensible placeholder for an auto-inferred field */\nfunction defaultPlaceholder(key: string, type: FieldType): string {\n switch (type) {\n case \"email\": return \"name@example.com\";\n case \"url\": return \"https://...\";\n case \"number\": return \"0\";\n case \"date\": return \"YYYY-MM-DD\";\n case \"textarea\": return `Enter ${titleCase(key).toLowerCase()}...`;\n default: return `Enter ${titleCase(key).toLowerCase()}`;\n }\n}\n\n/** Resolve fields from data keys when none provided */\nfunction resolveFields(\n fields: ResourceFormFieldDef[] | undefined,\n data: Record<string, unknown> | undefined,\n allData?: Record<string, unknown>[],\n): ResourceFormFieldDef[] {\n if (fields && fields.length > 0) {\n return fields.map((f) => {\n const inferred = f.type ?? (data ? inferFieldType(data[f.key]) : \"text\");\n return {\n ...f,\n label: f.label ?? titleCase(f.key),\n type: inferred,\n placeholder: f.placeholder ?? defaultPlaceholder(f.key, inferred),\n required: f.required ?? (inferred !== \"switch\" && inferred !== \"checkbox\" && inferred !== \"hidden\"),\n };\n });\n }\n\n if (!data) return [];\n\n return Object.keys(data)\n .filter((key) => key !== \"id\" && key !== \"created_at\" && key !== \"updated_at\")\n .map((key) => {\n // Check for select-like field first (string with few unique values)\n if (typeof data[key] === \"string\") {\n const selectInfo = inferSelectFromData(key, allData);\n if (selectInfo) {\n return {\n key,\n label: titleCase(key),\n type: selectInfo.type,\n options: selectInfo.options,\n placeholder: `Select ${titleCase(key).toLowerCase()}...`,\n required: true,\n };\n }\n }\n\n const inferred = inferFieldType(data[key]);\n return {\n key,\n label: titleCase(key),\n type: inferred,\n placeholder: defaultPlaceholder(key, inferred),\n required: inferred !== \"switch\" && inferred !== \"checkbox\" && inferred !== \"hidden\",\n };\n });\n}\n\n/** Build a zod schema from resolved fields */\nfunction buildSchema(fields: ResourceFormFieldDef[]) {\n const shape: Record<string, z.ZodTypeAny> = {};\n\n for (const field of fields) {\n const fieldType = field.type ?? \"text\";\n\n if (field.hidden || fieldType === \"hidden\") continue;\n\n // File/image fields store File objects — z.any() is intentional here because\n // the File API shape varies by platform (browser File, React Native asset, etc.)\n // and form libraries may wrap files differently.\n if (fieldType === \"file\" || fieldType === \"image\") {\n shape[field.key] = field.required !== false ? z.unknown().refine((v) => v != null, { message: `${field.label ?? titleCase(field.key)} is required` }) : z.unknown().optional();\n continue;\n }\n\n switch (fieldType) {\n case \"number\": {\n let schema = z.coerce.number({ message: \"Must be a number\" });\n if (field.min != null) schema = schema.min(field.min);\n if (field.max != null) schema = schema.max(field.max);\n shape[field.key] = field.required !== false\n ? schema\n : schema.optional();\n break;\n }\n case \"array\": {\n if (field.arrayFields) {\n const itemShape: Record<string, z.ZodTypeAny> = {};\n for (const sub of field.arrayFields) {\n const subType = sub.type ?? \"text\";\n if (subType === \"number\") {\n let s = z.coerce.number({ message: \"Must be a number\" });\n if (sub.min != null) s = s.min(sub.min);\n if (sub.max != null) s = s.max(sub.max);\n itemShape[sub.key] = sub.required !== false ? s : s.optional();\n } else if (subType === \"switch\" || subType === \"checkbox\") {\n itemShape[sub.key] = z.boolean().optional();\n } else {\n let s = z.string();\n if (sub.required !== false) {\n s = s.min(1, `${sub.label ?? titleCase(sub.key)} is required`);\n }\n if (subType === \"email\") s = s.email(\"Invalid email address\");\n if (subType === \"url\") s = s.url(\"Invalid URL\");\n itemShape[sub.key] = sub.required === false ? s.optional().or(z.literal(\"\")) : s;\n }\n }\n let arraySchema: z.ZodTypeAny = z.array(z.object(itemShape));\n if (field.arrayMin != null) arraySchema = (arraySchema as z.ZodArray<z.ZodTypeAny>).min(field.arrayMin, `Minimum ${field.arrayMin} items required`);\n if (field.arrayMax != null) arraySchema = (arraySchema as z.ZodArray<z.ZodTypeAny>).max(field.arrayMax, `Maximum ${field.arrayMax} items allowed`);\n shape[field.key] = arraySchema;\n }\n break;\n }\n case \"switch\":\n case \"checkbox\": {\n shape[field.key] = z.boolean().optional();\n break;\n }\n default: {\n // text, email, url, textarea, date, select\n let schema = z.string();\n if (field.required !== false) {\n schema = schema.min(1, `${field.label ?? titleCase(field.key)} is required`);\n }\n if (fieldType === \"email\") {\n schema = schema.email(\"Invalid email address\");\n }\n if (fieldType === \"url\") {\n schema = schema.url(\"Invalid URL\");\n }\n shape[field.key] = field.required === false\n ? schema.optional().or(z.literal(\"\"))\n : schema;\n break;\n }\n }\n }\n\n return z.object(shape).passthrough();\n}\n\n/** Build default values for a single array item from its sub-field definitions */\nexport function buildArrayItemDefaults(\n arrayFields: ResourceFormFieldDef[],\n): Record<string, unknown> {\n const defaults: Record<string, unknown> = {};\n for (const sub of arrayFields) {\n const subType = sub.type ?? \"text\";\n if (subType === \"number\") defaults[sub.key] = undefined;\n else if (subType === \"switch\" || subType === \"checkbox\") defaults[sub.key] = false;\n else defaults[sub.key] = sub.defaultValue ?? \"\";\n }\n return defaults;\n}\n\n/** Build default values from fields and existing data */\nfunction buildDefaults(\n fields: ResourceFormFieldDef[],\n data: Record<string, unknown> | undefined,\n action: \"create\" | \"edit\",\n prefillFromUrl: boolean,\n): Record<string, unknown> {\n const defaults: Record<string, unknown> = {};\n for (const field of fields) {\n if (data && field.key in data) {\n defaults[field.key] = data[field.key];\n } else if (field.defaultValue !== undefined) {\n defaults[field.key] = field.defaultValue;\n } else {\n const fieldType = field.type ?? \"text\";\n switch (fieldType) {\n case \"array\":\n defaults[field.key] = field.arrayMin\n ? Array.from({ length: field.arrayMin }, () =>\n buildArrayItemDefaults(field.arrayFields ?? []),\n )\n : [];\n break;\n case \"number\":\n defaults[field.key] = undefined;\n break;\n case \"switch\":\n case \"checkbox\":\n defaults[field.key] = false;\n break;\n default:\n defaults[field.key] = \"\";\n break;\n }\n }\n }\n\n // URL pre-fill for create mode\n if (typeof window !== \"undefined\" && typeof window.location !== \"undefined\" && typeof window.location.search === \"string\" && action === \"create\" && prefillFromUrl !== false) {\n const params = new URLSearchParams(window.location.search);\n for (const field of fields) {\n const val = params.get(field.key);\n if (val != null) {\n defaults[field.key] = field.type === \"number\" ? Number(val) : val;\n }\n }\n }\n\n return defaults;\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useResourceForm(options: UseResourceFormOptions): UseResourceFormReturn {\n const {\n resource,\n action,\n id,\n initialData,\n fields: fieldsProp,\n listData,\n onSuccess,\n onError,\n prefillFromUrl = true,\n validationMode,\n autosave,\n } = options;\n\n const autosaveEnabled = !!autosave;\n const debounceMs = typeof autosave === \"object\" ? autosave.debounceMs ?? 1000 : 1000;\n\n const provider = useDataProvider();\n const storage = useStorage();\n const queryClient = useQueryClient();\n\n // Fetch record for edit mode when no initialData\n const needsFetch = action === \"edit\" && !initialData && !!id;\n const fetchResult = useOne(\n resource,\n needsFetch ? id : undefined,\n { enabled: needsFetch },\n );\n\n const fetchedData = fetchResult.data ?? undefined;\n const effectiveData = initialData ?? fetchedData;\n const isLoading = needsFetch && fetchResult.isLoading;\n\n // Resolve fields\n const resolvedFields = useMemo(\n () => resolveFields(fieldsProp, effectiveData, listData),\n [fieldsProp, effectiveData, listData],\n );\n\n // Build schema\n const schema = useMemo(\n () => buildSchema(resolvedFields),\n [resolvedFields],\n );\n\n // Build defaults (with optional draft restoration)\n const defaultValues = useMemo(() => {\n const defaults = buildDefaults(resolvedFields, effectiveData, action, prefillFromUrl);\n if (autosaveEnabled && action === \"create\") {\n const key = `genesis-draft:${resource}:new`;\n const saved = storage.getItem(key);\n if (saved) {\n try {\n const draft = JSON.parse(saved);\n if (draft && typeof draft === \"object\" && !Array.isArray(draft)) {\n // Only restore keys that exist in the field definitions\n const validKeys = new Set(resolvedFields.map((f) => f.key));\n for (const k of Object.keys(draft)) {\n if (validKeys.has(k)) {\n defaults[k] = draft[k];\n }\n }\n } else {\n console.warn(`[useResourceForm] Draft for \"${resource}\" has unexpected shape, removing`);\n storage.removeItem(key);\n }\n } catch {\n console.warn(`[useResourceForm] Corrupt draft for \"${resource}\", removing`);\n storage.removeItem(key);\n }\n }\n }\n return defaults;\n }, [resolvedFields, effectiveData, action, prefillFromUrl, autosaveEnabled, resource, storage]);\n\n // Form\n // Cast the resolver because zod v4's passthrough() produces `unknown` for input,\n // but react-hook-form expects FieldValues (Record<string, any>).\n const resolver = useMemo(\n () => zodResolver(schema) as unknown as Resolver<Record<string, unknown>>,\n [schema],\n );\n\n const form = useForm<Record<string, unknown>>({\n resolver,\n defaultValues,\n values: action === \"edit\" ? defaultValues : undefined,\n mode: validationMode ?? \"onBlur\",\n reValidateMode: \"onChange\",\n shouldUnregister: true,\n });\n\n // Autosave effect — persist form values to storage for create mode\n useEffect(() => {\n if (!autosaveEnabled || action !== \"create\") return;\n const key = `genesis-draft:${resource}:new`;\n let timer: ReturnType<typeof setTimeout>;\n\n const subscription = form.watch((values) => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n try {\n storage.setItem(key, JSON.stringify(values));\n } catch (err) {\n console.warn(`[useResourceForm] Autosave failed for \"${resource}\":`, err instanceof Error ? err.message : String(err));\n }\n }, debounceMs);\n });\n\n return () => {\n clearTimeout(timer);\n subscription.unsubscribe();\n };\n }, [autosaveEnabled, resource, action, form, debounceMs, storage]);\n\n // Submit handler\n const handleSubmit = useCallback(\n async (formData: Record<string, unknown>) => {\n try {\n let result: Record<string, unknown>;\n if (action === \"create\") {\n const res = await provider.create(resource, { data: formData });\n result = res.data;\n } else {\n const recordId = id ?? String((effectiveData as Record<string, unknown>)?.id ?? \"\");\n if (!recordId) {\n throw new Error(\"Cannot update record: no ID available. Provide an id prop or ensure the record has an id field.\");\n }\n const res = await provider.update(resource, { id: recordId, data: formData });\n result = res.data;\n }\n // Clear draft on successful save\n if (autosaveEnabled && action === \"create\") {\n try {\n storage.removeItem(`genesis-draft:${resource}:new`);\n } catch (err) {\n console.warn(`[useResourceForm] Draft cleanup failed for \"${resource}\":`, err instanceof Error ? err.message : String(err));\n }\n }\n // Invalidate queries to refresh data\n await queryClient.invalidateQueries({ queryKey: [resource] });\n onSuccess?.(result);\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n form.setError(\"root\", { message: error.message });\n if (onError) {\n onError(error);\n } else {\n console.error(`[useResourceForm] ${action} failed for \"${resource}\":`, error);\n }\n }\n },\n // form is stable (from useForm) but included for correctness since we call form.setError\n [action, provider, resource, id, effectiveData, queryClient, onSuccess, onError, autosaveEnabled, form, storage],\n );\n\n const onSubmit = useCallback(\n (e?: { preventDefault?: () => void }) => form.handleSubmit(handleSubmit)(e as React.BaseSyntheticEvent),\n [form, handleSubmit],\n );\n\n const isSaving = form.formState.isSubmitting;\n\n return {\n form,\n resolvedFields,\n onSubmit,\n handleSave: handleSubmit,\n isSaving,\n isLoading,\n };\n}\n"]}