@trylight/app 0.1.7 → 0.1.9
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 +12 -4
- package/dist/client.d.ts +514 -16
- package/dist/client.js +243 -23
- package/dist/entities.d.ts +3 -1
- package/dist/entities.js +1 -1
- package/dist/server/entities.js +30 -8
- package/package.json +10 -12
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ unchanged; entities and the schema sync are new here.
|
|
|
8
8
|
| Import | What |
|
|
9
9
|
|---|---|
|
|
10
10
|
| `@trylight/app/schema` | `entity(name, columns, access)`: a Drizzle table plus `id`, `created_at`, `updated_at`, `created_by`, `field_sources`; `lightChanges` (the audit table); `lightImages` (the app's copy of its logos and photos); `fieldSource` (the `{url, evidence}` schema) |
|
|
11
|
-
| `@trylight/app` | `createApp<typeof contracts>()` → `app.table(entity)` (`list/get/create/update/delete`, `useList/useGet/useCreate/useUpdate/useDelete`, typed by the entity passed; `useList(...).rows` is never undefined), `app.contracts` (`useInvocation`), and `imageSrc(url)` (where a logo or photo loads from) |
|
|
11
|
+
| `@trylight/app` | `createApp<typeof contracts>()` → `app.table(entity)` (`list/get/create/update/delete`, `useList/useInfiniteList/useGet/useCreate/useUpdate/useDelete`, typed by the entity passed; `useList(...).rows` is never undefined), `app.contracts` (`useInvocation`), and `imageSrc(url)` (where a logo or photo loads from) |
|
|
12
12
|
| `@trylight/app/server` | `createLightServer({ schema, contracts })` → `light.db` (Drizzle), `light.session(request)`, `light.signIn(email, password)`, and the route handlers `light.entities`, `light.image`, `light.invoke.handler`, `light.chat` |
|
|
13
13
|
| `@trylight/app/platform` | the door for server functions, framing, analytics/error policies, the pane bridge |
|
|
14
14
|
| `@trylight/app/vite` | `lightDb()`: the dev server syncs `db/schema` into the database on every change |
|
|
@@ -27,9 +27,17 @@ edited value loses its source; sources sent are merged by column. Lists sort
|
|
|
27
27
|
by the asked column, then id. With no sign-in configured only a non-production
|
|
28
28
|
server asked on loopback answers (as "Local developer"); anything else gets 503.
|
|
29
29
|
|
|
30
|
-
Pages read and write through TanStack Query hooks
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
Pages read and write through TanStack Query hooks. `useUpdate` and
|
|
31
|
+
`useDelete` change every query under the table's key at once (`useList`,
|
|
32
|
+
`useInfiniteList`, `useGet`, and a page's own query keyed under it) and put
|
|
33
|
+
them back the moment a save fails; a saved write stays pending until the
|
|
34
|
+
table's refetch lands (no snap-back). `useInfiniteList` pages by `offset`
|
|
35
|
+
(the next page when one comes back full), keeps the last options' rows while
|
|
36
|
+
new ones load (`isSwitching`), sorts on the server, and prefetches the
|
|
37
|
+
sibling views it is given once it settles. A write refetches every page a
|
|
38
|
+
list has loaded, one after another, before its mutation settles; the
|
|
39
|
+
change itself shows at once. `search` matches words in any
|
|
40
|
+
text column, case-insensitively, on the server.
|
|
33
41
|
|
|
34
42
|
Joins and aggregates stay typed server functions over `light.db`.
|
|
35
43
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type QueryKey } from "@tanstack/react-query";
|
|
1
2
|
import type { Contracts } from "@shubh90/app-runtime/invoke/server";
|
|
2
3
|
import { EntityError, type ListOptions, type NewRow, type Patch, type Row } from "./entities.js";
|
|
3
4
|
import type { Entity } from "./schema.js";
|
|
@@ -10,6 +11,58 @@ export declare function createApp<C extends Contracts = Record<never, never>>():
|
|
|
10
11
|
table: typeof tableClient;
|
|
11
12
|
contracts: import("@shubh90/app-runtime/invoke").Invoker<C>;
|
|
12
13
|
};
|
|
14
|
+
/** Rows one page of `useInfiniteList` asks for; scrolling near the end asks for the next. */
|
|
15
|
+
export declare const PAGE_SIZE = 100;
|
|
16
|
+
/** A column, and which way: what a table header sorts by. */
|
|
17
|
+
export type Sorting = {
|
|
18
|
+
column: string;
|
|
19
|
+
desc: boolean;
|
|
20
|
+
};
|
|
21
|
+
/** What `useInfiniteList` takes: a list's options, less the paging it does itself. */
|
|
22
|
+
export type PageOptions<R> = Omit<ListOptions<R>, "limit" | "offset"> & {
|
|
23
|
+
/** Rows per page; PAGE_SIZE unless a page needs fewer or more. */
|
|
24
|
+
pageSize?: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A list as a table draws it (DataTable's `list`): its rows, whether more
|
|
28
|
+
* exist, and how to get them. `useList` and `useInfiniteList` both give one.
|
|
29
|
+
*/
|
|
30
|
+
export type ListView<R> = {
|
|
31
|
+
rows: R[];
|
|
32
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
33
|
+
isPending: boolean;
|
|
34
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
35
|
+
loadError: Error | null;
|
|
36
|
+
refetch: () => unknown;
|
|
37
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
38
|
+
hasMore: boolean;
|
|
39
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
40
|
+
loadMore: (() => void) | null;
|
|
41
|
+
isLoadingMore: boolean;
|
|
42
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
43
|
+
moreFailed: boolean;
|
|
44
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
45
|
+
isSwitching: boolean;
|
|
46
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
47
|
+
serverSort: {
|
|
48
|
+
sorting: Sorting | null;
|
|
49
|
+
setSorting: (next: Sorting | null) => void;
|
|
50
|
+
canSort: (column: string) => boolean;
|
|
51
|
+
} | null;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Where one row sat in a cached query before a write: its value, page and
|
|
55
|
+
* place, to put it back if the write fails. `fields` are the ones the write
|
|
56
|
+
* changed (an update's patch), so a failure takes back only those and never
|
|
57
|
+
* another save's change to the same row; null (a delete) is the whole row.
|
|
58
|
+
*/
|
|
59
|
+
type Before = {
|
|
60
|
+
key: QueryKey;
|
|
61
|
+
page: number;
|
|
62
|
+
index: number;
|
|
63
|
+
row: unknown;
|
|
64
|
+
fields: string[] | null;
|
|
65
|
+
};
|
|
13
66
|
/** One entity table's rows: plain calls, and TanStack Query hooks over them. */
|
|
14
67
|
declare function tableClient<T extends Entity>(entity: T): {
|
|
15
68
|
/**
|
|
@@ -31,10 +84,29 @@ declare function tableClient<T extends Entity>(entity: T): {
|
|
|
31
84
|
*/
|
|
32
85
|
useList: (options?: ListOptions<Row<T>>) => {
|
|
33
86
|
rows: (T["$inferSelect"] extends infer T_1 ? { [K in keyof T_1]: Date extends T_1[K] ? string | Exclude<T_1[K], T_1[K] & Date> : T_1[K]; } : never)[];
|
|
87
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
88
|
+
isPending: boolean;
|
|
89
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
90
|
+
loadError: Error | null;
|
|
91
|
+
refetch: () => unknown;
|
|
92
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
93
|
+
hasMore: boolean;
|
|
94
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
95
|
+
loadMore: (() => void) | null;
|
|
96
|
+
isLoadingMore: boolean;
|
|
97
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
98
|
+
moreFailed: boolean;
|
|
99
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
100
|
+
isSwitching: boolean;
|
|
101
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
102
|
+
serverSort: {
|
|
103
|
+
sorting: Sorting | null;
|
|
104
|
+
setSorting: (next: Sorting | null) => void;
|
|
105
|
+
canSort: (column: string) => boolean;
|
|
106
|
+
} | null;
|
|
34
107
|
data: (T["$inferSelect"] extends infer T_2 ? { [K in keyof T_2]: Date extends T_2[K] ? string | Exclude<T_2[K], T_2[K] & Date> : T_2[K]; } : never)[];
|
|
35
108
|
error: EntityError;
|
|
36
109
|
isError: true;
|
|
37
|
-
isPending: false;
|
|
38
110
|
isLoading: false;
|
|
39
111
|
isLoadingError: false;
|
|
40
112
|
isRefetchError: true;
|
|
@@ -54,14 +126,32 @@ declare function tableClient<T extends Entity>(entity: T): {
|
|
|
54
126
|
isRefetching: boolean;
|
|
55
127
|
isStale: boolean;
|
|
56
128
|
isEnabled: boolean;
|
|
57
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<(T["$inferSelect"] extends infer T_3 ? { [K in keyof T_3]: Date extends T_3[K] ? string | Exclude<T_3[K], T_3[K] & Date> : T_3[K]; } : never)[], EntityError>>;
|
|
58
129
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
59
130
|
} | {
|
|
60
131
|
rows: (T["$inferSelect"] extends infer T_3 ? { [K in keyof T_3]: Date extends T_3[K] ? string | Exclude<T_3[K], T_3[K] & Date> : T_3[K]; } : never)[];
|
|
132
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
133
|
+
isPending: boolean;
|
|
134
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
135
|
+
loadError: Error | null;
|
|
136
|
+
refetch: () => unknown;
|
|
137
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
138
|
+
hasMore: boolean;
|
|
139
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
140
|
+
loadMore: (() => void) | null;
|
|
141
|
+
isLoadingMore: boolean;
|
|
142
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
143
|
+
moreFailed: boolean;
|
|
144
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
145
|
+
isSwitching: boolean;
|
|
146
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
147
|
+
serverSort: {
|
|
148
|
+
sorting: Sorting | null;
|
|
149
|
+
setSorting: (next: Sorting | null) => void;
|
|
150
|
+
canSort: (column: string) => boolean;
|
|
151
|
+
} | null;
|
|
61
152
|
data: (T["$inferSelect"] extends infer T_4 ? { [K in keyof T_4]: Date extends T_4[K] ? string | Exclude<T_4[K], T_4[K] & Date> : T_4[K]; } : never)[];
|
|
62
153
|
error: null;
|
|
63
154
|
isError: false;
|
|
64
|
-
isPending: false;
|
|
65
155
|
isLoading: false;
|
|
66
156
|
isLoadingError: false;
|
|
67
157
|
isRefetchError: false;
|
|
@@ -81,14 +171,32 @@ declare function tableClient<T extends Entity>(entity: T): {
|
|
|
81
171
|
isRefetching: boolean;
|
|
82
172
|
isStale: boolean;
|
|
83
173
|
isEnabled: boolean;
|
|
84
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<(T["$inferSelect"] extends infer T_5 ? { [K in keyof T_5]: Date extends T_5[K] ? string | Exclude<T_5[K], T_5[K] & Date> : T_5[K]; } : never)[], EntityError>>;
|
|
85
174
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
86
175
|
} | {
|
|
87
176
|
rows: (T["$inferSelect"] extends infer T_5 ? { [K in keyof T_5]: Date extends T_5[K] ? string | Exclude<T_5[K], T_5[K] & Date> : T_5[K]; } : never)[];
|
|
177
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
178
|
+
isPending: boolean;
|
|
179
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
180
|
+
loadError: Error | null;
|
|
181
|
+
refetch: () => unknown;
|
|
182
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
183
|
+
hasMore: boolean;
|
|
184
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
185
|
+
loadMore: (() => void) | null;
|
|
186
|
+
isLoadingMore: boolean;
|
|
187
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
188
|
+
moreFailed: boolean;
|
|
189
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
190
|
+
isSwitching: boolean;
|
|
191
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
192
|
+
serverSort: {
|
|
193
|
+
sorting: Sorting | null;
|
|
194
|
+
setSorting: (next: Sorting | null) => void;
|
|
195
|
+
canSort: (column: string) => boolean;
|
|
196
|
+
} | null;
|
|
88
197
|
data: undefined;
|
|
89
198
|
error: EntityError;
|
|
90
199
|
isError: true;
|
|
91
|
-
isPending: false;
|
|
92
200
|
isLoading: false;
|
|
93
201
|
isLoadingError: true;
|
|
94
202
|
isRefetchError: false;
|
|
@@ -108,14 +216,32 @@ declare function tableClient<T extends Entity>(entity: T): {
|
|
|
108
216
|
isRefetching: boolean;
|
|
109
217
|
isStale: boolean;
|
|
110
218
|
isEnabled: boolean;
|
|
111
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<(T["$inferSelect"] extends infer T_6 ? { [K in keyof T_6]: Date extends T_6[K] ? string | Exclude<T_6[K], T_6[K] & Date> : T_6[K]; } : never)[], EntityError>>;
|
|
112
219
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
113
220
|
} | {
|
|
114
221
|
rows: (T["$inferSelect"] extends infer T_6 ? { [K in keyof T_6]: Date extends T_6[K] ? string | Exclude<T_6[K], T_6[K] & Date> : T_6[K]; } : never)[];
|
|
222
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
223
|
+
isPending: boolean;
|
|
224
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
225
|
+
loadError: Error | null;
|
|
226
|
+
refetch: () => unknown;
|
|
227
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
228
|
+
hasMore: boolean;
|
|
229
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
230
|
+
loadMore: (() => void) | null;
|
|
231
|
+
isLoadingMore: boolean;
|
|
232
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
233
|
+
moreFailed: boolean;
|
|
234
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
235
|
+
isSwitching: boolean;
|
|
236
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
237
|
+
serverSort: {
|
|
238
|
+
sorting: Sorting | null;
|
|
239
|
+
setSorting: (next: Sorting | null) => void;
|
|
240
|
+
canSort: (column: string) => boolean;
|
|
241
|
+
} | null;
|
|
115
242
|
data: undefined;
|
|
116
243
|
error: null;
|
|
117
244
|
isError: false;
|
|
118
|
-
isPending: true;
|
|
119
245
|
isLoading: true;
|
|
120
246
|
isLoadingError: false;
|
|
121
247
|
isRefetchError: false;
|
|
@@ -135,14 +261,32 @@ declare function tableClient<T extends Entity>(entity: T): {
|
|
|
135
261
|
isRefetching: boolean;
|
|
136
262
|
isStale: boolean;
|
|
137
263
|
isEnabled: boolean;
|
|
138
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<(T["$inferSelect"] extends infer T_7 ? { [K in keyof T_7]: Date extends T_7[K] ? string | Exclude<T_7[K], T_7[K] & Date> : T_7[K]; } : never)[], EntityError>>;
|
|
139
264
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
140
265
|
} | {
|
|
141
266
|
rows: (T["$inferSelect"] extends infer T_7 ? { [K in keyof T_7]: Date extends T_7[K] ? string | Exclude<T_7[K], T_7[K] & Date> : T_7[K]; } : never)[];
|
|
267
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
268
|
+
isPending: boolean;
|
|
269
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
270
|
+
loadError: Error | null;
|
|
271
|
+
refetch: () => unknown;
|
|
272
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
273
|
+
hasMore: boolean;
|
|
274
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
275
|
+
loadMore: (() => void) | null;
|
|
276
|
+
isLoadingMore: boolean;
|
|
277
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
278
|
+
moreFailed: boolean;
|
|
279
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
280
|
+
isSwitching: boolean;
|
|
281
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
282
|
+
serverSort: {
|
|
283
|
+
sorting: Sorting | null;
|
|
284
|
+
setSorting: (next: Sorting | null) => void;
|
|
285
|
+
canSort: (column: string) => boolean;
|
|
286
|
+
} | null;
|
|
142
287
|
data: undefined;
|
|
143
288
|
error: null;
|
|
144
289
|
isError: false;
|
|
145
|
-
isPending: true;
|
|
146
290
|
isLoadingError: false;
|
|
147
291
|
isRefetchError: false;
|
|
148
292
|
isSuccess: false;
|
|
@@ -162,14 +306,32 @@ declare function tableClient<T extends Entity>(entity: T): {
|
|
|
162
306
|
isRefetching: boolean;
|
|
163
307
|
isStale: boolean;
|
|
164
308
|
isEnabled: boolean;
|
|
165
|
-
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<(T["$inferSelect"] extends infer T_8 ? { [K in keyof T_8]: Date extends T_8[K] ? string | Exclude<T_8[K], T_8[K] & Date> : T_8[K]; } : never)[], EntityError>>;
|
|
166
309
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
167
310
|
} | {
|
|
168
311
|
rows: (T["$inferSelect"] extends infer T_8 ? { [K in keyof T_8]: Date extends T_8[K] ? string | Exclude<T_8[K], T_8[K] & Date> : T_8[K]; } : never)[];
|
|
312
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
313
|
+
isPending: boolean;
|
|
314
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
315
|
+
loadError: Error | null;
|
|
316
|
+
refetch: () => unknown;
|
|
317
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
318
|
+
hasMore: boolean;
|
|
319
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
320
|
+
loadMore: (() => void) | null;
|
|
321
|
+
isLoadingMore: boolean;
|
|
322
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
323
|
+
moreFailed: boolean;
|
|
324
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
325
|
+
isSwitching: boolean;
|
|
326
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
327
|
+
serverSort: {
|
|
328
|
+
sorting: Sorting | null;
|
|
329
|
+
setSorting: (next: Sorting | null) => void;
|
|
330
|
+
canSort: (column: string) => boolean;
|
|
331
|
+
} | null;
|
|
169
332
|
data: (T["$inferSelect"] extends infer T_9 ? { [K in keyof T_9]: Date extends T_9[K] ? string | Exclude<T_9[K], T_9[K] & Date> : T_9[K]; } : never)[];
|
|
170
333
|
isError: false;
|
|
171
334
|
error: null;
|
|
172
|
-
isPending: false;
|
|
173
335
|
isLoading: false;
|
|
174
336
|
isLoadingError: false;
|
|
175
337
|
isRefetchError: false;
|
|
@@ -189,19 +351,355 @@ declare function tableClient<T extends Entity>(entity: T): {
|
|
|
189
351
|
isRefetching: boolean;
|
|
190
352
|
isStale: boolean;
|
|
191
353
|
isEnabled: boolean;
|
|
192
|
-
|
|
354
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* A list of any size, a page at a time: `loadMore` (DataTable calls it as
|
|
358
|
+
* the end scrolls into view) fetches the next. New options (a tab, a
|
|
359
|
+
* filter, a search) keep the last rows on screen with `isSwitching` until
|
|
360
|
+
* theirs land, so a click never blanks the table. `prefetch` names the
|
|
361
|
+
* sibling views (the other tabs): their first pages load once this one
|
|
362
|
+
* settles, so switching to one is instant. Sorting is the server's, over
|
|
363
|
+
* every row; `sort` is where it starts.
|
|
364
|
+
*
|
|
365
|
+
* const list = app.table(leads).useInfiniteList({ where: { status: tab } }, { prefetch: TABS.map((t) => ({ where: { status: t } })) });
|
|
366
|
+
* <DataTable list={list} … />
|
|
367
|
+
*/
|
|
368
|
+
useInfiniteList: (options?: PageOptions<Row<T>>, { prefetch }?: {
|
|
369
|
+
prefetch?: PageOptions<Row<T>>[];
|
|
370
|
+
}) => {
|
|
371
|
+
rows: (T["$inferSelect"] extends infer T_1 ? { [K in keyof T_1]: Date extends T_1[K] ? string | Exclude<T_1[K], T_1[K] & Date> : T_1[K]; } : never)[];
|
|
372
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
373
|
+
isPending: boolean;
|
|
374
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
375
|
+
loadError: Error | null;
|
|
376
|
+
refetch: () => unknown;
|
|
377
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
378
|
+
hasMore: boolean;
|
|
379
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
380
|
+
loadMore: (() => void) | null;
|
|
381
|
+
isLoadingMore: boolean;
|
|
382
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
383
|
+
moreFailed: boolean;
|
|
384
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
385
|
+
isSwitching: boolean;
|
|
386
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
387
|
+
serverSort: {
|
|
388
|
+
sorting: Sorting | null;
|
|
389
|
+
setSorting: (next: Sorting | null) => void;
|
|
390
|
+
canSort: (column: string) => boolean;
|
|
391
|
+
} | null;
|
|
392
|
+
data: import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_2 ? { [K in keyof T_2]: Date extends T_2[K] ? string | Exclude<T_2[K], T_2[K] & Date> : T_2[K]; } : never)[], unknown>;
|
|
393
|
+
error: Error;
|
|
394
|
+
isError: true;
|
|
395
|
+
isLoading: false;
|
|
396
|
+
isLoadingError: false;
|
|
397
|
+
isRefetchError: true;
|
|
398
|
+
isSuccess: false;
|
|
399
|
+
isPlaceholderData: false;
|
|
400
|
+
status: "error";
|
|
401
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_3 ? { [K in keyof T_3]: Date extends T_3[K] ? string | Exclude<T_3[K], T_3[K] & Date> : T_3[K]; } : never)[], unknown>, Error>>;
|
|
402
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_3 ? { [K in keyof T_3]: Date extends T_3[K] ? string | Exclude<T_3[K], T_3[K] & Date> : T_3[K]; } : never)[], unknown>, Error>>;
|
|
403
|
+
hasNextPage: boolean;
|
|
404
|
+
hasPreviousPage: boolean;
|
|
405
|
+
isFetchNextPageError: boolean;
|
|
406
|
+
isFetchingNextPage: boolean;
|
|
407
|
+
isFetchPreviousPageError: boolean;
|
|
408
|
+
isFetchingPreviousPage: boolean;
|
|
409
|
+
dataUpdatedAt: number;
|
|
410
|
+
errorUpdatedAt: number;
|
|
411
|
+
failureCount: number;
|
|
412
|
+
failureReason: Error | null;
|
|
413
|
+
errorUpdateCount: number;
|
|
414
|
+
isFetched: boolean;
|
|
415
|
+
isFetchedAfterMount: boolean;
|
|
416
|
+
isFetching: boolean;
|
|
417
|
+
isInitialLoading: boolean;
|
|
418
|
+
isPaused: boolean;
|
|
419
|
+
isRefetching: boolean;
|
|
420
|
+
isStale: boolean;
|
|
421
|
+
isEnabled: boolean;
|
|
422
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
423
|
+
} | {
|
|
424
|
+
rows: (T["$inferSelect"] extends infer T_3 ? { [K in keyof T_3]: Date extends T_3[K] ? string | Exclude<T_3[K], T_3[K] & Date> : T_3[K]; } : never)[];
|
|
425
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
426
|
+
isPending: boolean;
|
|
427
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
428
|
+
loadError: Error | null;
|
|
429
|
+
refetch: () => unknown;
|
|
430
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
431
|
+
hasMore: boolean;
|
|
432
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
433
|
+
loadMore: (() => void) | null;
|
|
434
|
+
isLoadingMore: boolean;
|
|
435
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
436
|
+
moreFailed: boolean;
|
|
437
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
438
|
+
isSwitching: boolean;
|
|
439
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
440
|
+
serverSort: {
|
|
441
|
+
sorting: Sorting | null;
|
|
442
|
+
setSorting: (next: Sorting | null) => void;
|
|
443
|
+
canSort: (column: string) => boolean;
|
|
444
|
+
} | null;
|
|
445
|
+
data: import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_4 ? { [K in keyof T_4]: Date extends T_4[K] ? string | Exclude<T_4[K], T_4[K] & Date> : T_4[K]; } : never)[], unknown>;
|
|
446
|
+
error: null;
|
|
447
|
+
isError: false;
|
|
448
|
+
isLoading: false;
|
|
449
|
+
isLoadingError: false;
|
|
450
|
+
isRefetchError: false;
|
|
451
|
+
isFetchNextPageError: false;
|
|
452
|
+
isFetchPreviousPageError: false;
|
|
453
|
+
isSuccess: true;
|
|
454
|
+
isPlaceholderData: false;
|
|
455
|
+
status: "success";
|
|
456
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_5 ? { [K in keyof T_5]: Date extends T_5[K] ? string | Exclude<T_5[K], T_5[K] & Date> : T_5[K]; } : never)[], unknown>, Error>>;
|
|
457
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_5 ? { [K in keyof T_5]: Date extends T_5[K] ? string | Exclude<T_5[K], T_5[K] & Date> : T_5[K]; } : never)[], unknown>, Error>>;
|
|
458
|
+
hasNextPage: boolean;
|
|
459
|
+
hasPreviousPage: boolean;
|
|
460
|
+
isFetchingNextPage: boolean;
|
|
461
|
+
isFetchingPreviousPage: boolean;
|
|
462
|
+
dataUpdatedAt: number;
|
|
463
|
+
errorUpdatedAt: number;
|
|
464
|
+
failureCount: number;
|
|
465
|
+
failureReason: Error | null;
|
|
466
|
+
errorUpdateCount: number;
|
|
467
|
+
isFetched: boolean;
|
|
468
|
+
isFetchedAfterMount: boolean;
|
|
469
|
+
isFetching: boolean;
|
|
470
|
+
isInitialLoading: boolean;
|
|
471
|
+
isPaused: boolean;
|
|
472
|
+
isRefetching: boolean;
|
|
473
|
+
isStale: boolean;
|
|
474
|
+
isEnabled: boolean;
|
|
475
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
476
|
+
} | {
|
|
477
|
+
rows: (T["$inferSelect"] extends infer T_5 ? { [K in keyof T_5]: Date extends T_5[K] ? string | Exclude<T_5[K], T_5[K] & Date> : T_5[K]; } : never)[];
|
|
478
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
479
|
+
isPending: boolean;
|
|
480
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
481
|
+
loadError: Error | null;
|
|
482
|
+
refetch: () => unknown;
|
|
483
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
484
|
+
hasMore: boolean;
|
|
485
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
486
|
+
loadMore: (() => void) | null;
|
|
487
|
+
isLoadingMore: boolean;
|
|
488
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
489
|
+
moreFailed: boolean;
|
|
490
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
491
|
+
isSwitching: boolean;
|
|
492
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
493
|
+
serverSort: {
|
|
494
|
+
sorting: Sorting | null;
|
|
495
|
+
setSorting: (next: Sorting | null) => void;
|
|
496
|
+
canSort: (column: string) => boolean;
|
|
497
|
+
} | null;
|
|
498
|
+
data: undefined;
|
|
499
|
+
error: Error;
|
|
500
|
+
isError: true;
|
|
501
|
+
isLoading: false;
|
|
502
|
+
isLoadingError: true;
|
|
503
|
+
isRefetchError: false;
|
|
504
|
+
isFetchNextPageError: false;
|
|
505
|
+
isFetchPreviousPageError: false;
|
|
506
|
+
isSuccess: false;
|
|
507
|
+
isPlaceholderData: false;
|
|
508
|
+
status: "error";
|
|
509
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_6 ? { [K in keyof T_6]: Date extends T_6[K] ? string | Exclude<T_6[K], T_6[K] & Date> : T_6[K]; } : never)[], unknown>, Error>>;
|
|
510
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_6 ? { [K in keyof T_6]: Date extends T_6[K] ? string | Exclude<T_6[K], T_6[K] & Date> : T_6[K]; } : never)[], unknown>, Error>>;
|
|
511
|
+
hasNextPage: boolean;
|
|
512
|
+
hasPreviousPage: boolean;
|
|
513
|
+
isFetchingNextPage: boolean;
|
|
514
|
+
isFetchingPreviousPage: boolean;
|
|
515
|
+
dataUpdatedAt: number;
|
|
516
|
+
errorUpdatedAt: number;
|
|
517
|
+
failureCount: number;
|
|
518
|
+
failureReason: Error | null;
|
|
519
|
+
errorUpdateCount: number;
|
|
520
|
+
isFetched: boolean;
|
|
521
|
+
isFetchedAfterMount: boolean;
|
|
522
|
+
isFetching: boolean;
|
|
523
|
+
isInitialLoading: boolean;
|
|
524
|
+
isPaused: boolean;
|
|
525
|
+
isRefetching: boolean;
|
|
526
|
+
isStale: boolean;
|
|
527
|
+
isEnabled: boolean;
|
|
528
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
529
|
+
} | {
|
|
530
|
+
rows: (T["$inferSelect"] extends infer T_6 ? { [K in keyof T_6]: Date extends T_6[K] ? string | Exclude<T_6[K], T_6[K] & Date> : T_6[K]; } : never)[];
|
|
531
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
532
|
+
isPending: boolean;
|
|
533
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
534
|
+
loadError: Error | null;
|
|
535
|
+
refetch: () => unknown;
|
|
536
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
537
|
+
hasMore: boolean;
|
|
538
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
539
|
+
loadMore: (() => void) | null;
|
|
540
|
+
isLoadingMore: boolean;
|
|
541
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
542
|
+
moreFailed: boolean;
|
|
543
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
544
|
+
isSwitching: boolean;
|
|
545
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
546
|
+
serverSort: {
|
|
547
|
+
sorting: Sorting | null;
|
|
548
|
+
setSorting: (next: Sorting | null) => void;
|
|
549
|
+
canSort: (column: string) => boolean;
|
|
550
|
+
} | null;
|
|
551
|
+
data: undefined;
|
|
552
|
+
error: null;
|
|
553
|
+
isError: false;
|
|
554
|
+
isLoading: true;
|
|
555
|
+
isLoadingError: false;
|
|
556
|
+
isRefetchError: false;
|
|
557
|
+
isFetchNextPageError: false;
|
|
558
|
+
isFetchPreviousPageError: false;
|
|
559
|
+
isSuccess: false;
|
|
560
|
+
isPlaceholderData: false;
|
|
561
|
+
status: "pending";
|
|
562
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_7 ? { [K in keyof T_7]: Date extends T_7[K] ? string | Exclude<T_7[K], T_7[K] & Date> : T_7[K]; } : never)[], unknown>, Error>>;
|
|
563
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_7 ? { [K in keyof T_7]: Date extends T_7[K] ? string | Exclude<T_7[K], T_7[K] & Date> : T_7[K]; } : never)[], unknown>, Error>>;
|
|
564
|
+
hasNextPage: boolean;
|
|
565
|
+
hasPreviousPage: boolean;
|
|
566
|
+
isFetchingNextPage: boolean;
|
|
567
|
+
isFetchingPreviousPage: boolean;
|
|
568
|
+
dataUpdatedAt: number;
|
|
569
|
+
errorUpdatedAt: number;
|
|
570
|
+
failureCount: number;
|
|
571
|
+
failureReason: Error | null;
|
|
572
|
+
errorUpdateCount: number;
|
|
573
|
+
isFetched: boolean;
|
|
574
|
+
isFetchedAfterMount: boolean;
|
|
575
|
+
isFetching: boolean;
|
|
576
|
+
isInitialLoading: boolean;
|
|
577
|
+
isPaused: boolean;
|
|
578
|
+
isRefetching: boolean;
|
|
579
|
+
isStale: boolean;
|
|
580
|
+
isEnabled: boolean;
|
|
581
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
582
|
+
} | {
|
|
583
|
+
rows: (T["$inferSelect"] extends infer T_7 ? { [K in keyof T_7]: Date extends T_7[K] ? string | Exclude<T_7[K], T_7[K] & Date> : T_7[K]; } : never)[];
|
|
584
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
585
|
+
isPending: boolean;
|
|
586
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
587
|
+
loadError: Error | null;
|
|
588
|
+
refetch: () => unknown;
|
|
589
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
590
|
+
hasMore: boolean;
|
|
591
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
592
|
+
loadMore: (() => void) | null;
|
|
593
|
+
isLoadingMore: boolean;
|
|
594
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
595
|
+
moreFailed: boolean;
|
|
596
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
597
|
+
isSwitching: boolean;
|
|
598
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
599
|
+
serverSort: {
|
|
600
|
+
sorting: Sorting | null;
|
|
601
|
+
setSorting: (next: Sorting | null) => void;
|
|
602
|
+
canSort: (column: string) => boolean;
|
|
603
|
+
} | null;
|
|
604
|
+
data: undefined;
|
|
605
|
+
error: null;
|
|
606
|
+
isError: false;
|
|
607
|
+
isLoadingError: false;
|
|
608
|
+
isRefetchError: false;
|
|
609
|
+
isFetchNextPageError: false;
|
|
610
|
+
isFetchPreviousPageError: false;
|
|
611
|
+
isSuccess: false;
|
|
612
|
+
isPlaceholderData: false;
|
|
613
|
+
status: "pending";
|
|
614
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_8 ? { [K in keyof T_8]: Date extends T_8[K] ? string | Exclude<T_8[K], T_8[K] & Date> : T_8[K]; } : never)[], unknown>, Error>>;
|
|
615
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_8 ? { [K in keyof T_8]: Date extends T_8[K] ? string | Exclude<T_8[K], T_8[K] & Date> : T_8[K]; } : never)[], unknown>, Error>>;
|
|
616
|
+
hasNextPage: boolean;
|
|
617
|
+
hasPreviousPage: boolean;
|
|
618
|
+
isFetchingNextPage: boolean;
|
|
619
|
+
isFetchingPreviousPage: boolean;
|
|
620
|
+
dataUpdatedAt: number;
|
|
621
|
+
errorUpdatedAt: number;
|
|
622
|
+
failureCount: number;
|
|
623
|
+
failureReason: Error | null;
|
|
624
|
+
errorUpdateCount: number;
|
|
625
|
+
isFetched: boolean;
|
|
626
|
+
isFetchedAfterMount: boolean;
|
|
627
|
+
isFetching: boolean;
|
|
628
|
+
isLoading: boolean;
|
|
629
|
+
isInitialLoading: boolean;
|
|
630
|
+
isPaused: boolean;
|
|
631
|
+
isRefetching: boolean;
|
|
632
|
+
isStale: boolean;
|
|
633
|
+
isEnabled: boolean;
|
|
634
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
635
|
+
} | {
|
|
636
|
+
rows: (T["$inferSelect"] extends infer T_8 ? { [K in keyof T_8]: Date extends T_8[K] ? string | Exclude<T_8[K], T_8[K] & Date> : T_8[K]; } : never)[];
|
|
637
|
+
/** Nothing to show yet: the first answer is on its way. */
|
|
638
|
+
isPending: boolean;
|
|
639
|
+
/** Why the first answer failed (a refetch that fails keeps the rows); `refetch` asks again. */
|
|
640
|
+
loadError: Error | null;
|
|
641
|
+
refetch: () => unknown;
|
|
642
|
+
/** More rows exist than are loaded (useList: it stopped at its limit). */
|
|
643
|
+
hasMore: boolean;
|
|
644
|
+
/** Loads the next page; null when this list can't (useList: raise the limit or page it). */
|
|
645
|
+
loadMore: (() => void) | null;
|
|
646
|
+
isLoadingMore: boolean;
|
|
647
|
+
/** The last page asked for failed: `loadMore` asks again, and nothing asks by itself until then. */
|
|
648
|
+
moreFailed: boolean;
|
|
649
|
+
/** Showing the last options' rows while the new options' load (a tab or filter just changed). */
|
|
650
|
+
isSwitching: boolean;
|
|
651
|
+
/** Sorted by the server, which sees every row; null when the table sorts what's loaded. */
|
|
652
|
+
serverSort: {
|
|
653
|
+
sorting: Sorting | null;
|
|
654
|
+
setSorting: (next: Sorting | null) => void;
|
|
655
|
+
canSort: (column: string) => boolean;
|
|
656
|
+
} | null;
|
|
657
|
+
data: import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_9 ? { [K in keyof T_9]: Date extends T_9[K] ? string | Exclude<T_9[K], T_9[K] & Date> : T_9[K]; } : never)[], unknown>;
|
|
658
|
+
isError: false;
|
|
659
|
+
error: null;
|
|
660
|
+
isLoading: false;
|
|
661
|
+
isLoadingError: false;
|
|
662
|
+
isRefetchError: false;
|
|
663
|
+
isSuccess: true;
|
|
664
|
+
isPlaceholderData: true;
|
|
665
|
+
isFetchNextPageError: false;
|
|
666
|
+
isFetchPreviousPageError: false;
|
|
667
|
+
status: "success";
|
|
668
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_10 ? { [K in keyof T_10]: Date extends T_10[K] ? string | Exclude<T_10[K], T_10[K] & Date> : T_10[K]; } : never)[], unknown>, Error>>;
|
|
669
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<(T["$inferSelect"] extends infer T_10 ? { [K in keyof T_10]: Date extends T_10[K] ? string | Exclude<T_10[K], T_10[K] & Date> : T_10[K]; } : never)[], unknown>, Error>>;
|
|
670
|
+
hasNextPage: boolean;
|
|
671
|
+
hasPreviousPage: boolean;
|
|
672
|
+
isFetchingNextPage: boolean;
|
|
673
|
+
isFetchingPreviousPage: boolean;
|
|
674
|
+
dataUpdatedAt: number;
|
|
675
|
+
errorUpdatedAt: number;
|
|
676
|
+
failureCount: number;
|
|
677
|
+
failureReason: Error | null;
|
|
678
|
+
errorUpdateCount: number;
|
|
679
|
+
isFetched: boolean;
|
|
680
|
+
isFetchedAfterMount: boolean;
|
|
681
|
+
isFetching: boolean;
|
|
682
|
+
isInitialLoading: boolean;
|
|
683
|
+
isPaused: boolean;
|
|
684
|
+
isRefetching: boolean;
|
|
685
|
+
isStale: boolean;
|
|
686
|
+
isEnabled: boolean;
|
|
193
687
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
194
688
|
};
|
|
195
689
|
/** One row; `null` waits (nothing open yet). */
|
|
196
690
|
useGet: (id: string | null) => import("@tanstack/react-query").UseQueryResult<T["$inferSelect"] extends infer T_1 ? { [K in keyof T_1]: Date extends T_1[K] ? string | Exclude<T_1[K], T_1[K] & Date> : T_1[K]; } : never, EntityError>;
|
|
197
691
|
useCreate: () => import("@tanstack/react-query").UseMutationResult<T["$inferSelect"] extends infer T_1 ? { [K in keyof T_1]: Date extends T_1[K] ? string | Exclude<T_1[K], T_1[K] & Date> : T_1[K]; } : never, EntityError, NewRow<T>, unknown>;
|
|
198
|
-
/**
|
|
692
|
+
/**
|
|
693
|
+
* Shows the patch at once in every cached list, page and row under the
|
|
694
|
+
* table's key; a save that fails takes it back at once (and the mutation's
|
|
695
|
+
* error says why), then the table refetches.
|
|
696
|
+
*/
|
|
199
697
|
useUpdate: () => import("@tanstack/react-query").UseMutationResult<T["$inferSelect"] extends infer T_1 ? { [K in keyof T_1]: Date extends T_1[K] ? string | Exclude<T_1[K], T_1[K] & Date> : T_1[K]; } : never, EntityError, {
|
|
200
698
|
id: string;
|
|
201
699
|
patch: Patch<T>;
|
|
202
|
-
},
|
|
203
|
-
/** Takes the row out of every cached list at once. */
|
|
204
|
-
useDelete: () => import("@tanstack/react-query").UseMutationResult<void, EntityError, string,
|
|
700
|
+
}, Before[]>;
|
|
701
|
+
/** Takes the row out of every cached list at once; a delete that fails puts it back. */
|
|
702
|
+
useDelete: () => import("@tanstack/react-query").UseMutationResult<void, EntityError, string, Before[]>;
|
|
205
703
|
};
|
|
206
704
|
export type TableClient<T extends Entity> = ReturnType<typeof tableClient<T>>;
|
|
207
705
|
/** To the login page, and back here after. */
|
package/dist/client.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
// `app.table(companies)` reads and writes that table's rows through the
|
|
6
6
|
// entities route; its hooks keep TanStack Query's cache in step, so an edit
|
|
7
7
|
// shows at once and never snaps back. `app.contracts` asks the org's agents.
|
|
8
|
-
import { keepPreviousData, skipToken, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { hashKey, keepPreviousData, skipToken, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
9
|
+
import { useEffect, useMemo, useState } from "react";
|
|
9
10
|
import { createInvoker } from "@shubh90/app-runtime/invoke";
|
|
10
|
-
import { getTableName } from "drizzle-orm";
|
|
11
|
+
import { getTableColumns, getTableName } from "drizzle-orm";
|
|
11
12
|
import { z } from "zod/v4";
|
|
12
|
-
import { ENTITIES_PATH, EntityError } from "./entities.js";
|
|
13
|
+
import { ENTITIES_PATH, EntityError, LIST_LIMIT } from "./entities.js";
|
|
13
14
|
export { EntityError, LIST_LIMIT } from "./entities.js";
|
|
14
15
|
export { siteOf } from "./schema.js";
|
|
15
16
|
export { imageSrc } from "./images.js";
|
|
@@ -19,12 +20,113 @@ export function createApp() {
|
|
|
19
20
|
}
|
|
20
21
|
/** Every list's rows before its first answer: one array, so they never change identity while pending. */
|
|
21
22
|
const NO_ROWS = [];
|
|
23
|
+
/** Rows one page of `useInfiniteList` asks for; scrolling near the end asks for the next. */
|
|
24
|
+
export const PAGE_SIZE = 100;
|
|
25
|
+
/** How long after a list settles its sibling views' first pages are fetched: the page's own work goes first. */
|
|
26
|
+
const PREFETCH_AFTER_MS = 300;
|
|
27
|
+
/** A prefetched first page counts as fresh this long, so settling again doesn't fetch it again. */
|
|
28
|
+
const PREFETCH_FRESH_MS = 30_000;
|
|
29
|
+
/** A cached row as any query under a table's key holds it: an object with the row's id. */
|
|
30
|
+
const cachedRow = z.looseObject({ id: z.string() });
|
|
31
|
+
/** A cached useInfiniteList: its pages of rows. */
|
|
32
|
+
const cachedPages = z.looseObject({ pages: z.array(z.array(z.unknown())) });
|
|
33
|
+
const cachedSources = z.record(z.string(), z.unknown());
|
|
34
|
+
/** The rows a cached query holds: a list's, each of a useInfiniteList's pages, or useGet's one row. */
|
|
35
|
+
function rowsOf(data) {
|
|
36
|
+
if (Array.isArray(data))
|
|
37
|
+
return [data];
|
|
38
|
+
const paged = cachedPages.safeParse(data);
|
|
39
|
+
if (paged.success)
|
|
40
|
+
return paged.data.pages;
|
|
41
|
+
return cachedRow.safeParse(data).success ? [[data]] : [];
|
|
42
|
+
}
|
|
43
|
+
/** The same query with its rows replaced, page by page, in the shape it came in. */
|
|
44
|
+
function withRows(data, rows) {
|
|
45
|
+
if (Array.isArray(data))
|
|
46
|
+
return rows[0];
|
|
47
|
+
const paged = cachedPages.safeParse(data);
|
|
48
|
+
if (paged.success)
|
|
49
|
+
return { ...paged.data, pages: rows };
|
|
50
|
+
// One row (useGet): kept as it was when the change takes it out (an open panel's row stays while it closes).
|
|
51
|
+
const [[row = data] = []] = rows;
|
|
52
|
+
return row;
|
|
53
|
+
}
|
|
54
|
+
const isRow = (item, id) => {
|
|
55
|
+
const row = cachedRow.safeParse(item);
|
|
56
|
+
return row.success && row.data.id === id;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Every query under a table's key with one row changed (null takes it out):
|
|
60
|
+
* useList's, useInfiniteList's, useGet's, and a page's own server-function
|
|
61
|
+
* list keyed under the table, so a write shows everywhere at once. Returns
|
|
62
|
+
* where the row was, so a write that fails puts back just that row, never
|
|
63
|
+
* another write's change made since. A query holding anything else is left alone.
|
|
64
|
+
*/
|
|
65
|
+
function changeRow(client, key, id, fields, change) {
|
|
66
|
+
const before = [];
|
|
67
|
+
for (const [queryKey, data] of client.getQueriesData({ queryKey: key })) {
|
|
68
|
+
const pages = rowsOf(data);
|
|
69
|
+
pages.forEach((rows, page) => {
|
|
70
|
+
const index = rows.findIndex((item) => isRow(item, id));
|
|
71
|
+
if (index >= 0)
|
|
72
|
+
before.push({ key: queryKey, page, index, row: rows[index], fields });
|
|
73
|
+
});
|
|
74
|
+
if (!pages.some((rows) => rows.some((item) => isRow(item, id))))
|
|
75
|
+
continue;
|
|
76
|
+
client.setQueryData(queryKey, withRows(data, pages.map((rows) => rows.flatMap((item) => {
|
|
77
|
+
const row = cachedRow.safeParse(item);
|
|
78
|
+
if (!row.success || row.data.id !== id)
|
|
79
|
+
return [item];
|
|
80
|
+
const next = change(row.data);
|
|
81
|
+
return next === null ? [] : [next];
|
|
82
|
+
}))));
|
|
83
|
+
}
|
|
84
|
+
return before;
|
|
85
|
+
}
|
|
86
|
+
/** The queries under a table's key that hold one row: the only ones a write to it touches (or cancels). */
|
|
87
|
+
function holding(client, key, id) {
|
|
88
|
+
return client
|
|
89
|
+
.getQueriesData({ queryKey: key })
|
|
90
|
+
.filter(([, data]) => rowsOf(data).some((rows) => rows.some((item) => isRow(item, id))))
|
|
91
|
+
.map(([queryKey]) => queryKey);
|
|
92
|
+
}
|
|
93
|
+
/** A failed write taken back: its fields of the row as they were (a delete: the row, where it was); everything else as it is now. */
|
|
94
|
+
function putBack(client, id, before) {
|
|
95
|
+
for (const { key, page, index, row, fields } of before) {
|
|
96
|
+
client.setQueryData(key, (data) => {
|
|
97
|
+
const pages = rowsOf(data);
|
|
98
|
+
const rows = pages[page];
|
|
99
|
+
if (rows === undefined)
|
|
100
|
+
return data;
|
|
101
|
+
const was = cachedRow.safeParse(row);
|
|
102
|
+
const there = rows.some((item) => isRow(item, id));
|
|
103
|
+
// An update's row gone meanwhile (deleted) stays gone; only a delete puts its row back.
|
|
104
|
+
if (!there && fields !== null)
|
|
105
|
+
return data;
|
|
106
|
+
const back = there
|
|
107
|
+
? rows.map((item) => {
|
|
108
|
+
if (!isRow(item, id))
|
|
109
|
+
return item;
|
|
110
|
+
const now = cachedRow.safeParse(item);
|
|
111
|
+
if (fields === null || !now.success || !was.success)
|
|
112
|
+
return row;
|
|
113
|
+
// Each field as it was, and gone if it wasn't there (a slim list the patch added a column to).
|
|
114
|
+
const kept = Object.entries(now.data).filter(([field]) => !fields.includes(field));
|
|
115
|
+
const restored = fields.filter((field) => Object.hasOwn(was.data, field)).map((field) => [field, was.data[field]]);
|
|
116
|
+
return Object.fromEntries([...kept, ...restored]);
|
|
117
|
+
})
|
|
118
|
+
: [...rows.slice(0, index), row, ...rows.slice(index)];
|
|
119
|
+
return withRows(data, pages.map((each, at) => (at === page ? back : each)));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
22
123
|
/** One entity table's rows: plain calls, and TanStack Query hooks over them. */
|
|
23
124
|
function tableClient(entity) {
|
|
24
125
|
const base = `${ENTITIES_PATH}/${getTableName(entity)}`;
|
|
25
126
|
const keys = {
|
|
26
127
|
all: ["light", base],
|
|
27
128
|
list: (options) => ["light", base, "list", options],
|
|
129
|
+
pages: (options) => ["light", base, "pages", options],
|
|
28
130
|
get: (id) => ["light", base, "get", id],
|
|
29
131
|
write: ["light", base, "write"]
|
|
30
132
|
};
|
|
@@ -35,18 +137,41 @@ function tableClient(entity) {
|
|
|
35
137
|
const remove = async (id) => {
|
|
36
138
|
await call("DELETE", `${base}/${id}`);
|
|
37
139
|
};
|
|
140
|
+
const columns = new Set(Object.keys(getTableColumns(entity)));
|
|
141
|
+
const isColumn = (column) => columns.has(column);
|
|
142
|
+
/** A header's sort as the server takes it. DataTable sorts only columns `canSort` allows, so any other is a bug. */
|
|
143
|
+
const sortBy = ({ column, desc }) => {
|
|
144
|
+
if (!isColumn(column))
|
|
145
|
+
throw new Error(`${getTableName(entity)} has no column ${column} to sort by.`);
|
|
146
|
+
return desc ? `-${column}` : column;
|
|
147
|
+
};
|
|
148
|
+
const pagesQuery = (options) => {
|
|
149
|
+
const { pageSize = PAGE_SIZE, ...rest } = options;
|
|
150
|
+
return {
|
|
151
|
+
queryKey: keys.pages(options),
|
|
152
|
+
queryFn: ({ pageParam }) => list({ ...rest, limit: pageSize, offset: pageParam }),
|
|
153
|
+
initialPageParam: 0,
|
|
154
|
+
// A full page may have more after it; a short one is the end (or one an optimistic delete shortened, until the write's refetch).
|
|
155
|
+
getNextPageParam: (last, all) => last.length < pageSize ? undefined : all.reduce((sum, page) => sum + page.length, 0)
|
|
156
|
+
};
|
|
157
|
+
};
|
|
38
158
|
/**
|
|
39
159
|
* After a write, refetch the table once the last write in flight settles.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
160
|
+
* A saved write stays pending until the refetch lands, so an edit never
|
|
161
|
+
* flickers back to its old value. A failed one is already taken back, so
|
|
162
|
+
* its error reaches the page (the toast saying so) without waiting.
|
|
43
163
|
*/
|
|
44
164
|
const useSettle = () => {
|
|
45
165
|
const client = useQueryClient();
|
|
46
|
-
|
|
166
|
+
const refetch = async () => {
|
|
47
167
|
if (client.isMutating({ mutationKey: keys.write }) === 1)
|
|
48
168
|
await client.invalidateQueries({ queryKey: keys.all });
|
|
49
169
|
};
|
|
170
|
+
return (_data, error) => {
|
|
171
|
+
if (error === null)
|
|
172
|
+
return refetch();
|
|
173
|
+
void refetch();
|
|
174
|
+
};
|
|
50
175
|
};
|
|
51
176
|
return {
|
|
52
177
|
/**
|
|
@@ -69,9 +194,77 @@ function tableClient(entity) {
|
|
|
69
194
|
useList: (options = {}) => {
|
|
70
195
|
const query = useQuery({ queryKey: keys.list(options), queryFn: () => list(options) });
|
|
71
196
|
const { data: rows = NO_ROWS } = query;
|
|
197
|
+
const view = {
|
|
198
|
+
rows,
|
|
199
|
+
isPending: query.isPending,
|
|
200
|
+
loadError: query.isLoadingError ? query.error : null,
|
|
201
|
+
refetch: query.refetch,
|
|
202
|
+
// Stopped at the server's limit (not one the page chose): DataTable says it shows only the first ones, never drops the rest silently.
|
|
203
|
+
hasMore: options.limit === undefined && rows.length >= LIST_LIMIT,
|
|
204
|
+
loadMore: null,
|
|
205
|
+
isLoadingMore: false,
|
|
206
|
+
moreFailed: false,
|
|
207
|
+
isSwitching: false,
|
|
208
|
+
serverSort: null
|
|
209
|
+
};
|
|
72
210
|
// A spread reads every field, so the page re-renders on any of them (isFetching
|
|
73
211
|
// too); a list page shows those states anyway, and it keeps the result plain.
|
|
74
|
-
return { ...query,
|
|
212
|
+
return { ...query, ...view };
|
|
213
|
+
},
|
|
214
|
+
/**
|
|
215
|
+
* A list of any size, a page at a time: `loadMore` (DataTable calls it as
|
|
216
|
+
* the end scrolls into view) fetches the next. New options (a tab, a
|
|
217
|
+
* filter, a search) keep the last rows on screen with `isSwitching` until
|
|
218
|
+
* theirs land, so a click never blanks the table. `prefetch` names the
|
|
219
|
+
* sibling views (the other tabs): their first pages load once this one
|
|
220
|
+
* settles, so switching to one is instant. Sorting is the server's, over
|
|
221
|
+
* every row; `sort` is where it starts.
|
|
222
|
+
*
|
|
223
|
+
* const list = app.table(leads).useInfiniteList({ where: { status: tab } }, { prefetch: TABS.map((t) => ({ where: { status: t } })) });
|
|
224
|
+
* <DataTable list={list} … />
|
|
225
|
+
*/
|
|
226
|
+
useInfiniteList: (options = {}, { prefetch = [] } = {}) => {
|
|
227
|
+
const client = useQueryClient();
|
|
228
|
+
const [sorting, setSorting] = useState(null);
|
|
229
|
+
const asked = sorting === null ? options : { ...options, sort: sortBy(sorting) };
|
|
230
|
+
const query = useInfiniteQuery({ ...pagesQuery(asked), placeholderData: keepPreviousData });
|
|
231
|
+
// Pages are by offset: a row added above the fold mid-scroll shifts the next page by one, so a row can come twice. The first stays.
|
|
232
|
+
const rows = useMemo(() => (query.data === undefined ? NO_ROWS : firstOfEach(query.data.pages.flat(), (row) => row.id)), [query.data]);
|
|
233
|
+
const settled = query.isSuccess && !query.isPlaceholderData;
|
|
234
|
+
// The sibling views as this one is sorted, less this one; keyed by their hash, so a page passing a new array each render changes nothing.
|
|
235
|
+
const siblings = prefetch.map((sibling) => (asked.sort === undefined ? sibling : { ...sibling, sort: asked.sort })).filter((sibling) => hashKey([sibling]) !== hashKey([asked]));
|
|
236
|
+
const siblingsKey = hashKey(siblings);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (!settled)
|
|
239
|
+
return;
|
|
240
|
+
const later = setTimeout(() => {
|
|
241
|
+
for (const sibling of siblings)
|
|
242
|
+
void client.prefetchInfiniteQuery({ ...pagesQuery(sibling), staleTime: PREFETCH_FRESH_MS });
|
|
243
|
+
}, PREFETCH_AFTER_MS);
|
|
244
|
+
return () => clearTimeout(later);
|
|
245
|
+
// `siblingsKey` stands for `siblings`, by value.
|
|
246
|
+
}, [settled, siblingsKey, client]);
|
|
247
|
+
const view = {
|
|
248
|
+
rows,
|
|
249
|
+
isPending: query.isPending,
|
|
250
|
+
loadError: query.isLoadingError ? query.error : null,
|
|
251
|
+
refetch: query.refetch,
|
|
252
|
+
hasMore: query.hasNextPage,
|
|
253
|
+
loadMore: () => {
|
|
254
|
+
if (query.hasNextPage && !query.isFetchingNextPage && !query.isPlaceholderData)
|
|
255
|
+
void query.fetchNextPage();
|
|
256
|
+
},
|
|
257
|
+
isLoadingMore: query.isFetchingNextPage,
|
|
258
|
+
moreFailed: query.isFetchNextPageError,
|
|
259
|
+
isSwitching: query.isPlaceholderData,
|
|
260
|
+
serverSort: {
|
|
261
|
+
// A header's choice, or the page's own `sort` until one is clicked.
|
|
262
|
+
sorting: sorting === null ? sortingOf(options.sort) : sorting,
|
|
263
|
+
setSorting,
|
|
264
|
+
canSort: isColumn
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
return { ...query, ...view };
|
|
75
268
|
},
|
|
76
269
|
/** One row; `null` waits (nothing open yet). */
|
|
77
270
|
useGet: (id) => useQuery({
|
|
@@ -88,7 +281,11 @@ function tableClient(entity) {
|
|
|
88
281
|
onSettled: settle
|
|
89
282
|
});
|
|
90
283
|
},
|
|
91
|
-
/**
|
|
284
|
+
/**
|
|
285
|
+
* Shows the patch at once in every cached list, page and row under the
|
|
286
|
+
* table's key; a save that fails takes it back at once (and the mutation's
|
|
287
|
+
* error says why), then the table refetches.
|
|
288
|
+
*/
|
|
92
289
|
useUpdate: () => {
|
|
93
290
|
const client = useQueryClient();
|
|
94
291
|
const settle = useSettle();
|
|
@@ -96,22 +293,26 @@ function tableClient(entity) {
|
|
|
96
293
|
mutationKey: keys.write,
|
|
97
294
|
mutationFn: ({ id, patch }) => update(id, patch),
|
|
98
295
|
onMutate: async ({ id, patch }) => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
296
|
+
// Only the queries holding this row: a tab switching meanwhile keeps loading.
|
|
297
|
+
await Promise.all(holding(client, keys.all, id).map((queryKey) => client.cancelQueries({ queryKey, exact: true })));
|
|
298
|
+
// A value set here loses its old source until the server answers with who set it; sources sent merge in.
|
|
299
|
+
// A row cached without its sources (a page's slim list) just takes the patch.
|
|
300
|
+
const patched = (row) => {
|
|
301
|
+
const sources = cachedSources.safeParse(row.field_sources);
|
|
302
|
+
if (!sources.success)
|
|
303
|
+
return { ...row, ...patch };
|
|
304
|
+
const kept = Object.fromEntries(Object.entries(sources.data).filter(([column]) => !(column in patch)));
|
|
305
|
+
return { ...row, ...patch, field_sources: Object.assign(kept, patch.field_sources) };
|
|
306
|
+
};
|
|
307
|
+
// Its sources change with its values (an edited value loses its old source), so they go back with them.
|
|
308
|
+
return changeRow(client, keys.all, id, [...Object.keys(patch), "field_sources"], patched);
|
|
110
309
|
},
|
|
310
|
+
// No context when onMutate itself threw: nothing was changed to put back.
|
|
311
|
+
onError: (_error, { id }, before = []) => putBack(client, id, before),
|
|
111
312
|
onSettled: settle
|
|
112
313
|
});
|
|
113
314
|
},
|
|
114
|
-
/** Takes the row out of every cached list at once. */
|
|
315
|
+
/** Takes the row out of every cached list at once; a delete that fails puts it back. */
|
|
115
316
|
useDelete: () => {
|
|
116
317
|
const client = useQueryClient();
|
|
117
318
|
const settle = useSettle();
|
|
@@ -119,14 +320,33 @@ function tableClient(entity) {
|
|
|
119
320
|
mutationKey: keys.write,
|
|
120
321
|
mutationFn: remove,
|
|
121
322
|
onMutate: async (id) => {
|
|
122
|
-
await client.cancelQueries({ queryKey:
|
|
123
|
-
client
|
|
323
|
+
await Promise.all(holding(client, keys.all, id).map((queryKey) => client.cancelQueries({ queryKey, exact: true })));
|
|
324
|
+
return changeRow(client, keys.all, id, null, () => null);
|
|
124
325
|
},
|
|
326
|
+
onError: (_error, id, before = []) => putBack(client, id, before),
|
|
125
327
|
onSettled: settle
|
|
126
328
|
});
|
|
127
329
|
}
|
|
128
330
|
};
|
|
129
331
|
}
|
|
332
|
+
/** A page's `sort` ("name", "-created_at") as a header shows it. */
|
|
333
|
+
function sortingOf(sort) {
|
|
334
|
+
if (sort === undefined)
|
|
335
|
+
return null;
|
|
336
|
+
return sort.startsWith("-") ? { column: sort.slice(1), desc: true } : { column: sort, desc: false };
|
|
337
|
+
}
|
|
338
|
+
/** Each row once, where it first came. */
|
|
339
|
+
function firstOfEach(rows, idOf) {
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
const once = [];
|
|
342
|
+
for (const row of rows) {
|
|
343
|
+
if (seen.has(idOf(row)))
|
|
344
|
+
continue;
|
|
345
|
+
seen.add(idOf(row));
|
|
346
|
+
once.push(row);
|
|
347
|
+
}
|
|
348
|
+
return once;
|
|
349
|
+
}
|
|
130
350
|
function json(body) {
|
|
131
351
|
return { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) };
|
|
132
352
|
}
|
package/dist/entities.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Entity, PlatformColumn } from "./schema.js";
|
|
2
2
|
export declare const ENTITIES_PATH = "/api/light/entities";
|
|
3
|
-
/** The most rows one list returns; page past it with `offset
|
|
3
|
+
/** The most rows one list returns; page past it with `offset` (useInfiniteList does). */
|
|
4
4
|
export declare const LIST_LIMIT = 1000;
|
|
5
5
|
/** A value as JSON carries it: a Date column arrives as its ISO string. */
|
|
6
6
|
type Wire<T> = {
|
|
@@ -19,6 +19,8 @@ export type Where<R> = {
|
|
|
19
19
|
export type Sort<R> = (keyof R & string) | `-${keyof R & string}`;
|
|
20
20
|
export type ListOptions<R> = {
|
|
21
21
|
where?: Where<R>;
|
|
22
|
+
/** Rows with every one of these words in some text column of their own, case-insensitively: the server searches, so a paged list finds rows not loaded yet. */
|
|
23
|
+
search?: string;
|
|
22
24
|
sort?: Sort<R>;
|
|
23
25
|
/** At most LIST_LIMIT, which is also the default. */
|
|
24
26
|
limit?: number;
|
package/dist/entities.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const ENTITIES_PATH = "/api/light/entities";
|
|
2
|
-
/** The most rows one list returns; page past it with `offset
|
|
2
|
+
/** The most rows one list returns; page past it with `offset` (useInfiniteList does). */
|
|
3
3
|
export const LIST_LIMIT = 1000;
|
|
4
4
|
/** A refused or failed entities call, with the route's own reason. */
|
|
5
5
|
export class EntityError extends Error {
|
package/dist/server/entities.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// on any entity table, each request checked against the table's own Zod
|
|
3
3
|
// schema and the app's door. Each write names its person for the table's
|
|
4
4
|
// trigger, which records the change in light_changes (src/db/audit.ts).
|
|
5
|
-
import { and, asc, desc, eq, getTableColumns, getTableName, inArray, isNull, sql } from "drizzle-orm";
|
|
5
|
+
import { and, asc, desc, eq, getTableColumns, getTableName, ilike, inArray, isNull, or, sql } from "drizzle-orm";
|
|
6
6
|
import { isDeepStrictEqual } from "node:util";
|
|
7
7
|
import { createSchemaFactory } from "drizzle-zod";
|
|
8
8
|
import { isSameOrigin } from "@shubh90/app-runtime/chat/server";
|
|
@@ -10,6 +10,10 @@ import { z } from "zod/v4";
|
|
|
10
10
|
import { ENTITIES_PATH, LIST_LIMIT } from "../entities.js";
|
|
11
11
|
import { PLATFORM_COLUMNS, fieldSource, isEntity } from "../schema.js";
|
|
12
12
|
import { env } from "./env.js";
|
|
13
|
+
/** Drizzle's text column types: what a search reads. */
|
|
14
|
+
const SEARCHED_TYPES = new Set(["PgText", "PgVarchar", "PgChar"]);
|
|
15
|
+
/** A search box's worth of words; anything longer is a mistake, not a search. */
|
|
16
|
+
const SEARCH_MAX = 200;
|
|
13
17
|
/** Who writes when the app runs with no sign-in at all: a bare local dev server. */
|
|
14
18
|
const LOCAL_DEVELOPER = { id: null, name: "Local developer" };
|
|
15
19
|
// Postgres's integrity violations: the person's request, not a failure, told with Postgres's reason.
|
|
@@ -79,7 +83,9 @@ export function createEntitiesHandler(options) {
|
|
|
79
83
|
const patch = await readBody(request, table.update);
|
|
80
84
|
if (!patch.success)
|
|
81
85
|
return refuse(400, "invalid", z.prettifyError(patch.error));
|
|
82
|
-
|
|
86
|
+
// The page's own address: behind the platform's proxy the request's host is an internal hop, Origin (checked above) is not.
|
|
87
|
+
const origin = request.headers.get("origin");
|
|
88
|
+
return answer(() => update(db, table, actor, origin === null ? url.origin : origin, id, patch.data));
|
|
83
89
|
}
|
|
84
90
|
if (id !== null && method === "DELETE")
|
|
85
91
|
return answer(() => remove(db, table, actor, id));
|
|
@@ -105,17 +111,23 @@ function tableRules(entity) {
|
|
|
105
111
|
field_sources: sources
|
|
106
112
|
})
|
|
107
113
|
.strict();
|
|
114
|
+
// The columns a search reads: the table's own text columns (not the platform's, ids or JSON).
|
|
115
|
+
const searched = Object.entries(columns)
|
|
116
|
+
.filter(([name, c]) => !(name in PLATFORM_COLUMNS) && SEARCHED_TYPES.has(c.columnType))
|
|
117
|
+
.map(([, c]) => c);
|
|
108
118
|
const match = Object.fromEntries(Object.entries(createSelectSchema(entity).shape).map(([key, value]) => [key, z.union([value, z.array(value)])]));
|
|
109
119
|
return {
|
|
110
120
|
name: getTableName(entity),
|
|
111
121
|
entity,
|
|
112
122
|
column,
|
|
113
123
|
id: column("id"),
|
|
124
|
+
searched,
|
|
114
125
|
create: writable(createInsertSchema(entity).shape),
|
|
115
126
|
update: writable(createUpdateSchema(entity).shape),
|
|
116
127
|
list: z
|
|
117
128
|
.object({
|
|
118
129
|
where: z.object(match).partial().strict().default({}),
|
|
130
|
+
search: z.string().trim().max(SEARCH_MAX).default(""),
|
|
119
131
|
sort: z.literal([...names, ...names.map((name) => `-${name}`)]).default("-created_at"),
|
|
120
132
|
limit: z.int().min(1).max(LIST_LIMIT).default(LIST_LIMIT),
|
|
121
133
|
offset: z.int().min(0).default(0)
|
|
@@ -128,7 +140,7 @@ async function list(db, table, url) {
|
|
|
128
140
|
const query = table.list.safeParse(q === null ? {} : readJson(q));
|
|
129
141
|
if (!query.success)
|
|
130
142
|
return refuse(400, "invalid", z.prettifyError(query.error));
|
|
131
|
-
const { where, sort, limit, offset } = query.data;
|
|
143
|
+
const { where, search, sort, limit, offset } = query.data;
|
|
132
144
|
const order = sort.startsWith("-") ? desc(table.column(sort.slice(1))) : asc(table.column(sort));
|
|
133
145
|
const conditions = Object.entries(where).map(([name, value]) => {
|
|
134
146
|
const column = table.column(name);
|
|
@@ -138,6 +150,11 @@ async function list(db, table, url) {
|
|
|
138
150
|
return inArray(column, value);
|
|
139
151
|
return eq(column, value);
|
|
140
152
|
});
|
|
153
|
+
// Every word, each in some text column; a table with no text columns has nothing a search could match.
|
|
154
|
+
for (const word of search.split(/\s+/).filter(Boolean)) {
|
|
155
|
+
const pattern = `%${word.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
156
|
+
conditions.push(or(...table.searched.map((column) => ilike(column, pattern))) ?? sql `false`);
|
|
157
|
+
}
|
|
141
158
|
const rows = await db
|
|
142
159
|
.select()
|
|
143
160
|
.from(table.entity)
|
|
@@ -160,15 +177,21 @@ async function create(db, table, actor, data) {
|
|
|
160
177
|
.returning());
|
|
161
178
|
});
|
|
162
179
|
}
|
|
163
|
-
async function update(db, table, actor,
|
|
180
|
+
async function update(db, table, actor,
|
|
181
|
+
/** This app's own address: where a person's edit was said. */
|
|
182
|
+
origin, id, patch) {
|
|
164
183
|
return db.transaction(async (tx) => {
|
|
165
184
|
await nameActor(tx, actor);
|
|
166
185
|
const before = atMostOne(await tx.select().from(table.entity).where(eq(table.id, id)).for("update"));
|
|
167
186
|
if (before === null)
|
|
168
187
|
return null;
|
|
169
188
|
const changed = Object.keys(patch).filter((key) => !isDeepStrictEqual(patch[key], before[key]));
|
|
170
|
-
// A value a person sets is no longer what its source said
|
|
171
|
-
const
|
|
189
|
+
// A sourced value a person sets is no longer what its source said: its source is now them. Sources sent are merged in.
|
|
190
|
+
const stored = storedSources.parse(before.field_sources);
|
|
191
|
+
const edited = changed
|
|
192
|
+
.filter((column) => column in stored)
|
|
193
|
+
.map((column) => [column, { url: origin, evidence: `Edited by ${actor.name}` }]);
|
|
194
|
+
const field_sources = { ...withoutSources(stored, changed), ...Object.fromEntries(edited), ...patch.field_sources };
|
|
172
195
|
return exactlyOne(await tx
|
|
173
196
|
.update(table.entity)
|
|
174
197
|
.set({ ...patch, field_sources })
|
|
@@ -204,8 +227,7 @@ function exactlyOne(rows) {
|
|
|
204
227
|
// What's stored may come from an agent's own write: read as it is, never refused here.
|
|
205
228
|
const storedSources = z.record(z.string(), z.unknown());
|
|
206
229
|
function withoutSources(stored, columns) {
|
|
207
|
-
|
|
208
|
-
return Object.fromEntries(Object.entries(sources).filter(([column]) => !columns.includes(column)));
|
|
230
|
+
return Object.fromEntries(Object.entries(stored).filter(([column]) => !columns.includes(column)));
|
|
209
231
|
}
|
|
210
232
|
function signedIn(session) {
|
|
211
233
|
return session.user === null ? null : { id: session.user.id, name: session.user.name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trylight/app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "The Light SDK every v2 org app depends on: entities (Drizzle tables with a typed client), sign-in, contracts and in-app chat.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/shubh9/mii.git",
|
|
13
|
-
"directory": "packages/
|
|
13
|
+
"directory": "packages/app-sdk"
|
|
14
14
|
},
|
|
15
|
-
"packageManager": "pnpm@11.0.9",
|
|
16
15
|
"files": [
|
|
17
16
|
"dist",
|
|
18
17
|
"bin"
|
|
@@ -42,14 +41,6 @@
|
|
|
42
41
|
"default": "./dist/vite.js"
|
|
43
42
|
}
|
|
44
43
|
},
|
|
45
|
-
"scripts": {
|
|
46
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
47
|
-
"typecheck": "tsc --noEmit",
|
|
48
|
-
"lint": "eslint src test --max-warnings 0",
|
|
49
|
-
"test": "node --test --import tsx \"test/**/*.test.ts\"",
|
|
50
|
-
"check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
|
|
51
|
-
"prepack": "pnpm run build"
|
|
52
|
-
},
|
|
53
44
|
"peerDependencies": {
|
|
54
45
|
"@tanstack/react-query": ">=5.69",
|
|
55
46
|
"drizzle-orm": ">=0.45",
|
|
@@ -82,5 +73,12 @@
|
|
|
82
73
|
"typescript-eslint": "^8.70.1",
|
|
83
74
|
"vite": "^8.2.0",
|
|
84
75
|
"zod": "^4.6.5"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
79
|
+
"typecheck": "tsc --noEmit",
|
|
80
|
+
"lint": "eslint src test --max-warnings 0",
|
|
81
|
+
"test": "node --test --import tsx \"test/**/*.test.ts\"",
|
|
82
|
+
"check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build"
|
|
85
83
|
}
|
|
86
|
-
}
|
|
84
|
+
}
|