@nkzw/fate 0.0.1

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.
@@ -0,0 +1,5 @@
1
+ //#region src/record.ts
2
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
3
+
4
+ //#endregion
5
+ export { isRecord as t };
@@ -0,0 +1,163 @@
1
+ import { z } from "zod";
2
+ import { TRPCProcedureBuilder } from "@trpc/server";
3
+
4
+ //#region src/server/dataView.d.ts
5
+ declare const dataViewFieldsKey: unique symbol;
6
+ type AnyRecord = Record<string, unknown>;
7
+ type ResolverSelect<Context> = AnyRecord | ((options: {
8
+ args?: AnyRecord;
9
+ context?: Context;
10
+ }) => AnyRecord | void);
11
+ type Bivariant<Fn extends (...args: Array<any>) => unknown> = {
12
+ bivarianceHack(...args: Parameters<Fn>): ReturnType<Fn>;
13
+ }['bivarianceHack'];
14
+ type ResolverResolve<Item$1 extends AnyRecord, Context> = Bivariant<(options: {
15
+ args?: AnyRecord;
16
+ context?: Context;
17
+ item: Item$1;
18
+ }) => Promise<unknown> | unknown>;
19
+ /**
20
+ * Field configuration for selecting and resolving a computed value on the backend.
21
+ */
22
+ type ResolverField<Item$1 extends AnyRecord, Context> = {
23
+ kind: 'resolver';
24
+ resolve: ResolverResolve<Item$1, Context>;
25
+ select?: ResolverSelect<Context>;
26
+ };
27
+ type DataField<Item$1 extends AnyRecord, Context> = true | DataView<AnyRecord, Context> | ResolverField<Item$1, Context>;
28
+ /**
29
+ * Recursively serializes resolver results for transport across the network.
30
+ */
31
+ type Serializable<T> = T extends Date ? string : T extends Array<infer U> ? Array<Serializable<U>> : T extends object ? { [K in keyof T]: Serializable<T[K]> } : T;
32
+ /**
33
+ * Server-side mirror of a view definition describing how to select and resolve
34
+ * fields when fulfilling a client request.
35
+ */
36
+ type DataView<Item$1 extends AnyRecord, Context = unknown> = {
37
+ fields: Record<string, DataField<Item$1, Context>>;
38
+ kind?: 'resolver' | 'list';
39
+ typeName: string;
40
+ };
41
+ /**
42
+ * Convenience type for declaring the fields of a server data view.
43
+ */
44
+ type DataViewConfig<Item$1 extends AnyRecord, Context> = Record<string, DataField<Item$1, Context>>;
45
+ /**
46
+ * Declares a server data view that exposes an object's available fields to the client.
47
+ *
48
+ * @example
49
+ * const Post = dataView<PostItem>('Post')({
50
+ * id: true,
51
+ * title: true,
52
+ * });
53
+ */
54
+ declare function dataView<Item$1 extends AnyRecord, Context = unknown>(typeName?: string): <Fields$1 extends DataViewConfig<Item$1, Context>>(fields: Fields$1) => DataView<Item$1, Context> & {
55
+ readonly [dataViewFieldsKey]: Fields$1;
56
+ };
57
+ /**
58
+ * Marks a data view as a list resolver so the server can respond with
59
+ * connection information.
60
+ */
61
+ declare const list: <Item$1 extends AnyRecord, Context>(view: DataView<Item$1, Context>) => {
62
+ kind: "list";
63
+ fields: Record<string, DataField<Item$1, Context>>;
64
+ typeName: string;
65
+ };
66
+ /**
67
+ * Declares a resolver field inside a data view, optionally providing a
68
+ * selection for any data dependencies.
69
+ */
70
+ declare function resolver<Item$1 extends AnyRecord, Context = unknown>(config: {
71
+ resolve: ResolverResolve<Item$1, Context>;
72
+ select?: ResolverSelect<Context>;
73
+ }): ResolverField<Item$1, Context>;
74
+ type NonNullish<T> = Exclude<T, null | undefined>;
75
+ type WithNullish<Original, Value> = null extends Original ? undefined extends Original ? Value | null | undefined : Value | null : undefined extends Original ? Value | undefined : Value;
76
+ type ResolverResult<Field> = Field extends ResolverField<AnyRecord, unknown> ? Awaited<ReturnType<Field['resolve']>> : never;
77
+ type RelationResult<ItemField, V extends DataView<AnyRecord, unknown>> = NonNullish<ItemField> extends Array<unknown> ? WithNullish<ItemField, Array<RawDataViewResult<V>>> : WithNullish<ItemField, RawDataViewResult<V>>;
78
+ type ViewFieldConfig<V extends DataView<AnyRecord, unknown>> = V extends {
79
+ readonly [dataViewFieldsKey]: infer Fields;
80
+ } ? Fields : V['fields'];
81
+ type RawFieldResult<Item$1 extends AnyRecord, Key extends PropertyKey, Field extends DataField<Item$1, unknown>> = Field extends true ? Key extends keyof Item$1 ? Item$1[Key] : never : Field extends DataView<infer ChildItem, unknown> ? Key extends keyof Item$1 ? RelationResult<Item$1[Key], DataView<ChildItem, unknown>> : never : Field extends ResolverField<Item$1, unknown> ? ResolverResult<Field> : never;
82
+ type RawDataViewResult<V extends DataView<AnyRecord, unknown>> = V extends DataView<infer Item, unknown> ? { [K in keyof ViewFieldConfig<V>]: RawFieldResult<Item, K, ViewFieldConfig<V>[K]> } : never;
83
+ /**
84
+ * Resolved and serialized shape returned from a data view.
85
+ */
86
+ type DataViewResult<V extends DataView<AnyRecord, unknown>> = Serializable<RawDataViewResult<V>>;
87
+ /**
88
+ * Builds a resolver that applies a client's selection to a server data view,
89
+ * filtering fields, running nested resolvers, and shaping selects.
90
+ */
91
+ declare function createResolver<Item$1 extends AnyRecord, Context = unknown>({
92
+ args,
93
+ ctx,
94
+ select: initialSelect,
95
+ view
96
+ }: {
97
+ args?: AnyRecord;
98
+ ctx?: Context;
99
+ select: Iterable<string>;
100
+ view: DataView<Item$1, Context>;
101
+ }): {
102
+ resolve: (item: Item$1) => Promise<AnyRecord>;
103
+ resolveMany: (items: Array<AnyRecord>) => Promise<Array<AnyRecord>>;
104
+ select: Record<string, unknown>;
105
+ };
106
+ //#endregion
107
+ //#region src/server/connection.d.ts
108
+ type ConnectionInput = z.infer<typeof connectionInput>;
109
+ type AdditionalInputSchema = z.ZodObject<Record<string, z.ZodTypeAny>>;
110
+ type ConnectionInputWithAdditional<TAdditionalInput extends AdditionalInputSchema | undefined> = ConnectionInput & {
111
+ args?: ConnectionInput['args'] extends infer A ? A & (TAdditionalInput extends AdditionalInputSchema ? z.infer<TAdditionalInput> : object) : never;
112
+ };
113
+ type ConnectionCursor = string;
114
+ /**
115
+ * Connection item including the node and opaque cursor.
116
+ */
117
+
118
+ type ProcedureLike<TContext> = TRPCProcedureBuilder<TContext, any, any, any, any, any, any, false>;
119
+ type QueryFn<TContext, TItem, TInput extends ConnectionInput> = (options: {
120
+ ctx: TContext;
121
+ cursor?: ConnectionCursor;
122
+ direction: 'forward' | 'backward';
123
+ input: TInput;
124
+ skip?: number;
125
+ take: number;
126
+ }) => Promise<Array<TItem>>;
127
+ type MapFn<TContext, TItem, TNode, TInput extends ConnectionInput> = (options: {
128
+ ctx: TContext;
129
+ input: TInput;
130
+ items: Array<TItem>;
131
+ }) => Promise<Array<TNode>> | Array<TNode>;
132
+ /**
133
+ * Zod schema for connection args (`after`, `before`, `first`, `last`, etc.).
134
+ */
135
+ declare const connectionArgs: z.ZodOptional<z.ZodType<Record<string, unknown>, unknown, z.core.$ZodTypeInternals<Record<string, unknown>, unknown>>>;
136
+ declare const connectionInput: z.ZodObject<{
137
+ args: z.ZodOptional<z.ZodType<Record<string, unknown>, unknown, z.core.$ZodTypeInternals<Record<string, unknown>, unknown>>>;
138
+ select: z.ZodArray<z.ZodString>;
139
+ }, z.core.$strict>;
140
+ /**
141
+ * Converts an array of nodes into a list view connection result the client
142
+ * can normalize.
143
+ */
144
+
145
+ /**
146
+ * Wraps a tRPC procedure to handle cursor-based pagination with consistent
147
+ * connection semantics.
148
+ */
149
+ declare const withConnection: <TContext>(procedure: ProcedureLike<TContext>) => <TItem, TNode = TItem, TAdditionalInput extends AdditionalInputSchema | undefined = undefined>({
150
+ defaultSize,
151
+ getCursor,
152
+ input: additionalInput,
153
+ map,
154
+ query
155
+ }: {
156
+ defaultSize?: number;
157
+ getCursor?: (node: TNode) => ConnectionCursor;
158
+ input?: TAdditionalInput;
159
+ map?: MapFn<TContext, TItem, TNode, ConnectionInputWithAdditional<TAdditionalInput>>;
160
+ query: QueryFn<TContext, TItem, ConnectionInputWithAdditional<TAdditionalInput>>;
161
+ }) => ReturnType<ReturnType<ProcedureLike<TContext>["input"]>["query"]>;
162
+ //#endregion
163
+ export { type DataViewResult, connectionArgs, createResolver, dataView, list, resolver, withConnection };
package/lib/server.mjs ADDED
@@ -0,0 +1,439 @@
1
+ import { t as isRecord } from "./record-DnhZuvUe.mjs";
2
+ import { z } from "zod";
3
+
4
+ //#region src/server/prismaSelect.ts
5
+ const toPrismaArgs = (args$1) => {
6
+ const prismaArgs = {};
7
+ const isBackward = args$1.before !== void 0 || typeof args$1.last === "number";
8
+ if (typeof args$1.first === "number") prismaArgs.take = args$1.first + 1;
9
+ if (typeof args$1.last === "number") prismaArgs.take = -(args$1.last + 1);
10
+ const cursor = isBackward ? args$1.before : args$1.after;
11
+ if (cursor !== void 0) {
12
+ prismaArgs.cursor = { id: cursor };
13
+ prismaArgs.skip = 1;
14
+ }
15
+ return prismaArgs;
16
+ };
17
+ const isRecord$1 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
18
+ /**
19
+ * Narrows nested args to the slice relevant for a particular selection path.
20
+ */
21
+ function getScopedArgs(args$1, path) {
22
+ if (!args$1) return;
23
+ const segments = path.split(".");
24
+ let current = args$1;
25
+ for (const segment of segments) {
26
+ if (!isRecord$1(current)) return;
27
+ current = current[segment];
28
+ }
29
+ return isRecord$1(current) ? current : void 0;
30
+ }
31
+ /**
32
+ * Builds a Prisma `select` object from flattened selection paths and optional
33
+ * scoped args, always including the `id` field.
34
+ */
35
+ function prismaSelect(paths, args$1) {
36
+ const allPaths = [...new Set([...paths, "id"])];
37
+ const select = {};
38
+ for (const path of allPaths) {
39
+ const segments = path.split(".");
40
+ let current = select;
41
+ let currentPath = "";
42
+ segments.forEach((segment, index) => {
43
+ currentPath = currentPath ? `${currentPath}.${segment}` : segment;
44
+ if (index === segments.length - 1) {
45
+ if (segment === "cursor") return;
46
+ current[segment] = true;
47
+ return;
48
+ }
49
+ const existing = current[segment];
50
+ const relation = existing && typeof existing === "object" && existing !== null && "select" in existing ? existing : { select: {} };
51
+ const scopedArgs = getScopedArgs(args$1, currentPath);
52
+ if (scopedArgs) Object.assign(relation, toPrismaArgs(scopedArgs));
53
+ current[segment] = relation;
54
+ current = relation.select;
55
+ });
56
+ }
57
+ return select;
58
+ }
59
+
60
+ //#endregion
61
+ //#region src/server/connection.ts
62
+ const args = z.object({}).catchall(z.union([z.unknown(), z.lazy(() => args)]));
63
+ /**
64
+ * Zod schema for connection args (`after`, `before`, `first`, `last`, etc.).
65
+ */
66
+ const connectionArgs = args.optional();
67
+ const connectionInput = z.strictObject({
68
+ args: connectionArgs,
69
+ select: z.array(z.string())
70
+ });
71
+ const paginationArgKeys = new Set([
72
+ "after",
73
+ "before",
74
+ "first",
75
+ "last"
76
+ ]);
77
+ const paginationArgsSchema = z.strictObject({
78
+ after: z.string().optional(),
79
+ before: z.string().optional(),
80
+ first: z.number().int().positive().optional(),
81
+ last: z.number().int().positive().optional()
82
+ }).partial().refine(({ after, before }) => !(after && before), "Connection args can't include both 'after' and 'before'.").refine(({ first, last }) => !(first && last), "Connection args can't include both 'first' and 'last'.").refine(({ before, last }) => !last || before !== void 0, "Connection args using 'last' must also include 'before'.");
83
+ const extractPaginationArgs = (args$1) => args$1 ? Object.fromEntries(Object.entries(args$1).filter(([key]) => paginationArgKeys.has(key))) : {};
84
+ /**
85
+ * Converts an array of nodes into a list view connection result the client
86
+ * can normalize.
87
+ */
88
+ function arrayToConnection(nodes, { args: args$1, getCursor = (node) => String(node.id) } = {}) {
89
+ if (!nodes) return;
90
+ const paginationArgs = paginationArgsSchema.parse(extractPaginationArgs(args$1));
91
+ if (Object.keys(paginationArgs).length === 0) return {
92
+ items: nodes.map((node) => ({
93
+ cursor: getCursor(node),
94
+ node
95
+ })),
96
+ pagination: {
97
+ hasNext: false,
98
+ hasPrevious: false,
99
+ nextCursor: void 0,
100
+ previousCursor: void 0
101
+ }
102
+ };
103
+ const isBackward = paginationArgs.before !== void 0 || paginationArgs.last !== void 0;
104
+ const cursor = isBackward ? paginationArgs.before : paginationArgs.after;
105
+ const pageSize = paginationArgs.first ?? paginationArgs.last ?? nodes.length;
106
+ const cursorIndex = cursor === void 0 ? -1 : nodes.findIndex((node) => getCursor(node) === cursor);
107
+ const selectedNodes = cursorIndex < 0 ? nodes : isBackward ? nodes.slice(0, cursorIndex) : nodes.slice(cursorIndex + 1);
108
+ const hasNext = selectedNodes.length > pageSize;
109
+ const hasPrevious = nodes.length > selectedNodes.length;
110
+ const items = (isBackward ? selectedNodes.slice(Math.max(0, selectedNodes.length - pageSize)) : selectedNodes.slice(0, pageSize)).map((node) => ({
111
+ cursor: getCursor(node),
112
+ node
113
+ }));
114
+ const firstItem = items[0];
115
+ const lastItem = items.at(-1);
116
+ return {
117
+ items,
118
+ pagination: {
119
+ hasNext: isBackward ? hasPrevious : hasNext,
120
+ hasPrevious: isBackward ? hasNext : hasPrevious,
121
+ nextCursor: lastItem?.cursor,
122
+ previousCursor: (isBackward ? hasNext : hasPrevious) && firstItem ? firstItem.cursor : void 0
123
+ }
124
+ };
125
+ }
126
+ const isDataViewField$1 = (field) => Boolean(field) && typeof field === "object" && "fields" in field;
127
+ const assignIfChanged = (current, key, next, existing) => {
128
+ if (next === existing) return current;
129
+ if (!current) return { [key]: next };
130
+ current[key] = next;
131
+ return current;
132
+ };
133
+ function toConnectionResult({ args: args$1, item, path, view }) {
134
+ if (!isRecord(item)) return item;
135
+ let result = null;
136
+ const base = () => result ? {
137
+ ...item,
138
+ ...result
139
+ } : item;
140
+ for (const [field, config] of Object.entries(view.fields)) {
141
+ if (!isDataViewField$1(config)) continue;
142
+ const current = base()[field];
143
+ const nextPath = path ? `${path}.${field}` : field;
144
+ if (config.kind === "list") {
145
+ if (!Array.isArray(current)) continue;
146
+ const connection = arrayToConnection(current.map((item$1) => toConnectionResult({
147
+ args: args$1,
148
+ item: item$1,
149
+ path: nextPath,
150
+ view: config
151
+ })), { args: getScopedArgs(args$1, nextPath) });
152
+ result = assignIfChanged(result, field, connection, current);
153
+ continue;
154
+ }
155
+ if (Array.isArray(current)) {
156
+ const wrapped = current.map((entry) => toConnectionResult({
157
+ args: args$1,
158
+ item: entry,
159
+ path: nextPath,
160
+ view: config
161
+ }));
162
+ result = assignIfChanged(result, field, wrapped, current);
163
+ continue;
164
+ }
165
+ if (isRecord(current)) {
166
+ const wrapped = toConnectionResult({
167
+ args: args$1,
168
+ item: current,
169
+ path: nextPath,
170
+ view: config
171
+ });
172
+ result = assignIfChanged(result, field, wrapped, current);
173
+ }
174
+ }
175
+ return result ? {
176
+ ...item,
177
+ ...result
178
+ } : item;
179
+ }
180
+ /**
181
+ * Wraps a tRPC procedure to handle cursor-based pagination with consistent
182
+ * connection semantics.
183
+ */
184
+ const withConnection = (procedure) => ({ defaultSize = 20, getCursor = (node) => node.id, input: additionalInput, map, query }) => {
185
+ const inputSchema = additionalInput ? connectionInput.extend({ args: connectionArgs.and(additionalInput) }) : connectionInput;
186
+ return procedure.input(inputSchema).query(async (resolverOptions) => {
187
+ const { ctx, input } = resolverOptions;
188
+ const paginationArgs = paginationArgsSchema.parse(extractPaginationArgs(input.args));
189
+ const isBackward = paginationArgs.before !== void 0 || paginationArgs.last !== void 0;
190
+ const cursor = isBackward ? paginationArgs.before : paginationArgs.after;
191
+ const direction = isBackward ? "backward" : "forward";
192
+ const pageSize = paginationArgs.first ?? paginationArgs.last ?? defaultSize;
193
+ const rawItems = await query({
194
+ ctx,
195
+ cursor,
196
+ direction,
197
+ input,
198
+ skip: cursor ? 1 : void 0,
199
+ take: pageSize + 1
200
+ });
201
+ const hasMore = rawItems.length > pageSize;
202
+ const limitedItems = isBackward ? rawItems.slice(Math.max(0, rawItems.length - pageSize)) : rawItems.slice(0, pageSize);
203
+ const items = (map ? await map({
204
+ ctx,
205
+ input,
206
+ items: limitedItems
207
+ }) : limitedItems).map((node) => ({
208
+ cursor: getCursor(node),
209
+ node
210
+ }));
211
+ const firstItem = items[0];
212
+ const lastItem = items.at(-1);
213
+ return {
214
+ items,
215
+ pagination: {
216
+ hasNext: isBackward ? Boolean(cursor) : hasMore,
217
+ hasPrevious: isBackward ? hasMore : Boolean(cursor),
218
+ nextCursor: lastItem?.cursor,
219
+ previousCursor: (isBackward ? hasMore : Boolean(cursor)) ? firstItem?.cursor : void 0
220
+ }
221
+ };
222
+ });
223
+ };
224
+
225
+ //#endregion
226
+ //#region src/server/dataView.ts
227
+ const dataViewFieldsKey = Symbol("__fate__DataViewFields");
228
+ /**
229
+ * Declares a server data view that exposes an object's available fields to the client.
230
+ *
231
+ * @example
232
+ * const Post = dataView<PostItem>('Post')({
233
+ * id: true,
234
+ * title: true,
235
+ * });
236
+ */
237
+ function dataView(typeName) {
238
+ return (fields) => {
239
+ return {
240
+ [dataViewFieldsKey]: fields,
241
+ fields,
242
+ typeName
243
+ };
244
+ };
245
+ }
246
+ /**
247
+ * Marks a data view as a list resolver so the server can respond with
248
+ * connection information.
249
+ */
250
+ const list = (view) => {
251
+ return {
252
+ ...view,
253
+ kind: "list"
254
+ };
255
+ };
256
+ /**
257
+ * Declares a resolver field inside a data view, optionally providing a
258
+ * selection for any data dependencies.
259
+ */
260
+ function resolver(config) {
261
+ return {
262
+ kind: "resolver",
263
+ ...config
264
+ };
265
+ }
266
+ const isResolverField = (field) => Boolean(field) && typeof field === "object" && "kind" in field && field.kind === "resolver";
267
+ const isDataViewField = (field) => Boolean(field) && typeof field === "object" && "fields" in field;
268
+ const filterToViewFields = (item, view) => {
269
+ if (!isRecord(item)) return item;
270
+ const filtered = {};
271
+ for (const [field, config] of Object.entries(view.fields)) {
272
+ if (!(field in item)) continue;
273
+ const value = item[field];
274
+ if (isDataViewField(config)) {
275
+ if (Array.isArray(value)) {
276
+ filtered[field] = value.map((entry) => isRecord(entry) ? filterToViewFields(entry, config) : entry);
277
+ continue;
278
+ }
279
+ if (isRecord(value)) {
280
+ filtered[field] = filterToViewFields(value, config);
281
+ continue;
282
+ }
283
+ }
284
+ filtered[field] = value;
285
+ }
286
+ return filtered;
287
+ };
288
+ const mergeObject = (target, source) => {
289
+ for (const [key, value] of Object.entries(source)) {
290
+ const existing = target[key];
291
+ if (isRecord(existing) && isRecord(value)) {
292
+ mergeObject(existing, value);
293
+ continue;
294
+ }
295
+ target[key] = value;
296
+ }
297
+ };
298
+ const ensureRelationSelect = (select, path) => {
299
+ if (!path) return select;
300
+ const segments = path.split(".");
301
+ let current = select;
302
+ for (const segment of segments) {
303
+ const existing = current[segment];
304
+ if (isRecord(existing) && "select" in existing) {
305
+ const relation$1 = existing;
306
+ if (!isRecord(relation$1.select)) relation$1.select = {};
307
+ current = relation$1.select;
308
+ continue;
309
+ }
310
+ const relation = { select: {} };
311
+ current[segment] = relation;
312
+ current = relation.select;
313
+ }
314
+ return current;
315
+ };
316
+ const createSelectedNode = (view, path) => ({
317
+ path,
318
+ relations: /* @__PURE__ */ new Map(),
319
+ resolvers: /* @__PURE__ */ new Map(),
320
+ view
321
+ });
322
+ const assignPath = (node, segments, path, view, allowedPaths) => {
323
+ if (segments.length === 0) return;
324
+ const [segment, ...rest] = segments;
325
+ const field = view.fields[segment];
326
+ if (!field) return;
327
+ const nextPath = path ? `${path}.${segment}` : segment;
328
+ if (isResolverField(field)) {
329
+ if (rest.length === 0) node.resolvers.set(segment, field);
330
+ return;
331
+ }
332
+ if (isDataViewField(field)) {
333
+ let relationNode = node.relations.get(segment);
334
+ if (!relationNode) {
335
+ relationNode = createSelectedNode(field, nextPath);
336
+ node.relations.set(segment, relationNode);
337
+ }
338
+ if (rest.length === 0) {
339
+ collectViewPaths(nextPath, field, allowedPaths);
340
+ return;
341
+ }
342
+ assignPath(relationNode, rest, nextPath, field, allowedPaths);
343
+ return;
344
+ }
345
+ if (rest.length === 0) allowedPaths.add(nextPath);
346
+ };
347
+ const collectViewPaths = (basePath, view, allowedPaths) => {
348
+ for (const [field, child] of Object.entries(view.fields)) {
349
+ const nextPath = `${basePath}.${field}`;
350
+ if (child === true) {
351
+ allowedPaths.add(nextPath);
352
+ continue;
353
+ }
354
+ if (isDataViewField(child)) collectViewPaths(nextPath, child, allowedPaths);
355
+ }
356
+ };
357
+ const collectResolvers = (node, select, args$1, context) => {
358
+ for (const resolver$1 of node.resolvers.values()) {
359
+ if (!resolver$1.select) continue;
360
+ const addition = typeof resolver$1.select === "function" ? resolver$1.select({
361
+ args: args$1,
362
+ context
363
+ }) : resolver$1.select;
364
+ if (addition && isRecord(addition)) mergeObject(ensureRelationSelect(select, node.path), addition);
365
+ }
366
+ for (const relation of node.relations.values()) collectResolvers(relation, select, args$1, context);
367
+ };
368
+ const resolveNode = async (options) => {
369
+ const { item, node, options: resolverOptions } = options;
370
+ if (!isRecord(item)) return item;
371
+ let result = null;
372
+ const assign = (key, value) => {
373
+ if (!result) result = { ...item };
374
+ result[key] = value;
375
+ };
376
+ const base = () => result ?? item;
377
+ for (const [field, resolver$1] of node.resolvers) {
378
+ const value = await resolver$1.resolve({
379
+ ...resolverOptions,
380
+ item: base()
381
+ });
382
+ if (value !== void 0) assign(field, value);
383
+ }
384
+ for (const [field, relationNode] of node.relations) {
385
+ const current = base()[field];
386
+ if (Array.isArray(current)) {
387
+ const resolved = await Promise.all(current.map((entry) => resolveNode({
388
+ item: entry,
389
+ node: relationNode,
390
+ options: resolverOptions
391
+ })));
392
+ if (resolved.some((value, index) => value !== current[index])) assign(field, resolved);
393
+ continue;
394
+ }
395
+ if (current && typeof current === "object") {
396
+ const resolved = await resolveNode({
397
+ item: current,
398
+ node: relationNode,
399
+ options: resolverOptions
400
+ });
401
+ if (resolved !== current) assign(field, resolved);
402
+ }
403
+ }
404
+ return result ?? item;
405
+ };
406
+ /**
407
+ * Builds a resolver that applies a client's selection to a server data view,
408
+ * filtering fields, running nested resolvers, and shaping selects.
409
+ */
410
+ function createResolver({ args: args$1, ctx, select: initialSelect, view }) {
411
+ const allowedPaths = /* @__PURE__ */ new Set();
412
+ const root = createSelectedNode(view, null);
413
+ for (const path of initialSelect) {
414
+ if (!path) continue;
415
+ assignPath(root, path.split("."), null, view, allowedPaths);
416
+ }
417
+ const select = prismaSelect([...allowedPaths], args$1);
418
+ collectResolvers(root, select, args$1, ctx);
419
+ const resolve = async (item) => toConnectionResult({
420
+ args: args$1,
421
+ item: filterToViewFields(await resolveNode({
422
+ item,
423
+ node: root,
424
+ options: {
425
+ args: args$1,
426
+ context: ctx
427
+ }
428
+ }), root.view),
429
+ view: root.view
430
+ });
431
+ return {
432
+ resolve,
433
+ resolveMany: (items) => Promise.all(items.map((item) => resolve(item))),
434
+ select
435
+ };
436
+ }
437
+
438
+ //#endregion
439
+ export { connectionArgs, createResolver, dataView, list, resolver, withConnection };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@nkzw/fate",
3
+ "version": "0.0.1",
4
+ "description": "fate is a modern data client for React.",
5
+ "homepage": "https://github.com/nkzw-tech/fate",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/nkzw-tech/fate"
9
+ },
10
+ "license": "MIT",
11
+ "author": {
12
+ "name": "Christoph Nakazawa",
13
+ "email": "christoph.pojer@gmail.com"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/index.d.mts",
19
+ "development": "./src/index.ts",
20
+ "default": "./lib/index.mjs"
21
+ },
22
+ "./cli": {
23
+ "types": "./lib/cli.d.mts",
24
+ "development": "./src/cli.ts",
25
+ "default": "./lib/cli.mjs"
26
+ },
27
+ "./server": {
28
+ "types": "./lib/server.d.mts",
29
+ "development": "./src/server.ts",
30
+ "default": "./lib/server.mjs"
31
+ }
32
+ },
33
+ "main": "./lib/index.mjs",
34
+ "types": "./lib/index.d.mts",
35
+ "bin": {
36
+ "fate": "./lib/cli.mjs"
37
+ },
38
+ "files": [
39
+ "lib"
40
+ ],
41
+ "dependencies": {
42
+ "superjson": "^2.2.6",
43
+ "zod": "^4.1.13"
44
+ },
45
+ "devDependencies": {
46
+ "@trpc/client": "^11.7.2",
47
+ "@trpc/server": "^11.7.2"
48
+ },
49
+ "peerDependencies": {
50
+ "@trpc/client": "^11.6.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsdown -d lib --target=node24 src/index.ts src/server.ts src/cli.ts"
54
+ }
55
+ }