@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/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/lib/cli.mjs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { styleText } from "node:util";
|
|
5
|
+
|
|
6
|
+
//#region src/codegen/schema.ts
|
|
7
|
+
const isDataViewField = (field) => Boolean(field) && typeof field === "object" && "fields" in field;
|
|
8
|
+
/**
|
|
9
|
+
* Builds the schema object used by the CLI generator from your data views and
|
|
10
|
+
* list resolver configs.
|
|
11
|
+
*/
|
|
12
|
+
const createFateSchema = (dataViews, lists) => {
|
|
13
|
+
const canonicalViews = /* @__PURE__ */ new Map();
|
|
14
|
+
const entities = {};
|
|
15
|
+
const fateTypes = /* @__PURE__ */ new Map();
|
|
16
|
+
const processing = /* @__PURE__ */ new Set();
|
|
17
|
+
const ensureType = (view) => {
|
|
18
|
+
const typeName = view.typeName;
|
|
19
|
+
const canonicalView = canonicalViews.get(typeName) ?? view;
|
|
20
|
+
const existing = fateTypes.get(typeName);
|
|
21
|
+
if (existing && !processing.has(typeName)) return typeName;
|
|
22
|
+
if (processing.has(typeName)) return typeName;
|
|
23
|
+
processing.add(typeName);
|
|
24
|
+
const fields = existing?.fields ?? {};
|
|
25
|
+
for (const [field, child] of Object.entries(canonicalView.fields)) if (isDataViewField(child)) {
|
|
26
|
+
const relationType = ensureType(child);
|
|
27
|
+
fields[field] = child.kind === "list" ? { listOf: relationType } : { type: relationType };
|
|
28
|
+
}
|
|
29
|
+
const descriptor = { type: typeName };
|
|
30
|
+
if (Object.keys(fields).length) descriptor.fields = fields;
|
|
31
|
+
fateTypes.set(typeName, descriptor);
|
|
32
|
+
processing.delete(typeName);
|
|
33
|
+
return typeName;
|
|
34
|
+
};
|
|
35
|
+
for (const view of dataViews) {
|
|
36
|
+
const typeName = view.typeName;
|
|
37
|
+
if (!typeName) throw new Error("Data view is missing a type name.");
|
|
38
|
+
if (!canonicalViews.has(typeName)) canonicalViews.set(typeName, view);
|
|
39
|
+
entities[typeName.toLowerCase()] = { type: typeName };
|
|
40
|
+
}
|
|
41
|
+
for (const [name, list] of Object.entries(lists)) {
|
|
42
|
+
const config = "fields" in list ? { view: list } : list;
|
|
43
|
+
const typeName = ensureType(config.view);
|
|
44
|
+
entities[typeName.toLowerCase()] = {
|
|
45
|
+
list: name,
|
|
46
|
+
listProcedure: config.procedure,
|
|
47
|
+
type: typeName
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
entities,
|
|
52
|
+
types: Array.from(fateTypes.values())
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/cli.ts
|
|
58
|
+
const root = process.cwd();
|
|
59
|
+
const [, , command, moduleName, targetFile] = process.argv;
|
|
60
|
+
if (command !== "generate" || !moduleName || !targetFile) {
|
|
61
|
+
console.error(`${styleText("bold", "Usage:")} ${styleText("blue", `pnpm fate generate <moduleName> <targetFile>`)}
|
|
62
|
+
|
|
63
|
+
Generates the fate client from the server's tRPC router.
|
|
64
|
+
|
|
65
|
+
${styleText("dim", "<moduleName>")} The module name to import the tRPC router from.
|
|
66
|
+
${styleText("dim", "<targetFile>")} The file path to write the generated client to.
|
|
67
|
+
|
|
68
|
+
${styleText("bold", "Example:")} ${styleText("blue", `pnpm fate generate @org/server/trpc/router.ts client/lib/fate.generated.ts`)}
|
|
69
|
+
`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const formatRelation = (value) => "listOf" in value ? `{ listOf: '${value.listOf}' }` : `{ type: '${value.type}' }`;
|
|
73
|
+
const formatTypes = (types) => {
|
|
74
|
+
if (!types.length) return "[]";
|
|
75
|
+
const lines = ["["];
|
|
76
|
+
for (const typeConfig of types) {
|
|
77
|
+
lines.push(" {");
|
|
78
|
+
if (typeConfig.fields) {
|
|
79
|
+
lines.push(" fields: {");
|
|
80
|
+
for (const [field, relation] of Object.entries(typeConfig.fields)) lines.push(` ${field}: ${formatRelation(relation)},`);
|
|
81
|
+
lines.push(" },");
|
|
82
|
+
}
|
|
83
|
+
lines.push(` type: '${typeConfig.type}',`, " },");
|
|
84
|
+
}
|
|
85
|
+
lines.push("]");
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
};
|
|
88
|
+
const indentBlock = (value, spaces) => value.split("\n").map((line) => line.length ? `${" ".repeat(spaces)}${line}` : line).join("\n");
|
|
89
|
+
const generate = async () => {
|
|
90
|
+
console.log(styleText("bold", `Generating fate client…\n`));
|
|
91
|
+
const [{ appRouter, Lists, ...dataViews }] = await Promise.all([import(moduleName)]);
|
|
92
|
+
const { entities, types } = createFateSchema(Object.values(dataViews), Lists);
|
|
93
|
+
const routerRecord = appRouter._def?.record ?? {};
|
|
94
|
+
const mutationEntries = [];
|
|
95
|
+
const byIdEntries = [];
|
|
96
|
+
const listEntries = [];
|
|
97
|
+
for (const [router, procedures] of Object.entries(routerRecord)) {
|
|
98
|
+
const entity = entities[router];
|
|
99
|
+
if (!entity) continue;
|
|
100
|
+
for (const [procedureName, procedure] of Object.entries(procedures)) {
|
|
101
|
+
const type = procedure?._def?.type;
|
|
102
|
+
if (!type) continue;
|
|
103
|
+
if (type === "mutation") {
|
|
104
|
+
mutationEntries.push({
|
|
105
|
+
entityType: entity.type,
|
|
106
|
+
name: `${router}.${procedureName}`,
|
|
107
|
+
procedure: procedureName,
|
|
108
|
+
router
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (procedureName === "byId" && type === "query") {
|
|
113
|
+
byIdEntries.push({
|
|
114
|
+
entityType: entity.type,
|
|
115
|
+
router
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const listProcedure = entity.listProcedure ?? "list";
|
|
120
|
+
if (procedureName === listProcedure && type === "query" && entity.list) listEntries.push({
|
|
121
|
+
list: entity.list,
|
|
122
|
+
procedure: listProcedure,
|
|
123
|
+
router
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
mutationEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
128
|
+
byIdEntries.sort((a, b) => a.entityType.localeCompare(b.entityType));
|
|
129
|
+
listEntries.sort((a, b) => a.list.localeCompare(b.list));
|
|
130
|
+
const viewTypes = Array.from(["AppRouter", ...new Set(mutationEntries.map((entry) => entry.entityType))]).sort();
|
|
131
|
+
const mutationResolverLines = mutationEntries.map(({ name, procedure, router }) => `'${name}': (client: TRPCClientType) => client.${router}.${procedure}.mutate,`);
|
|
132
|
+
const mutationConfigLines = mutationEntries.map(({ entityType, name, procedure, router }) => `'${name}': mutation<
|
|
133
|
+
${entityType},
|
|
134
|
+
RouterInputs['${router}']['${procedure}'],
|
|
135
|
+
RouterOutputs['${router}']['${procedure}']
|
|
136
|
+
>('${entityType}'),`);
|
|
137
|
+
const byIdLines = byIdEntries.map(({ entityType, router }) => `${entityType}: (client: TRPCClientType) => ({
|
|
138
|
+
args,
|
|
139
|
+
ids,
|
|
140
|
+
select,
|
|
141
|
+
}: { args?: Record<string, unknown>; ids: Array<string | number>; select: Array<string> }) =>
|
|
142
|
+
client.${router}.byId.query({
|
|
143
|
+
args,
|
|
144
|
+
ids: ids.map(String),
|
|
145
|
+
select,
|
|
146
|
+
}),`);
|
|
147
|
+
const listLines = listEntries.map(({ list, procedure, router }) => `${list}: (client: TRPCClientType) => client.${router}.${procedure}.query,`);
|
|
148
|
+
const typeImports = `import type { ${viewTypes.join(", ")} } from '${moduleName}';`;
|
|
149
|
+
const typesBlock = indentBlock(formatTypes(types), 6);
|
|
150
|
+
const mutationResolverBlock = indentBlock(mutationResolverLines.join("\n"), 4);
|
|
151
|
+
const mutationConfigBlock = indentBlock(mutationConfigLines.join("\n"), 6);
|
|
152
|
+
const byIdBlock = indentBlock(byIdLines.join("\n"), 8);
|
|
153
|
+
const listsBlockContent = listLines.join("\n");
|
|
154
|
+
const source = `// @generated by \`pnpm fate generate\`
|
|
155
|
+
${typeImports}
|
|
156
|
+
import { createTRPCProxyClient } from '@trpc/client';
|
|
157
|
+
import { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
|
|
158
|
+
import { createClient, createTRPCTransport, mutation } from 'react-fate';
|
|
159
|
+
|
|
160
|
+
type TRPCClientType = ReturnType<typeof createTRPCProxyClient<AppRouter>>;
|
|
161
|
+
type RouterInputs = inferRouterInputs<AppRouter>;
|
|
162
|
+
type RouterOutputs = inferRouterOutputs<AppRouter>;
|
|
163
|
+
|
|
164
|
+
export const createFateClient = (options: {
|
|
165
|
+
links: Parameters<typeof createTRPCProxyClient>[0]['links'];
|
|
166
|
+
}) => {
|
|
167
|
+
const trpcClient = createTRPCProxyClient<AppRouter>(options);
|
|
168
|
+
|
|
169
|
+
const mutations = {
|
|
170
|
+
${mutationResolverBlock}
|
|
171
|
+
} as const;
|
|
172
|
+
|
|
173
|
+
return createClient({
|
|
174
|
+
mutations: {
|
|
175
|
+
${mutationConfigBlock}
|
|
176
|
+
},
|
|
177
|
+
transport: createTRPCTransport<AppRouter, typeof mutations>({
|
|
178
|
+
byId: {
|
|
179
|
+
${byIdBlock}
|
|
180
|
+
},
|
|
181
|
+
client: trpcClient,
|
|
182
|
+
${listLines.length ? ` lists: {\n${indentBlock(listsBlockContent, 8)}\n },\n` : ""} mutations,
|
|
183
|
+
}),
|
|
184
|
+
types: ${typesBlock},
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
`;
|
|
188
|
+
const outputPath = path.join(root, targetFile);
|
|
189
|
+
writeFileSync(outputPath, source);
|
|
190
|
+
console.log(styleText("green", ` \u2713 fate client generated at '${path.relative(root, outputPath)}'.`));
|
|
191
|
+
};
|
|
192
|
+
await generate();
|
|
193
|
+
|
|
194
|
+
//#endregion
|
|
195
|
+
export { };
|