@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.
- package/LICENSE +21 -0
- package/README.md +995 -0
- package/lib/cli.d.mts +1 -0
- package/lib/cli.mjs +195 -0
- package/lib/index.d.mts +585 -0
- package/lib/index.mjs +1842 -0
- package/lib/record-DnhZuvUe.mjs +5 -0
- package/lib/server.d.mts +163 -0
- package/lib/server.mjs +439 -0
- package/package.json +55 -0
package/lib/server.d.mts
ADDED
|
@@ -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
|
+
}
|