@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.
- package/README.md +142 -0
- package/dist/{chunk-4W4W7F4I.js → chunk-6MBACQYA.js} +6 -3
- package/dist/chunk-6MBACQYA.js.map +1 -0
- package/dist/chunk-7NUL32W7.js +14 -0
- package/dist/chunk-7NUL32W7.js.map +1 -0
- package/dist/{chunk-6V2TIN3G.js → chunk-GGSXSI6Z.js} +6 -5
- package/dist/chunk-GGSXSI6Z.js.map +1 -0
- package/dist/{chunk-5FTUB2M6.js → chunk-IHWWJUEN.js} +47 -15
- package/dist/chunk-IHWWJUEN.js.map +1 -0
- package/dist/chunk-NW26Z3LZ.js +303 -0
- package/dist/chunk-NW26Z3LZ.js.map +1 -0
- package/dist/{chunk-QFTMHOEH.js → chunk-P4OIIRQS.js} +22 -13
- package/dist/chunk-P4OIIRQS.js.map +1 -0
- package/dist/{chunk-3P4PHPMJ.js → chunk-ZK5C3OOT.js} +3 -3
- package/dist/{chunk-3P4PHPMJ.js.map → chunk-ZK5C3OOT.js.map} +1 -1
- package/dist/form-types.d.ts +6 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +6 -5
- package/dist/hooks/related-keys.d.ts +11 -0
- package/dist/hooks/related-keys.js +3 -0
- package/dist/hooks/related-keys.js.map +1 -0
- package/dist/hooks/use-one.d.ts +1 -1
- package/dist/hooks/use-one.js +2 -2
- package/dist/hooks/use-resource-form.d.ts +19 -1
- package/dist/hooks/use-resource-form.js +4 -3
- package/dist/hooks/use-resource.d.ts +14 -1
- package/dist/hooks/use-resource.js +3 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +9 -8
- package/dist/provider/index.d.ts +140 -6
- package/dist/provider/index.js +3 -3
- package/dist/schema/index.d.ts +3 -3
- package/dist/schema/index.js +1 -1
- package/dist/{types-DUbRb8qV.d.ts → types-zILrlHuV.d.ts} +27 -2
- package/dist/{write-outcome-aYToY3dK.d.ts → write-outcome-BE3Tk6bA.d.ts} +10 -1
- package/package.json +2 -2
- package/dist/chunk-4W4W7F4I.js.map +0 -1
- package/dist/chunk-5FTUB2M6.js.map +0 -1
- package/dist/chunk-6AA4ISUA.js +0 -125
- package/dist/chunk-6AA4ISUA.js.map +0 -1
- package/dist/chunk-6V2TIN3G.js.map +0 -1
- 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
|
-
/**
|
|
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-
|
|
1
|
+
export { buildArrayItemDefaults, useResourceForm } from '../chunk-IHWWJUEN.js';
|
|
2
2
|
import '../chunk-RYEXWQFD.js';
|
|
3
|
-
import '../chunk-
|
|
4
|
-
import '../chunk-
|
|
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 {
|
|
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-
|
|
2
|
-
import '../chunk-
|
|
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,
|
|
2
|
-
export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from './write-outcome-
|
|
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-
|
|
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-
|
|
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 {
|
|
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-
|
|
10
|
-
export { useResource } from './chunk-
|
|
11
|
-
|
|
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
|
package/dist/provider/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { D as DataProvider } from '../types-
|
|
2
|
-
export { A as ArrayFilter,
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/provider/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { createSupabaseProvider } from '../chunk-
|
|
2
|
-
export { DataWriteError, isDataWriteError } from '../chunk-
|
|
3
|
-
export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage } from '../chunk-
|
|
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
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { a as AuditEntry, F as FieldPermissions, h as FieldReference, i as FieldSchema, j as FilterOperator,
|
|
3
|
-
export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from '../write-outcome-
|
|
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. */
|
package/dist/schema/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { decodeRecordKey, encodeRecordKey, keyFields } from '../chunk-KV2HW4G4.js';
|
|
2
|
-
export { DataWriteError, isDataWriteError } from '../chunk-
|
|
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 `(
|
|
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,
|
|
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-
|
|
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
|
+
"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": "^
|
|
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"]}
|