@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
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @marktiderman/genesis-core
|
|
2
|
+
|
|
3
|
+
Genesis's platform-agnostic data layer: the `DataProvider` contract, providers
|
|
4
|
+
(`createSupabaseProvider`, `createMockProvider`), the React roots that put a
|
|
5
|
+
provider in context, and the hooks that read and write through it
|
|
6
|
+
(`useResource`, `useOne`, `useResourceForm`).
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @marktiderman/genesis-core @tanstack/react-query react-hook-form zod @hookform/resolvers
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Mounting: share your app's query cache
|
|
13
|
+
|
|
14
|
+
| Root | Query cache | Use when |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `DataProviderRoot` | **Your app's**: mount it inside your `QueryClientProvider` | The app already uses TanStack Query (most apps) |
|
|
17
|
+
| `GenesisProvider` | **Its own new `QueryClient`**, unless you pass `queryClient` | Prototypes, mock data, apps with no query client yet |
|
|
18
|
+
|
|
19
|
+
`GenesisProvider` without `queryClient` starts a second cache, so invalidating
|
|
20
|
+
a key in app code doesn't refresh Genesis lists and vice versa. In an app that
|
|
21
|
+
has a `QueryClientProvider`, do this:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
function DataRoot({ children }: { children: ReactNode }) {
|
|
25
|
+
const provider = useMemo(() => createSupabaseProvider(supabase, options), []);
|
|
26
|
+
return <DataProviderRoot provider={provider}>{children}</DataProviderRoot>;
|
|
27
|
+
}
|
|
28
|
+
// …inside <QueryClientProvider client={queryClient}>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or `<GenesisProvider provider={provider} queryClient={queryClient}>`.
|
|
32
|
+
|
|
33
|
+
Genesis query keys start with the resource name: `[resource, "list", params]` and
|
|
34
|
+
`[resource, "detail", id, select]`. Invalidating `[resource]` refreshes all of them.
|
|
35
|
+
|
|
36
|
+
## Supabase under RLS
|
|
37
|
+
|
|
38
|
+
`createSupabaseProvider(client, options?)` reads and writes through the client
|
|
39
|
+
you pass, under its session and row-level security. Writes never report a save
|
|
40
|
+
that didn't happen:
|
|
41
|
+
|
|
42
|
+
- **Zero rows is a failure.** `update` and `deleteOne` ask for the affected
|
|
43
|
+
rows. When RLS skips the row, the provider reads it back: still visible
|
|
44
|
+
throws `DataWriteError("forbidden")`; no longer visible throws
|
|
45
|
+
`DataWriteError("gone")`. (Under RLS, "deleted" and "hidden from you" look
|
|
46
|
+
the same, so both are `gone`.)
|
|
47
|
+
- **Database errors are typed.** Every write error is a `DataWriteError` whose
|
|
48
|
+
`outcome` comes from its code (`writeOutcomeFromCode`), with the code on
|
|
49
|
+
`error.code` and the raw error as `cause`:
|
|
50
|
+
|
|
51
|
+
| Code | Outcome |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `42501` (incl. "new row violates row-level security policy") | `forbidden` |
|
|
54
|
+
| `23***` (`23505` unique, `23503` foreign key, `23502` not null, `23514` check), `22***` | `invalid` |
|
|
55
|
+
| `PGRST301`–`PGRST303`, `28000`, `28P01` | `unauthenticated` |
|
|
56
|
+
| `PGRST116` | `gone` |
|
|
57
|
+
| anything else (`P0001` from a function, network failures) | `error`, with the database's message |
|
|
58
|
+
|
|
59
|
+
- Reads are unchanged: a read error is still a plain `Error` with `code`.
|
|
60
|
+
|
|
61
|
+
### Options
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const provider = createSupabaseProvider(supabase, {
|
|
65
|
+
// Tables whose delete trigger moves the row to a recycle bin.
|
|
66
|
+
binTables: ["notes", "commitments"], // or (resource) => boolean
|
|
67
|
+
onWrite: async (event) => { /* { resource, kind, id, record? } after every committed write */ },
|
|
68
|
+
resources: {
|
|
69
|
+
projects: {
|
|
70
|
+
noun: "project", // "You can't change this project."
|
|
71
|
+
prepare: (values, { action, id }) => ({ name: String(values.name ?? "").trim() }),
|
|
72
|
+
create: async (row) => {
|
|
73
|
+
const { data, error } = await supabase.rpc("create_project", { _name: row.name });
|
|
74
|
+
if (error) throw error; // mapped by its code
|
|
75
|
+
return data; // the new id (read back) or the new row
|
|
76
|
+
},
|
|
77
|
+
invalidates: [["staff"], ["me"]],
|
|
78
|
+
},
|
|
79
|
+
customers: {
|
|
80
|
+
update: async (id, row) => {
|
|
81
|
+
const { data, error } = await supabase.rpc("update_customer", { _id: id, ...row });
|
|
82
|
+
if (error) throw error;
|
|
83
|
+
return data; // the row, its id, or nothing when no row changed
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
tasks: {
|
|
87
|
+
deleteMode: "bin",
|
|
88
|
+
toFormValues: (row) => ({ ...row, done: row.status === "done" }),
|
|
89
|
+
prepare: ({ done, ...rest }) => ({ ...rest, status: done ? "done" : "open" }),
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Option | What it does |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `binTables` / `resources[r].deleteMode: "bin"` | Recycle-bin delete: check the row is visible, delete it (asking for no rows, since a binning trigger reports none), read it back. Gone from view is success; still visible is `forbidden`; not visible to begin with is `gone`. Requires binned rows to drop out of the caller's view. `deleteMode: "hard"` opts a table back out. List every soft-delete table: one that isn't listed reports a successful bin as `gone`. |
|
|
98
|
+
| `prepare(values, context)` | Runs before every create and update, overrides included. Gets a copy of the values; may be async. Throw `DataWriteError("invalid", message)` to refuse; nothing is sent. |
|
|
99
|
+
| `create(row)` | Replaces the insert, e.g. a security-definer RPC. Return the row, or its id to have it read back. If the read-back fails, the create is still reported saved (with just the id), so nobody retries into a duplicate. |
|
|
100
|
+
| `update(id, row)` | Replaces the update. Return the row, or its id (or `true`) to have it read back. Returning nothing (or `false`) means no row changed: the provider reads the row back and throws `forbidden` or `gone`, so a function that silently skips a row never reports a save. |
|
|
101
|
+
| `toFormValues(row)` | Row → edit-form values, for a form shaped differently from its table. Served as `DataProvider.toFormValues`, which `useResourceForm` applies in edit mode. The reverse is `prepare`. |
|
|
102
|
+
| `invalidates` | Other query keys a write makes stale, as a list or `(event) => keys`. Served as `DataProvider.invalidates`, which `useResource` and `useResourceForm` (and so `ResourcePage` and `ResourceForm`) invalidate after their writes, on top of `[resource]`. |
|
|
103
|
+
| `onWrite(event)` | Called after every committed create, update and delete, including writes made straight through the provider. Awaited; if it throws, the error is logged and the write is still reported saved. |
|
|
104
|
+
|
|
105
|
+
Both hooks also take an `invalidates` option for keys one screen cares about:
|
|
106
|
+
`useResource("tasks", { invalidates: [["me"]] })`.
|
|
107
|
+
|
|
108
|
+
### Handling failures
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
try {
|
|
112
|
+
await provider.update("projects", { id, data });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isDataWriteError(error)) {
|
|
115
|
+
switch (error.outcome) {
|
|
116
|
+
case "forbidden": /* hide the control, say why */ break;
|
|
117
|
+
case "gone": /* close the record, refresh the list */ break;
|
|
118
|
+
case "invalid": /* show error.message by the form */ break;
|
|
119
|
+
case "unauthenticated": /* send to sign-in */ break;
|
|
120
|
+
default: /* show error.message */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Races, and what the read-back can't tell you
|
|
127
|
+
|
|
128
|
+
- A read-back runs after the write, so it reports what is true then. If
|
|
129
|
+
another session deletes the row in between, a refused update reads as
|
|
130
|
+
`gone`. Neither case ever reports success.
|
|
131
|
+
- A bin delete of a row someone else deletes first reports success: the row
|
|
132
|
+
is gone from view, which is what was asked.
|
|
133
|
+
- An update whose new row would fail the table's SELECT policy is refused by
|
|
134
|
+
Postgres (`42501`) and rolled back, so it surfaces as `forbidden`.
|
|
135
|
+
- The provider addresses rows by an `id` column.
|
|
136
|
+
|
|
137
|
+
## Write outcomes
|
|
138
|
+
|
|
139
|
+
`DataWriteError`, `isDataWriteError` and `WriteOutcome` are exported from both
|
|
140
|
+
the root and `@marktiderman/genesis-core/schema`. Check with
|
|
141
|
+
`isDataWriteError(error)` rather than `instanceof`, which fails when two copies
|
|
142
|
+
of the package are installed.
|
|
@@ -3,12 +3,15 @@ var DataWriteError = class extends Error {
|
|
|
3
3
|
outcome;
|
|
4
4
|
current;
|
|
5
5
|
currentVersion;
|
|
6
|
+
/** The store's own error code, when it gave one. */
|
|
7
|
+
code;
|
|
6
8
|
constructor(outcome, message, options = {}) {
|
|
7
|
-
super(message);
|
|
9
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
8
10
|
this.name = "DataWriteError";
|
|
9
11
|
this.outcome = outcome;
|
|
10
12
|
this.current = options.current;
|
|
11
13
|
this.currentVersion = options.currentVersion;
|
|
14
|
+
if (options.code !== void 0) this.code = options.code;
|
|
12
15
|
}
|
|
13
16
|
};
|
|
14
17
|
function isDataWriteError(error) {
|
|
@@ -16,5 +19,5 @@ function isDataWriteError(error) {
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export { DataWriteError, isDataWriteError };
|
|
19
|
-
//# sourceMappingURL=chunk-
|
|
20
|
-
//# sourceMappingURL=chunk-
|
|
22
|
+
//# sourceMappingURL=chunk-6MBACQYA.js.map
|
|
23
|
+
//# sourceMappingURL=chunk-6MBACQYA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/write-outcome.ts"],"names":[],"mappings":";AAgDO,IAAM,cAAA,GAAN,cAA6B,KAAA,CAAM;AAAA,EAC/B,OAAA;AAAA,EACA,OAAA;AAAA,EACA,cAAA;AAAA;AAAA,EAEA,IAAA;AAAA,EAET,WAAA,CAAY,OAAA,EAAuB,OAAA,EAAiB,OAAA,GAAiC,EAAC,EAAG;AACvF,IAAA,KAAA,CAAM,OAAA,EAAS,QAAQ,KAAA,KAAU,MAAA,GAAY,SAAY,EAAE,KAAA,EAAO,OAAA,CAAQ,KAAA,EAAO,CAAA;AACjF,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;AAC9B,IAAA,IAAI,OAAA,CAAQ,IAAA,KAAS,MAAA,EAAW,IAAA,CAAK,OAAO,OAAA,CAAQ,IAAA;AAAA,EACtD;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-6MBACQYA.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 * The store's own error code, when it gave one (a Postgres SQLSTATE such as\n * `23505`, or a PostgREST code such as `PGRST301`).\n */\n code?: string;\n /** The underlying error, kept for logging. */\n cause?: unknown;\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 /** The store's own error code, when it gave one. */\n readonly code?: string;\n\n constructor(outcome: WriteOutcome, message: string, options: DataWriteErrorOptions = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = \"DataWriteError\";\n this.outcome = outcome;\n this.current = options.current;\n this.currentVersion = options.currentVersion;\n if (options.code !== undefined) this.code = options.code;\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"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/hooks/related-keys.ts
|
|
2
|
+
function relatedKeys(provider, event, extra) {
|
|
3
|
+
let fromProvider = [];
|
|
4
|
+
try {
|
|
5
|
+
fromProvider = provider.invalidates?.(event) ?? [];
|
|
6
|
+
} catch (error) {
|
|
7
|
+
console.error(`[genesis-core] invalidates failed for "${event.resource}":`, error);
|
|
8
|
+
}
|
|
9
|
+
return [...fromProvider, ...extra ?? []];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { relatedKeys };
|
|
13
|
+
//# sourceMappingURL=chunk-7NUL32W7.js.map
|
|
14
|
+
//# sourceMappingURL=chunk-7NUL32W7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/related-keys.ts"],"names":[],"mappings":";AAOO,SAAS,WAAA,CACd,QAAA,EACA,KAAA,EACA,KAAA,EACmB;AACnB,EAAA,IAAI,eAAkC,EAAC;AACvC,EAAA,IAAI;AACF,IAAA,YAAA,GAAe,QAAA,CAAS,WAAA,GAAc,KAAK,CAAA,IAAK,EAAC;AAAA,EACnD,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,uCAAA,EAA0C,KAAA,CAAM,QAAQ,MAAM,KAAK,CAAA;AAAA,EACnF;AACA,EAAA,OAAO,CAAC,GAAG,YAAA,EAAc,GAAI,KAAA,IAAS,EAAG,CAAA;AAC3C","file":"chunk-7NUL32W7.js","sourcesContent":["import type { DataProvider, InvalidationKey, WriteEvent } from \"../provider/types\";\n\n/**\n * The cache keys a committed write makes stale beyond `[resource]`: the\n * provider's own (`DataProvider.invalidates`) plus the caller's. A provider\n * that throws here is logged and skipped; the write already committed.\n */\nexport function relatedKeys(\n provider: DataProvider,\n event: WriteEvent,\n extra: InvalidationKey[] | undefined,\n): InvalidationKey[] {\n let fromProvider: InvalidationKey[] = [];\n try {\n fromProvider = provider.invalidates?.(event) ?? [];\n } catch (error) {\n console.error(`[genesis-core] invalidates failed for \"${event.resource}\":`, error);\n }\n return [...fromProvider, ...(extra ?? [])];\n}\n"]}
|
|
@@ -168,7 +168,8 @@ function GenesisProvider({
|
|
|
168
168
|
children,
|
|
169
169
|
provider,
|
|
170
170
|
mock,
|
|
171
|
-
storage
|
|
171
|
+
storage,
|
|
172
|
+
queryClient: queryClientProp
|
|
172
173
|
}) {
|
|
173
174
|
if (provider && mock) {
|
|
174
175
|
if (process.env.NODE_ENV !== "production") {
|
|
@@ -183,7 +184,7 @@ function GenesisProvider({
|
|
|
183
184
|
);
|
|
184
185
|
}, [provider, mock]);
|
|
185
186
|
const queryClient = useMemo(
|
|
186
|
-
() => new QueryClient({
|
|
187
|
+
() => queryClientProp ?? new QueryClient({
|
|
187
188
|
defaultOptions: {
|
|
188
189
|
queries: {
|
|
189
190
|
staleTime: 5 * 60 * 1e3
|
|
@@ -191,12 +192,12 @@ function GenesisProvider({
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
}),
|
|
194
|
-
[]
|
|
195
|
+
[queryClientProp]
|
|
195
196
|
);
|
|
196
197
|
const resolvedStorage = storage ?? defaultStorage;
|
|
197
198
|
return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(StorageContext.Provider, { value: resolvedStorage, children: /* @__PURE__ */ jsx(DataProviderRoot, { provider: resolvedProvider, children }) }) });
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage };
|
|
201
|
-
//# sourceMappingURL=chunk-
|
|
202
|
-
//# sourceMappingURL=chunk-
|
|
202
|
+
//# sourceMappingURL=chunk-GGSXSI6Z.js.map
|
|
203
|
+
//# sourceMappingURL=chunk-GGSXSI6Z.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider/mock-provider.ts","../src/provider/context.tsx"],"names":[],"mappings":"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","file":"chunk-GGSXSI6Z.js","sourcesContent":["import type {\n DataProvider,\n ListParams,\n FilterParam,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Mock DataProvider — operates on in-memory arrays, deep-cloned on creation\n// ---------------------------------------------------------------------------\n\nexport interface MockProviderOptions {\n /** Simulated latency in ms (default 0). */\n delay?: number;\n}\n\ntype Row = Record<string, unknown>;\n\nexport function createMockProvider(\n datasets: Record<string, Row[]>,\n options: MockProviderOptions = {},\n): DataProvider {\n const { delay = 0 } = options;\n\n // Deep clone so mutations don't affect the original data.\n const store: Record<string, Row[]> = JSON.parse(JSON.stringify(datasets));\n\n // Instance-scoped ID counter (NOT module-level).\n let nextId = 1;\n\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n function getTable(resource: string): Row[] {\n if (!store[resource]) {\n store[resource] = [];\n }\n return store[resource];\n }\n\n async function withDelay<T>(value: T): Promise<T> {\n if (delay > 0) {\n await new Promise((r) => setTimeout(r, delay));\n }\n return value;\n }\n\n function matchesFilter(row: Row, filter: FilterParam): boolean {\n const val = row[filter.field];\n\n switch (filter.operator) {\n case \"eq\":\n return val === filter.value;\n case \"neq\":\n return val !== filter.value;\n case \"gt\":\n return (val as number) > (filter.value as number);\n case \"lt\":\n return (val as number) < (filter.value as number);\n case \"gte\":\n return (val as number) >= (filter.value as number);\n case \"lte\":\n return (val as number) <= (filter.value as number);\n case \"contains\":\n return typeof val === \"string\" &&\n val.toLowerCase().includes(String(filter.value).toLowerCase());\n case \"in\":\n return filter.value.includes(val as string | number | boolean);\n case \"is_null\":\n return val === null || val === undefined;\n case \"is_not_null\":\n return val !== null && val !== undefined;\n case \"between\": {\n const [lo, hi] = filter.value;\n return (val as number) >= (lo as number) && (val as number) <= (hi as number);\n }\n }\n }\n\n function matchesSearch(\n row: Row,\n search: { query: string; fields: string[] },\n ): boolean {\n const q = search.query.toLowerCase();\n return search.fields.some((field) => {\n const val = row[field];\n return typeof val === \"string\" && val.toLowerCase().includes(q);\n });\n }\n\n function compareValues(a: unknown, b: unknown): number {\n if (typeof a === \"string\" && typeof b === \"string\") {\n return a.localeCompare(b, undefined, { numeric: true });\n }\n if (typeof a === \"number\" && typeof b === \"number\") {\n return a - b;\n }\n // Fallback: coerce to string.\n return String(a ?? \"\").localeCompare(String(b ?? \"\"), undefined, {\n numeric: true,\n });\n }\n\n function applyListParams(table: Row[], params: ListParams): { data: Row[]; total: number } {\n let rows = [...table];\n\n // Filters (AND).\n if (params.filters) {\n for (const f of params.filters) {\n rows = rows.filter((r) => matchesFilter(r, f));\n }\n }\n\n // Search (OR across fields, AND with filters).\n if (params.search && params.search.query) {\n rows = rows.filter((r) => matchesSearch(r, params.search!));\n }\n\n const total = rows.length;\n\n // Sort.\n if (params.sort) {\n const { field, order } = params.sort;\n rows.sort((a, b) => {\n const cmp = compareValues(a[field], b[field]);\n return order === \"desc\" ? -cmp : cmp;\n });\n }\n\n // Pagination (offset-based).\n if (params.pagination) {\n const { page, perPage } = params.pagination;\n const start = (page - 1) * perPage;\n rows = rows.slice(start, start + perPage);\n }\n\n return { data: rows, total };\n }\n\n // ---------------------------------------------------------------------------\n // DataProvider implementation\n // ---------------------------------------------------------------------------\n\n const provider: DataProvider = {\n async getList(_resource, params) {\n const table = getTable(_resource);\n const result = applyListParams(table, params);\n return withDelay(result) as never;\n },\n\n async getOne(_resource, params) {\n const table = getTable(_resource);\n const row = table.find((r) => String(r.id) === String(params.id));\n if (!row) {\n throw new Error(\n `[MockProvider] Record not found: ${_resource}#${params.id}`,\n );\n }\n return withDelay({ data: JSON.parse(JSON.stringify(row)) }) as never;\n },\n\n async getMany(_resource, params) {\n const table = getTable(_resource);\n const idSet = new Set(params.ids.map(String));\n const rows = table.filter((r) => idSet.has(String(r.id)));\n return withDelay({ data: JSON.parse(JSON.stringify(rows)) }) as never;\n },\n\n async create(_resource, params) {\n const table = getTable(_resource);\n const id = String(nextId++);\n const row = { id, ...params.data } as Row;\n table.push(row);\n return withDelay({ data: JSON.parse(JSON.stringify(row)) }) as never;\n },\n\n async update(_resource, params) {\n const table = getTable(_resource);\n const idx = table.findIndex((r) => String(r.id) === String(params.id));\n if (idx === -1) {\n throw new Error(\n `[MockProvider] Record not found: ${_resource}#${params.id}`,\n );\n }\n table[idx] = { ...table[idx], ...params.data };\n return withDelay({ data: JSON.parse(JSON.stringify(table[idx])) }) as never;\n },\n\n async deleteOne(_resource, params) {\n const table = getTable(_resource);\n const idx = table.findIndex((r) => String(r.id) === String(params.id));\n if (idx === -1) {\n throw new Error(\n `[MockProvider] Record not found: ${_resource}#${params.id}`,\n );\n }\n table.splice(idx, 1);\n return withDelay({ data: { id: params.id } });\n },\n };\n\n return provider;\n}\n","import {\n createContext,\n useContext,\n useMemo,\n type ReactNode,\n} from \"react\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport type { DataProvider } from \"./types\";\nimport { createMockProvider, type MockProviderOptions } from \"./mock-provider\";\nimport type { StorageAdapter } from \"../storage/types\";\nimport { createLocalStorageAdapter } from \"../storage/adapters\";\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\nconst DataProviderContext = createContext<DataProvider | null>(null);\nconst StorageContext = createContext<StorageAdapter>(createLocalStorageAdapter());\n\n// ---------------------------------------------------------------------------\n// DataProviderRoot — thin context wrapper\n// ---------------------------------------------------------------------------\n\n/**\n * Puts a `DataProvider` in context, and nothing else. Mount it inside your\n * app's own `QueryClientProvider` and the Genesis hooks share that cache:\n * the same query keys (`[resource, \"list\", params]`,\n * `[resource, \"detail\", id, select]`), so invalidating `[resource]` from app\n * code refreshes Genesis lists too, and vice versa.\n */\nexport interface DataProviderRootProps {\n provider: DataProvider;\n children: ReactNode;\n}\n\nexport function DataProviderRoot({ provider, children }: DataProviderRootProps) {\n return (\n <DataProviderContext.Provider value={provider}>\n {children}\n </DataProviderContext.Provider>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useDataProvider(): DataProvider {\n const ctx = useContext(DataProviderContext);\n if (!ctx) {\n throw new Error(\n \"useDataProvider must be used inside <DataProviderRoot> or <GenesisProvider>. \" +\n \"Wrap your app (or the relevant subtree) in one of these providers.\",\n );\n }\n return ctx;\n}\n\nexport function useStorage(): StorageAdapter {\n return useContext(StorageContext);\n}\n\n// ---------------------------------------------------------------------------\n// GenesisProvider — convenience wrapper\n// ---------------------------------------------------------------------------\n\ntype Row = Record<string, unknown>;\n\nexport type GenesisProviderProps =\n | {\n children: ReactNode;\n /** Pass a ready-made DataProvider. Mutually exclusive with `mock`. */\n provider: DataProvider;\n mock?: never;\n /** Platform-specific storage adapter. Defaults to localStorage (SSR-safe). */\n storage?: StorageAdapter;\n /**\n * The TanStack Query client to use. Without it, GenesisProvider creates\n * its OWN client — a second cache beside any the app already has, so app\n * queries and Genesis queries would not see each other's invalidations.\n * An app with a `QueryClientProvider` should pass its client here, or\n * use `DataProviderRoot` inside that provider instead.\n */\n queryClient?: QueryClient;\n }\n | {\n children: ReactNode;\n provider?: never;\n /** Pass mock datasets. Mutually exclusive with `provider`. */\n mock: {\n datasets: Record<string, Row[]>;\n options?: MockProviderOptions;\n };\n /** Platform-specific storage adapter. Defaults to localStorage (SSR-safe). */\n storage?: StorageAdapter;\n /** See the `provider` variant. */\n queryClient?: QueryClient;\n };\n\nconst defaultStorage = createLocalStorageAdapter();\n\n/**\n * Convenience root: a `QueryClientProvider`, storage, and a data provider\n * (yours, or a mock built from datasets). It creates its own `QueryClient`\n * unless you pass `queryClient`; an app that already has one should pass it,\n * or mount `DataProviderRoot` inside its existing `QueryClientProvider`.\n */\nexport function GenesisProvider({\n children,\n provider,\n mock,\n storage,\n queryClient: queryClientProp,\n}: GenesisProviderProps) {\n if (provider && mock) {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\"[GenesisProvider] Both `provider` and `mock` were passed. `provider` takes precedence. This is likely a mistake.\");\n }\n }\n\n // Build provider from mock datasets when no explicit provider is given.\n const resolvedProvider = useMemo(() => {\n if (provider) return provider;\n if (mock) return createMockProvider(mock.datasets, mock.options);\n throw new Error(\n \"<GenesisProvider> requires either a `provider` or `mock` prop.\",\n );\n }, [provider, mock]);\n\n // The caller's QueryClient, or an internal one with sensible defaults.\n const queryClient = useMemo(\n () =>\n queryClientProp ??\n new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 5 * 60 * 1000, // 5 minutes\n },\n },\n }),\n [queryClientProp],\n );\n\n const resolvedStorage = storage ?? defaultStorage;\n\n return (\n <QueryClientProvider client={queryClient}>\n <StorageContext.Provider value={resolvedStorage}>\n <DataProviderRoot provider={resolvedProvider}>\n {children}\n </DataProviderRoot>\n </StorageContext.Provider>\n </QueryClientProvider>\n );\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { titleCase } from './chunk-RYEXWQFD.js';
|
|
2
|
-
import { useOne } from './chunk-
|
|
3
|
-
import {
|
|
2
|
+
import { useOne } from './chunk-ZK5C3OOT.js';
|
|
3
|
+
import { relatedKeys } from './chunk-7NUL32W7.js';
|
|
4
|
+
import { useDataProvider, useStorage } from './chunk-GGSXSI6Z.js';
|
|
4
5
|
import { useMemo, useEffect, useCallback } from 'react';
|
|
5
6
|
import { useForm } from 'react-hook-form';
|
|
6
7
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
@@ -49,7 +50,8 @@ function defaultPlaceholder(key, type) {
|
|
|
49
50
|
return `Enter ${titleCase(key).toLowerCase()}`;
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
function resolveFields(fields, data, allData) {
|
|
53
|
+
function resolveFields(fields, data, allData, autoPlaceholder = true) {
|
|
54
|
+
const placeholderFor = (key, type) => autoPlaceholder ? defaultPlaceholder(key, type) : void 0;
|
|
53
55
|
if (fields && fields.length > 0) {
|
|
54
56
|
return fields.map((f) => {
|
|
55
57
|
const inferred = f.type ?? (data ? inferFieldType(data[f.key]) : "text");
|
|
@@ -57,7 +59,7 @@ function resolveFields(fields, data, allData) {
|
|
|
57
59
|
...f,
|
|
58
60
|
label: f.label ?? titleCase(f.key),
|
|
59
61
|
type: inferred,
|
|
60
|
-
placeholder: f.placeholder ??
|
|
62
|
+
placeholder: f.placeholder ?? placeholderFor(f.key, inferred),
|
|
61
63
|
required: f.required ?? (inferred !== "switch" && inferred !== "checkbox" && inferred !== "hidden")
|
|
62
64
|
};
|
|
63
65
|
});
|
|
@@ -72,7 +74,7 @@ function resolveFields(fields, data, allData) {
|
|
|
72
74
|
label: titleCase(key),
|
|
73
75
|
type: selectInfo.type,
|
|
74
76
|
options: selectInfo.options,
|
|
75
|
-
placeholder: `Select ${titleCase(key).toLowerCase()}
|
|
77
|
+
placeholder: autoPlaceholder ? `Select ${titleCase(key).toLowerCase()}...` : void 0,
|
|
76
78
|
required: true
|
|
77
79
|
};
|
|
78
80
|
}
|
|
@@ -82,7 +84,7 @@ function resolveFields(fields, data, allData) {
|
|
|
82
84
|
key,
|
|
83
85
|
label: titleCase(key),
|
|
84
86
|
type: inferred,
|
|
85
|
-
placeholder:
|
|
87
|
+
placeholder: placeholderFor(key, inferred),
|
|
86
88
|
required: inferred !== "switch" && inferred !== "checkbox" && inferred !== "hidden"
|
|
87
89
|
};
|
|
88
90
|
});
|
|
@@ -123,6 +125,12 @@ function buildSchema(fields) {
|
|
|
123
125
|
}
|
|
124
126
|
if (subType === "email") s = s.email("Invalid email address");
|
|
125
127
|
if (subType === "url") s = s.url("Invalid URL");
|
|
128
|
+
if (sub.maxLength != null) {
|
|
129
|
+
s = s.max(
|
|
130
|
+
sub.maxLength,
|
|
131
|
+
`${sub.label ?? titleCase(sub.key)} must be ${sub.maxLength} characters or fewer`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
126
134
|
itemShape[sub.key] = sub.required === false ? s.optional().or(z.literal("")) : s;
|
|
127
135
|
}
|
|
128
136
|
}
|
|
@@ -149,6 +157,12 @@ function buildSchema(fields) {
|
|
|
149
157
|
if (fieldType === "url") {
|
|
150
158
|
schema = schema.url("Invalid URL");
|
|
151
159
|
}
|
|
160
|
+
if (field.maxLength != null) {
|
|
161
|
+
schema = schema.max(
|
|
162
|
+
field.maxLength,
|
|
163
|
+
`${field.label ?? titleCase(field.key)} must be ${field.maxLength} characters or fewer`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
152
166
|
shape[field.key] = field.required === false ? schema.optional().or(z.literal("")) : schema;
|
|
153
167
|
break;
|
|
154
168
|
}
|
|
@@ -217,8 +231,10 @@ function useResourceForm(options) {
|
|
|
217
231
|
onSuccess,
|
|
218
232
|
onError,
|
|
219
233
|
prefillFromUrl = true,
|
|
234
|
+
autoPlaceholder = true,
|
|
220
235
|
validationMode,
|
|
221
|
-
autosave
|
|
236
|
+
autosave,
|
|
237
|
+
invalidates
|
|
222
238
|
} = options;
|
|
223
239
|
const autosaveEnabled = !!autosave;
|
|
224
240
|
const debounceMs = typeof autosave === "object" ? autosave.debounceMs ?? 1e3 : 1e3;
|
|
@@ -234,16 +250,20 @@ function useResourceForm(options) {
|
|
|
234
250
|
const fetchedData = fetchResult.data ?? void 0;
|
|
235
251
|
const effectiveData = initialData ?? fetchedData;
|
|
236
252
|
const isLoading = needsFetch && fetchResult.isLoading;
|
|
253
|
+
const formSource = useMemo(
|
|
254
|
+
() => action === "edit" && effectiveData && provider.toFormValues ? provider.toFormValues(resource, effectiveData) : effectiveData,
|
|
255
|
+
[action, effectiveData, provider, resource]
|
|
256
|
+
);
|
|
237
257
|
const resolvedFields = useMemo(
|
|
238
|
-
() => resolveFields(fieldsProp,
|
|
239
|
-
[fieldsProp,
|
|
258
|
+
() => resolveFields(fieldsProp, formSource, listData, autoPlaceholder),
|
|
259
|
+
[fieldsProp, formSource, listData, autoPlaceholder]
|
|
240
260
|
);
|
|
241
261
|
const schema = useMemo(
|
|
242
262
|
() => buildSchema(resolvedFields),
|
|
243
263
|
[resolvedFields]
|
|
244
264
|
);
|
|
245
265
|
const defaultValues = useMemo(() => {
|
|
246
|
-
const defaults = buildDefaults(resolvedFields,
|
|
266
|
+
const defaults = buildDefaults(resolvedFields, formSource, action, prefillFromUrl);
|
|
247
267
|
if (autosaveEnabled && action === "create") {
|
|
248
268
|
const key = `genesis-draft:${resource}:new`;
|
|
249
269
|
const saved = storage.getItem(key);
|
|
@@ -268,7 +288,7 @@ function useResourceForm(options) {
|
|
|
268
288
|
}
|
|
269
289
|
}
|
|
270
290
|
return defaults;
|
|
271
|
-
}, [resolvedFields,
|
|
291
|
+
}, [resolvedFields, formSource, action, prefillFromUrl, autosaveEnabled, resource, storage]);
|
|
272
292
|
const resolver = useMemo(
|
|
273
293
|
() => zodResolver(schema),
|
|
274
294
|
[schema]
|
|
@@ -304,11 +324,12 @@ function useResourceForm(options) {
|
|
|
304
324
|
async (formData) => {
|
|
305
325
|
try {
|
|
306
326
|
let result;
|
|
327
|
+
let recordId;
|
|
307
328
|
if (action === "create") {
|
|
308
329
|
const res = await provider.create(resource, { data: formData });
|
|
309
330
|
result = res.data;
|
|
310
331
|
} else {
|
|
311
|
-
|
|
332
|
+
recordId = id ?? String(effectiveData?.id ?? "");
|
|
312
333
|
if (!recordId) {
|
|
313
334
|
throw new Error("Cannot update record: no ID available. Provide an id prop or ensure the record has an id field.");
|
|
314
335
|
}
|
|
@@ -323,6 +344,17 @@ function useResourceForm(options) {
|
|
|
323
344
|
}
|
|
324
345
|
}
|
|
325
346
|
await queryClient.invalidateQueries({ queryKey: [resource] });
|
|
347
|
+
const related = relatedKeys(
|
|
348
|
+
provider,
|
|
349
|
+
{
|
|
350
|
+
resource,
|
|
351
|
+
kind: action === "create" ? "create" : "update",
|
|
352
|
+
id: recordId ?? result.id,
|
|
353
|
+
record: result
|
|
354
|
+
},
|
|
355
|
+
invalidates
|
|
356
|
+
);
|
|
357
|
+
await Promise.all(related.map((queryKey) => queryClient.invalidateQueries({ queryKey: [...queryKey] })));
|
|
326
358
|
onSuccess?.(result);
|
|
327
359
|
} catch (err) {
|
|
328
360
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -335,7 +367,7 @@ function useResourceForm(options) {
|
|
|
335
367
|
}
|
|
336
368
|
},
|
|
337
369
|
// form is stable (from useForm) but included for correctness since we call form.setError
|
|
338
|
-
[action, provider, resource, id, effectiveData, queryClient, onSuccess, onError, autosaveEnabled, form, storage]
|
|
370
|
+
[action, provider, resource, id, effectiveData, queryClient, onSuccess, onError, autosaveEnabled, form, storage, invalidates]
|
|
339
371
|
);
|
|
340
372
|
const onSubmit = useCallback(
|
|
341
373
|
(e) => form.handleSubmit(handleSubmit)(e),
|
|
@@ -353,5 +385,5 @@ function useResourceForm(options) {
|
|
|
353
385
|
}
|
|
354
386
|
|
|
355
387
|
export { buildArrayItemDefaults, useResourceForm };
|
|
356
|
-
//# sourceMappingURL=chunk-
|
|
357
|
-
//# sourceMappingURL=chunk-
|
|
388
|
+
//# sourceMappingURL=chunk-IHWWJUEN.js.map
|
|
389
|
+
//# sourceMappingURL=chunk-IHWWJUEN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-resource-form.ts"],"names":[],"mappings":"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","file":"chunk-IHWWJUEN.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\";\nimport type { BaseRecord, InvalidationKey } from \"../provider/types\";\nimport { relatedKeys } from \"./related-keys\";\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 /**\n * Pre-fill create form defaults from URL query params: a param named like a\n * field (`?status=draft`) becomes that field's starting value. Default true.\n * Pass false for a form that should not read the page's URL, e.g. one\n * opened from a list whose own filters live in the query string.\n */\n prefillFromUrl?: boolean;\n /**\n * Generate a placeholder (\"Enter name\", \"name@example.com\") for a field\n * whose `placeholder` is unset. Default true. A placeholder that only\n * repeats the label adds noise without information, so `ResourceForm`\n * turns this off unless asked.\n */\n autoPlaceholder?: 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 * Other query keys to invalidate after a save, on top of `[resource]` and\n * the provider's own `invalidates`.\n */\n invalidates?: InvalidationKey[];\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 autoPlaceholder = true,\n): ResourceFormFieldDef[] {\n const placeholderFor = (key: string, type: FieldType): string | undefined =>\n autoPlaceholder ? defaultPlaceholder(key, type) : undefined;\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 ?? placeholderFor(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: autoPlaceholder\n ? `Select ${titleCase(key).toLowerCase()}...`\n : undefined,\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: placeholderFor(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 if (sub.maxLength != null) {\n s = s.max(\n sub.maxLength,\n `${sub.label ?? titleCase(sub.key)} must be ${sub.maxLength} characters or fewer`,\n );\n }\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 if (field.maxLength != null) {\n schema = schema.max(\n field.maxLength,\n `${field.label ?? titleCase(field.key)} must be ${field.maxLength} characters or fewer`,\n );\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 autoPlaceholder = true,\n validationMode,\n autosave,\n invalidates,\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 // In edit mode the form starts from the provider's form shape of the row\n // (`DataProvider.toFormValues`), for a resource whose form differs from\n // its table. The record id still comes from the row itself.\n const formSource = useMemo(\n () =>\n action === \"edit\" && effectiveData && provider.toFormValues\n ? provider.toFormValues(resource, effectiveData)\n : effectiveData,\n [action, effectiveData, provider, resource],\n );\n\n // Resolve fields\n const resolvedFields = useMemo(\n () => resolveFields(fieldsProp, formSource, listData, autoPlaceholder),\n [fieldsProp, formSource, listData, autoPlaceholder],\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, formSource, 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, formSource, 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 let recordId: string | number | undefined;\n if (action === \"create\") {\n const res = await provider.create(resource, { data: formData });\n result = res.data;\n } else {\n 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: the resource's own, then any\n // related keys the provider or the caller named.\n await queryClient.invalidateQueries({ queryKey: [resource] });\n const related = relatedKeys(\n provider,\n {\n resource,\n kind: action === \"create\" ? \"create\" : \"update\",\n id: (recordId ?? result.id) as string | number,\n record: result as BaseRecord,\n },\n invalidates,\n );\n await Promise.all(related.map((queryKey) => queryClient.invalidateQueries({ queryKey: [...queryKey] })));\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, invalidates],\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"]}
|