@craftainer/crud-sdk 0.0.0-stage → 0.2.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/LICENSE +674 -0
- package/README.md +207 -2
- package/dist/auth-DwtNEKrw.mjs +22 -0
- package/dist/auth-DwtNEKrw.mjs.map +1 -0
- package/dist/cache/index.d.mts +166 -0
- package/dist/cache/index.mjs +972 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/client-B8fSsx4L.d.mts +316 -0
- package/dist/client-Bv0yCuwC.mjs +923 -0
- package/dist/client-Bv0yCuwC.mjs.map +1 -0
- package/dist/codegen/cli.d.mts +1 -0
- package/dist/codegen/cli.mjs +131 -0
- package/dist/codegen/cli.mjs.map +1 -0
- package/dist/events-Cqj9Rbon.mjs +616 -0
- package/dist/events-Cqj9Rbon.mjs.map +1 -0
- package/dist/events-DpSWzSN9.d.mts +88 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.mjs +3 -0
- package/dist/interfaces-s73cVfIF.d.mts +109 -0
- package/dist/tanstack/index.d.mts +245 -0
- package/dist/tanstack/index.mjs +315 -0
- package/dist/tanstack/index.mjs.map +1 -0
- package/package.json +80 -4
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { r as CrudEvent } from "../interfaces-s73cVfIF.mjs";
|
|
2
|
+
import { a as SubscribeEventsOptions, c as CachePolicy, d as tanstackCachePolicy, i as EventsConfig, l as ManifestCacheHints, n as EventHub, o as getEventHub, r as EventHubOptions, s as subscribeEvents, t as EventHandlers } from "../events-DpSWzSN9.mjs";
|
|
3
|
+
import { D as JsonObject, O as Manifest, r as CrudClient, s as ResourceClient } from "../client-B8fSsx4L.mjs";
|
|
4
|
+
import { DataTag, QueryClient } from "@tanstack/query-core";
|
|
5
|
+
//#region src/tanstack/adapter.d.ts
|
|
6
|
+
type Init = {
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
};
|
|
9
|
+
/** What `tanstackApi` returns: every slice the factories use, bound to one resource. */
|
|
10
|
+
interface TanstackApi<Read, Create, Update> {
|
|
11
|
+
list(query: CrudQuery | undefined, init?: Init): Promise<Read[]>;
|
|
12
|
+
get(id: number, init?: Init): Promise<Read>;
|
|
13
|
+
stats(query: CrudQuery | undefined, init?: Init): Promise<JsonObject>;
|
|
14
|
+
predict(query: CrudQuery | undefined, init?: Init): Promise<JsonObject>;
|
|
15
|
+
revisions(id: number, init?: Init): Promise<JsonObject[]>;
|
|
16
|
+
/** The manifest the client already loaded (the client's loader takes no signal). */
|
|
17
|
+
manifest(init?: Init): Promise<Manifest>;
|
|
18
|
+
create(body: Create): Promise<Read>;
|
|
19
|
+
update(id: number, patch: Update, init?: {
|
|
20
|
+
ifMatch?: string;
|
|
21
|
+
}): Promise<Read>;
|
|
22
|
+
delete(id: number): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A query is the client's `ListOptions`, `StatsOptions` or `PredictOptions`
|
|
26
|
+
* without `signal` (`{ filters, sort, skip, limit, ... }`); TanStack's signal
|
|
27
|
+
* is added per call.
|
|
28
|
+
*/
|
|
29
|
+
export declare function tanstackApi<Read extends object, Create extends object, Update extends object>(client: CrudClient, resource: ResourceClient<Read, Create, Update>): TanstackApi<Read, Create, Update>;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/tanstack/index.d.ts
|
|
32
|
+
/** A query object as the client's query builder takes it. */
|
|
33
|
+
export type CrudQuery = Readonly<Record<string, unknown>>;
|
|
34
|
+
type Key = readonly unknown[];
|
|
35
|
+
/**
|
|
36
|
+
* Who a query belongs to: the client's stable identity and the caller scope
|
|
37
|
+
* (`await client.namespace()`). Two clients, or two callers, that share one
|
|
38
|
+
* `QueryClient` never see each other's cached data.
|
|
39
|
+
*/
|
|
40
|
+
export interface CrudNamespace {
|
|
41
|
+
readonly client: string;
|
|
42
|
+
readonly scope: string;
|
|
43
|
+
}
|
|
44
|
+
/** The kinds that sit at `key[4]`. */
|
|
45
|
+
export type CrudKind = "list" | "record" | "stats" | "predict" | "revisions";
|
|
46
|
+
/** Which queries a {@link CrudKeys.filter} matches; every member narrows. */
|
|
47
|
+
export interface CrudKeyFilter {
|
|
48
|
+
readonly client: string;
|
|
49
|
+
/** Omit for every resource of the client (including manifests). */
|
|
50
|
+
readonly resource?: string;
|
|
51
|
+
/** Omit for every caller scope. */
|
|
52
|
+
readonly scope?: string;
|
|
53
|
+
/** Skips this caller scope (every other scope matches). */
|
|
54
|
+
readonly exceptScope?: string;
|
|
55
|
+
/** Omit for every kind. */
|
|
56
|
+
readonly kinds?: readonly CrudKind[];
|
|
57
|
+
/** A record id; matches only the `record` and `revisions` keys of that record. */
|
|
58
|
+
readonly id?: number;
|
|
59
|
+
}
|
|
60
|
+
/** Spread into `invalidateQueries`, `removeQueries` or `cancelQueries`. */
|
|
61
|
+
export type CrudKeyMatcher = {
|
|
62
|
+
readonly queryKey: readonly unknown[];
|
|
63
|
+
} | {
|
|
64
|
+
readonly predicate: (query: {
|
|
65
|
+
readonly queryKey: readonly unknown[];
|
|
66
|
+
}) => boolean;
|
|
67
|
+
};
|
|
68
|
+
/** The key factory type. */
|
|
69
|
+
export interface CrudKeys {
|
|
70
|
+
readonly all: readonly ["crud"];
|
|
71
|
+
/** Everything of one client. */
|
|
72
|
+
client(client: string): readonly ["crud", string];
|
|
73
|
+
/** Everything of one resource on one client, every scope and kind. */
|
|
74
|
+
resource(client: string, resource: string): readonly ["crud", string, string];
|
|
75
|
+
/** All lists of one resource and scope, plain and infinite. */
|
|
76
|
+
lists(ns: CrudNamespace, resource: string): readonly ["crud", string, string, string, "list"];
|
|
77
|
+
list(ns: CrudNamespace, resource: string, query?: CrudQuery): Key;
|
|
78
|
+
infiniteList(ns: CrudNamespace, resource: string, query: CrudQuery, limit: number): Key;
|
|
79
|
+
/** All record queries of one resource and scope. */
|
|
80
|
+
records(ns: CrudNamespace, resource: string): readonly ["crud", string, string, string, "record"];
|
|
81
|
+
record(ns: CrudNamespace, resource: string, id: number): readonly ["crud", string, string, string, "record", number];
|
|
82
|
+
stats(ns: CrudNamespace, resource: string, query?: CrudQuery): Key;
|
|
83
|
+
predict(ns: CrudNamespace, resource: string, query?: CrudQuery): Key;
|
|
84
|
+
revisions(ns: CrudNamespace, resource: string, id: number): readonly ["crud", string, string, string, "revisions", number];
|
|
85
|
+
/** The manifest of one caller scope. */
|
|
86
|
+
manifest(ns: CrudNamespace): readonly ["crud", string, "_manifest", string];
|
|
87
|
+
/**
|
|
88
|
+
* A matcher for `invalidateQueries({ ...crudKeys.filter(f) })`. A client
|
|
89
|
+
* or resource filter is one key prefix; narrowing by scope, kind or id
|
|
90
|
+
* (which sit after the resource) uses a predicate.
|
|
91
|
+
*/
|
|
92
|
+
filter(filter: CrudKeyFilter): CrudKeyMatcher;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The key factory. Keys are
|
|
96
|
+
* `["crud", client, resource, scope, kind, ...]` (the manifest is
|
|
97
|
+
* `["crud", client, "_manifest", scope]`), so a prefix invalidates by client
|
|
98
|
+
* or by resource on one client; `resource` is the manifest name, or
|
|
99
|
+
* `name@apiVersion` for a pinned version (a different string is a different
|
|
100
|
+
* resource: pass the same one to the invalidate helpers). `_manifest` is
|
|
101
|
+
* reserved; a scope-only filter also matches the scope's manifest.
|
|
102
|
+
*/
|
|
103
|
+
export declare const crudKeys: CrudKeys;
|
|
104
|
+
/** What a query function receives from TanStack Query (a subset). */
|
|
105
|
+
export interface QueryContext {
|
|
106
|
+
readonly signal: AbortSignal;
|
|
107
|
+
}
|
|
108
|
+
export interface QueryOptionsInit {
|
|
109
|
+
/** TanStack's `staleTime` in milliseconds. Default 0 (the SDK cache revalidates for free). */
|
|
110
|
+
readonly staleTime?: number;
|
|
111
|
+
}
|
|
112
|
+
/** Plain query options with a typed key; spread into `useQuery`/`fetchQuery`. */
|
|
113
|
+
export interface CrudQueryOptions<T> {
|
|
114
|
+
readonly queryKey: DataTag<Key, T, Error>;
|
|
115
|
+
readonly queryFn: (context: QueryContext) => Promise<T>;
|
|
116
|
+
readonly staleTime: number;
|
|
117
|
+
}
|
|
118
|
+
export interface CrudInfiniteQueryOptions<T> {
|
|
119
|
+
readonly queryKey: DataTag<Key, {
|
|
120
|
+
pages: T[][];
|
|
121
|
+
pageParams: number[];
|
|
122
|
+
}, Error>;
|
|
123
|
+
readonly queryFn: (context: QueryContext & {
|
|
124
|
+
pageParam: number;
|
|
125
|
+
}) => Promise<T[]>;
|
|
126
|
+
readonly initialPageParam: number;
|
|
127
|
+
/** `skip + limit` unless the page was short. */
|
|
128
|
+
readonly getNextPageParam: (lastPage: T[], allPages: T[][], lastPageParam: number) => number | undefined;
|
|
129
|
+
readonly staleTime: number;
|
|
130
|
+
}
|
|
131
|
+
/** The slices of a resource API the factories call (structural: TS1's resource client fits). */
|
|
132
|
+
export interface ListApi<T> {
|
|
133
|
+
list(query: CrudQuery | undefined, init?: {
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
}): Promise<T[]>;
|
|
136
|
+
}
|
|
137
|
+
export interface RecordApi<T> {
|
|
138
|
+
get(id: number, init?: {
|
|
139
|
+
signal?: AbortSignal;
|
|
140
|
+
}): Promise<T>;
|
|
141
|
+
}
|
|
142
|
+
export interface StatsApi<S> {
|
|
143
|
+
stats(query: CrudQuery | undefined, init?: {
|
|
144
|
+
signal?: AbortSignal;
|
|
145
|
+
}): Promise<S>;
|
|
146
|
+
}
|
|
147
|
+
export interface PredictApi<P> {
|
|
148
|
+
predict(query: CrudQuery | undefined, init?: {
|
|
149
|
+
signal?: AbortSignal;
|
|
150
|
+
}): Promise<P>;
|
|
151
|
+
}
|
|
152
|
+
export interface RevisionsApi<R> {
|
|
153
|
+
revisions(id: number, init?: {
|
|
154
|
+
signal?: AbortSignal;
|
|
155
|
+
}): Promise<R[]>;
|
|
156
|
+
}
|
|
157
|
+
export interface ManifestApi<M> {
|
|
158
|
+
manifest(init?: {
|
|
159
|
+
signal?: AbortSignal;
|
|
160
|
+
}): Promise<M>;
|
|
161
|
+
}
|
|
162
|
+
export interface CreateApi<T, C> {
|
|
163
|
+
create(body: C): Promise<T>;
|
|
164
|
+
}
|
|
165
|
+
export interface UpdateApi<T, P> {
|
|
166
|
+
update(id: number, patch: P, init?: {
|
|
167
|
+
ifMatch?: string;
|
|
168
|
+
}): Promise<T>;
|
|
169
|
+
}
|
|
170
|
+
export interface DeleteApi {
|
|
171
|
+
delete(id: number): Promise<unknown>;
|
|
172
|
+
}
|
|
173
|
+
export declare function listOptions<T>(ns: CrudNamespace, resource: string, api: ListApi<T>, query?: CrudQuery, init?: QueryOptionsInit): CrudQueryOptions<T[]>;
|
|
174
|
+
export declare function recordOptions<T>(ns: CrudNamespace, resource: string, api: RecordApi<T>, id: number, init?: QueryOptionsInit): CrudQueryOptions<T>;
|
|
175
|
+
export declare function statsOptions<S>(ns: CrudNamespace, resource: string, api: StatsApi<S>, query?: CrudQuery, init?: QueryOptionsInit): CrudQueryOptions<S>;
|
|
176
|
+
export declare function predictOptions<P>(ns: CrudNamespace, resource: string, api: PredictApi<P>, query?: CrudQuery, init?: QueryOptionsInit): CrudQueryOptions<P>;
|
|
177
|
+
export declare function revisionsOptions<R>(ns: CrudNamespace, resource: string, api: RevisionsApi<R>, id: number, init?: QueryOptionsInit): CrudQueryOptions<R[]>;
|
|
178
|
+
export declare function manifestOptions<M>(ns: CrudNamespace, api: ManifestApi<M>, init?: QueryOptionsInit): CrudQueryOptions<M>;
|
|
179
|
+
export interface InfiniteListInit extends QueryOptionsInit {
|
|
180
|
+
/** Page size; default 100 (the server's default). */
|
|
181
|
+
readonly limit?: number;
|
|
182
|
+
/**
|
|
183
|
+
* Keys of the paging parameters in the query object handed to `api.list`.
|
|
184
|
+
* Default `skip`/`limit`, which is what `tanstackApi` (the client's
|
|
185
|
+
* `ListOptions`) reads; change them only for a hand-written `ListApi`. The
|
|
186
|
+
* client maps them to the manifest's `skip_param`/`limit_param` itself.
|
|
187
|
+
*/
|
|
188
|
+
readonly skipKey?: string;
|
|
189
|
+
readonly limitKey?: string;
|
|
190
|
+
}
|
|
191
|
+
/** Offset paging: the next page starts at `skip + limit`, unless the last page was short. */
|
|
192
|
+
export declare function infiniteListOptions<T>(ns: CrudNamespace, resource: string, api: ListApi<T>, query?: CrudQuery, init?: InfiniteListInit): CrudInfiniteQueryOptions<T>;
|
|
193
|
+
export type Invalidator = Pick<QueryClient, "invalidateQueries" | "removeQueries" | "setQueryData">;
|
|
194
|
+
/** One resource of one client; `resource` is the string used in its keys. */
|
|
195
|
+
export interface CrudTarget {
|
|
196
|
+
readonly client: string;
|
|
197
|
+
readonly resource: string;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Invalidates the list, stats and predict queries of `resource` on `client`,
|
|
201
|
+
* for every caller scope (a write changes what every caller sees).
|
|
202
|
+
*/
|
|
203
|
+
export declare function invalidateCollections(queryClient: Pick<QueryClient, "invalidateQueries">, { client, resource }: CrudTarget): Promise<void>;
|
|
204
|
+
/** Invalidates everything cached for `resource` on `client`: one key prefix. */
|
|
205
|
+
export declare function invalidateResource(queryClient: Pick<QueryClient, "invalidateQueries">, { client, resource }: CrudTarget): Promise<void>;
|
|
206
|
+
/** Invalidates one record's `record` and `revisions` queries, for every scope. */
|
|
207
|
+
export declare function invalidateRecord(queryClient: Pick<QueryClient, "invalidateQueries">, { client, resource }: CrudTarget, id: number): Promise<void>;
|
|
208
|
+
export interface CreateMutationOptions<T, C> {
|
|
209
|
+
readonly mutationKey: readonly ["crud", string, string, string, "create"];
|
|
210
|
+
readonly mutationFn: (body: C) => Promise<T>;
|
|
211
|
+
readonly onSuccess: () => Promise<void>;
|
|
212
|
+
}
|
|
213
|
+
export interface UpdateMutationOptions<T, P> {
|
|
214
|
+
readonly mutationKey: readonly ["crud", string, string, string, "update"];
|
|
215
|
+
readonly mutationFn: (variables: UpdateVariables<P>) => Promise<T>;
|
|
216
|
+
readonly onSuccess: (record: T, variables: UpdateVariables<P>) => Promise<void>;
|
|
217
|
+
}
|
|
218
|
+
export interface DeleteMutationOptions {
|
|
219
|
+
readonly mutationKey: readonly ["crud", string, string, string, "delete"];
|
|
220
|
+
readonly mutationFn: (id: number) => Promise<unknown>;
|
|
221
|
+
readonly onSuccess: (result: unknown, id: number) => Promise<void>;
|
|
222
|
+
}
|
|
223
|
+
/** Create: invalidate lists, stats and predict. */
|
|
224
|
+
export declare function createMutationOptions<T, C>(queryClient: Invalidator, ns: CrudNamespace, resource: string, api: CreateApi<T, C>): CreateMutationOptions<T, C>;
|
|
225
|
+
export interface UpdateVariables<P> {
|
|
226
|
+
readonly id: number;
|
|
227
|
+
readonly patch: P;
|
|
228
|
+
/** Sent as `If-Match`; a stale one fails with 412. */
|
|
229
|
+
readonly ifMatch?: string;
|
|
230
|
+
}
|
|
231
|
+
/** Update: `setQueryData` from the PATCH response, invalidate lists, stats, predict and revisions. */
|
|
232
|
+
export declare function updateMutationOptions<T, P>(queryClient: Invalidator, ns: CrudNamespace, resource: string, api: UpdateApi<T, P>): UpdateMutationOptions<T, P>;
|
|
233
|
+
/** Delete: drop the record query, invalidate lists, stats, predict and revisions. */
|
|
234
|
+
export declare function deleteMutationOptions(queryClient: Invalidator, ns: CrudNamespace, resource: string, api: DeleteApi): DeleteMutationOptions;
|
|
235
|
+
/**
|
|
236
|
+
* Invalidate TanStack queries from the resource's event stream. Events
|
|
237
|
+
* invalidate and never apply snapshots: `create` invalidates lists, stats and
|
|
238
|
+
* predict; `update`/`delete`/`restore` also the record and its revisions; a
|
|
239
|
+
* reconnect (or an event with no usable record id) invalidates the resource.
|
|
240
|
+
* Returns the unsubscribe.
|
|
241
|
+
*/
|
|
242
|
+
export declare function bridgeEvents(queryClient: Pick<QueryClient, "invalidateQueries">, hub: EventHub, target: CrudTarget): () => void;
|
|
243
|
+
//#endregion
|
|
244
|
+
export { type CachePolicy, type CrudEvent, type EventHandlers, type EventHub, type EventHubOptions, type EventsConfig, type ManifestCacheHints, type SubscribeEventsOptions, type TanstackApi, getEventHub, subscribeEvents, tanstackCachePolicy };
|
|
245
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { i as tanstackCachePolicy, n as subscribeEvents, t as getEventHub } from "../events-Cqj9Rbon.mjs";
|
|
2
|
+
//#region src/tanstack/adapter.ts
|
|
3
|
+
/**
|
|
4
|
+
* A query is the client's `ListOptions`, `StatsOptions` or `PredictOptions`
|
|
5
|
+
* without `signal` (`{ filters, sort, skip, limit, ... }`); TanStack's signal
|
|
6
|
+
* is added per call.
|
|
7
|
+
*/
|
|
8
|
+
function tanstackApi(client, resource) {
|
|
9
|
+
return {
|
|
10
|
+
list: (query, init) => resource.list({
|
|
11
|
+
...query,
|
|
12
|
+
...init
|
|
13
|
+
}),
|
|
14
|
+
get: (id, init) => resource.get(id, init),
|
|
15
|
+
stats: (query, init) => resource.stats({
|
|
16
|
+
...query,
|
|
17
|
+
...init
|
|
18
|
+
}),
|
|
19
|
+
predict: (query, init) => resource.predict({
|
|
20
|
+
...query,
|
|
21
|
+
...init
|
|
22
|
+
}),
|
|
23
|
+
revisions: (id, init) => resource.revisions(id, init),
|
|
24
|
+
manifest: () => client.manifest(),
|
|
25
|
+
create: (body) => resource.create(body),
|
|
26
|
+
update: (id, patch, init) => resource.update(id, patch, init),
|
|
27
|
+
delete: (id) => resource.delete(id)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/tanstack/index.ts
|
|
32
|
+
/**
|
|
33
|
+
* The key factory. Keys are
|
|
34
|
+
* `["crud", client, resource, scope, kind, ...]` (the manifest is
|
|
35
|
+
* `["crud", client, "_manifest", scope]`), so a prefix invalidates by client
|
|
36
|
+
* or by resource on one client; `resource` is the manifest name, or
|
|
37
|
+
* `name@apiVersion` for a pinned version (a different string is a different
|
|
38
|
+
* resource: pass the same one to the invalidate helpers). `_manifest` is
|
|
39
|
+
* reserved; a scope-only filter also matches the scope's manifest.
|
|
40
|
+
*/
|
|
41
|
+
const crudKeys = {
|
|
42
|
+
all: ["crud"],
|
|
43
|
+
client: (client) => ["crud", client],
|
|
44
|
+
resource: (client, resource) => [
|
|
45
|
+
"crud",
|
|
46
|
+
client,
|
|
47
|
+
resource
|
|
48
|
+
],
|
|
49
|
+
lists: (ns, resource) => [
|
|
50
|
+
"crud",
|
|
51
|
+
ns.client,
|
|
52
|
+
resource,
|
|
53
|
+
ns.scope,
|
|
54
|
+
"list"
|
|
55
|
+
],
|
|
56
|
+
list: (ns, resource, query) => query === void 0 ? crudKeys.lists(ns, resource) : [...crudKeys.lists(ns, resource), query],
|
|
57
|
+
infiniteList: (ns, resource, query, limit) => [
|
|
58
|
+
...crudKeys.lists(ns, resource),
|
|
59
|
+
"infinite",
|
|
60
|
+
query,
|
|
61
|
+
limit
|
|
62
|
+
],
|
|
63
|
+
records: (ns, resource) => [
|
|
64
|
+
"crud",
|
|
65
|
+
ns.client,
|
|
66
|
+
resource,
|
|
67
|
+
ns.scope,
|
|
68
|
+
"record"
|
|
69
|
+
],
|
|
70
|
+
record: (ns, resource, id) => [...crudKeys.records(ns, resource), id],
|
|
71
|
+
stats: (ns, resource, query) => query === void 0 ? [
|
|
72
|
+
"crud",
|
|
73
|
+
ns.client,
|
|
74
|
+
resource,
|
|
75
|
+
ns.scope,
|
|
76
|
+
"stats"
|
|
77
|
+
] : [
|
|
78
|
+
"crud",
|
|
79
|
+
ns.client,
|
|
80
|
+
resource,
|
|
81
|
+
ns.scope,
|
|
82
|
+
"stats",
|
|
83
|
+
query
|
|
84
|
+
],
|
|
85
|
+
predict: (ns, resource, query) => query === void 0 ? [
|
|
86
|
+
"crud",
|
|
87
|
+
ns.client,
|
|
88
|
+
resource,
|
|
89
|
+
ns.scope,
|
|
90
|
+
"predict"
|
|
91
|
+
] : [
|
|
92
|
+
"crud",
|
|
93
|
+
ns.client,
|
|
94
|
+
resource,
|
|
95
|
+
ns.scope,
|
|
96
|
+
"predict",
|
|
97
|
+
query
|
|
98
|
+
],
|
|
99
|
+
revisions: (ns, resource, id) => [
|
|
100
|
+
"crud",
|
|
101
|
+
ns.client,
|
|
102
|
+
resource,
|
|
103
|
+
ns.scope,
|
|
104
|
+
"revisions",
|
|
105
|
+
id
|
|
106
|
+
],
|
|
107
|
+
manifest: (ns) => [
|
|
108
|
+
"crud",
|
|
109
|
+
ns.client,
|
|
110
|
+
"_manifest",
|
|
111
|
+
ns.scope
|
|
112
|
+
],
|
|
113
|
+
filter: (filter) => {
|
|
114
|
+
const { client, resource, scope, exceptScope, kinds, id } = filter;
|
|
115
|
+
if (scope === void 0 && exceptScope === void 0 && kinds === void 0 && id === void 0) return { queryKey: resource === void 0 ? crudKeys.client(client) : crudKeys.resource(client, resource) };
|
|
116
|
+
return { predicate: ({ queryKey: key }) => key[0] === "crud" && key[1] === client && (resource === void 0 || key[2] === resource) && (scope === void 0 || key[3] === scope) && (exceptScope === void 0 || key[3] !== exceptScope) && (kinds === void 0 || kinds.includes(key[4])) && (id === void 0 || (key[4] === "record" || key[4] === "revisions") && key[5] === id) };
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function tag(key) {
|
|
120
|
+
return key;
|
|
121
|
+
}
|
|
122
|
+
function listOptions(ns, resource, api, query, init = {}) {
|
|
123
|
+
return {
|
|
124
|
+
queryKey: tag(crudKeys.list(ns, resource, query)),
|
|
125
|
+
queryFn: ({ signal }) => api.list(query, { signal }),
|
|
126
|
+
staleTime: init.staleTime ?? 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function recordOptions(ns, resource, api, id, init = {}) {
|
|
130
|
+
return {
|
|
131
|
+
queryKey: tag(crudKeys.record(ns, resource, id)),
|
|
132
|
+
queryFn: ({ signal }) => api.get(id, { signal }),
|
|
133
|
+
staleTime: init.staleTime ?? 0
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function statsOptions(ns, resource, api, query, init = {}) {
|
|
137
|
+
return {
|
|
138
|
+
queryKey: tag(crudKeys.stats(ns, resource, query)),
|
|
139
|
+
queryFn: ({ signal }) => api.stats(query, { signal }),
|
|
140
|
+
staleTime: init.staleTime ?? 0
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function predictOptions(ns, resource, api, query, init = {}) {
|
|
144
|
+
return {
|
|
145
|
+
queryKey: tag(crudKeys.predict(ns, resource, query)),
|
|
146
|
+
queryFn: ({ signal }) => api.predict(query, { signal }),
|
|
147
|
+
staleTime: init.staleTime ?? 0
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function revisionsOptions(ns, resource, api, id, init = {}) {
|
|
151
|
+
return {
|
|
152
|
+
queryKey: tag(crudKeys.revisions(ns, resource, id)),
|
|
153
|
+
queryFn: ({ signal }) => api.revisions(id, { signal }),
|
|
154
|
+
staleTime: init.staleTime ?? 0
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function manifestOptions(ns, api, init = {}) {
|
|
158
|
+
return {
|
|
159
|
+
queryKey: tag(crudKeys.manifest(ns)),
|
|
160
|
+
queryFn: ({ signal }) => api.manifest({ signal }),
|
|
161
|
+
staleTime: init.staleTime ?? 0
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Offset paging: the next page starts at `skip + limit`, unless the last page was short. */
|
|
165
|
+
function infiniteListOptions(ns, resource, api, query = {}, init = {}) {
|
|
166
|
+
const limit = init.limit ?? 100;
|
|
167
|
+
const skipKey = init.skipKey ?? "skip";
|
|
168
|
+
const limitKey = init.limitKey ?? "limit";
|
|
169
|
+
return {
|
|
170
|
+
queryKey: tag(crudKeys.infiniteList(ns, resource, query, limit)),
|
|
171
|
+
queryFn: ({ signal, pageParam }) => api.list({
|
|
172
|
+
...query,
|
|
173
|
+
[skipKey]: pageParam,
|
|
174
|
+
[limitKey]: limit
|
|
175
|
+
}, { signal }),
|
|
176
|
+
initialPageParam: 0,
|
|
177
|
+
getNextPageParam: (lastPage, _all, lastPageParam) => lastPage.length < limit ? void 0 : lastPageParam + limit,
|
|
178
|
+
staleTime: init.staleTime ?? 0
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const COLLECTION_KINDS = [
|
|
182
|
+
"list",
|
|
183
|
+
"stats",
|
|
184
|
+
"predict"
|
|
185
|
+
];
|
|
186
|
+
/**
|
|
187
|
+
* Invalidates the list, stats and predict queries of `resource` on `client`,
|
|
188
|
+
* for every caller scope (a write changes what every caller sees).
|
|
189
|
+
*/
|
|
190
|
+
function invalidateCollections(queryClient, { client, resource }) {
|
|
191
|
+
return queryClient.invalidateQueries(crudKeys.filter({
|
|
192
|
+
client,
|
|
193
|
+
resource,
|
|
194
|
+
kinds: COLLECTION_KINDS
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
/** Invalidates everything cached for `resource` on `client`: one key prefix. */
|
|
198
|
+
function invalidateResource(queryClient, { client, resource }) {
|
|
199
|
+
return queryClient.invalidateQueries(crudKeys.filter({
|
|
200
|
+
client,
|
|
201
|
+
resource
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
/** Invalidates one record's `record` and `revisions` queries, for every scope. */
|
|
205
|
+
function invalidateRecord(queryClient, { client, resource }, id) {
|
|
206
|
+
return queryClient.invalidateQueries(crudKeys.filter({
|
|
207
|
+
client,
|
|
208
|
+
resource,
|
|
209
|
+
kinds: ["record", "revisions"],
|
|
210
|
+
id
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
/** Create: invalidate lists, stats and predict. */
|
|
214
|
+
function createMutationOptions(queryClient, ns, resource, api) {
|
|
215
|
+
return {
|
|
216
|
+
mutationKey: [
|
|
217
|
+
"crud",
|
|
218
|
+
ns.client,
|
|
219
|
+
resource,
|
|
220
|
+
ns.scope,
|
|
221
|
+
"create"
|
|
222
|
+
],
|
|
223
|
+
mutationFn: (body) => api.create(body),
|
|
224
|
+
onSuccess: () => invalidateCollections(queryClient, {
|
|
225
|
+
client: ns.client,
|
|
226
|
+
resource
|
|
227
|
+
})
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** Update: `setQueryData` from the PATCH response, invalidate lists, stats, predict and revisions. */
|
|
231
|
+
function updateMutationOptions(queryClient, ns, resource, api) {
|
|
232
|
+
return {
|
|
233
|
+
mutationKey: [
|
|
234
|
+
"crud",
|
|
235
|
+
ns.client,
|
|
236
|
+
resource,
|
|
237
|
+
ns.scope,
|
|
238
|
+
"update"
|
|
239
|
+
],
|
|
240
|
+
mutationFn: ({ id, patch, ifMatch }) => api.update(id, patch, ifMatch === void 0 ? {} : { ifMatch }),
|
|
241
|
+
onSuccess: async (record, { id }) => {
|
|
242
|
+
queryClient.setQueryData(crudKeys.record(ns, resource, id), record);
|
|
243
|
+
await Promise.all([
|
|
244
|
+
invalidateCollections(queryClient, {
|
|
245
|
+
client: ns.client,
|
|
246
|
+
resource
|
|
247
|
+
}),
|
|
248
|
+
queryClient.invalidateQueries(crudKeys.filter({
|
|
249
|
+
client: ns.client,
|
|
250
|
+
resource,
|
|
251
|
+
exceptScope: ns.scope,
|
|
252
|
+
kinds: ["record"],
|
|
253
|
+
id
|
|
254
|
+
})),
|
|
255
|
+
queryClient.invalidateQueries(crudKeys.filter({
|
|
256
|
+
client: ns.client,
|
|
257
|
+
resource,
|
|
258
|
+
kinds: ["revisions"],
|
|
259
|
+
id
|
|
260
|
+
}))
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Delete: drop the record query, invalidate lists, stats, predict and revisions. */
|
|
266
|
+
function deleteMutationOptions(queryClient, ns, resource, api) {
|
|
267
|
+
return {
|
|
268
|
+
mutationKey: [
|
|
269
|
+
"crud",
|
|
270
|
+
ns.client,
|
|
271
|
+
resource,
|
|
272
|
+
ns.scope,
|
|
273
|
+
"delete"
|
|
274
|
+
],
|
|
275
|
+
mutationFn: (id) => api.delete(id),
|
|
276
|
+
onSuccess: async (_result, id) => {
|
|
277
|
+
queryClient.removeQueries(crudKeys.filter({
|
|
278
|
+
client: ns.client,
|
|
279
|
+
resource,
|
|
280
|
+
kinds: ["record"],
|
|
281
|
+
id
|
|
282
|
+
}));
|
|
283
|
+
await Promise.all([invalidateCollections(queryClient, {
|
|
284
|
+
client: ns.client,
|
|
285
|
+
resource
|
|
286
|
+
}), queryClient.invalidateQueries(crudKeys.filter({
|
|
287
|
+
client: ns.client,
|
|
288
|
+
resource,
|
|
289
|
+
kinds: ["revisions"],
|
|
290
|
+
id
|
|
291
|
+
}))]);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Invalidate TanStack queries from the resource's event stream. Events
|
|
297
|
+
* invalidate and never apply snapshots: `create` invalidates lists, stats and
|
|
298
|
+
* predict; `update`/`delete`/`restore` also the record and its revisions; a
|
|
299
|
+
* reconnect (or an event with no usable record id) invalidates the resource.
|
|
300
|
+
* Returns the unsubscribe.
|
|
301
|
+
*/
|
|
302
|
+
function bridgeEvents(queryClient, hub, target) {
|
|
303
|
+
return hub.subscribe({
|
|
304
|
+
onEvent: async (event) => {
|
|
305
|
+
const pending = [invalidateCollections(queryClient, target)];
|
|
306
|
+
if (event.action !== "create") pending.push(invalidateRecord(queryClient, target, event.recordId));
|
|
307
|
+
await Promise.all(pending);
|
|
308
|
+
},
|
|
309
|
+
onReset: () => invalidateResource(queryClient, target)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
export { bridgeEvents, createMutationOptions, crudKeys, deleteMutationOptions, getEventHub, infiniteListOptions, invalidateCollections, invalidateRecord, invalidateResource, listOptions, manifestOptions, predictOptions, recordOptions, revisionsOptions, statsOptions, subscribeEvents, tanstackApi, tanstackCachePolicy, updateMutationOptions };
|
|
314
|
+
|
|
315
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/tanstack/adapter.ts","../../src/tanstack/index.ts"],"sourcesContent":["// Adapts TS1's `ResourceClient` (options-object methods) to the structural\n// slices the `./tanstack` factories take (`list(query, { signal })`, ...).\nimport type { CrudClient, ResourceClient } from \"../client.ts\";\nimport type { JsonObject, Manifest } from \"../manifest.ts\";\nimport type { CrudQuery } from \"./index.ts\";\n\ntype Init = { signal?: AbortSignal };\n\n/** What `tanstackApi` returns: every slice the factories use, bound to one resource. */\nexport interface TanstackApi<Read, Create, Update> {\n list(query: CrudQuery | undefined, init?: Init): Promise<Read[]>;\n get(id: number, init?: Init): Promise<Read>;\n stats(query: CrudQuery | undefined, init?: Init): Promise<JsonObject>;\n predict(query: CrudQuery | undefined, init?: Init): Promise<JsonObject>;\n revisions(id: number, init?: Init): Promise<JsonObject[]>;\n /** The manifest the client already loaded (the client's loader takes no signal). */\n manifest(init?: Init): Promise<Manifest>;\n create(body: Create): Promise<Read>;\n update(id: number, patch: Update, init?: { ifMatch?: string }): Promise<Read>;\n delete(id: number): Promise<void>;\n}\n\n/**\n * A query is the client's `ListOptions`, `StatsOptions` or `PredictOptions`\n * without `signal` (`{ filters, sort, skip, limit, ... }`); TanStack's signal\n * is added per call.\n */\nexport function tanstackApi<\n Read extends object,\n Create extends object,\n Update extends object,\n>(\n client: CrudClient,\n resource: ResourceClient<Read, Create, Update>,\n): TanstackApi<Read, Create, Update> {\n return {\n list: (query, init) => resource.list({ ...query, ...init }),\n get: (id, init) => resource.get(id, init),\n stats: (query, init) => resource.stats({ ...query, ...init }),\n predict: (query, init) => resource.predict({ ...query, ...init }),\n revisions: (id, init) => resource.revisions(id, init),\n manifest: () => client.manifest(),\n create: (body) => resource.create(body),\n update: (id, patch, init) => resource.update(id, patch, init),\n delete: (id) => resource.delete(id),\n };\n}\n","// Entry point `./tanstack`: `queryOptions` factories, key factory and the\n// invalidation bridge (TS2). Needs the optional peer `@tanstack/query-core`\n// (types only: nothing here imports it at runtime). query-core has no\n// `queryOptions`, so the factories return plain `{ queryKey, queryFn,\n// staleTime }` objects with `queryKey` tagged through `DataTag`; the React\n// wrappers live in lib-crud-views-ts.\nimport type { DataTag, InfiniteData, QueryClient } from \"@tanstack/query-core\";\nimport type { EventHub } from \"../events.ts\";\n\nexport { tanstackApi } from \"./adapter.ts\";\nexport type { TanstackApi } from \"./adapter.ts\";\nexport { tanstackCachePolicy } from \"../cache/policy.ts\";\nexport type { CachePolicy, ManifestCacheHints } from \"../cache/policy.ts\";\nexport { getEventHub, subscribeEvents } from \"../events.ts\";\nexport type {\n EventHandlers,\n EventHub,\n EventHubOptions,\n EventsConfig,\n SubscribeEventsOptions,\n} from \"../events.ts\";\nexport type { CrudEvent } from \"../interfaces.ts\";\n\n/** A query object as the client's query builder takes it. */\nexport type CrudQuery = Readonly<Record<string, unknown>>;\n\ntype Key = readonly unknown[];\n\n/**\n * Who a query belongs to: the client's stable identity and the caller scope\n * (`await client.namespace()`). Two clients, or two callers, that share one\n * `QueryClient` never see each other's cached data.\n */\nexport interface CrudNamespace {\n readonly client: string;\n readonly scope: string;\n}\n\n/** The kinds that sit at `key[4]`. */\nexport type CrudKind = \"list\" | \"record\" | \"stats\" | \"predict\" | \"revisions\";\n\n/** Which queries a {@link CrudKeys.filter} matches; every member narrows. */\nexport interface CrudKeyFilter {\n readonly client: string;\n /** Omit for every resource of the client (including manifests). */\n readonly resource?: string;\n /** Omit for every caller scope. */\n readonly scope?: string;\n /** Skips this caller scope (every other scope matches). */\n readonly exceptScope?: string;\n /** Omit for every kind. */\n readonly kinds?: readonly CrudKind[];\n /** A record id; matches only the `record` and `revisions` keys of that record. */\n readonly id?: number;\n}\n\n/** Spread into `invalidateQueries`, `removeQueries` or `cancelQueries`. */\nexport type CrudKeyMatcher =\n | { readonly queryKey: readonly unknown[] }\n | {\n readonly predicate: (query: {\n readonly queryKey: readonly unknown[];\n }) => boolean;\n };\n\n/** The key factory type. */\nexport interface CrudKeys {\n readonly all: readonly [\"crud\"];\n /** Everything of one client. */\n client(client: string): readonly [\"crud\", string];\n /** Everything of one resource on one client, every scope and kind. */\n resource(client: string, resource: string): readonly [\"crud\", string, string];\n /** All lists of one resource and scope, plain and infinite. */\n lists(\n ns: CrudNamespace,\n resource: string,\n ): readonly [\"crud\", string, string, string, \"list\"];\n list(ns: CrudNamespace, resource: string, query?: CrudQuery): Key;\n infiniteList(\n ns: CrudNamespace,\n resource: string,\n query: CrudQuery,\n limit: number,\n ): Key;\n /** All record queries of one resource and scope. */\n records(\n ns: CrudNamespace,\n resource: string,\n ): readonly [\"crud\", string, string, string, \"record\"];\n record(\n ns: CrudNamespace,\n resource: string,\n id: number,\n ): readonly [\"crud\", string, string, string, \"record\", number];\n stats(ns: CrudNamespace, resource: string, query?: CrudQuery): Key;\n predict(ns: CrudNamespace, resource: string, query?: CrudQuery): Key;\n revisions(\n ns: CrudNamespace,\n resource: string,\n id: number,\n ): readonly [\"crud\", string, string, string, \"revisions\", number];\n /** The manifest of one caller scope. */\n manifest(ns: CrudNamespace): readonly [\"crud\", string, \"_manifest\", string];\n /**\n * A matcher for `invalidateQueries({ ...crudKeys.filter(f) })`. A client\n * or resource filter is one key prefix; narrowing by scope, kind or id\n * (which sit after the resource) uses a predicate.\n */\n filter(filter: CrudKeyFilter): CrudKeyMatcher;\n}\n\n/**\n * The key factory. Keys are\n * `[\"crud\", client, resource, scope, kind, ...]` (the manifest is\n * `[\"crud\", client, \"_manifest\", scope]`), so a prefix invalidates by client\n * or by resource on one client; `resource` is the manifest name, or\n * `name@apiVersion` for a pinned version (a different string is a different\n * resource: pass the same one to the invalidate helpers). `_manifest` is\n * reserved; a scope-only filter also matches the scope's manifest.\n */\nexport const crudKeys: CrudKeys = {\n all: [\"crud\"],\n client: (client) => [\"crud\", client],\n resource: (client, resource) => [\"crud\", client, resource],\n lists: (ns, resource) => [\"crud\", ns.client, resource, ns.scope, \"list\"],\n list: (ns, resource, query) =>\n query === undefined\n ? crudKeys.lists(ns, resource)\n : [...crudKeys.lists(ns, resource), query],\n infiniteList: (ns, resource, query, limit) => [\n ...crudKeys.lists(ns, resource),\n \"infinite\",\n query,\n limit,\n ],\n records: (ns, resource) => [\"crud\", ns.client, resource, ns.scope, \"record\"],\n record: (ns, resource, id) => [...crudKeys.records(ns, resource), id],\n stats: (ns, resource, query) =>\n query === undefined\n ? [\"crud\", ns.client, resource, ns.scope, \"stats\"]\n : [\"crud\", ns.client, resource, ns.scope, \"stats\", query],\n predict: (ns, resource, query) =>\n query === undefined\n ? [\"crud\", ns.client, resource, ns.scope, \"predict\"]\n : [\"crud\", ns.client, resource, ns.scope, \"predict\", query],\n revisions: (ns, resource, id) => [\n \"crud\",\n ns.client,\n resource,\n ns.scope,\n \"revisions\",\n id,\n ],\n manifest: (ns) => [\"crud\", ns.client, \"_manifest\", ns.scope],\n filter: (filter) => {\n const { client, resource, scope, exceptScope, kinds, id } = filter;\n if (\n scope === undefined &&\n exceptScope === undefined &&\n kinds === undefined &&\n id === undefined\n ) {\n return {\n queryKey:\n resource === undefined\n ? crudKeys.client(client)\n : crudKeys.resource(client, resource),\n };\n }\n return {\n predicate: ({ queryKey: key }) =>\n key[0] === \"crud\" &&\n key[1] === client &&\n (resource === undefined || key[2] === resource) &&\n (scope === undefined || key[3] === scope) &&\n (exceptScope === undefined || key[3] !== exceptScope) &&\n (kinds === undefined || kinds.includes(key[4] as CrudKind)) &&\n (id === undefined ||\n ((key[4] === \"record\" || key[4] === \"revisions\") && key[5] === id)),\n };\n },\n};\n\n/** What a query function receives from TanStack Query (a subset). */\nexport interface QueryContext {\n readonly signal: AbortSignal;\n}\n\nexport interface QueryOptionsInit {\n /** TanStack's `staleTime` in milliseconds. Default 0 (the SDK cache revalidates for free). */\n readonly staleTime?: number;\n}\n\n/** Plain query options with a typed key; spread into `useQuery`/`fetchQuery`. */\nexport interface CrudQueryOptions<T> {\n readonly queryKey: DataTag<Key, T, Error>;\n readonly queryFn: (context: QueryContext) => Promise<T>;\n readonly staleTime: number;\n}\n\nexport interface CrudInfiniteQueryOptions<T> {\n readonly queryKey: DataTag<\n Key,\n { pages: T[][]; pageParams: number[] },\n Error\n >;\n readonly queryFn: (\n context: QueryContext & { pageParam: number },\n ) => Promise<T[]>;\n readonly initialPageParam: number;\n /** `skip + limit` unless the page was short. */\n readonly getNextPageParam: (\n lastPage: T[],\n allPages: T[][],\n lastPageParam: number,\n ) => number | undefined;\n readonly staleTime: number;\n}\n\n/** The slices of a resource API the factories call (structural: TS1's resource client fits). */\nexport interface ListApi<T> {\n list(\n query: CrudQuery | undefined,\n init?: { signal?: AbortSignal },\n ): Promise<T[]>;\n}\nexport interface RecordApi<T> {\n get(id: number, init?: { signal?: AbortSignal }): Promise<T>;\n}\nexport interface StatsApi<S> {\n stats(\n query: CrudQuery | undefined,\n init?: { signal?: AbortSignal },\n ): Promise<S>;\n}\nexport interface PredictApi<P> {\n predict(\n query: CrudQuery | undefined,\n init?: { signal?: AbortSignal },\n ): Promise<P>;\n}\nexport interface RevisionsApi<R> {\n revisions(id: number, init?: { signal?: AbortSignal }): Promise<R[]>;\n}\nexport interface ManifestApi<M> {\n manifest(init?: { signal?: AbortSignal }): Promise<M>;\n}\nexport interface CreateApi<T, C> {\n create(body: C): Promise<T>;\n}\nexport interface UpdateApi<T, P> {\n update(id: number, patch: P, init?: { ifMatch?: string }): Promise<T>;\n}\nexport interface DeleteApi {\n delete(id: number): Promise<unknown>;\n}\n\nfunction tag<T>(key: Key): DataTag<Key, T, Error> {\n return key as DataTag<Key, T, Error>;\n}\n\nexport function listOptions<T>(\n ns: CrudNamespace,\n resource: string,\n api: ListApi<T>,\n query?: CrudQuery,\n init: QueryOptionsInit = {},\n): CrudQueryOptions<T[]> {\n return {\n queryKey: tag(crudKeys.list(ns, resource, query)),\n queryFn: ({ signal }) => api.list(query, { signal }),\n staleTime: init.staleTime ?? 0,\n };\n}\n\nexport function recordOptions<T>(\n ns: CrudNamespace,\n resource: string,\n api: RecordApi<T>,\n id: number,\n init: QueryOptionsInit = {},\n): CrudQueryOptions<T> {\n return {\n queryKey: tag(crudKeys.record(ns, resource, id)),\n queryFn: ({ signal }) => api.get(id, { signal }),\n staleTime: init.staleTime ?? 0,\n };\n}\n\nexport function statsOptions<S>(\n ns: CrudNamespace,\n resource: string,\n api: StatsApi<S>,\n query?: CrudQuery,\n init: QueryOptionsInit = {},\n): CrudQueryOptions<S> {\n return {\n queryKey: tag(crudKeys.stats(ns, resource, query)),\n queryFn: ({ signal }) => api.stats(query, { signal }),\n staleTime: init.staleTime ?? 0,\n };\n}\n\nexport function predictOptions<P>(\n ns: CrudNamespace,\n resource: string,\n api: PredictApi<P>,\n query?: CrudQuery,\n init: QueryOptionsInit = {},\n): CrudQueryOptions<P> {\n return {\n queryKey: tag(crudKeys.predict(ns, resource, query)),\n queryFn: ({ signal }) => api.predict(query, { signal }),\n staleTime: init.staleTime ?? 0,\n };\n}\n\nexport function revisionsOptions<R>(\n ns: CrudNamespace,\n resource: string,\n api: RevisionsApi<R>,\n id: number,\n init: QueryOptionsInit = {},\n): CrudQueryOptions<R[]> {\n return {\n queryKey: tag(crudKeys.revisions(ns, resource, id)),\n queryFn: ({ signal }) => api.revisions(id, { signal }),\n staleTime: init.staleTime ?? 0,\n };\n}\n\nexport function manifestOptions<M>(\n ns: CrudNamespace,\n api: ManifestApi<M>,\n init: QueryOptionsInit = {},\n): CrudQueryOptions<M> {\n return {\n queryKey: tag(crudKeys.manifest(ns)),\n queryFn: ({ signal }) => api.manifest({ signal }),\n staleTime: init.staleTime ?? 0,\n };\n}\n\nexport interface InfiniteListInit extends QueryOptionsInit {\n /** Page size; default 100 (the server's default). */\n readonly limit?: number;\n /**\n * Keys of the paging parameters in the query object handed to `api.list`.\n * Default `skip`/`limit`, which is what `tanstackApi` (the client's\n * `ListOptions`) reads; change them only for a hand-written `ListApi`. The\n * client maps them to the manifest's `skip_param`/`limit_param` itself.\n */\n readonly skipKey?: string;\n readonly limitKey?: string;\n}\n\n/** Offset paging: the next page starts at `skip + limit`, unless the last page was short. */\nexport function infiniteListOptions<T>(\n ns: CrudNamespace,\n resource: string,\n api: ListApi<T>,\n query: CrudQuery = {},\n init: InfiniteListInit = {},\n): CrudInfiniteQueryOptions<T> {\n const limit = init.limit ?? 100;\n const skipKey = init.skipKey ?? \"skip\";\n const limitKey = init.limitKey ?? \"limit\";\n return {\n queryKey: tag<InfiniteData<T[], number>>(\n crudKeys.infiniteList(ns, resource, query, limit),\n ),\n queryFn: ({ signal, pageParam }) =>\n api.list(\n { ...query, [skipKey]: pageParam, [limitKey]: limit },\n { signal },\n ),\n initialPageParam: 0,\n getNextPageParam: (lastPage, _all, lastPageParam) =>\n lastPage.length < limit ? undefined : lastPageParam + limit,\n staleTime: init.staleTime ?? 0,\n };\n}\n\nexport type Invalidator = Pick<\n QueryClient,\n \"invalidateQueries\" | \"removeQueries\" | \"setQueryData\"\n>;\n\nconst COLLECTION_KINDS = [\"list\", \"stats\", \"predict\"] as const;\n\n/** One resource of one client; `resource` is the string used in its keys. */\nexport interface CrudTarget {\n readonly client: string;\n readonly resource: string;\n}\n\n/**\n * Invalidates the list, stats and predict queries of `resource` on `client`,\n * for every caller scope (a write changes what every caller sees).\n */\nexport function invalidateCollections(\n queryClient: Pick<QueryClient, \"invalidateQueries\">,\n { client, resource }: CrudTarget,\n): Promise<void> {\n return queryClient.invalidateQueries(\n crudKeys.filter({ client, resource, kinds: COLLECTION_KINDS }),\n );\n}\n\n/** Invalidates everything cached for `resource` on `client`: one key prefix. */\nexport function invalidateResource(\n queryClient: Pick<QueryClient, \"invalidateQueries\">,\n { client, resource }: CrudTarget,\n): Promise<void> {\n return queryClient.invalidateQueries(crudKeys.filter({ client, resource }));\n}\n\n/** Invalidates one record's `record` and `revisions` queries, for every scope. */\nexport function invalidateRecord(\n queryClient: Pick<QueryClient, \"invalidateQueries\">,\n { client, resource }: CrudTarget,\n id: number,\n): Promise<void> {\n return queryClient.invalidateQueries(\n crudKeys.filter({\n client,\n resource,\n kinds: [\"record\", \"revisions\"],\n id,\n }),\n );\n}\n\nexport interface CreateMutationOptions<T, C> {\n readonly mutationKey: readonly [\"crud\", string, string, string, \"create\"];\n readonly mutationFn: (body: C) => Promise<T>;\n readonly onSuccess: () => Promise<void>;\n}\n\nexport interface UpdateMutationOptions<T, P> {\n readonly mutationKey: readonly [\"crud\", string, string, string, \"update\"];\n readonly mutationFn: (variables: UpdateVariables<P>) => Promise<T>;\n readonly onSuccess: (\n record: T,\n variables: UpdateVariables<P>,\n ) => Promise<void>;\n}\n\nexport interface DeleteMutationOptions {\n readonly mutationKey: readonly [\"crud\", string, string, string, \"delete\"];\n readonly mutationFn: (id: number) => Promise<unknown>;\n readonly onSuccess: (result: unknown, id: number) => Promise<void>;\n}\n\n/** Create: invalidate lists, stats and predict. */\nexport function createMutationOptions<T, C>(\n queryClient: Invalidator,\n ns: CrudNamespace,\n resource: string,\n api: CreateApi<T, C>,\n): CreateMutationOptions<T, C> {\n return {\n mutationKey: [\"crud\", ns.client, resource, ns.scope, \"create\"],\n mutationFn: (body: C) => api.create(body),\n onSuccess: () =>\n invalidateCollections(queryClient, { client: ns.client, resource }),\n };\n}\n\nexport interface UpdateVariables<P> {\n readonly id: number;\n readonly patch: P;\n /** Sent as `If-Match`; a stale one fails with 412. */\n readonly ifMatch?: string;\n}\n\n/** Update: `setQueryData` from the PATCH response, invalidate lists, stats, predict and revisions. */\nexport function updateMutationOptions<T, P>(\n queryClient: Invalidator,\n ns: CrudNamespace,\n resource: string,\n api: UpdateApi<T, P>,\n): UpdateMutationOptions<T, P> {\n return {\n mutationKey: [\"crud\", ns.client, resource, ns.scope, \"update\"],\n mutationFn: ({ id, patch, ifMatch }: UpdateVariables<P>) =>\n api.update(id, patch, ifMatch === undefined ? {} : { ifMatch }),\n onSuccess: async (record: T, { id }: UpdateVariables<P>) => {\n queryClient.setQueryData(crudKeys.record(ns, resource, id), record);\n await Promise.all([\n invalidateCollections(queryClient, { client: ns.client, resource }),\n // The caller's own record was just set; other scopes' are stale.\n queryClient.invalidateQueries(\n crudKeys.filter({\n client: ns.client,\n resource,\n exceptScope: ns.scope,\n kinds: [\"record\"],\n id,\n }),\n ),\n queryClient.invalidateQueries(\n crudKeys.filter({\n client: ns.client,\n resource,\n kinds: [\"revisions\"],\n id,\n }),\n ),\n ]);\n },\n };\n}\n\n/** Delete: drop the record query, invalidate lists, stats, predict and revisions. */\nexport function deleteMutationOptions(\n queryClient: Invalidator,\n ns: CrudNamespace,\n resource: string,\n api: DeleteApi,\n): DeleteMutationOptions {\n return {\n mutationKey: [\"crud\", ns.client, resource, ns.scope, \"delete\"],\n mutationFn: (id: number) => api.delete(id),\n onSuccess: async (_result: unknown, id: number) => {\n queryClient.removeQueries(\n crudKeys.filter({\n client: ns.client,\n resource,\n kinds: [\"record\"],\n id,\n }),\n );\n await Promise.all([\n invalidateCollections(queryClient, { client: ns.client, resource }),\n queryClient.invalidateQueries(\n crudKeys.filter({\n client: ns.client,\n resource,\n kinds: [\"revisions\"],\n id,\n }),\n ),\n ]);\n },\n };\n}\n\n/**\n * Invalidate TanStack queries from the resource's event stream. Events\n * invalidate and never apply snapshots: `create` invalidates lists, stats and\n * predict; `update`/`delete`/`restore` also the record and its revisions; a\n * reconnect (or an event with no usable record id) invalidates the resource.\n * Returns the unsubscribe.\n */\nexport function bridgeEvents(\n queryClient: Pick<QueryClient, \"invalidateQueries\">,\n hub: EventHub,\n target: CrudTarget,\n): () => void {\n return hub.subscribe({\n onEvent: async (event) => {\n const pending = [invalidateCollections(queryClient, target)];\n if (event.action !== \"create\") {\n pending.push(invalidateRecord(queryClient, target, event.recordId));\n }\n await Promise.all(pending);\n },\n onReset: () => invalidateResource(queryClient, target),\n });\n}\n"],"mappings":"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"}
|