@nkzw/fate 0.1.2 → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +810 -129
- package/docs/api/classes/FateClient.md +832 -0
- package/docs/api/functions/clientRoot.md +27 -0
- package/docs/api/functions/createClient.md +21 -0
- package/docs/api/functions/createHTTPTransport.md +51 -0
- package/docs/api/functions/createTRPCTransport.md +46 -0
- package/docs/api/functions/getListEntries.md +15 -0
- package/docs/api/functions/getSelectionPlan.md +36 -0
- package/docs/api/functions/isRecord.md +15 -0
- package/docs/api/functions/isViewTag.md +17 -0
- package/docs/api/functions/liveConnectionTopic.md +19 -0
- package/docs/api/functions/liveEntityTopic.md +19 -0
- package/docs/api/functions/liveGlobalConnectionTopic.md +15 -0
- package/docs/api/functions/mutation.md +32 -0
- package/docs/api/functions/toEntityId.md +21 -0
- package/docs/api/functions/view.md +26 -0
- package/docs/api/index.md +81 -0
- package/docs/api/interfaces/FateThenable.md +31 -0
- package/docs/api/interfaces/Transport.md +199 -0
- package/docs/api/server/classes/FateRequestError.md +67 -0
- package/docs/api/server/drizzle/functions/createDrizzleFate.md +431 -0
- package/docs/api/server/drizzle/functions/createDrizzleSourceAdapter.md +21 -0
- package/docs/api/server/drizzle/functions/createDrizzleSourceRegistry.md +21 -0
- package/docs/api/server/drizzle/index.md +23 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyConfig.md +15 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyInput.md +5 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleQueryExtra.md +21 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleSourceAdapter.md +337 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleViewConfig.md +29 -0
- package/docs/api/server/functions/bindSourceProcedures.md +32 -0
- package/docs/api/server/functions/computed.md +43 -0
- package/docs/api/server/functions/count.md +21 -0
- package/docs/api/server/functions/createFateFetchHandler.md +21 -0
- package/docs/api/server/functions/createFateServer.md +41 -0
- package/docs/api/server/functions/createHonoFateHandler.md +21 -0
- package/docs/api/server/functions/createLiveEventBus.md +17 -0
- package/docs/api/server/functions/createNestedSourcePlan.md +103 -0
- package/docs/api/server/functions/createResolver.md +74 -0
- package/docs/api/server/functions/createSourcePlan.md +95 -0
- package/docs/api/server/functions/createSourceProcedures.md +46 -0
- package/docs/api/server/functions/dataView.md +32 -0
- package/docs/api/server/functions/field.md +15 -0
- package/docs/api/server/functions/getNestedSelection.md +19 -0
- package/docs/api/server/functions/getScopedArgs.md +21 -0
- package/docs/api/server/functions/hasNestedSelection.md +19 -0
- package/docs/api/server/functions/list.md +28 -0
- package/docs/api/server/functions/refetchSourceById.md +55 -0
- package/docs/api/server/functions/resolveSourceById.md +55 -0
- package/docs/api/server/functions/resolveSourceByIds.md +55 -0
- package/docs/api/server/functions/resolveSourceConnection.md +67 -0
- package/docs/api/server/functions/resolver.md +42 -0
- package/docs/api/server/functions/toPrismaSelect.md +25 -0
- package/docs/api/server/functions/withConnection.md +24 -0
- package/docs/api/server/index.md +109 -0
- package/docs/api/server/prisma/functions/createPrismaFate.md +435 -0
- package/docs/api/server/prisma/functions/createPrismaSourceAdapter.md +21 -0
- package/docs/api/server/prisma/functions/createPrismaSourceRegistry.md +21 -0
- package/docs/api/server/prisma/functions/prismaConnectionArgs.md +57 -0
- package/docs/api/server/prisma/index.md +23 -0
- package/docs/api/server/prisma/type-aliases/PrismaDelegate.md +59 -0
- package/docs/api/server/prisma/type-aliases/PrismaQueryExtra.md +5 -0
- package/docs/api/server/prisma/type-aliases/PrismaSourceAdapter.md +343 -0
- package/docs/api/server/prisma/type-aliases/PrismaViewConfig.md +31 -0
- package/docs/api/server/type-aliases/ComputedField.md +55 -0
- package/docs/api/server/type-aliases/ComputedSelection.md +5 -0
- package/docs/api/server/type-aliases/ConnectionItem.md +29 -0
- package/docs/api/server/type-aliases/ConnectionPagination.md +39 -0
- package/docs/api/server/type-aliases/ConnectionResult.md +29 -0
- package/docs/api/server/type-aliases/CountSelection.md +29 -0
- package/docs/api/server/type-aliases/CountWhere.md +5 -0
- package/docs/api/server/type-aliases/DataViewListOptions.md +13 -0
- package/docs/api/server/type-aliases/DataViewOrderBy.md +5 -0
- package/docs/api/server/type-aliases/DataViewOrderDirection.md +5 -0
- package/docs/api/server/type-aliases/DataViewResult.md +11 -0
- package/docs/api/server/type-aliases/Entity.md +21 -0
- package/docs/api/server/type-aliases/FateServer.md +75 -0
- package/docs/api/server/type-aliases/FateServerManifest.md +5 -0
- package/docs/api/server/type-aliases/FieldSelection.md +21 -0
- package/docs/api/server/type-aliases/LiveConnectionEventType.md +5 -0
- package/docs/api/server/type-aliases/LiveConnectionSourceEvent.md +5 -0
- package/docs/api/server/type-aliases/LiveEventBus.md +5 -0
- package/docs/api/server/type-aliases/LiveEventType.md +5 -0
- package/docs/api/server/type-aliases/LiveSourceEvent.md +5 -0
- package/docs/api/server/type-aliases/NativeFateAPI.md +47 -0
- package/docs/api/server/type-aliases/OrderDirection.md +5 -0
- package/docs/api/server/type-aliases/SourceConfig.md +55 -0
- package/docs/api/server/type-aliases/SourceDefinition.md +55 -0
- package/docs/api/server/type-aliases/SourceOrder.md +5 -0
- package/docs/api/server/type-aliases/SourceOrderField.md +21 -0
- package/docs/api/server/type-aliases/SourcePlan.md +29 -0
- package/docs/api/server/type-aliases/SourcePlanNode.md +29 -0
- package/docs/api/server/type-aliases/SourceRegistry.md +11 -0
- package/docs/api/server/type-aliases/SourceRelation.md +15 -0
- package/docs/api/server/type-aliases/SourceRelationConfig.md +23 -0
- package/docs/api/server/variables/byIdInput.md +5 -0
- package/docs/api/server/variables/connectionArgs.md +7 -0
- package/docs/api/type-aliases/ConnectionMetadata.md +7 -0
- package/docs/api/type-aliases/ConnectionRef.md +13 -0
- package/docs/api/type-aliases/Entity.md +15 -0
- package/docs/api/type-aliases/EntityId.md +7 -0
- package/docs/api/type-aliases/FateLiveConnectionEvent.md +5 -0
- package/docs/api/type-aliases/FateLiveEvent.md +5 -0
- package/docs/api/type-aliases/FateMutations.md +7 -0
- package/docs/api/type-aliases/FateOperation.md +5 -0
- package/docs/api/type-aliases/FateProtocolRequest.md +5 -0
- package/docs/api/type-aliases/FateProtocolResponse.md +5 -0
- package/docs/api/type-aliases/FateRecord.md +7 -0
- package/docs/api/type-aliases/FateRoots.md +7 -0
- package/docs/api/type-aliases/InferFateAPI.md +11 -0
- package/docs/api/type-aliases/List.md +5 -0
- package/docs/api/type-aliases/ListEntry.md +5 -0
- package/docs/api/type-aliases/ListItem.md +13 -0
- package/docs/api/type-aliases/Mask.md +15 -0
- package/docs/api/type-aliases/MutationDefinition.md +21 -0
- package/docs/api/type-aliases/MutationEntity.md +13 -0
- package/docs/api/type-aliases/MutationIdentifier.md +19 -0
- package/docs/api/type-aliases/MutationInput.md +13 -0
- package/docs/api/type-aliases/MutationResult.md +13 -0
- package/docs/api/type-aliases/NodesItem.md +13 -0
- package/docs/api/type-aliases/Pagination.md +39 -0
- package/docs/api/type-aliases/Request.md +7 -0
- package/docs/api/type-aliases/RequestMode.md +7 -0
- package/docs/api/type-aliases/RequestOptions.md +7 -0
- package/docs/api/type-aliases/RequestResult.md +18 -0
- package/docs/api/type-aliases/Selection.md +13 -0
- package/docs/api/type-aliases/Snapshot.md +7 -0
- package/docs/api/type-aliases/TypeConfig.md +41 -0
- package/docs/api/type-aliases/View.md +17 -0
- package/docs/api/type-aliases/ViewData.md +18 -0
- package/docs/api/type-aliases/ViewEntity.md +13 -0
- package/docs/api/type-aliases/ViewEntityName.md +13 -0
- package/docs/api/type-aliases/ViewRef.md +13 -0
- package/docs/api/type-aliases/ViewSelection.md +13 -0
- package/docs/api/type-aliases/ViewSnapshot.md +18 -0
- package/docs/api/type-aliases/ViewTag.md +7 -0
- package/docs/api/variables/ConnectionTag.md +7 -0
- package/docs/guide/actions.md +263 -0
- package/docs/guide/core-concepts.md +22 -0
- package/docs/guide/getting-started.md +57 -0
- package/docs/guide/list-views.md +86 -0
- package/docs/guide/live-views.md +245 -0
- package/docs/guide/requests.md +130 -0
- package/docs/guide/server-integration.md +630 -0
- package/docs/guide/views.md +278 -0
- package/docs/guide/void-integration.md +167 -0
- package/docs/index.md +4 -0
- package/lib/cli.mjs +17 -250
- package/lib/clientStub.d.mts +6 -0
- package/lib/clientStub.mjs +6 -0
- package/lib/executor-BqwHdN3n.d.mts +468 -0
- package/lib/index.d.mts +64 -3
- package/lib/index.mjs +1965 -559
- package/lib/list-4wRNSVgI.d.mts +10 -0
- package/lib/list-C_mi6GnE.mjs +16 -0
- package/lib/list.d.mts +2 -0
- package/lib/list.mjs +2 -0
- package/lib/liveTopics-DzNtJaBD.mjs +197 -0
- package/lib/{record-DnhZuvUe.mjs → record-AeZJC9fd.mjs} +1 -2
- package/lib/record-ENh92gyb.d.mts +315 -0
- package/lib/server/drizzle.d.mts +304 -0
- package/lib/server/drizzle.mjs +688 -0
- package/lib/server/prisma.d.mts +309 -0
- package/lib/server/prisma.mjs +345 -0
- package/lib/server.d.mts +82 -184
- package/lib/server.mjs +870 -416
- package/lib/sourceRouter-Bwpkr062.mjs +972 -0
- package/lib/{types-Djlh7mMH.d.mts → types-Dz46PXr3.d.mts} +327 -197
- package/lib/vite.d.mts +17 -0
- package/lib/vite.mjs +592 -0
- package/package.json +44 -6
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
import { t as isRecord } from "./record-AeZJC9fd.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/server/queryArgs.ts
|
|
4
|
+
const toPrismaArgs = (args) => {
|
|
5
|
+
const prismaArgs = {};
|
|
6
|
+
const isBackward = args.before !== void 0 || typeof args.last === "number";
|
|
7
|
+
if (typeof args.first === "number") prismaArgs.take = args.first + 1;
|
|
8
|
+
if (typeof args.last === "number") prismaArgs.take = -(args.last + 1);
|
|
9
|
+
const cursor = isBackward ? args.before : args.after;
|
|
10
|
+
if (cursor !== void 0) {
|
|
11
|
+
prismaArgs.cursor = { id: cursor };
|
|
12
|
+
prismaArgs.skip = 1;
|
|
13
|
+
}
|
|
14
|
+
return prismaArgs;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Narrows nested args to the slice relevant for a particular selection path.
|
|
18
|
+
*/
|
|
19
|
+
function getScopedArgs(args, path) {
|
|
20
|
+
if (!args) return;
|
|
21
|
+
const segments = path.split(".");
|
|
22
|
+
let current = args;
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
if (!isRecord(current)) return;
|
|
25
|
+
current = current[segment];
|
|
26
|
+
}
|
|
27
|
+
return isRecord(current) ? current : void 0;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/server/connection.ts
|
|
31
|
+
const isConnectionResult = (value) => isRecord(value) && Array.isArray(value.items) && isRecord(value.pagination) && typeof value.pagination.hasNext === "boolean" && typeof value.pagination.hasPrevious === "boolean";
|
|
32
|
+
const args = z.object({}).catchall(z.union([z.unknown(), z.lazy(() => args)]));
|
|
33
|
+
/**
|
|
34
|
+
* Zod schema for connection args (`after`, `before`, `first`, `last`, etc.).
|
|
35
|
+
*/
|
|
36
|
+
const connectionArgs = args.optional();
|
|
37
|
+
const connectionInput = z.strictObject({
|
|
38
|
+
args: connectionArgs,
|
|
39
|
+
select: z.array(z.string())
|
|
40
|
+
});
|
|
41
|
+
const paginationArgKeys = new Set([
|
|
42
|
+
"after",
|
|
43
|
+
"before",
|
|
44
|
+
"first",
|
|
45
|
+
"last"
|
|
46
|
+
]);
|
|
47
|
+
const paginationArgsSchema = z.strictObject({
|
|
48
|
+
after: z.string().optional(),
|
|
49
|
+
before: z.string().optional(),
|
|
50
|
+
first: z.number().int().positive().optional(),
|
|
51
|
+
last: z.number().int().positive().optional()
|
|
52
|
+
}).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'.");
|
|
53
|
+
const extractPaginationArgs = (args) => args ? Object.fromEntries(Object.entries(args).filter(([key]) => paginationArgKeys.has(key))) : {};
|
|
54
|
+
async function resolveConnection({ ctx, defaultSize = 20, getCursor = (node) => node.id, input, map, query }) {
|
|
55
|
+
const paginationArgs = paginationArgsSchema.parse(extractPaginationArgs(input.args));
|
|
56
|
+
const isBackward = paginationArgs.before !== void 0 || paginationArgs.last !== void 0;
|
|
57
|
+
const cursor = isBackward ? paginationArgs.before : paginationArgs.after;
|
|
58
|
+
const direction = isBackward ? "backward" : "forward";
|
|
59
|
+
const pageSize = paginationArgs.first ?? paginationArgs.last ?? defaultSize;
|
|
60
|
+
const rawItems = await query({
|
|
61
|
+
ctx,
|
|
62
|
+
cursor,
|
|
63
|
+
direction,
|
|
64
|
+
input,
|
|
65
|
+
skip: cursor ? 1 : void 0,
|
|
66
|
+
take: pageSize + 1
|
|
67
|
+
});
|
|
68
|
+
const hasMore = rawItems.length > pageSize;
|
|
69
|
+
const limitedItems = isBackward ? rawItems.slice(Math.max(0, rawItems.length - pageSize)) : rawItems.slice(0, pageSize);
|
|
70
|
+
const items = (map ? await map({
|
|
71
|
+
ctx,
|
|
72
|
+
input,
|
|
73
|
+
items: limitedItems
|
|
74
|
+
}) : limitedItems).map((node) => ({
|
|
75
|
+
cursor: getCursor(node),
|
|
76
|
+
node
|
|
77
|
+
}));
|
|
78
|
+
const firstItem = items[0];
|
|
79
|
+
const lastItem = items.at(-1);
|
|
80
|
+
return {
|
|
81
|
+
items,
|
|
82
|
+
pagination: {
|
|
83
|
+
hasNext: isBackward ? Boolean(cursor) : hasMore,
|
|
84
|
+
hasPrevious: isBackward ? hasMore : Boolean(cursor),
|
|
85
|
+
nextCursor: lastItem?.cursor,
|
|
86
|
+
previousCursor: (isBackward ? hasMore : Boolean(cursor)) ? firstItem?.cursor : void 0
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Converts an array of nodes into a list view connection result the client
|
|
92
|
+
* can normalize.
|
|
93
|
+
*/
|
|
94
|
+
function arrayToConnection(nodes, { args, getCursor = (node) => String(node.id) } = {}) {
|
|
95
|
+
if (!nodes) return;
|
|
96
|
+
const paginationArgs = paginationArgsSchema.parse(extractPaginationArgs(args));
|
|
97
|
+
if (Object.keys(paginationArgs).length === 0) return {
|
|
98
|
+
items: nodes.map((node) => ({
|
|
99
|
+
cursor: getCursor(node),
|
|
100
|
+
node
|
|
101
|
+
})),
|
|
102
|
+
pagination: {
|
|
103
|
+
hasNext: false,
|
|
104
|
+
hasPrevious: false,
|
|
105
|
+
nextCursor: void 0,
|
|
106
|
+
previousCursor: void 0
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const isBackward = paginationArgs.before !== void 0 || paginationArgs.last !== void 0;
|
|
110
|
+
const cursor = isBackward ? paginationArgs.before : paginationArgs.after;
|
|
111
|
+
const pageSize = paginationArgs.first ?? paginationArgs.last ?? nodes.length;
|
|
112
|
+
const cursorIndex = cursor === void 0 ? -1 : nodes.findIndex((node) => getCursor(node) === cursor);
|
|
113
|
+
const selectedNodes = cursorIndex < 0 ? nodes : isBackward ? nodes.slice(0, cursorIndex) : nodes.slice(cursorIndex + 1);
|
|
114
|
+
const hasNext = selectedNodes.length > pageSize;
|
|
115
|
+
const hasPrevious = nodes.length > selectedNodes.length;
|
|
116
|
+
const items = (isBackward ? selectedNodes.slice(Math.max(0, selectedNodes.length - pageSize)) : selectedNodes.slice(0, pageSize)).map((node) => ({
|
|
117
|
+
cursor: getCursor(node),
|
|
118
|
+
node
|
|
119
|
+
}));
|
|
120
|
+
const firstItem = items[0];
|
|
121
|
+
const lastItem = items.at(-1);
|
|
122
|
+
return {
|
|
123
|
+
items,
|
|
124
|
+
pagination: {
|
|
125
|
+
hasNext: isBackward ? hasPrevious : hasNext,
|
|
126
|
+
hasPrevious: isBackward ? hasNext : hasPrevious,
|
|
127
|
+
nextCursor: lastItem?.cursor,
|
|
128
|
+
previousCursor: (isBackward ? hasNext : hasPrevious) && firstItem ? firstItem.cursor : void 0
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const isDataViewField$1 = (field) => Boolean(field) && typeof field === "object" && "fields" in field;
|
|
133
|
+
const assignIfChanged = (current, key, next, existing) => {
|
|
134
|
+
if (next === existing) return current;
|
|
135
|
+
if (!current) return { [key]: next };
|
|
136
|
+
current[key] = next;
|
|
137
|
+
return current;
|
|
138
|
+
};
|
|
139
|
+
function toConnectionResult({ args, item, path, view }) {
|
|
140
|
+
if (!isRecord(item)) return item;
|
|
141
|
+
let result = null;
|
|
142
|
+
const base = () => result ? {
|
|
143
|
+
...item,
|
|
144
|
+
...result
|
|
145
|
+
} : item;
|
|
146
|
+
for (const [field, config] of Object.entries(view.fields)) {
|
|
147
|
+
if (!isDataViewField$1(config)) continue;
|
|
148
|
+
const current = base()[field];
|
|
149
|
+
const nextPath = path ? `${path}.${field}` : field;
|
|
150
|
+
if (config.kind === "list") {
|
|
151
|
+
if (isConnectionResult(current)) {
|
|
152
|
+
const wrappedItems = current.items.map((entry) => ({
|
|
153
|
+
...entry,
|
|
154
|
+
node: toConnectionResult({
|
|
155
|
+
args,
|
|
156
|
+
item: entry.node,
|
|
157
|
+
path: nextPath,
|
|
158
|
+
view: config
|
|
159
|
+
})
|
|
160
|
+
}));
|
|
161
|
+
const connection = wrappedItems.some((entry, index) => entry.node !== current.items[index]?.node) || wrappedItems.length !== current.items.length ? {
|
|
162
|
+
...current,
|
|
163
|
+
items: wrappedItems
|
|
164
|
+
} : current;
|
|
165
|
+
result = assignIfChanged(result, field, connection, current);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!Array.isArray(current)) continue;
|
|
169
|
+
const connection = arrayToConnection(current.map((item) => toConnectionResult({
|
|
170
|
+
args,
|
|
171
|
+
item,
|
|
172
|
+
path: nextPath,
|
|
173
|
+
view: config
|
|
174
|
+
})), { args: getScopedArgs(args, nextPath) });
|
|
175
|
+
result = assignIfChanged(result, field, connection, current);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(current)) {
|
|
179
|
+
const wrapped = current.map((entry) => toConnectionResult({
|
|
180
|
+
args,
|
|
181
|
+
item: entry,
|
|
182
|
+
path: nextPath,
|
|
183
|
+
view: config
|
|
184
|
+
}));
|
|
185
|
+
result = assignIfChanged(result, field, wrapped, current);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (isRecord(current)) {
|
|
189
|
+
const wrapped = toConnectionResult({
|
|
190
|
+
args,
|
|
191
|
+
item: current,
|
|
192
|
+
path: nextPath,
|
|
193
|
+
view: config
|
|
194
|
+
});
|
|
195
|
+
result = assignIfChanged(result, field, wrapped, current);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return result ? {
|
|
199
|
+
...item,
|
|
200
|
+
...result
|
|
201
|
+
} : item;
|
|
202
|
+
}
|
|
203
|
+
function withConnection(procedure) {
|
|
204
|
+
return ({ defaultSize = 20, getCursor = (node) => node.id, input: additionalInput, map, query }) => {
|
|
205
|
+
const inputSchema = additionalInput ? connectionInput.extend({ args: connectionArgs.and(additionalInput) }) : connectionInput;
|
|
206
|
+
return procedure.input(inputSchema).query(async (resolverOptions) => {
|
|
207
|
+
const { ctx, input } = resolverOptions;
|
|
208
|
+
return resolveConnection({
|
|
209
|
+
ctx,
|
|
210
|
+
defaultSize,
|
|
211
|
+
getCursor,
|
|
212
|
+
input,
|
|
213
|
+
map,
|
|
214
|
+
query
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/server/prismaSelect.ts
|
|
221
|
+
const toPrismaOrderBy = (orderBy) => orderBy.map((entry) => ({ [entry.field]: entry.direction }));
|
|
222
|
+
const mergeObject = (target, source) => {
|
|
223
|
+
for (const [key, value] of Object.entries(source)) {
|
|
224
|
+
const existing = target[key];
|
|
225
|
+
if (existing && value && typeof existing === "object" && typeof value === "object" && !Array.isArray(existing) && !Array.isArray(value)) {
|
|
226
|
+
mergeObject(existing, value);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
target[key] = value;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const stableStringify = (value) => {
|
|
233
|
+
if (Array.isArray(value)) return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
234
|
+
if (value && typeof value === "object") return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
|
|
235
|
+
return JSON.stringify(value);
|
|
236
|
+
};
|
|
237
|
+
const getConflictingCountRelations = (node) => {
|
|
238
|
+
const signaturesByRelation = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const computed of node.computeds.values()) {
|
|
240
|
+
if (!computed.select) continue;
|
|
241
|
+
for (const selection of Object.values(computed.select)) {
|
|
242
|
+
if (selection.kind !== "count") continue;
|
|
243
|
+
const signatures = signaturesByRelation.get(selection.relation) ?? /* @__PURE__ */ new Set();
|
|
244
|
+
signatures.add(stableStringify(selection.where ?? null));
|
|
245
|
+
signaturesByRelation.set(selection.relation, signatures);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return new Set([...signaturesByRelation.entries()].filter(([, signatures]) => signatures.size > 1).map(([relation]) => relation));
|
|
249
|
+
};
|
|
250
|
+
const ensureRelationSelect = (select, path) => {
|
|
251
|
+
if (!path) return select;
|
|
252
|
+
const segments = path.split(".");
|
|
253
|
+
let current = select;
|
|
254
|
+
for (const segment of segments) {
|
|
255
|
+
const existing = current[segment];
|
|
256
|
+
if (existing && typeof existing === "object" && "select" in existing) {
|
|
257
|
+
const relation = existing;
|
|
258
|
+
if (!relation.select || typeof relation.select !== "object" || Array.isArray(relation.select)) relation.select = {};
|
|
259
|
+
current = relation.select;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const relation = { select: {} };
|
|
263
|
+
current[segment] = relation;
|
|
264
|
+
current = relation.select;
|
|
265
|
+
}
|
|
266
|
+
return current;
|
|
267
|
+
};
|
|
268
|
+
const assignDependencyPath = (select, path) => {
|
|
269
|
+
const segments = path.split(".");
|
|
270
|
+
let current = select;
|
|
271
|
+
for (const [index, segment] of segments.entries()) {
|
|
272
|
+
if (index === segments.length - 1) {
|
|
273
|
+
current[segment] = true;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
current = ensureRelationSelect(current, segment);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const applyComputedSelections = (node, select) => {
|
|
280
|
+
const conflictingCountRelations = getConflictingCountRelations(node);
|
|
281
|
+
for (const computed of node.computeds.values()) {
|
|
282
|
+
if (!computed.select) continue;
|
|
283
|
+
for (const selection of Object.values(computed.select)) {
|
|
284
|
+
if (selection.kind === "count") {
|
|
285
|
+
if (conflictingCountRelations.has(selection.relation)) continue;
|
|
286
|
+
const target = ensureRelationSelect(select, node.path);
|
|
287
|
+
const existing = target._count ?? { select: {} };
|
|
288
|
+
target._count = existing;
|
|
289
|
+
const countSelect = existing.select ?? {};
|
|
290
|
+
existing.select = countSelect;
|
|
291
|
+
countSelect[selection.relation] = selection.where ? { where: selection.where } : true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
assignDependencyPath(ensureRelationSelect(select, node.path), selection.path);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const relation of node.relations.values()) applyComputedSelections(relation, select);
|
|
298
|
+
};
|
|
299
|
+
const collectResolverSelections = (node, select, args, context) => {
|
|
300
|
+
for (const resolver of node.resolvers.values()) {
|
|
301
|
+
if (!resolver.select) continue;
|
|
302
|
+
const addition = typeof resolver.select === "function" ? resolver.select({
|
|
303
|
+
args,
|
|
304
|
+
context
|
|
305
|
+
}) : resolver.select;
|
|
306
|
+
if (addition && typeof addition === "object" && !Array.isArray(addition)) mergeObject(ensureRelationSelect(select, node.path), addition);
|
|
307
|
+
}
|
|
308
|
+
for (const relation of node.relations.values()) collectResolverSelections(relation, select, args, context);
|
|
309
|
+
};
|
|
310
|
+
const buildNodeSelect = (node) => {
|
|
311
|
+
const select = {};
|
|
312
|
+
for (const field of node.selectedFields) {
|
|
313
|
+
if (field === "cursor") continue;
|
|
314
|
+
select[field] = true;
|
|
315
|
+
}
|
|
316
|
+
for (const [field, relation] of node.relations) {
|
|
317
|
+
const relationSelect = buildNodeSelect(relation);
|
|
318
|
+
const scopedArgs = relation.args;
|
|
319
|
+
const relationConfig = "source" in node ? node.source?.relations?.[field] : void 0;
|
|
320
|
+
const orderBy = (relationConfig ? relationConfig.kind === "many" || relationConfig.kind === "manyToMany" : false) && "orderBy" in relation ? toPrismaOrderBy(relation.orderBy ?? []) : [];
|
|
321
|
+
select[field] = scopedArgs && Object.keys(scopedArgs).length ? {
|
|
322
|
+
...toPrismaArgs(scopedArgs),
|
|
323
|
+
...orderBy.length ? { orderBy } : null,
|
|
324
|
+
select: relationSelect
|
|
325
|
+
} : {
|
|
326
|
+
...orderBy.length ? { orderBy } : null,
|
|
327
|
+
select: relationSelect
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return select;
|
|
331
|
+
};
|
|
332
|
+
function toPrismaSelect(plan) {
|
|
333
|
+
const select = buildNodeSelect(plan.root);
|
|
334
|
+
applyComputedSelections(plan.root, select);
|
|
335
|
+
collectResolverSelections(plan.root, select, plan.args, plan.ctx);
|
|
336
|
+
return select;
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/server/dataView.ts
|
|
340
|
+
const dataViewFieldsKey = Symbol("__fate__DataViewFields");
|
|
341
|
+
const dataViewBaseKey = Symbol("__fate__DataViewBase");
|
|
342
|
+
const dataViewListOptionsKey = Symbol("__fate__DataViewListOptions");
|
|
343
|
+
const computedStateKey = Symbol("__fate__ComputedState");
|
|
344
|
+
const isDataView = (value) => isRecord(value) && isRecord(value.fields) && typeof value.typeName === "string";
|
|
345
|
+
const getBaseDataView = (view) => view[dataViewBaseKey] ?? view;
|
|
346
|
+
const getDataViewListOptions = (view) => view[dataViewListOptionsKey];
|
|
347
|
+
/**
|
|
348
|
+
* Declares a server data view that exposes an object's available fields to the client.
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* const Post = dataView<PostItem>('Post')({
|
|
352
|
+
* id: true,
|
|
353
|
+
* title: true,
|
|
354
|
+
* });
|
|
355
|
+
*/
|
|
356
|
+
function dataView(typeName) {
|
|
357
|
+
return (fields) => {
|
|
358
|
+
return {
|
|
359
|
+
[dataViewFieldsKey]: fields,
|
|
360
|
+
fields,
|
|
361
|
+
typeName
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Marks a data view as a list resolver so the server can respond with
|
|
367
|
+
* connection information.
|
|
368
|
+
*/
|
|
369
|
+
const list = (view, options) => {
|
|
370
|
+
return {
|
|
371
|
+
...view,
|
|
372
|
+
[dataViewBaseKey]: getBaseDataView(view),
|
|
373
|
+
...options ? { [dataViewListOptionsKey]: options } : null,
|
|
374
|
+
kind: "list"
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* Declares a resolver field inside a data view, optionally providing a
|
|
379
|
+
* selection for any data dependencies.
|
|
380
|
+
*/
|
|
381
|
+
function resolver(config) {
|
|
382
|
+
return {
|
|
383
|
+
kind: "resolver",
|
|
384
|
+
...config
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const field = (path) => ({
|
|
388
|
+
kind: "field",
|
|
389
|
+
path
|
|
390
|
+
});
|
|
391
|
+
const count = (relation, options) => ({
|
|
392
|
+
kind: "count",
|
|
393
|
+
relation,
|
|
394
|
+
where: options?.where
|
|
395
|
+
});
|
|
396
|
+
function computed(config) {
|
|
397
|
+
return {
|
|
398
|
+
kind: "computed",
|
|
399
|
+
...config
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const isResolverField = (field) => Boolean(field) && typeof field === "object" && "kind" in field && field.kind === "resolver";
|
|
403
|
+
const isComputedField = (field) => Boolean(field) && typeof field === "object" && "kind" in field && field.kind === "computed";
|
|
404
|
+
const isDataViewField = (field) => Boolean(field) && typeof field === "object" && "fields" in field;
|
|
405
|
+
const getValueAtPath = (item, path) => {
|
|
406
|
+
let current = item;
|
|
407
|
+
for (const segment of path.split(".")) {
|
|
408
|
+
if (!isRecord(current)) return;
|
|
409
|
+
current = current[segment];
|
|
410
|
+
}
|
|
411
|
+
return current;
|
|
412
|
+
};
|
|
413
|
+
const getComputedState = (item) => item[computedStateKey];
|
|
414
|
+
const attachComputedState = (item, field, deps) => {
|
|
415
|
+
const state = getComputedState(item) ?? {};
|
|
416
|
+
state[field] = {
|
|
417
|
+
...state[field] ?? {},
|
|
418
|
+
...deps
|
|
419
|
+
};
|
|
420
|
+
Object.defineProperty(item, computedStateKey, {
|
|
421
|
+
configurable: true,
|
|
422
|
+
enumerable: false,
|
|
423
|
+
value: state,
|
|
424
|
+
writable: true
|
|
425
|
+
});
|
|
426
|
+
return item;
|
|
427
|
+
};
|
|
428
|
+
const getComputedDeps = (item, field, select) => {
|
|
429
|
+
const deps = { ...getComputedState(item)?.[field] ?? {} };
|
|
430
|
+
if (!select) return deps;
|
|
431
|
+
for (const [name, selection] of Object.entries(select)) {
|
|
432
|
+
if (deps[name] !== void 0) continue;
|
|
433
|
+
if (selection.kind === "count") {
|
|
434
|
+
deps[name] = getValueAtPath(item, `_count.${selection.relation}`) ?? 0;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
deps[name] = getValueAtPath(item, selection.path);
|
|
438
|
+
}
|
|
439
|
+
return deps;
|
|
440
|
+
};
|
|
441
|
+
const filterToViewFields = (item, view, selectedPaths, prefix = null) => {
|
|
442
|
+
if (!isRecord(item)) return item;
|
|
443
|
+
const filtered = {};
|
|
444
|
+
for (const [field, config] of Object.entries(view.fields)) {
|
|
445
|
+
const path = prefix ? `${prefix}.${field}` : field;
|
|
446
|
+
let hasSelection = selectedPaths.has(path);
|
|
447
|
+
if (!hasSelection) {
|
|
448
|
+
for (const selected of selectedPaths) if (selected.startsWith(`${path}.`)) {
|
|
449
|
+
hasSelection = true;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!hasSelection) continue;
|
|
454
|
+
if (!(field in item)) continue;
|
|
455
|
+
const value = item[field];
|
|
456
|
+
if (isDataViewField(config)) {
|
|
457
|
+
if (isConnectionResult(value)) {
|
|
458
|
+
filtered[field] = {
|
|
459
|
+
...value,
|
|
460
|
+
items: value.items.map((entry) => ({
|
|
461
|
+
...entry,
|
|
462
|
+
node: isRecord(entry.node) ? filterToViewFields(entry.node, config, selectedPaths, path) : entry.node
|
|
463
|
+
}))
|
|
464
|
+
};
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (Array.isArray(value)) {
|
|
468
|
+
filtered[field] = value.map((entry) => isRecord(entry) ? filterToViewFields(entry, config, selectedPaths, path) : entry);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (isRecord(value)) {
|
|
472
|
+
filtered[field] = filterToViewFields(value, config, selectedPaths, path);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
filtered[field] = value;
|
|
477
|
+
}
|
|
478
|
+
return filtered;
|
|
479
|
+
};
|
|
480
|
+
const createSelectedNode = (view, path, args) => ({
|
|
481
|
+
args: path ? getScopedArgs(args, path) : args,
|
|
482
|
+
computeds: /* @__PURE__ */ new Map(),
|
|
483
|
+
path,
|
|
484
|
+
relations: /* @__PURE__ */ new Map(),
|
|
485
|
+
resolvers: /* @__PURE__ */ new Map(),
|
|
486
|
+
selectedFields: /* @__PURE__ */ new Set(),
|
|
487
|
+
view
|
|
488
|
+
});
|
|
489
|
+
const assignPath = (node, segments, path, args, selectedPaths, view) => {
|
|
490
|
+
if (segments.length === 0) return;
|
|
491
|
+
const [segment, ...rest] = segments;
|
|
492
|
+
const field = view.fields[segment];
|
|
493
|
+
if (!field) return;
|
|
494
|
+
const nextPath = path ? `${path}.${segment}` : segment;
|
|
495
|
+
if (isResolverField(field)) {
|
|
496
|
+
if (rest.length === 0) {
|
|
497
|
+
node.resolvers.set(segment, field);
|
|
498
|
+
selectedPaths.add(nextPath);
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (isComputedField(field)) {
|
|
503
|
+
if (rest.length === 0) {
|
|
504
|
+
node.computeds.set(segment, field);
|
|
505
|
+
selectedPaths.add(nextPath);
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (isDataViewField(field)) {
|
|
510
|
+
let relationNode = node.relations.get(segment);
|
|
511
|
+
if (!relationNode) {
|
|
512
|
+
relationNode = createSelectedNode(field, nextPath, args);
|
|
513
|
+
node.relations.set(segment, relationNode);
|
|
514
|
+
}
|
|
515
|
+
if (field.fields.id === true) {
|
|
516
|
+
relationNode.selectedFields.add("id");
|
|
517
|
+
selectedPaths.add(`${nextPath}.id`);
|
|
518
|
+
}
|
|
519
|
+
if (rest.length === 0) {
|
|
520
|
+
selectedPaths.add(nextPath);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
assignPath(relationNode, rest, nextPath, args, selectedPaths, field);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (rest.length === 0) {
|
|
527
|
+
node.selectedFields.add(segment);
|
|
528
|
+
selectedPaths.add(nextPath);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
const resolveNode = async ({ item, node, options: resolverOptions }) => {
|
|
532
|
+
if (!isRecord(item)) return item;
|
|
533
|
+
let result = null;
|
|
534
|
+
const assign = (key, value) => {
|
|
535
|
+
if (!result) {
|
|
536
|
+
result = { ...item };
|
|
537
|
+
const state = getComputedState(item);
|
|
538
|
+
if (state) Object.defineProperty(result, computedStateKey, {
|
|
539
|
+
configurable: true,
|
|
540
|
+
enumerable: false,
|
|
541
|
+
value: state,
|
|
542
|
+
writable: true
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
result[key] = value;
|
|
546
|
+
};
|
|
547
|
+
const getItem = () => result ?? item;
|
|
548
|
+
for (const [field, resolver] of node.resolvers) {
|
|
549
|
+
if (resolver.authorize) {
|
|
550
|
+
if (!await resolver.authorize(getItem(), resolverOptions.context, resolverOptions.args)) {
|
|
551
|
+
assign(field, null);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const value = await resolver.resolve(getItem(), resolverOptions.context, resolverOptions.args);
|
|
556
|
+
if (value !== void 0) assign(field, value);
|
|
557
|
+
}
|
|
558
|
+
for (const [field, computedField] of node.computeds) {
|
|
559
|
+
if (computedField.authorize) {
|
|
560
|
+
if (!await computedField.authorize(getItem(), resolverOptions.context, resolverOptions.args)) {
|
|
561
|
+
assign(field, null);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const value = await computedField.resolve(getItem(), getComputedDeps(getItem(), field, computedField.select), resolverOptions.context, resolverOptions.args);
|
|
566
|
+
if (value !== void 0) assign(field, value);
|
|
567
|
+
}
|
|
568
|
+
for (const [field, relationNode] of node.relations) {
|
|
569
|
+
const current = getItem()[field];
|
|
570
|
+
if (isConnectionResult(current)) {
|
|
571
|
+
const resolvedItems = await Promise.all(current.items.map(async (entry) => ({
|
|
572
|
+
...entry,
|
|
573
|
+
node: await resolveNode({
|
|
574
|
+
item: entry.node,
|
|
575
|
+
node: relationNode,
|
|
576
|
+
options: resolverOptions
|
|
577
|
+
})
|
|
578
|
+
})));
|
|
579
|
+
if (resolvedItems.some((value, index) => value.node !== current.items[index]?.node)) assign(field, {
|
|
580
|
+
...current,
|
|
581
|
+
items: resolvedItems
|
|
582
|
+
});
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (Array.isArray(current)) {
|
|
586
|
+
const resolved = await Promise.all(current.map((entry) => resolveNode({
|
|
587
|
+
item: entry,
|
|
588
|
+
node: relationNode,
|
|
589
|
+
options: resolverOptions
|
|
590
|
+
})));
|
|
591
|
+
if (resolved.some((value, index) => value !== current[index])) assign(field, resolved);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (current && typeof current === "object") {
|
|
595
|
+
const resolved = await resolveNode({
|
|
596
|
+
item: current,
|
|
597
|
+
node: relationNode,
|
|
598
|
+
options: resolverOptions
|
|
599
|
+
});
|
|
600
|
+
if (resolved !== current) assign(field, resolved);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return result ?? item;
|
|
604
|
+
};
|
|
605
|
+
/**
|
|
606
|
+
* Builds a generic execution plan for a client's selection against a server
|
|
607
|
+
* data view.
|
|
608
|
+
*/
|
|
609
|
+
function createViewPlan({ args, ctx, select: initialSelect, view }) {
|
|
610
|
+
const selectedPaths = /* @__PURE__ */ new Set();
|
|
611
|
+
selectedPaths.add("id");
|
|
612
|
+
const root = createSelectedNode(view, null, args);
|
|
613
|
+
root.selectedFields.add("id");
|
|
614
|
+
for (const path of initialSelect) {
|
|
615
|
+
if (!path) continue;
|
|
616
|
+
assignPath(root, path.split("."), null, args, selectedPaths, view);
|
|
617
|
+
}
|
|
618
|
+
const resolve = async (item) => toConnectionResult({
|
|
619
|
+
args,
|
|
620
|
+
item: filterToViewFields(await resolveNode({
|
|
621
|
+
item,
|
|
622
|
+
node: root,
|
|
623
|
+
options: {
|
|
624
|
+
args,
|
|
625
|
+
context: ctx
|
|
626
|
+
}
|
|
627
|
+
}), root.view, selectedPaths),
|
|
628
|
+
view: root.view
|
|
629
|
+
});
|
|
630
|
+
return {
|
|
631
|
+
args,
|
|
632
|
+
ctx,
|
|
633
|
+
resolve,
|
|
634
|
+
resolveMany: (items) => Promise.all(items.map((item) => resolve(item))),
|
|
635
|
+
root,
|
|
636
|
+
selectedPaths,
|
|
637
|
+
view
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Builds a resolver that applies a client's selection to a server data view,
|
|
642
|
+
* filtering fields, running nested resolvers, and shaping Prisma selects.
|
|
643
|
+
*/
|
|
644
|
+
function createResolver({ args, ctx, select, view }) {
|
|
645
|
+
const plan = createViewPlan({
|
|
646
|
+
args,
|
|
647
|
+
ctx,
|
|
648
|
+
select,
|
|
649
|
+
view
|
|
650
|
+
});
|
|
651
|
+
return {
|
|
652
|
+
resolve: plan.resolve,
|
|
653
|
+
resolveMany: plan.resolveMany,
|
|
654
|
+
select: toPrismaSelect(plan)
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/server/source.ts
|
|
659
|
+
const ascending = (field) => ({
|
|
660
|
+
direction: "asc",
|
|
661
|
+
field
|
|
662
|
+
});
|
|
663
|
+
const toSourceOrder = (orderBy) => {
|
|
664
|
+
if (!orderBy) return;
|
|
665
|
+
const sourceOrder = (Array.isArray(orderBy) ? orderBy : [orderBy]).flatMap((entry) => Object.entries(entry).map(([field, direction]) => ({
|
|
666
|
+
direction,
|
|
667
|
+
field
|
|
668
|
+
})));
|
|
669
|
+
return sourceOrder.length ? sourceOrder : void 0;
|
|
670
|
+
};
|
|
671
|
+
const getDataViewSourceConfig = (input) => {
|
|
672
|
+
const view = getBaseDataView(input);
|
|
673
|
+
const orderBy = toSourceOrder(getDataViewListOptions(input)?.orderBy);
|
|
674
|
+
return {
|
|
675
|
+
...orderBy ? { orderBy } : null,
|
|
676
|
+
view
|
|
677
|
+
};
|
|
678
|
+
};
|
|
679
|
+
const collectDataViewConfigs = (input) => {
|
|
680
|
+
const configsByFields = /* @__PURE__ */ new Map();
|
|
681
|
+
const seenValues = /* @__PURE__ */ new Set();
|
|
682
|
+
const visit = (value) => {
|
|
683
|
+
if (!value || typeof value !== "object" || seenValues.has(value)) return;
|
|
684
|
+
seenValues.add(value);
|
|
685
|
+
if (isDataView(value)) {
|
|
686
|
+
const config = getDataViewSourceConfig(value);
|
|
687
|
+
const existingConfig = configsByFields.get(config.view.fields);
|
|
688
|
+
if (existingConfig) {
|
|
689
|
+
if (config.orderBy) existingConfig.orderBy = config.orderBy;
|
|
690
|
+
} else configsByFields.set(config.view.fields, config);
|
|
691
|
+
for (const field of Object.values(config.view.fields)) visit(field);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (Array.isArray(value)) {
|
|
695
|
+
for (const entry of value) visit(entry);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
699
|
+
if (key === "Root") continue;
|
|
700
|
+
visit(entry);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
visit(input);
|
|
704
|
+
return [...configsByFields.values()];
|
|
705
|
+
};
|
|
706
|
+
function createSourceDefinition(view, config) {
|
|
707
|
+
return {
|
|
708
|
+
...config,
|
|
709
|
+
relations: void 0,
|
|
710
|
+
view
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const getRelationView = (view, field) => {
|
|
714
|
+
const config = view.fields[field];
|
|
715
|
+
if (!isDataView(config)) return null;
|
|
716
|
+
const orderBy = toSourceOrder(getDataViewListOptions(config)?.orderBy);
|
|
717
|
+
return {
|
|
718
|
+
kind: config.kind === "list" ? "many" : "one",
|
|
719
|
+
...orderBy ? { orderBy } : null,
|
|
720
|
+
view: config
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
function createSourceDefinitions(configs, options) {
|
|
724
|
+
const sources = configs.map((config) => createSourceDefinition(config.view, {
|
|
725
|
+
adapter: config.adapter,
|
|
726
|
+
id: config.id ?? "id",
|
|
727
|
+
orderBy: config.orderBy
|
|
728
|
+
}));
|
|
729
|
+
const sourcesByView = /* @__PURE__ */ new Map();
|
|
730
|
+
const sourcesByFields = /* @__PURE__ */ new Map();
|
|
731
|
+
for (const source of sources) {
|
|
732
|
+
sourcesByView.set(source.view, source);
|
|
733
|
+
sourcesByFields.set(source.view.fields, source);
|
|
734
|
+
}
|
|
735
|
+
for (const [index, config] of configs.entries()) {
|
|
736
|
+
const source = sources[index];
|
|
737
|
+
const relations = {};
|
|
738
|
+
const relationFields = new Set([...Object.keys(config.relations ?? {}), ...Object.keys(config.view.fields).filter((field) => getRelationView(config.view, field))]);
|
|
739
|
+
for (const field of relationFields) {
|
|
740
|
+
const relationView = getRelationView(config.view, field);
|
|
741
|
+
if (!relationView) throw new Error(`Source ${config.view.typeName}.${field} must reference a data view relation.`);
|
|
742
|
+
const relationSource = sourcesByView.get(relationView.view) ?? sourcesByFields.get(relationView.view.fields);
|
|
743
|
+
if (!relationSource) throw new Error(`Source ${config.view.typeName}.${field} references an unregistered data view.`);
|
|
744
|
+
const relationConfig = {
|
|
745
|
+
...options?.resolveRelation?.({
|
|
746
|
+
config,
|
|
747
|
+
field,
|
|
748
|
+
kind: relationView.kind,
|
|
749
|
+
source,
|
|
750
|
+
target: relationSource
|
|
751
|
+
}),
|
|
752
|
+
...relationView.orderBy ? { orderBy: relationView.orderBy } : null,
|
|
753
|
+
...config.relations?.[field] ?? {}
|
|
754
|
+
};
|
|
755
|
+
if (!relationConfig.foreignKey || !relationConfig.localKey) throw new Error(`Source ${config.view.typeName}.${field} requires foreignKey and localKey relation metadata.`);
|
|
756
|
+
relations[field] = {
|
|
757
|
+
...relationConfig,
|
|
758
|
+
kind: relationConfig.through ? "manyToMany" : relationView.kind,
|
|
759
|
+
source: relationSource
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (Object.keys(relations).length > 0) source.relations = relations;
|
|
763
|
+
}
|
|
764
|
+
return sources;
|
|
765
|
+
}
|
|
766
|
+
const hasNestedSelection = (select, field) => {
|
|
767
|
+
for (const path of select) if (path === field || path.startsWith(`${field}.`)) return true;
|
|
768
|
+
return false;
|
|
769
|
+
};
|
|
770
|
+
const getNestedSelection = (select, field) => {
|
|
771
|
+
const nested = [];
|
|
772
|
+
for (const path of select) if (path.startsWith(`${field}.`)) nested.push(path.slice(field.length + 1));
|
|
773
|
+
return nested;
|
|
774
|
+
};
|
|
775
|
+
const getNestedSourceInput = (input, field) => ({
|
|
776
|
+
args: getScopedArgs(input.args, field),
|
|
777
|
+
select: getNestedSelection(input.select, field)
|
|
778
|
+
});
|
|
779
|
+
const resolveSourceReference = (source) => typeof source === "function" ? source() : source;
|
|
780
|
+
const getSourceOrder = (source, override) => {
|
|
781
|
+
const base = [...override ?? source.orderBy ?? []];
|
|
782
|
+
if (!base.some((entry) => entry.field === source.id)) base.push(ascending(source.id));
|
|
783
|
+
return base.length ? base : [ascending(source.id)];
|
|
784
|
+
};
|
|
785
|
+
const attachSourceNode = (node, source, overrideOrder) => {
|
|
786
|
+
const relations = /* @__PURE__ */ new Map();
|
|
787
|
+
for (const [field, relationNode] of node.relations) {
|
|
788
|
+
const relation = source.relations?.[field];
|
|
789
|
+
if (!relation) continue;
|
|
790
|
+
const relationSource = resolveSourceReference(relation.source);
|
|
791
|
+
relations.set(field, attachSourceNode(relationNode, relationSource, relation.orderBy));
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
...node,
|
|
795
|
+
orderBy: getSourceOrder(source, overrideOrder),
|
|
796
|
+
relations,
|
|
797
|
+
source
|
|
798
|
+
};
|
|
799
|
+
};
|
|
800
|
+
function createSourcePlan({ args, ctx, select, source }) {
|
|
801
|
+
const plan = createViewPlan({
|
|
802
|
+
args,
|
|
803
|
+
ctx,
|
|
804
|
+
select,
|
|
805
|
+
view: source.view
|
|
806
|
+
});
|
|
807
|
+
return {
|
|
808
|
+
...plan,
|
|
809
|
+
root: attachSourceNode(plan.root, source),
|
|
810
|
+
source
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function createNestedSourcePlan({ ctx, field, input, source }) {
|
|
814
|
+
return createSourcePlan({
|
|
815
|
+
...getNestedSourceInput(input, field),
|
|
816
|
+
ctx,
|
|
817
|
+
source
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
//#endregion
|
|
821
|
+
//#region src/server/executor.ts
|
|
822
|
+
const createSourceRegistry = (entries) => new Map(entries);
|
|
823
|
+
const getSourceExecutor = ({ plan, registry }) => {
|
|
824
|
+
const executor = registry.get(plan.source);
|
|
825
|
+
if (!executor) throw new Error(`No executor registered for source ${plan.source.view.typeName}.`);
|
|
826
|
+
return executor;
|
|
827
|
+
};
|
|
828
|
+
const createPlan = ({ ctx, input, source }) => createSourcePlan({
|
|
829
|
+
...input,
|
|
830
|
+
ctx,
|
|
831
|
+
source
|
|
832
|
+
});
|
|
833
|
+
const resolveSourceByIds = async ({ ctx, extra, ids, input, registry, source }) => {
|
|
834
|
+
const plan = createPlan({
|
|
835
|
+
ctx,
|
|
836
|
+
input,
|
|
837
|
+
source
|
|
838
|
+
});
|
|
839
|
+
const executor = getSourceExecutor({
|
|
840
|
+
plan,
|
|
841
|
+
registry
|
|
842
|
+
});
|
|
843
|
+
const planRecord = plan;
|
|
844
|
+
if (executor.byIds) return plan.resolveMany(await executor.byIds({
|
|
845
|
+
ctx,
|
|
846
|
+
extra,
|
|
847
|
+
ids,
|
|
848
|
+
plan: planRecord
|
|
849
|
+
}));
|
|
850
|
+
if (!executor.byId) throw new Error(`Source ${plan.source.view.typeName} does not support byIds execution.`);
|
|
851
|
+
const items = await Promise.all(ids.map((id) => executor.byId?.({
|
|
852
|
+
ctx,
|
|
853
|
+
extra,
|
|
854
|
+
id,
|
|
855
|
+
plan: planRecord
|
|
856
|
+
})));
|
|
857
|
+
return plan.resolveMany(items.flatMap((item) => item ? [item] : []));
|
|
858
|
+
};
|
|
859
|
+
const resolveSourceById = async ({ ctx, extra, id, input, registry, source }) => {
|
|
860
|
+
const plan = createPlan({
|
|
861
|
+
ctx,
|
|
862
|
+
input,
|
|
863
|
+
source
|
|
864
|
+
});
|
|
865
|
+
const executor = getSourceExecutor({
|
|
866
|
+
plan,
|
|
867
|
+
registry
|
|
868
|
+
});
|
|
869
|
+
const planRecord = plan;
|
|
870
|
+
if (executor.byId) {
|
|
871
|
+
const item = await executor.byId({
|
|
872
|
+
ctx,
|
|
873
|
+
extra,
|
|
874
|
+
id,
|
|
875
|
+
plan: planRecord
|
|
876
|
+
});
|
|
877
|
+
return item ? plan.resolve(item) : null;
|
|
878
|
+
}
|
|
879
|
+
if (!executor.byIds) throw new Error(`Source ${plan.source.view.typeName} does not support byId execution.`);
|
|
880
|
+
const items = await executor.byIds({
|
|
881
|
+
ctx,
|
|
882
|
+
extra,
|
|
883
|
+
ids: [id],
|
|
884
|
+
plan: planRecord
|
|
885
|
+
});
|
|
886
|
+
return items[0] ? plan.resolve(items[0]) : null;
|
|
887
|
+
};
|
|
888
|
+
const resolveSourceConnection = async ({ ctx, cursor, direction, extra, input, registry, skip, source, take }) => {
|
|
889
|
+
const plan = createPlan({
|
|
890
|
+
ctx,
|
|
891
|
+
input,
|
|
892
|
+
source
|
|
893
|
+
});
|
|
894
|
+
const executor = getSourceExecutor({
|
|
895
|
+
plan,
|
|
896
|
+
registry
|
|
897
|
+
});
|
|
898
|
+
const planRecord = plan;
|
|
899
|
+
if (!executor.connection) throw new Error(`Source ${plan.source.view.typeName} does not support connection execution.`);
|
|
900
|
+
return plan.resolveMany(await executor.connection({
|
|
901
|
+
ctx,
|
|
902
|
+
cursor,
|
|
903
|
+
direction,
|
|
904
|
+
extra,
|
|
905
|
+
plan: planRecord,
|
|
906
|
+
skip,
|
|
907
|
+
take
|
|
908
|
+
}));
|
|
909
|
+
};
|
|
910
|
+
const refetchSourceById = async ({ ctx, extra, id, input, registry, source }) => resolveSourceById({
|
|
911
|
+
ctx,
|
|
912
|
+
extra,
|
|
913
|
+
id,
|
|
914
|
+
input,
|
|
915
|
+
registry,
|
|
916
|
+
source
|
|
917
|
+
});
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/server/input.ts
|
|
920
|
+
const byIdInput = z.object({
|
|
921
|
+
args: connectionArgs,
|
|
922
|
+
ids: z.array(z.string().min(1)).nonempty(),
|
|
923
|
+
select: z.array(z.string())
|
|
924
|
+
});
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/server/sourceRouter.ts
|
|
927
|
+
const normalizeSourceOptions = (input) => "source" in input ? input : { source: input };
|
|
928
|
+
/**
|
|
929
|
+
* Creates standard `byId` and `list` procedures for a Fate source.
|
|
930
|
+
*
|
|
931
|
+
* Use this when a router also has custom mutations or custom queries and you
|
|
932
|
+
* want to spread the generated source procedures into that router.
|
|
933
|
+
*/
|
|
934
|
+
function createSourceProcedures({ byId, createConnectionProcedure, list, procedure, registry, source }) {
|
|
935
|
+
const procedures = {};
|
|
936
|
+
if (byId !== false) procedures.byId = procedure.input(byIdInput).query(async ({ ctx, input }) => resolveSourceByIds({
|
|
937
|
+
ctx,
|
|
938
|
+
ids: input.ids,
|
|
939
|
+
input,
|
|
940
|
+
registry,
|
|
941
|
+
source
|
|
942
|
+
}));
|
|
943
|
+
if (list !== false) {
|
|
944
|
+
if (!createConnectionProcedure) throw new Error(`Source ${source.view.typeName} requires createConnectionProcedure to build a list procedure.`);
|
|
945
|
+
procedures.list = createConnectionProcedure({
|
|
946
|
+
defaultSize: (typeof list === "object" ? list : void 0)?.defaultSize,
|
|
947
|
+
query: async ({ ctx, cursor, direction, input, skip, take }) => resolveSourceConnection({
|
|
948
|
+
ctx,
|
|
949
|
+
cursor,
|
|
950
|
+
direction,
|
|
951
|
+
input,
|
|
952
|
+
registry,
|
|
953
|
+
skip,
|
|
954
|
+
source,
|
|
955
|
+
take
|
|
956
|
+
})
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
return procedures;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Binds the app-specific tRPC pieces once and returns a compact helper for
|
|
963
|
+
* source procedures.
|
|
964
|
+
*/
|
|
965
|
+
function bindSourceProcedures(defaults) {
|
|
966
|
+
return (input) => createSourceProcedures({
|
|
967
|
+
...defaults,
|
|
968
|
+
...normalizeSourceOptions(input)
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
//#endregion
|
|
972
|
+
export { list as C, resolveConnection as D, connectionArgs as E, withConnection as O, isDataView as S, toPrismaSelect as T, computed as _, refetchSourceById as a, dataView as b, resolveSourceConnection as c, createSourceDefinitions as d, createSourcePlan as f, attachComputedState as g, hasNestedSelection as h, createSourceRegistry as i, getScopedArgs as k, collectDataViewConfigs as l, getNestedSelection as m, createSourceProcedures as n, resolveSourceById as o, getDataViewSourceConfig as p, byIdInput as r, resolveSourceByIds as s, bindSourceProcedures as t, createNestedSourcePlan as u, count as v, resolver as w, field as x, createResolver as y };
|