@marktiderman/genesis-core 1.3.0 → 1.5.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 +223 -0
- package/dist/chunk-5NHR6ZXS.js +31 -0
- package/dist/chunk-5NHR6ZXS.js.map +1 -0
- package/dist/chunk-7NUL32W7.js +14 -0
- package/dist/chunk-7NUL32W7.js.map +1 -0
- package/dist/chunk-BQOJX67X.js +31 -0
- package/dist/chunk-BQOJX67X.js.map +1 -0
- package/dist/{chunk-QFTMHOEH.js → chunk-DVW5GXWZ.js} +84 -31
- package/dist/chunk-DVW5GXWZ.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-HT43MPKW.js +3 -0
- package/dist/{chunk-EL7YGOTY.js.map → chunk-HT43MPKW.js.map} +1 -1
- package/dist/chunk-HUNGCTRA.js +33 -0
- package/dist/chunk-HUNGCTRA.js.map +1 -0
- package/dist/{chunk-5FTUB2M6.js → chunk-IZT2O57C.js} +160 -40
- package/dist/chunk-IZT2O57C.js.map +1 -0
- package/dist/chunk-QH4P5MI2.js +278 -0
- package/dist/chunk-QH4P5MI2.js.map +1 -0
- package/dist/chunk-RLY7TZUY.js +444 -0
- package/dist/chunk-RLY7TZUY.js.map +1 -0
- package/dist/chunk-XQO4OH2N.js +32 -0
- package/dist/chunk-XQO4OH2N.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 +5 -1
- package/dist/hooks/index.js +11 -6
- package/dist/hooks/mutation-mode.d.ts +109 -0
- package/dist/hooks/mutation-mode.js +3 -0
- package/dist/hooks/mutation-mode.js.map +1 -0
- package/dist/hooks/related-keys.d.ts +12 -0
- package/dist/hooks/related-keys.js +3 -0
- package/dist/hooks/related-keys.js.map +1 -0
- package/dist/hooks/use-capabilities.d.ts +21 -0
- package/dist/hooks/use-capabilities.js +5 -0
- package/dist/hooks/use-capabilities.js.map +1 -0
- package/dist/hooks/use-one.d.ts +2 -1
- package/dist/hooks/use-one.js +2 -2
- package/dist/hooks/use-resource-form.d.ts +53 -2
- package/dist/hooks/use-resource-form.js +7 -3
- package/dist/hooks/use-resource.d.ts +42 -5
- package/dist/hooks/use-resource.js +4 -2
- package/dist/hooks/use-unsaved-changes-guard.d.ts +33 -0
- package/dist/hooks/use-unsaved-changes-guard.js +3 -0
- package/dist/hooks/use-unsaved-changes-guard.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +14 -9
- package/dist/provider/index.d.ts +212 -6
- package/dist/provider/index.js +5 -3
- package/dist/read-outcome-BQkYcV4t.d.ts +88 -0
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +2 -1
- package/dist/{types-DUbRb8qV.d.ts → types-C5bjsbxz.d.ts} +13 -255
- package/dist/types-Dsqqr9s7.d.ts +281 -0
- package/package.json +2 -2
- package/dist/chunk-4W4W7F4I.js +0 -20
- 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-EL7YGOTY.js +0 -3
- package/dist/chunk-QFTMHOEH.js.map +0 -1
- package/dist/write-outcome-aYToY3dK.d.ts +0 -38
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
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.
|
|
143
|
+
|
|
144
|
+
### Field-level errors
|
|
145
|
+
|
|
146
|
+
A `DataWriteError` may carry `fields`: field (column) name → a message safe to
|
|
147
|
+
show by that input. `useResourceForm` puts each one on its field
|
|
148
|
+
(`setError(field, { type: "server" })`), focuses the first, and sets
|
|
149
|
+
`errors.root` to the error's message as the summary; a field the form does not
|
|
150
|
+
show appears only in the summary.
|
|
151
|
+
|
|
152
|
+
`createSupabaseProvider` fills `fields` where Postgres says which column it
|
|
153
|
+
means (`fieldErrorsFromPostgrest`), and replaces the error's message with a
|
|
154
|
+
plain sentence (the raw text stays on `cause`):
|
|
155
|
+
|
|
156
|
+
| Code | Read from | Field message | Error message |
|
|
157
|
+
| --- | --- | --- | --- |
|
|
158
|
+
| `23505` unique | `details`: `Key (email)=(…) already exists.` | "This value is already in use." (several columns: "This combination is already in use.") | "Email is already in use." |
|
|
159
|
+
| `23502` not null | `message`: `null value in column "title" …` | "This field is required." | "Title is required." |
|
|
160
|
+
| `23503` foreign key (insert/update) | `details`: `Key (owner_id)=(…) is not present in table …` | "The selected item doesn't exist." | "Owner Id: the selected item doesn't exist." |
|
|
161
|
+
|
|
162
|
+
Only plain column names are taken from the store's text; the values it echoes
|
|
163
|
+
and constraint names never reach a message. Anything else (expression indexes,
|
|
164
|
+
check constraints) is left unmapped. Your own provider or `prepare` rule can
|
|
165
|
+
throw `new DataWriteError("invalid", "…", { fields: { slug: "Pick another slug." } })`.
|
|
166
|
+
|
|
167
|
+
## Mutation modes
|
|
168
|
+
|
|
169
|
+
`useResource` (its `create`, `update`, `remove`) and `useResourceForm` take
|
|
170
|
+
`mutationMode`:
|
|
171
|
+
|
|
172
|
+
| Mode | Cache | Provider | Returned promise / `onSuccess` |
|
|
173
|
+
| --- | --- | --- | --- |
|
|
174
|
+
| `"pessimistic"` (default) | refetched after the write | at once | after the write |
|
|
175
|
+
| `"optimistic"` | patched at once; put back if the write fails | at once | at once |
|
|
176
|
+
| `"undoable"` | patched at once; put back on undo or failure | after `undoDelayMs` (default 5000) unless undone | at once |
|
|
177
|
+
|
|
178
|
+
- **Failures** in `optimistic` / `undoable` mode go to `onError` (the caller
|
|
179
|
+
was already told it worked, and the cache has been put back). Show them
|
|
180
|
+
there. `useResourceForm` also puts them on the form, if it is still open.
|
|
181
|
+
- **The undo control is yours.** Core has no UI: `onUndoable(write)` is called
|
|
182
|
+
with `{ resource, kind, id, strategy, delayMs, undo(), commit() }`.
|
|
183
|
+
genesis-ui's `ResourceForm` and `ResourcePage` show a sonner toast with an
|
|
184
|
+
Undo button.
|
|
185
|
+
- **A provider that serves `undo`** (`capabilities().undo === true` and an
|
|
186
|
+
`undo` method) gets an `undoable` write at once; `undo()` then calls
|
|
187
|
+
`provider.undo(resource, { auditId })` with the audit id the write returned
|
|
188
|
+
(`strategy: "provider"`). Without an audit id no undo is offered.
|
|
189
|
+
- **Leaving sends.** When the hook unmounts during the delay (the form closes,
|
|
190
|
+
the route changes), waiting writes are sent at once, as React Admin does.
|
|
191
|
+
Closing or reloading the tab also sends them, and holds the page with the
|
|
192
|
+
browser's prompt so the requests can finish.
|
|
193
|
+
- **What is patched:** an update merges the values into the row in every
|
|
194
|
+
cached list (`[resource, "list", …]`) and detail (`[resource, "detail", id,
|
|
195
|
+
…]`); a delete removes the row and lowers `total`. A create patches nothing
|
|
196
|
+
(its id and place in a sorted list are the server's), so `optimistic` /
|
|
197
|
+
`undoable` only change when it is sent and when the caller moves on. A
|
|
198
|
+
rollback restores only the row it patched, so concurrent writes on other
|
|
199
|
+
rows survive.
|
|
200
|
+
- **Refetching waits.** While an optimistic or undoable write on a resource is
|
|
201
|
+
pending, the hooks hold off invalidating `[resource]`; the last write to
|
|
202
|
+
settle refetches. Other code that invalidates the resource (or a window-focus
|
|
203
|
+
refetch) during the delay shows the server's rows until then.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const tasks = useResource("tasks", {
|
|
207
|
+
mutationMode: "undoable",
|
|
208
|
+
onUndoable: (write) => toast("Deleted.", { action: { label: "Undo", onClick: () => void write.undo() } }),
|
|
209
|
+
onError: (error) => toast.error(error.message),
|
|
210
|
+
});
|
|
211
|
+
await tasks.remove(id); // resolves at once; sent in 5 s unless undone
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Unsaved changes
|
|
215
|
+
|
|
216
|
+
`useResourceForm({ warnWhenUnsaved: true })` (default off) warns while the
|
|
217
|
+
form is dirty: the browser's prompt on closing or reloading the tab, and
|
|
218
|
+
`confirmLeave()` for in-app navigation. It returns `isDirty` (false again once
|
|
219
|
+
a save succeeds or is handed off) and `confirmLeave()` (true when it is fine to
|
|
220
|
+
leave; asks with `window.confirm` otherwise). For other state,
|
|
221
|
+
`useUnsavedChangesGuard({ when, message, confirm })` gives the same
|
|
222
|
+
`confirmLeave` and `beforeunload` handling. Neither depends on a router; see
|
|
223
|
+
genesis-ui's SKILL.md "Unsaved changes" for React Router and TanStack Router.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/schema/write-outcome.ts
|
|
2
|
+
var DataWriteError = class extends Error {
|
|
3
|
+
outcome;
|
|
4
|
+
current;
|
|
5
|
+
currentVersion;
|
|
6
|
+
/** The store's own error code, when it gave one. */
|
|
7
|
+
code;
|
|
8
|
+
/**
|
|
9
|
+
* Per-field messages (field name → safe message), when the store said which
|
|
10
|
+
* values it rejected. See `DataWriteErrorOptions.fields`.
|
|
11
|
+
*/
|
|
12
|
+
fields;
|
|
13
|
+
constructor(outcome, message, options = {}) {
|
|
14
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
15
|
+
this.name = "DataWriteError";
|
|
16
|
+
this.outcome = outcome;
|
|
17
|
+
this.current = options.current;
|
|
18
|
+
this.currentVersion = options.currentVersion;
|
|
19
|
+
if (options.code !== void 0) this.code = options.code;
|
|
20
|
+
if (options.fields && Object.keys(options.fields).length > 0) {
|
|
21
|
+
this.fields = Object.freeze({ ...options.fields });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function isDataWriteError(error) {
|
|
26
|
+
return error instanceof Error && error.name === "DataWriteError" && typeof error.outcome === "string";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { DataWriteError, isDataWriteError };
|
|
30
|
+
//# sourceMappingURL=chunk-5NHR6ZXS.js.map
|
|
31
|
+
//# sourceMappingURL=chunk-5NHR6ZXS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/write-outcome.ts"],"names":[],"mappings":";AAuDO,IAAM,cAAA,GAAN,cAA6B,KAAA,CAAM;AAAA,EAC/B,OAAA;AAAA,EACA,OAAA;AAAA,EACA,cAAA;AAAA;AAAA,EAEA,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA,MAAA;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;AACpD,IAAA,IAAI,OAAA,CAAQ,UAAU,MAAA,CAAO,IAAA,CAAK,QAAQ,MAAM,CAAA,CAAE,SAAS,CAAA,EAAG;AAC5D,MAAA,IAAA,CAAK,SAAS,MAAA,CAAO,MAAA,CAAO,EAAE,GAAG,OAAA,CAAQ,QAAQ,CAAA;AAAA,IACnD;AAAA,EACF;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-5NHR6ZXS.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 /**\n * Per-field messages, keyed by field (column) name, for a write the store\n * rejected because of particular values. Each message is safe to show next\n * to its input: plain words, never the store's raw text or the value.\n * `useResourceForm` puts each one on its field.\n */\n fields?: Record<string, 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 * Per-field messages (field name → safe message), when the store said which\n * values it rejected. See `DataWriteErrorOptions.fields`.\n */\n readonly fields?: Readonly<Record<string, 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 if (options.fields && Object.keys(options.fields).length > 0) {\n this.fields = Object.freeze({ ...options.fields });\n }\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"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useDataProvider } from './chunk-GGSXSI6Z.js';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
|
|
4
|
+
var providerIds = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
var nextProviderId = 1;
|
|
6
|
+
function providerId(provider) {
|
|
7
|
+
let id = providerIds.get(provider);
|
|
8
|
+
if (id === void 0) {
|
|
9
|
+
id = nextProviderId++;
|
|
10
|
+
providerIds.set(provider, id);
|
|
11
|
+
}
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
function capabilitiesQueryKey(provider, resource) {
|
|
15
|
+
return ["__capabilities", providerId(provider), resource ?? ""];
|
|
16
|
+
}
|
|
17
|
+
function useCapabilities(resource, options = {}) {
|
|
18
|
+
const provider = useDataProvider();
|
|
19
|
+
return useQuery({
|
|
20
|
+
queryKey: capabilitiesQueryKey(provider, resource),
|
|
21
|
+
queryFn: () => provider.capabilities(resource),
|
|
22
|
+
enabled: (options.enabled ?? true) && !!provider.capabilities,
|
|
23
|
+
// Fresh for as long as this provider is in use; a swapped-out provider's
|
|
24
|
+
// entry has no observers and is collected after the usual gcTime.
|
|
25
|
+
staleTime: Infinity
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { capabilitiesQueryKey, useCapabilities };
|
|
30
|
+
//# sourceMappingURL=chunk-BQOJX67X.js.map
|
|
31
|
+
//# sourceMappingURL=chunk-BQOJX67X.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-capabilities.ts"],"names":[],"mappings":";;;AAMA,IAAM,WAAA,uBAAkB,OAAA,EAAwB;AAChD,IAAI,cAAA,GAAiB,CAAA;AAErB,SAAS,WAAW,QAAA,EAA0B;AAC5C,EAAA,IAAI,EAAA,GAAK,WAAA,CAAY,GAAA,CAAI,QAAQ,CAAA;AACjC,EAAA,IAAI,OAAO,MAAA,EAAW;AACpB,IAAA,EAAA,GAAK,cAAA,EAAA;AACL,IAAA,WAAA,CAAY,GAAA,CAAI,UAAU,EAAE,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,EAAA;AACT;AAQO,SAAS,oBAAA,CAAqB,UAAkB,QAAA,EAAkD;AACvG,EAAA,OAAO,CAAC,gBAAA,EAAkB,UAAA,CAAW,QAAQ,CAAA,EAAG,YAAY,EAAE,CAAA;AAChE;AAOO,SAAS,eAAA,CACd,QAAA,EACA,OAAA,GAAiC,EAAC,EACI;AACtC,EAAA,MAAM,WAAW,eAAA,EAAgB;AACjC,EAAA,OAAO,QAAA,CAA+B;AAAA,IACpC,QAAA,EAAU,oBAAA,CAAqB,QAAA,EAAU,QAAQ,CAAA;AAAA,IACjD,OAAA,EAAS,MAAM,QAAA,CAAS,YAAA,CAAc,QAAQ,CAAA;AAAA,IAC9C,UAAU,OAAA,CAAQ,OAAA,IAAW,IAAA,KAAS,CAAC,CAAC,QAAA,CAAS,YAAA;AAAA;AAAA;AAAA,IAGjD,SAAA,EAAW;AAAA,GACZ,CAAA;AACH","file":"chunk-BQOJX67X.js","sourcesContent":["import { useQuery, type UseQueryResult } from \"@tanstack/react-query\";\nimport { useDataProvider } from \"../provider/context\";\nimport type { ProviderCapabilities } from \"../schema/types\";\n\n// A stable id per provider object, so capabilities cached for one provider\n// are never served for another that shares the same QueryClient.\nconst providerIds = new WeakMap<object, number>();\nlet nextProviderId = 1;\n\nfunction providerId(provider: object): number {\n let id = providerIds.get(provider);\n if (id === undefined) {\n id = nextProviderId++;\n providerIds.set(provider, id);\n }\n return id;\n}\n\n/**\n * The query key for `provider`'s capabilities for `resource`. It includes\n * the provider's identity: swapping the provider (another tenant, a\n * different store) asks the new one instead of reusing the old answer.\n * @internal\n */\nexport function capabilitiesQueryKey(provider: object, resource: string | undefined): readonly unknown[] {\n return [\"__capabilities\", providerId(provider), resource ?? \"\"];\n}\n\n/**\n * What the data provider can do for `resource` (`provider.capabilities`),\n * cached per provider and resource. `data` stays undefined for a provider\n * that doesn't report capabilities, so a UI keeps its defaults.\n */\nexport function useCapabilities(\n resource: string | undefined,\n options: { enabled?: boolean } = {},\n): UseQueryResult<ProviderCapabilities> {\n const provider = useDataProvider();\n return useQuery<ProviderCapabilities>({\n queryKey: capabilitiesQueryKey(provider, resource),\n queryFn: () => provider.capabilities!(resource),\n enabled: (options.enabled ?? true) && !!provider.capabilities,\n // Fresh for as long as this provider is in use; a swapped-out provider's\n // entry has no observers and is collected after the usual gcTime.\n staleTime: Infinity,\n });\n}\n"]}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { invalidateResource, useDelayedWrites, executeWrite, cachedRecord } from './chunk-QH4P5MI2.js';
|
|
2
|
+
import { relatedKeys } from './chunk-7NUL32W7.js';
|
|
3
|
+
import { useDataProvider } from './chunk-GGSXSI6Z.js';
|
|
2
4
|
import { useState, useCallback, useMemo } from 'react';
|
|
3
|
-
import { useQueryClient, useQuery, useMutation } from '@tanstack/react-query';
|
|
5
|
+
import { useQueryClient, useQuery, keepPreviousData, useMutation } from '@tanstack/react-query';
|
|
4
6
|
|
|
5
7
|
function stableFilterKey(filters) {
|
|
6
8
|
if (!filters) return void 0;
|
|
@@ -19,21 +21,28 @@ function useResource(resource, options = {}) {
|
|
|
19
21
|
defaultSort,
|
|
20
22
|
defaultFilters,
|
|
21
23
|
defaultPerPage = 25,
|
|
24
|
+
defaultPage = 1,
|
|
25
|
+
defaultSearch = "",
|
|
22
26
|
select,
|
|
23
27
|
searchFields: searchFieldsOption,
|
|
24
28
|
enabled = true,
|
|
29
|
+
invalidates,
|
|
30
|
+
keepPreviousData: keepPreviousData$1 = false,
|
|
25
31
|
onCreateSuccess,
|
|
26
32
|
onUpdateSuccess,
|
|
27
33
|
onDeleteSuccess,
|
|
28
|
-
onError
|
|
34
|
+
onError,
|
|
35
|
+
mutationMode = "pessimistic",
|
|
36
|
+
undoDelayMs,
|
|
37
|
+
onUndoable
|
|
29
38
|
} = options;
|
|
30
39
|
const provider = useDataProvider();
|
|
31
40
|
const queryClient = useQueryClient();
|
|
32
41
|
const [sort, setSortRaw] = useState(defaultSort);
|
|
33
42
|
const [filters, setFiltersRaw] = useState(defaultFilters ?? []);
|
|
34
|
-
const [page, setPage] = useState(1);
|
|
43
|
+
const [page, setPage] = useState(Math.max(1, Math.floor(defaultPage)));
|
|
35
44
|
const [perPage, setPerPageRaw] = useState(defaultPerPage);
|
|
36
|
-
const [searchQuery, setSearchQuery] = useState(
|
|
45
|
+
const [searchQuery, setSearchQuery] = useState(defaultSearch);
|
|
37
46
|
const defaultFiltersKey = stableFilterKey(defaultFilters);
|
|
38
47
|
const [syncedFiltersKey, setSyncedFiltersKey] = useState(defaultFiltersKey);
|
|
39
48
|
if (defaultFiltersKey !== syncedFiltersKey) {
|
|
@@ -76,11 +85,17 @@ function useResource(resource, options = {}) {
|
|
|
76
85
|
const list = useQuery({
|
|
77
86
|
queryKey: listQueryKey,
|
|
78
87
|
queryFn: () => provider.getList(resource, params),
|
|
79
|
-
enabled
|
|
88
|
+
enabled,
|
|
89
|
+
placeholderData: keepPreviousData$1 ? keepPreviousData : void 0
|
|
80
90
|
});
|
|
81
|
-
const
|
|
82
|
-
() =>
|
|
83
|
-
|
|
91
|
+
const invalidateAfter = useCallback(
|
|
92
|
+
(event) => {
|
|
93
|
+
void invalidateResource(queryClient, resource);
|
|
94
|
+
for (const queryKey of relatedKeys(provider, event, invalidates)) {
|
|
95
|
+
void queryClient.invalidateQueries({ queryKey: [...queryKey] });
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
[queryClient, resource, provider, invalidates]
|
|
84
99
|
);
|
|
85
100
|
const defaultOnError = useCallback(
|
|
86
101
|
(error) => {
|
|
@@ -88,37 +103,75 @@ function useResource(resource, options = {}) {
|
|
|
88
103
|
},
|
|
89
104
|
[resource]
|
|
90
105
|
);
|
|
106
|
+
const reportError = onError ?? defaultOnError;
|
|
107
|
+
const delayed = useDelayedWrites();
|
|
108
|
+
const modeArgs = {
|
|
109
|
+
queryClient,
|
|
110
|
+
provider,
|
|
111
|
+
resource,
|
|
112
|
+
mode: mutationMode,
|
|
113
|
+
undoDelayMs,
|
|
114
|
+
onUndoable,
|
|
115
|
+
delayed,
|
|
116
|
+
failedLater: reportError
|
|
117
|
+
};
|
|
91
118
|
const createMutation = useMutation({
|
|
92
119
|
mutationFn: async (data) => {
|
|
93
|
-
const
|
|
94
|
-
|
|
120
|
+
const outcome = await executeWrite({
|
|
121
|
+
...modeArgs,
|
|
122
|
+
kind: "create",
|
|
123
|
+
data,
|
|
124
|
+
send: async () => {
|
|
125
|
+
const result = await provider.create(resource, { data });
|
|
126
|
+
return { record: result.data, audit: result.audit };
|
|
127
|
+
},
|
|
128
|
+
committed: (record) => {
|
|
129
|
+
invalidateAfter({ resource, kind: "create", id: record.id, record });
|
|
130
|
+
onCreateSuccess?.(record);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return outcome.status === "committed" ? outcome.record : data;
|
|
95
134
|
},
|
|
96
|
-
|
|
97
|
-
invalidateResource();
|
|
98
|
-
onCreateSuccess?.(data);
|
|
99
|
-
},
|
|
100
|
-
onError: onError ?? defaultOnError
|
|
135
|
+
onError: reportError
|
|
101
136
|
});
|
|
102
137
|
const updateMutation = useMutation({
|
|
103
138
|
mutationFn: async ({ id, data }) => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
139
|
+
const before = cachedRecord(queryClient, resource, id);
|
|
140
|
+
const outcome = await executeWrite({
|
|
141
|
+
...modeArgs,
|
|
142
|
+
kind: "update",
|
|
143
|
+
id,
|
|
144
|
+
data,
|
|
145
|
+
send: async () => {
|
|
146
|
+
const result = await provider.update(resource, { id, data });
|
|
147
|
+
return { record: result.data, audit: result.audit };
|
|
148
|
+
},
|
|
149
|
+
committed: (record) => {
|
|
150
|
+
invalidateAfter({ resource, kind: "update", id, record });
|
|
151
|
+
onUpdateSuccess?.(record);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return outcome.status === "committed" ? outcome.record : { ...before ?? {}, ...data, id: before?.id ?? id };
|
|
110
155
|
},
|
|
111
|
-
onError:
|
|
156
|
+
onError: reportError
|
|
112
157
|
});
|
|
113
158
|
const deleteMutation = useMutation({
|
|
114
159
|
mutationFn: async (id) => {
|
|
115
|
-
await
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
await executeWrite({
|
|
161
|
+
...modeArgs,
|
|
162
|
+
kind: "delete",
|
|
163
|
+
id,
|
|
164
|
+
send: async () => {
|
|
165
|
+
const result = await provider.deleteOne(resource, { id });
|
|
166
|
+
return { record: void 0, audit: result?.audit };
|
|
167
|
+
},
|
|
168
|
+
committed: () => {
|
|
169
|
+
invalidateAfter({ resource, kind: "delete", id });
|
|
170
|
+
onDeleteSuccess?.();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
120
173
|
},
|
|
121
|
-
onError:
|
|
174
|
+
onError: reportError
|
|
122
175
|
});
|
|
123
176
|
const create = useCallback(
|
|
124
177
|
(data) => createMutation.mutateAsync(data),
|
|
@@ -152,5 +205,5 @@ function useResource(resource, options = {}) {
|
|
|
152
205
|
}
|
|
153
206
|
|
|
154
207
|
export { useResource };
|
|
155
|
-
//# sourceMappingURL=chunk-
|
|
156
|
-
//# sourceMappingURL=chunk-
|
|
208
|
+
//# sourceMappingURL=chunk-DVW5GXWZ.js.map
|
|
209
|
+
//# sourceMappingURL=chunk-DVW5GXWZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-resource.ts"],"names":["keepPreviousData","keepPreviousPlaceholder"],"mappings":"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","file":"chunk-DVW5GXWZ.js","sourcesContent":["import { useState, useCallback, useMemo } from \"react\";\nimport {\n keepPreviousData as keepPreviousPlaceholder,\n useQuery,\n useMutation,\n useQueryClient,\n type UseQueryResult,\n type UseMutationResult,\n} from \"@tanstack/react-query\";\nimport { useDataProvider } from \"../provider/context\";\nimport type {\n BaseRecord,\n Identifier,\n SortParam,\n FilterParam,\n ListParams,\n ListResult,\n InvalidationKey,\n WriteEvent,\n} from \"../provider/types\";\nimport { relatedKeys } from \"./related-keys\";\nimport {\n cachedRecord,\n executeWrite,\n invalidateResource,\n useDelayedWrites,\n type MutationModeOptions,\n} from \"./mutation-mode\";\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface UseResourceOptions<T> extends MutationModeOptions {\n defaultSort?: SortParam;\n /**\n * Re-synced whenever its value changes (deep-equal, order-independent),\n * not just its identity — safe to pass a scope/audience-derived array that\n * changes across renders. This only covers CHANGES on an already-mounted\n * hook, though: if this starts `undefined` while the real value resolves\n * asynchronously, gate `enabled` on that same readiness signal too — the\n * hook cannot tell \"no defaultFilters intended\" apart from \"not resolved\n * yet,\" so an ungated hook's very first fetch can still run unscoped.\n */\n defaultFilters?: FilterParam[];\n defaultPerPage?: number;\n /** The page to start on (from 1), e.g. restored from the URL. Default 1. */\n defaultPage?: number;\n /** The search to start with, e.g. restored from the URL. Default \"\". */\n defaultSearch?: string;\n /** Supabase select/join syntax, e.g. \"*, author:profiles(name)\". */\n select?: string;\n /** Fields to search across when setSearch is called. Required for server-side search. */\n searchFields?: string[];\n /** Disable auto-fetching. */\n enabled?: boolean;\n /**\n * Other query keys to invalidate after a create, update or delete, on top\n * of `[resource]` and the provider's own `invalidates` (e.g. `[[\"me\"]]`\n * when a write changes the signed-in user's roles).\n */\n invalidates?: InvalidationKey[];\n /**\n * While the next page / sort / search is loading, keep returning the\n * previous result (`list.isPlaceholderData` is true meanwhile) instead of\n * dropping to `undefined`. A paged list then stays on screen between pages\n * rather than flashing a skeleton. Default false.\n */\n keepPreviousData?: boolean;\n /**\n * After the write commits, with the provider's row. In `optimistic` and\n * `undoable` modes that is after `create` / `update` / `remove` have\n * already resolved.\n */\n onCreateSuccess?: (data: T) => void;\n onUpdateSuccess?: (data: T) => void;\n onDeleteSuccess?: () => void;\n /**\n * A write failed. In `optimistic` and `undoable` modes this is the only\n * place a failure is reported (the cache has already been put back), so\n * show it to the user here.\n */\n onError?: (error: Error) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Return type\n// ---------------------------------------------------------------------------\n\nexport interface UseResourceReturn<T> {\n /** TanStack Query result for the list query. */\n list: UseQueryResult<ListResult<T>>;\n /** Current list params (pagination, sort, filters, search). */\n params: ListParams;\n setSort: (sort: SortParam | undefined) => void;\n setFilters: (filters: FilterParam[]) => void;\n setPage: (page: number) => void;\n setPerPage: (perPage: number) => void;\n setSearch: (query: string) => void;\n /**\n * Create a new record. Returns the created item. In `optimistic` and\n * `undoable` modes it resolves at once with the values given (no `id` yet;\n * `onCreateSuccess` gets the stored row).\n */\n create: (data: Partial<T>) => Promise<T>;\n /**\n * Update an existing record by id. Returns the updated item. In\n * `optimistic` and `undoable` modes it resolves at once with the cached\n * row merged with `data`.\n */\n update: (id: Identifier, data: Partial<T>) => Promise<T>;\n /** Delete a record by id. Resolves at once in `optimistic` / `undoable` modes. */\n remove: (id: Identifier) => Promise<void>;\n /** True when any mutation is in-flight. */\n isMutating: boolean;\n createMutation: UseMutationResult<T, Error, Partial<T>>;\n updateMutation: UseMutationResult<T, Error, { id: Identifier; data: Partial<T> }>;\n deleteMutation: UseMutationResult<void, Error, Identifier>;\n}\n\n// ---------------------------------------------------------------------------\n// Filter identity — a deep-equality key independent of object-key order, so\n// a caller that rebuilds the same logical filters via a differently-ordered\n// spread (e.g. `{...base, operator: \"eq\"}` vs `{operator: \"eq\", ...base}`)\n// doesn't trip a spurious resync + pagination reset on an unrelated rerender.\n// ---------------------------------------------------------------------------\n\nfunction stableFilterKey(filters: FilterParam[] | undefined): string | undefined {\n if (!filters) return undefined;\n return JSON.stringify(\n filters.map((f) => {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(f).sort()) {\n sorted[key] = (f as unknown as Record<string, unknown>)[key];\n }\n return sorted;\n }),\n );\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useResource<T extends BaseRecord = BaseRecord>(\n resource: string,\n options: UseResourceOptions<T> = {},\n): UseResourceReturn<T> {\n const {\n defaultSort,\n defaultFilters,\n defaultPerPage = 25,\n defaultPage = 1,\n defaultSearch = \"\",\n select,\n searchFields: searchFieldsOption,\n enabled = true,\n invalidates,\n keepPreviousData = false,\n onCreateSuccess,\n onUpdateSuccess,\n onDeleteSuccess,\n onError,\n mutationMode = \"pessimistic\",\n undoDelayMs,\n onUndoable,\n } = options;\n\n const provider = useDataProvider();\n const queryClient = useQueryClient();\n\n // -------------------------------------------------------------------------\n // State\n // -------------------------------------------------------------------------\n\n const [sort, setSortRaw] = useState<SortParam | undefined>(defaultSort);\n const [filters, setFiltersRaw] = useState<FilterParam[]>(defaultFilters ?? []);\n const [page, setPage] = useState(Math.max(1, Math.floor(defaultPage)));\n const [perPage, setPerPageRaw] = useState(defaultPerPage);\n const [searchQuery, setSearchQuery] = useState(defaultSearch);\n\n // `defaultFilters` often resolves asynchronously — a caller's scope/audience\n // (team, project, ...) can still be loading on first render, so the array\n // it passes in changes value (not just identity) after mount, and may later\n // clear back to undefined (e.g. the scope itself is cleared). Re-syncing\n // here, during render rather than in an effect, means `filters` is already\n // correct by the time `params`/`list` below compute for this pass — no\n // stray unscoped fetch on the frame before an effect would have caught up.\n //\n // This only closes the gap for `defaultFilters` CHANGING across renders on\n // an already-mounted, already-enabled hook (e.g. a scope switch). It cannot\n // help the very first render: `filters` still seeds from whatever\n // `defaultFilters` is on mount, and `undefined` here is genuinely ambiguous\n // between \"this caller never uses defaultFilters\" (in which case forcing a\n // fetch to wait would break it forever) and \"the real value hasn't resolved\n // yet.\" A caller in the second case MUST also gate `enabled` on that same\n // readiness signal for its very first fetch to be correctly scoped — this\n // hook has no way to tell the two cases apart on its own.\n const defaultFiltersKey = stableFilterKey(defaultFilters);\n const [syncedFiltersKey, setSyncedFiltersKey] = useState(defaultFiltersKey);\n if (defaultFiltersKey !== syncedFiltersKey) {\n setSyncedFiltersKey(defaultFiltersKey);\n setFiltersRaw(defaultFilters ?? []);\n setPage(1);\n }\n\n const setSort = useCallback((s: SortParam | undefined) => {\n setSortRaw(s);\n setPage(1);\n }, []);\n\n const setFilters = useCallback((f: FilterParam[]) => {\n setFiltersRaw(f);\n setPage(1);\n }, []);\n\n const setPerPage = useCallback((value: number) => {\n setPerPageRaw(value);\n setPage(1);\n }, []);\n\n const setSearch = useCallback((query: string) => {\n setSearchQuery(query);\n setPage(1);\n }, []);\n\n // -------------------------------------------------------------------------\n // Params\n // -------------------------------------------------------------------------\n\n const params = useMemo<ListParams>(() => {\n const p: ListParams = {\n pagination: { page, perPage },\n sort,\n filters: filters.length > 0 ? filters : undefined,\n select,\n };\n if (searchQuery && searchFieldsOption && searchFieldsOption.length > 0) {\n p.search = { query: searchQuery, fields: searchFieldsOption };\n }\n return p;\n }, [page, perPage, sort, filters, select, searchQuery, searchFieldsOption]);\n\n // -------------------------------------------------------------------------\n // List query\n // -------------------------------------------------------------------------\n\n const listQueryKey = useMemo(\n () => [resource, \"list\", params] as const,\n [resource, params],\n );\n\n const list = useQuery<ListResult<T>>({\n queryKey: listQueryKey,\n queryFn: () => provider.getList<T>(resource, params),\n enabled,\n placeholderData: keepPreviousData ? keepPreviousPlaceholder : undefined,\n });\n\n // -------------------------------------------------------------------------\n // Mutations\n // -------------------------------------------------------------------------\n\n const invalidateAfter = useCallback(\n (event: WriteEvent) => {\n // Waits while an optimistic/undoable write on the resource is pending.\n void invalidateResource(queryClient, resource);\n for (const queryKey of relatedKeys(provider, event, invalidates)) {\n void queryClient.invalidateQueries({ queryKey: [...queryKey] });\n }\n },\n [queryClient, resource, provider, invalidates],\n );\n\n const defaultOnError = useCallback(\n (error: Error) => {\n console.error(`[useResource] ${resource} mutation failed:`, error.message);\n },\n [resource],\n );\n const reportError = onError ?? defaultOnError;\n\n const delayed = useDelayedWrites();\n const modeArgs = {\n queryClient,\n provider,\n resource,\n mode: mutationMode,\n undoDelayMs,\n onUndoable,\n delayed,\n failedLater: reportError,\n };\n\n const createMutation = useMutation<T, Error, Partial<T>>({\n mutationFn: async (data) => {\n const outcome = await executeWrite<T>({\n ...modeArgs,\n kind: \"create\",\n data: data as Record<string, unknown>,\n send: async () => {\n const result = await provider.create<T>(resource, { data });\n return { record: result.data, audit: result.audit };\n },\n committed: (record) => {\n invalidateAfter({ resource, kind: \"create\", id: record.id, record });\n onCreateSuccess?.(record);\n },\n });\n return outcome.status === \"committed\" ? outcome.record : (data as T);\n },\n onError: reportError,\n });\n\n const updateMutation = useMutation<\n T,\n Error,\n { id: Identifier; data: Partial<T> }\n >({\n mutationFn: async ({ id, data }) => {\n const before = cachedRecord(queryClient, resource, id);\n const outcome = await executeWrite<T>({\n ...modeArgs,\n kind: \"update\",\n id,\n data: data as Record<string, unknown>,\n send: async () => {\n const result = await provider.update<T>(resource, { id, data });\n return { record: result.data, audit: result.audit };\n },\n committed: (record) => {\n invalidateAfter({ resource, kind: \"update\", id, record });\n onUpdateSuccess?.(record);\n },\n });\n return outcome.status === \"committed\"\n ? outcome.record\n : ({ ...(before ?? {}), ...data, id: before?.id ?? id } as T);\n },\n onError: reportError,\n });\n\n const deleteMutation = useMutation<void, Error, Identifier>({\n mutationFn: async (id) => {\n await executeWrite<void>({\n ...modeArgs,\n kind: \"delete\",\n id,\n send: async () => {\n const result = await provider.deleteOne(resource, { id });\n return { record: undefined, audit: result?.audit };\n },\n committed: () => {\n invalidateAfter({ resource, kind: \"delete\", id });\n onDeleteSuccess?.();\n },\n });\n },\n onError: reportError,\n });\n\n // -------------------------------------------------------------------------\n // Convenience wrappers\n // -------------------------------------------------------------------------\n\n const create = useCallback(\n (data: Partial<T>) => createMutation.mutateAsync(data),\n [createMutation],\n );\n\n const update = useCallback(\n (id: Identifier, data: Partial<T>) =>\n updateMutation.mutateAsync({ id, data }),\n [updateMutation],\n );\n\n const remove = useCallback(\n (id: Identifier) => deleteMutation.mutateAsync(id),\n [deleteMutation],\n );\n\n const isMutating =\n createMutation.isPending ||\n updateMutation.isPending ||\n deleteMutation.isPending;\n\n return {\n list,\n params,\n setSort,\n setFilters,\n setPage,\n setPerPage,\n setSearch,\n create,\n update,\n remove,\n isMutating,\n createMutation,\n updateMutation,\n deleteMutation,\n };\n}\n"]}
|
|
@@ -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 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-HT43MPKW.js"}
|