@archlast/cli 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/README.md +141 -0
- package/dist/analyzer.d.ts +96 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +404 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +106 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +322875 -0
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +36 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +23 -0
- package/dist/commands/data.d.ts +6 -0
- package/dist/commands/data.d.ts.map +1 -0
- package/dist/commands/data.js +300 -0
- package/dist/commands/deploy.d.ts +9 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +59 -0
- package/dist/commands/dev.d.ts +10 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +132 -0
- package/dist/commands/generate.d.ts +6 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +100 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/logs.d.ts +10 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +38 -0
- package/dist/commands/pull.d.ts +16 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +415 -0
- package/dist/commands/restart.d.ts +11 -0
- package/dist/commands/restart.d.ts.map +1 -0
- package/dist/commands/restart.js +63 -0
- package/dist/commands/start.d.ts +11 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +74 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +69 -0
- package/dist/commands/stop.d.ts +8 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +23 -0
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +77 -0
- package/dist/docker/compose.d.ts +3 -0
- package/dist/docker/compose.d.ts.map +1 -0
- package/dist/docker/compose.js +47 -0
- package/dist/docker/config.d.ts +12 -0
- package/dist/docker/config.d.ts.map +1 -0
- package/dist/docker/config.js +183 -0
- package/dist/docker/manager.d.ts +19 -0
- package/dist/docker/manager.d.ts.map +1 -0
- package/dist/docker/manager.js +239 -0
- package/dist/docker/ports.d.ts +6 -0
- package/dist/docker/ports.d.ts.map +1 -0
- package/dist/docker/restart-on-deploy.d.ts +6 -0
- package/dist/docker/restart-on-deploy.d.ts.map +1 -0
- package/dist/docker/types.d.ts +36 -0
- package/dist/docker/types.d.ts.map +1 -0
- package/dist/docker/types.js +1 -0
- package/dist/events-listener.d.ts +19 -0
- package/dist/events-listener.d.ts.map +1 -0
- package/dist/events-listener.js +105 -0
- package/dist/generator.d.ts +44 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +1816 -0
- package/dist/generators/di.d.ts +21 -0
- package/dist/generators/di.d.ts.map +1 -0
- package/dist/generators/di.js +100 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/project.d.ts +18 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/protocol.d.ts +58 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +5 -0
- package/dist/uploader.d.ts +63 -0
- package/dist/uploader.d.ts.map +1 -0
- package/dist/uploader.js +255 -0
- package/dist/watcher.d.ts +13 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +38 -0
- package/package.json +58 -0
- package/scripts/postinstall.cjs +65 -0
|
@@ -0,0 +1,1816 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
5
|
+
import { generateDI } from "./generators/di";
|
|
6
|
+
// Helper to resolve imports
|
|
7
|
+
function resolveImport(typeText, sourceFile) {
|
|
8
|
+
return typeText.replace(/import\("([^"]+)"\)\./g, (match, path) => {
|
|
9
|
+
if (path.includes("packages/server/src/db/sqlite")) {
|
|
10
|
+
return "Document";
|
|
11
|
+
}
|
|
12
|
+
return "";
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export class TypeGenerator {
|
|
16
|
+
archlastPath;
|
|
17
|
+
constructor(archlastPath) {
|
|
18
|
+
this.archlastPath = path.resolve(archlastPath);
|
|
19
|
+
}
|
|
20
|
+
async generate(analysis) {
|
|
21
|
+
const srcPath = path.join(this.archlastPath, "src");
|
|
22
|
+
const generatedDir = path.join(this.archlastPath, "_generated");
|
|
23
|
+
// Ensure directory exists
|
|
24
|
+
if (!fs.existsSync(generatedDir)) {
|
|
25
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
// Generate types using ts-morph for sophisticated type inference
|
|
28
|
+
const typeInfos = await this.analyzeTypesWithTsMorph(srcPath);
|
|
29
|
+
const rpcTypeInfos = await this.analyzeRpcTypesWithTsMorph(srcPath);
|
|
30
|
+
const apiTypes = await this.generateApiTypes(typeInfos);
|
|
31
|
+
const serverTypes = await this.generateServerTypes();
|
|
32
|
+
const indexExport = this.generateIndexExport();
|
|
33
|
+
const rpcTypes = await this.generateRpcTypes(rpcTypeInfos);
|
|
34
|
+
const trpcRouter = await this.generateTrpcRouter(rpcTypeInfos);
|
|
35
|
+
const apiPath = path.join(generatedDir, "api.ts");
|
|
36
|
+
const serverPath = path.join(generatedDir, "server.ts");
|
|
37
|
+
const indexPath = path.join(generatedDir, "index.ts");
|
|
38
|
+
const rpcPath = path.join(generatedDir, "rpc.ts");
|
|
39
|
+
const trpcRouterPath = path.join(generatedDir, "trpc-router.ts");
|
|
40
|
+
fs.writeFileSync(apiPath, apiTypes, "utf-8");
|
|
41
|
+
fs.writeFileSync(serverPath, serverTypes, "utf-8");
|
|
42
|
+
fs.writeFileSync(indexPath, indexExport, "utf-8");
|
|
43
|
+
fs.writeFileSync(rpcPath, rpcTypes, "utf-8");
|
|
44
|
+
fs.writeFileSync(trpcRouterPath, trpcRouter, "utf-8");
|
|
45
|
+
// Generate CRUD handlers
|
|
46
|
+
await this.generateCrudHandlers(generatedDir);
|
|
47
|
+
// Generate DI registration code if there are injectables
|
|
48
|
+
if (analysis.injectables && analysis.injectables.length > 0) {
|
|
49
|
+
await generateDI(generatedDir, analysis.injectables);
|
|
50
|
+
console.log(` Generated DI registration with ${analysis.injectables.length} providers`);
|
|
51
|
+
}
|
|
52
|
+
// Log RPC generation
|
|
53
|
+
if (rpcTypeInfos.length > 0) {
|
|
54
|
+
console.log(` Generated ${rpcTypeInfos.length} RPC procedure types`);
|
|
55
|
+
console.log(` Generated tRPC router with AppRouter type`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async analyzeTypesWithTsMorph(srcPath) {
|
|
59
|
+
const project = new Project({
|
|
60
|
+
skipAddingFilesFromTsConfig: true,
|
|
61
|
+
});
|
|
62
|
+
project.addSourceFilesAtPaths(path.join(srcPath, "**/*.ts"));
|
|
63
|
+
const typeInfos = [];
|
|
64
|
+
const sourceFiles = project.getSourceFiles();
|
|
65
|
+
for (const sourceFile of sourceFiles) {
|
|
66
|
+
const fileName = sourceFile.getBaseName();
|
|
67
|
+
const moduleName = fileName.replace(/\.ts$/, "");
|
|
68
|
+
const exportedVariables = sourceFile
|
|
69
|
+
.getVariableDeclarations()
|
|
70
|
+
.filter((v) => v.isExported());
|
|
71
|
+
for (const variable of exportedVariables) {
|
|
72
|
+
const name = variable.getName();
|
|
73
|
+
const initializer = variable.getInitializer();
|
|
74
|
+
if (initializer && initializer.getKind() === SyntaxKind.CallExpression) {
|
|
75
|
+
const callExpr = initializer.asKindOrThrow(SyntaxKind.CallExpression);
|
|
76
|
+
const functionName = callExpr.getExpression().getText();
|
|
77
|
+
if (functionName === "mutation" ||
|
|
78
|
+
functionName === "query" ||
|
|
79
|
+
functionName === "action") {
|
|
80
|
+
const typeInfo = this.extractTypeInfo(callExpr, moduleName, name, functionName, sourceFile);
|
|
81
|
+
if (typeInfo) {
|
|
82
|
+
typeInfos.push(typeInfo);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return typeInfos;
|
|
89
|
+
}
|
|
90
|
+
async analyzeRpcTypesWithTsMorph(srcPath) {
|
|
91
|
+
const project = new Project({
|
|
92
|
+
skipAddingFilesFromTsConfig: true,
|
|
93
|
+
});
|
|
94
|
+
project.addSourceFilesAtPaths(path.join(srcPath, "**/*.ts"));
|
|
95
|
+
const typeInfos = [];
|
|
96
|
+
const sourceFiles = project.getSourceFiles();
|
|
97
|
+
for (const sourceFile of sourceFiles) {
|
|
98
|
+
const fileName = sourceFile.getBaseName();
|
|
99
|
+
const moduleName = fileName.replace(/\.ts$/, "");
|
|
100
|
+
const exportedVariables = sourceFile
|
|
101
|
+
.getVariableDeclarations()
|
|
102
|
+
.filter((v) => v.isExported());
|
|
103
|
+
for (const variable of exportedVariables) {
|
|
104
|
+
const name = variable.getName();
|
|
105
|
+
const initializer = variable.getInitializer();
|
|
106
|
+
if (initializer && initializer.getKind() === SyntaxKind.CallExpression) {
|
|
107
|
+
const callExpr = initializer.asKindOrThrow(SyntaxKind.CallExpression);
|
|
108
|
+
const callText = callExpr.getExpression().getText();
|
|
109
|
+
// Check for rpc.query or rpc.mutation
|
|
110
|
+
if (callText === "rpc.query" || callText === "rpc.mutation") {
|
|
111
|
+
const typeInfo = this.extractRpcTypeInfo(callExpr, moduleName, name, callText === "rpc.query" ? "query" : "mutation", sourceFile);
|
|
112
|
+
if (typeInfo) {
|
|
113
|
+
typeInfos.push(typeInfo);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return typeInfos;
|
|
120
|
+
}
|
|
121
|
+
extractRpcTypeInfo(callExpr, moduleName, name, procedureType, sourceFile) {
|
|
122
|
+
const firstArg = callExpr.getArguments()[0];
|
|
123
|
+
let handlerFunc = null;
|
|
124
|
+
let authMode = undefined;
|
|
125
|
+
if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
126
|
+
const obj = firstArg.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
127
|
+
const handlerProp = obj?.getProperty("handler");
|
|
128
|
+
if (handlerProp && handlerProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
129
|
+
handlerFunc = handlerProp
|
|
130
|
+
.asKind(SyntaxKind.PropertyAssignment)
|
|
131
|
+
?.getInitializer()
|
|
132
|
+
?.asKind(SyntaxKind.ArrowFunction);
|
|
133
|
+
}
|
|
134
|
+
// Extract auth mode
|
|
135
|
+
const authProp = obj?.getProperty("auth");
|
|
136
|
+
if (authProp && authProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
137
|
+
const authInit = authProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
|
|
138
|
+
if (authInit && authInit.getKind() === SyntaxKind.StringLiteral) {
|
|
139
|
+
const authValue = authInit.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
140
|
+
if (authValue === "required" ||
|
|
141
|
+
authValue === "optional" ||
|
|
142
|
+
authValue === "public") {
|
|
143
|
+
authMode = authValue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!handlerFunc)
|
|
149
|
+
return null;
|
|
150
|
+
let argsType = "Record<string, never>";
|
|
151
|
+
let hasArgs = false;
|
|
152
|
+
// Extract args from Zod schema
|
|
153
|
+
if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
154
|
+
const obj = firstArg.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
155
|
+
const argsProp = obj?.getProperty("args");
|
|
156
|
+
if (argsProp && argsProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
157
|
+
const argsInit = argsProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
|
|
158
|
+
if (argsInit) {
|
|
159
|
+
// Handle both direct object literal { id: z.string() } and z.object({ id: z.string() })
|
|
160
|
+
if (argsInit.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
161
|
+
argsType = this.parseZodSchema(argsInit);
|
|
162
|
+
hasArgs = true;
|
|
163
|
+
}
|
|
164
|
+
else if (argsInit.getKind() === SyntaxKind.CallExpression) {
|
|
165
|
+
// Check if it's z.object({ ... })
|
|
166
|
+
const callText = argsInit.getText();
|
|
167
|
+
if (callText.startsWith("z.object(") || callText.startsWith("v.object(")) {
|
|
168
|
+
// Get the first argument of z.object() call
|
|
169
|
+
const callExprArgs = argsInit.asKind(SyntaxKind.CallExpression)?.getArguments();
|
|
170
|
+
if (callExprArgs && callExprArgs.length > 0) {
|
|
171
|
+
const objectArg = callExprArgs[0];
|
|
172
|
+
if (objectArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
173
|
+
argsType = this.parseZodSchema(objectArg);
|
|
174
|
+
hasArgs = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Fallback to inferred args type
|
|
183
|
+
if (!hasArgs) {
|
|
184
|
+
const params = handlerFunc.getParameters();
|
|
185
|
+
if (params.length >= 2) {
|
|
186
|
+
const argsParam = params[1];
|
|
187
|
+
const typeObj = argsParam.getType();
|
|
188
|
+
if (typeObj.isObject() &&
|
|
189
|
+
!typeObj.isArray() &&
|
|
190
|
+
typeObj.getProperties().length > 0) {
|
|
191
|
+
const props = typeObj.getProperties().map((p) => {
|
|
192
|
+
const decl = p.getValueDeclaration() ?? p.getDeclarations()[0];
|
|
193
|
+
const propType = decl
|
|
194
|
+
? decl
|
|
195
|
+
.getType()
|
|
196
|
+
.getText(decl)
|
|
197
|
+
.replace(/import\("[^"]+"\)\./g, "")
|
|
198
|
+
: "any";
|
|
199
|
+
return `${p.getName()}: ${propType}`;
|
|
200
|
+
});
|
|
201
|
+
argsType = `{ ${props.join("; ")} }`;
|
|
202
|
+
hasArgs = true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Extract return type
|
|
207
|
+
let returnType = this.inferReturnType(handlerFunc, sourceFile);
|
|
208
|
+
// Calculate import path relative to _generated directory
|
|
209
|
+
// sourceFile.getFilePath() gives absolute path, we need relative path like ../src/rpc-procedures
|
|
210
|
+
const filePath = sourceFile.getFilePath();
|
|
211
|
+
const srcDir = path.join(this.archlastPath, "src");
|
|
212
|
+
let relativePath = path.relative(srcDir, filePath);
|
|
213
|
+
// Remove .ts extension
|
|
214
|
+
relativePath = relativePath.replace(/\.ts$/, "");
|
|
215
|
+
// Create import path from _generated: ../src/filename
|
|
216
|
+
const importPath = `../src/${relativePath}`;
|
|
217
|
+
return {
|
|
218
|
+
name,
|
|
219
|
+
moduleName,
|
|
220
|
+
procedureType,
|
|
221
|
+
argsType,
|
|
222
|
+
returnType,
|
|
223
|
+
auth: authMode,
|
|
224
|
+
importPath,
|
|
225
|
+
hasArgs,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
extractTypeInfo(callExpr, moduleName, name, type, sourceFile) {
|
|
229
|
+
const firstArg = callExpr.getArguments()[0];
|
|
230
|
+
let handlerFunc = null;
|
|
231
|
+
let authMode = undefined;
|
|
232
|
+
if (firstArg.getKind() === SyntaxKind.ArrowFunction) {
|
|
233
|
+
handlerFunc = firstArg.asKind(SyntaxKind.ArrowFunction);
|
|
234
|
+
}
|
|
235
|
+
else if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
236
|
+
const obj = firstArg.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
237
|
+
const handlerProp = obj?.getProperty("handler");
|
|
238
|
+
if (handlerProp && handlerProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
239
|
+
handlerFunc = handlerProp
|
|
240
|
+
.asKind(SyntaxKind.PropertyAssignment)
|
|
241
|
+
?.getInitializer()
|
|
242
|
+
?.asKind(SyntaxKind.ArrowFunction);
|
|
243
|
+
}
|
|
244
|
+
// Extract auth mode
|
|
245
|
+
const authProp = obj?.getProperty("auth");
|
|
246
|
+
if (authProp && authProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
247
|
+
const authInit = authProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
|
|
248
|
+
if (authInit && authInit.getKind() === SyntaxKind.StringLiteral) {
|
|
249
|
+
const authValue = authInit.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
250
|
+
if (authValue === "required" ||
|
|
251
|
+
authValue === "optional" ||
|
|
252
|
+
authValue === "public") {
|
|
253
|
+
authMode = authValue;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!handlerFunc)
|
|
259
|
+
return null;
|
|
260
|
+
let argsType = "Record<string, never>";
|
|
261
|
+
// Extract args from Zod schema
|
|
262
|
+
if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
263
|
+
const obj = firstArg.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
264
|
+
const argsProp = obj?.getProperty("args");
|
|
265
|
+
if (argsProp && argsProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
266
|
+
const argsInit = argsProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
|
|
267
|
+
if (argsInit) {
|
|
268
|
+
// Handle both direct object literal { id: z.string() } and z.object({ id: z.string() })
|
|
269
|
+
if (argsInit.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
270
|
+
argsType = this.parseZodSchema(argsInit);
|
|
271
|
+
}
|
|
272
|
+
else if (argsInit.getKind() === SyntaxKind.CallExpression) {
|
|
273
|
+
// Check if it's z.object({ ... })
|
|
274
|
+
const callText = argsInit.getText();
|
|
275
|
+
if (callText.startsWith("z.object(") || callText.startsWith("v.object(")) {
|
|
276
|
+
// Get the first argument of z.object() call
|
|
277
|
+
const callExprArgs = argsInit.asKind(SyntaxKind.CallExpression)?.getArguments();
|
|
278
|
+
if (callExprArgs && callExprArgs.length > 0) {
|
|
279
|
+
const objectArg = callExprArgs[0];
|
|
280
|
+
if (objectArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
281
|
+
argsType = this.parseZodSchema(objectArg);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Fallback to inferred args type
|
|
290
|
+
if (argsType === "Record<string, never>") {
|
|
291
|
+
const params = handlerFunc.getParameters();
|
|
292
|
+
if (params.length >= 2) {
|
|
293
|
+
const argsParam = params[1];
|
|
294
|
+
const typeObj = argsParam.getType();
|
|
295
|
+
if (typeObj.isObject() &&
|
|
296
|
+
!typeObj.isArray() &&
|
|
297
|
+
typeObj.getProperties().length > 0) {
|
|
298
|
+
const props = typeObj.getProperties().map((p) => {
|
|
299
|
+
const decl = p.getValueDeclaration() ?? p.getDeclarations()[0];
|
|
300
|
+
const propType = decl
|
|
301
|
+
? decl
|
|
302
|
+
.getType()
|
|
303
|
+
.getText(decl)
|
|
304
|
+
.replace(/import\("[^"]+"\)\./g, "")
|
|
305
|
+
: "any";
|
|
306
|
+
return `${p.getName()}: ${propType}`;
|
|
307
|
+
});
|
|
308
|
+
argsType = `{ ${props.join("; ")} }`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Extract return type
|
|
313
|
+
let returnType = this.inferReturnType(handlerFunc, sourceFile);
|
|
314
|
+
return {
|
|
315
|
+
name,
|
|
316
|
+
moduleName,
|
|
317
|
+
type,
|
|
318
|
+
argsType,
|
|
319
|
+
returnType,
|
|
320
|
+
auth: authMode,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
parseZodSchema(argsInit) {
|
|
324
|
+
const props = argsInit.asKind(SyntaxKind.ObjectLiteralExpression)?.getProperties();
|
|
325
|
+
const typeProps = [];
|
|
326
|
+
props?.forEach((p) => {
|
|
327
|
+
const prop = p.asKind(SyntaxKind.PropertyAssignment);
|
|
328
|
+
if (prop) {
|
|
329
|
+
const propName = prop.getName();
|
|
330
|
+
const init = prop.getInitializer();
|
|
331
|
+
let propType = "any";
|
|
332
|
+
let optional = false;
|
|
333
|
+
const text = init?.getText() || "";
|
|
334
|
+
if (text.includes("v.optional(") || text.includes("z.optional(")) {
|
|
335
|
+
optional = true;
|
|
336
|
+
}
|
|
337
|
+
if (text.includes("v.string()") || text.includes("z.string()")) {
|
|
338
|
+
propType = "string";
|
|
339
|
+
}
|
|
340
|
+
else if (text.includes("v.number()") || text.includes("z.number()")) {
|
|
341
|
+
propType = "number";
|
|
342
|
+
}
|
|
343
|
+
else if (text.includes("v.boolean()") || text.includes("z.boolean()")) {
|
|
344
|
+
propType = "boolean";
|
|
345
|
+
}
|
|
346
|
+
else if (text.includes("v.array(") || text.includes("z.array(")) {
|
|
347
|
+
if (text.includes("v.object(") || text.includes("z.object(")) {
|
|
348
|
+
propType = "any[]";
|
|
349
|
+
}
|
|
350
|
+
else if (text.includes("v.string()") || text.includes("z.string()")) {
|
|
351
|
+
propType = "string[]";
|
|
352
|
+
}
|
|
353
|
+
else if (text.includes("v.number()") || text.includes("z.number()")) {
|
|
354
|
+
propType = "number[]";
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
propType = "any[]";
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (text.includes("v.object(") || text.includes("z.object(")) {
|
|
361
|
+
propType = "Record<string, any>";
|
|
362
|
+
}
|
|
363
|
+
if (optional) {
|
|
364
|
+
propType = `${propType} | undefined`;
|
|
365
|
+
}
|
|
366
|
+
typeProps.push(`${propName}: ${propType};`);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
return typeProps.length > 0 ? `{ ${typeProps.join(" ")} }` : "Record<string, never>";
|
|
370
|
+
}
|
|
371
|
+
inferReturnType(handlerFunc, sourceFile) {
|
|
372
|
+
const returnTypeNode = handlerFunc.getReturnType();
|
|
373
|
+
let returnTypeText = returnTypeNode.getText();
|
|
374
|
+
// Unwrap Promise<T> -> T
|
|
375
|
+
if (returnTypeText.startsWith("Promise<") && returnTypeText.endsWith(">")) {
|
|
376
|
+
returnTypeText = returnTypeText.slice(8, -1);
|
|
377
|
+
}
|
|
378
|
+
// Clean up return type if it contains import paths
|
|
379
|
+
returnTypeText = resolveImport(returnTypeText, sourceFile);
|
|
380
|
+
// Replace any with DataModel types even if TypeScript inferred it
|
|
381
|
+
if (returnTypeText.includes("any")) {
|
|
382
|
+
const body = handlerFunc.getBody();
|
|
383
|
+
if (body) {
|
|
384
|
+
const bodyText = body.getText();
|
|
385
|
+
// More flexible regex to handle multiline method chaining - match either direct call or chained call
|
|
386
|
+
let tableMatch = bodyText.match(/\.(query|list|get|insert|update|delete)\s*\(\s*["'](\w+)["']/);
|
|
387
|
+
const tableName = tableMatch ? tableMatch[2] : null;
|
|
388
|
+
if (tableName) {
|
|
389
|
+
const tableType = `DataModel["${tableName}"]`;
|
|
390
|
+
// Check what operation is being performed
|
|
391
|
+
const isFindMany = bodyText.includes(".findMany()") || bodyText.includes(".list(");
|
|
392
|
+
const isNullable = bodyText.includes(".findFirst()") ||
|
|
393
|
+
bodyText.includes(".findUnique()") ||
|
|
394
|
+
bodyText.includes(".get(");
|
|
395
|
+
// Replace based on operation or existing type
|
|
396
|
+
if (returnTypeText === "any[]" || (returnTypeText === "any" && isFindMany)) {
|
|
397
|
+
returnTypeText = `${tableType}[]`;
|
|
398
|
+
}
|
|
399
|
+
else if (returnTypeText === "any | null" ||
|
|
400
|
+
returnTypeText === "any|null" ||
|
|
401
|
+
(returnTypeText === "any" && isNullable)) {
|
|
402
|
+
returnTypeText = `${tableType} | null`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// If we got an object literal with 'any' fields, try to refine the types from the body
|
|
408
|
+
if (returnTypeText.includes(": any")) {
|
|
409
|
+
const body = handlerFunc.getBody();
|
|
410
|
+
if (body) {
|
|
411
|
+
const bodyText = body.getText();
|
|
412
|
+
// Extract field names from the return type
|
|
413
|
+
const fieldMatches = returnTypeText.matchAll(/(\w+):\s*any/g);
|
|
414
|
+
const fields = Array.from(fieldMatches, (m) => m[1]);
|
|
415
|
+
if (fields.length > 0) {
|
|
416
|
+
// Extract table name for type inference (handle multiline)
|
|
417
|
+
const tableMatch = bodyText.match(/ctx\.db\.(query|list|get|insert|update|delete)\s*\(\s*["'](\w+)["']/s);
|
|
418
|
+
const tableName = tableMatch ? tableMatch[2] : null;
|
|
419
|
+
const tableType = tableName ? `DataModel["${tableName}"]` : "any";
|
|
420
|
+
const refinedFields = fields.map((field) => {
|
|
421
|
+
// Check Promise.all destructuring first
|
|
422
|
+
const destructureMatch = bodyText.match(/const\s*\[([^\]]+)\]\s*=\s*await\s+Promise\.all\s*\(\s*\[([\s\S]+?)\]\s*\)/);
|
|
423
|
+
if (destructureMatch) {
|
|
424
|
+
const vars = destructureMatch[1]
|
|
425
|
+
.split(",")
|
|
426
|
+
.map((v) => v.trim());
|
|
427
|
+
const fieldIndex = vars.indexOf(field);
|
|
428
|
+
if (fieldIndex !== -1) {
|
|
429
|
+
const arrayContent = destructureMatch[2];
|
|
430
|
+
const expressions = [];
|
|
431
|
+
let currentExpr = "";
|
|
432
|
+
let depth = 0;
|
|
433
|
+
for (let i = 0; i < arrayContent.length; i++) {
|
|
434
|
+
const char = arrayContent[i];
|
|
435
|
+
if (char === "(" || char === "[" || char === "{")
|
|
436
|
+
depth++;
|
|
437
|
+
else if (char === ")" || char === "]" || char === "}")
|
|
438
|
+
depth--;
|
|
439
|
+
if (char === "," && depth === 0) {
|
|
440
|
+
expressions.push(currentExpr.trim());
|
|
441
|
+
currentExpr = "";
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
currentExpr += char;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (currentExpr.trim())
|
|
448
|
+
expressions.push(currentExpr.trim());
|
|
449
|
+
if (expressions[fieldIndex]) {
|
|
450
|
+
const expr = expressions[fieldIndex].trim();
|
|
451
|
+
if (expr.includes(".count()"))
|
|
452
|
+
return `${field}: number`;
|
|
453
|
+
if (expr.includes(".findMany()") || expr.includes(".list("))
|
|
454
|
+
return `${field}: ${tableType}[]`;
|
|
455
|
+
if (expr.includes(".findFirst()") ||
|
|
456
|
+
expr.includes(".findUnique()") ||
|
|
457
|
+
expr.includes(".get("))
|
|
458
|
+
return `${field}: ${tableType} | null`;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Look for variable declarations
|
|
463
|
+
const declPattern = new RegExp(`(const|let|var)\\s+${field}\\s*=\\s*(?:await\\s+)?(.+?)(?:;|\\n)`, "s");
|
|
464
|
+
const match = declPattern.exec(bodyText);
|
|
465
|
+
if (match) {
|
|
466
|
+
const rhs = match[2].trim();
|
|
467
|
+
if (rhs.includes(".count()"))
|
|
468
|
+
return `${field}: number`;
|
|
469
|
+
if (rhs.includes(".findMany()") || rhs.includes(".list("))
|
|
470
|
+
return `${field}: ${tableType}[]`;
|
|
471
|
+
if (rhs.includes(".findFirst()") ||
|
|
472
|
+
rhs.includes(".findUnique()") ||
|
|
473
|
+
rhs.includes(".get("))
|
|
474
|
+
return `${field}: ${tableType} | null`;
|
|
475
|
+
if (rhs.includes(".insert("))
|
|
476
|
+
return `${field}: string`;
|
|
477
|
+
if (rhs.includes(".insertMany("))
|
|
478
|
+
return `${field}: string[]`;
|
|
479
|
+
if (rhs.includes(".updateMany()") || rhs.includes(".deleteMany()"))
|
|
480
|
+
return `${field}: number`;
|
|
481
|
+
}
|
|
482
|
+
return `${field}: any`;
|
|
483
|
+
});
|
|
484
|
+
// Reconstruct the type
|
|
485
|
+
returnTypeText = `{ ${refinedFields.join("; ")} }`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Better handling of common return types
|
|
490
|
+
if (returnTypeText === "any" || returnTypeText === "void" || returnTypeText === "never") {
|
|
491
|
+
// Try to infer from the body
|
|
492
|
+
const body = handlerFunc.getBody();
|
|
493
|
+
if (body) {
|
|
494
|
+
const bodyText = body.getText();
|
|
495
|
+
// Extract table name from db operations (handle multiline)
|
|
496
|
+
const tableMatch = bodyText.match(/ctx\.db\.(query|list|get|insert|update|delete)\s*\(\s*["'](\w+)["']/s);
|
|
497
|
+
const tableName = tableMatch ? tableMatch[2] : null;
|
|
498
|
+
const tableType = tableName ? `DataModel["${tableName}"]` : "any";
|
|
499
|
+
// Check for common db patterns (check more specific ones first)
|
|
500
|
+
if (bodyText.includes(".insertMany(")) {
|
|
501
|
+
returnTypeText = "string[]";
|
|
502
|
+
}
|
|
503
|
+
else if (bodyText.includes(".updateMany(") || bodyText.includes(".deleteMany(")) {
|
|
504
|
+
returnTypeText = "number";
|
|
505
|
+
}
|
|
506
|
+
else if (bodyText.includes(".findMany()") || bodyText.includes(".list(")) {
|
|
507
|
+
returnTypeText = `${tableType}[]`;
|
|
508
|
+
}
|
|
509
|
+
else if (bodyText.includes(".findFirst()") ||
|
|
510
|
+
bodyText.includes(".findUnique()")) {
|
|
511
|
+
returnTypeText = `${tableType} | null`;
|
|
512
|
+
}
|
|
513
|
+
else if (bodyText.includes(".get(")) {
|
|
514
|
+
returnTypeText = `${tableType} | null`;
|
|
515
|
+
}
|
|
516
|
+
else if (bodyText.includes(".count()")) {
|
|
517
|
+
// Check if it's a final return or intermediate
|
|
518
|
+
if (bodyText.match(/return\s+.*\.count\(\)/)) {
|
|
519
|
+
returnTypeText = "number";
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
else if (bodyText.includes(".insert(")) {
|
|
523
|
+
returnTypeText = "string";
|
|
524
|
+
}
|
|
525
|
+
else if (bodyText.includes(".update(") || bodyText.includes(".delete(")) {
|
|
526
|
+
// Check if it's the last operation
|
|
527
|
+
if (bodyText.match(/return\s+.*\.(?:update|delete)\(/)) {
|
|
528
|
+
returnTypeText = "void";
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Object literal return - try to extract shape
|
|
532
|
+
const objMatch = bodyText.match(/return\s*{\s*([^}]+)}\s*;?/);
|
|
533
|
+
if (objMatch && returnTypeText === "any") {
|
|
534
|
+
const fields = objMatch[1]
|
|
535
|
+
.split(",")
|
|
536
|
+
.map((f) => {
|
|
537
|
+
const parts = f.trim().split(":");
|
|
538
|
+
if (parts.length === 1) {
|
|
539
|
+
// Shorthand property
|
|
540
|
+
return parts[0].trim();
|
|
541
|
+
}
|
|
542
|
+
return parts[0].trim();
|
|
543
|
+
})
|
|
544
|
+
.filter(Boolean);
|
|
545
|
+
if (fields.length > 0) {
|
|
546
|
+
// Try to infer field types from variable declarations
|
|
547
|
+
const fieldTypes = fields.map((field) => {
|
|
548
|
+
// Check Promise.all destructuring first
|
|
549
|
+
const destructureMatch = bodyText.match(/const\s*\[([^\]]+)\]\s*=\s*await\s+Promise\.all\s*\(\s*\[([\s\S]+?)\]\s*\)/);
|
|
550
|
+
if (destructureMatch) {
|
|
551
|
+
const vars = destructureMatch[1]
|
|
552
|
+
.split(",")
|
|
553
|
+
.map((v) => v.trim());
|
|
554
|
+
const fieldIndex = vars.indexOf(field);
|
|
555
|
+
if (fieldIndex !== -1) {
|
|
556
|
+
const arrayContent = destructureMatch[2];
|
|
557
|
+
const expressions = [];
|
|
558
|
+
let currentExpr = "";
|
|
559
|
+
let depth = 0;
|
|
560
|
+
for (let i = 0; i < arrayContent.length; i++) {
|
|
561
|
+
const char = arrayContent[i];
|
|
562
|
+
if (char === "(" || char === "[" || char === "{")
|
|
563
|
+
depth++;
|
|
564
|
+
else if (char === ")" || char === "]" || char === "}")
|
|
565
|
+
depth--;
|
|
566
|
+
if (char === "," && depth === 0) {
|
|
567
|
+
expressions.push(currentExpr.trim());
|
|
568
|
+
currentExpr = "";
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
currentExpr += char;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (currentExpr.trim())
|
|
575
|
+
expressions.push(currentExpr.trim());
|
|
576
|
+
if (expressions[fieldIndex]) {
|
|
577
|
+
const expr = expressions[fieldIndex].trim();
|
|
578
|
+
if (expr.includes(".count()"))
|
|
579
|
+
return `${field}: number`;
|
|
580
|
+
if (expr.includes(".findMany()") || expr.includes(".list("))
|
|
581
|
+
return `${field}: ${tableType}[]`;
|
|
582
|
+
if (expr.includes(".findFirst()") ||
|
|
583
|
+
expr.includes(".findUnique()") ||
|
|
584
|
+
expr.includes(".get("))
|
|
585
|
+
return `${field}: ${tableType} | null`;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// More comprehensive field type detection - handle await and multiline
|
|
590
|
+
const fieldDeclPattern = new RegExp(`(const|let|var)\\s+${field}\\s*=\\s*(?:await\\s+)?(.+?)(?:;|\\n)`, "s");
|
|
591
|
+
const fieldDecl = fieldDeclPattern.exec(bodyText);
|
|
592
|
+
if (fieldDecl) {
|
|
593
|
+
const rhs = fieldDecl[2].trim();
|
|
594
|
+
if (rhs.includes(".count()"))
|
|
595
|
+
return `${field}: number`;
|
|
596
|
+
if (rhs.includes(".findMany()") || rhs.includes(".list("))
|
|
597
|
+
return `${field}: ${tableType}[]`;
|
|
598
|
+
if (rhs.includes(".findFirst()") ||
|
|
599
|
+
rhs.includes(".findUnique()") ||
|
|
600
|
+
rhs.includes(".get("))
|
|
601
|
+
return `${field}: ${tableType} | null`;
|
|
602
|
+
if (rhs.includes(".insert("))
|
|
603
|
+
return `${field}: string`;
|
|
604
|
+
if (rhs.includes(".insertMany("))
|
|
605
|
+
return `${field}: string[]`;
|
|
606
|
+
if (rhs.includes(".updateMany()") || rhs.includes(".deleteMany()"))
|
|
607
|
+
return `${field}: number`;
|
|
608
|
+
if (rhs.includes(".update(") || rhs.includes(".delete("))
|
|
609
|
+
return `${field}: void`;
|
|
610
|
+
}
|
|
611
|
+
return `${field}: any`;
|
|
612
|
+
});
|
|
613
|
+
returnTypeText = `{ ${fieldTypes.join("; ")} }`;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Clean up complex types
|
|
619
|
+
returnTypeText = returnTypeText.replace(/import\("[^"]+"\)\./g, "");
|
|
620
|
+
return returnTypeText;
|
|
621
|
+
}
|
|
622
|
+
async generateDataModelFromSchema() {
|
|
623
|
+
// Check for both single file and folder-based schemas
|
|
624
|
+
const schemaPath = join(this.archlastPath, "src", "schema.ts");
|
|
625
|
+
const schemaFolderPath = join(this.archlastPath, "src", "schema");
|
|
626
|
+
const schemaIndexPath = join(schemaFolderPath, "index.ts");
|
|
627
|
+
let schemaContent = "";
|
|
628
|
+
let schemaSource = "";
|
|
629
|
+
if (await Bun.file(schemaPath).exists()) {
|
|
630
|
+
schemaContent = await Bun.file(schemaPath).text();
|
|
631
|
+
schemaSource = "schema.ts";
|
|
632
|
+
console.log(`Reading schema from ${schemaPath}`);
|
|
633
|
+
}
|
|
634
|
+
else if (await Bun.file(schemaIndexPath).exists()) {
|
|
635
|
+
schemaContent = await Bun.file(schemaIndexPath).text();
|
|
636
|
+
schemaSource = "schema/index.ts";
|
|
637
|
+
console.log(`Reading schema from ${schemaIndexPath}`);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// Scan folder for schema files
|
|
641
|
+
const folderFiles = await this.scanSchemaFolder(schemaFolderPath);
|
|
642
|
+
schemaContent = folderFiles.map(f => f.content).join("\n");
|
|
643
|
+
schemaSource = "schema/*.ts";
|
|
644
|
+
console.log(`Reading schema from folder: ${schemaFolderPath}`);
|
|
645
|
+
}
|
|
646
|
+
// Extract table definitions from schema
|
|
647
|
+
// Supports both:
|
|
648
|
+
// 1. Standalone: export const tasks = defineTable({ ...
|
|
649
|
+
// 2. Inside defineSchema: tasks: defineTable({ ...
|
|
650
|
+
// Regex matches both patterns
|
|
651
|
+
const tableMatches = schemaContent.matchAll(/(?:export\s+(?:const|let)\s+)?(\w+)\s*(?::\s*\w+)?\s*[:=]\s*defineTable\s*\(\s*\{([\s\S]+?)\n\s*}(?:,\s*({[\s\S]+?\n\s*}))?\s*\)/g);
|
|
652
|
+
const tables = {};
|
|
653
|
+
for (const match of tableMatches) {
|
|
654
|
+
const tableName = match[1];
|
|
655
|
+
const fields = match[2];
|
|
656
|
+
const options = match[3];
|
|
657
|
+
console.log(`Processing table: ${tableName}`);
|
|
658
|
+
tables[tableName] = {
|
|
659
|
+
fields: [],
|
|
660
|
+
createFields: [],
|
|
661
|
+
updateFields: [],
|
|
662
|
+
fieldMetadata: {}
|
|
663
|
+
};
|
|
664
|
+
// Match field definitions line by line to avoid greedy regex issues
|
|
665
|
+
const lines = fields.split("\n");
|
|
666
|
+
for (const line of lines) {
|
|
667
|
+
const fieldMatch = line.match(/^\s*(\w+):\s*v\.([a-zA-Z]+)\(([^)]*)\)(.*)/);
|
|
668
|
+
if (!fieldMatch)
|
|
669
|
+
continue;
|
|
670
|
+
const fieldName = fieldMatch[1];
|
|
671
|
+
console.log(` Found field: ${fieldName}`);
|
|
672
|
+
if (fieldName === "_id")
|
|
673
|
+
continue;
|
|
674
|
+
const baseType = fieldMatch[2];
|
|
675
|
+
const modifiers = fieldMatch[4] || "";
|
|
676
|
+
let tsType = "any";
|
|
677
|
+
switch (baseType) {
|
|
678
|
+
case "string":
|
|
679
|
+
tsType = "string";
|
|
680
|
+
break;
|
|
681
|
+
case "number":
|
|
682
|
+
tsType = "number";
|
|
683
|
+
break;
|
|
684
|
+
case "boolean":
|
|
685
|
+
tsType = "boolean";
|
|
686
|
+
break;
|
|
687
|
+
case "id":
|
|
688
|
+
tsType = "string";
|
|
689
|
+
break;
|
|
690
|
+
case "date":
|
|
691
|
+
tsType = "Date";
|
|
692
|
+
break;
|
|
693
|
+
case "datetime":
|
|
694
|
+
tsType = "Date";
|
|
695
|
+
break;
|
|
696
|
+
case "array": {
|
|
697
|
+
const inner = (fieldMatch[3] || "").trim();
|
|
698
|
+
if (inner.includes("v.string") || inner.includes("z.string")) {
|
|
699
|
+
tsType = "string[]";
|
|
700
|
+
}
|
|
701
|
+
else if (inner.includes("v.number") || inner.includes("z.number")) {
|
|
702
|
+
tsType = "number[]";
|
|
703
|
+
}
|
|
704
|
+
else if (inner.includes("v.boolean") || inner.includes("z.boolean")) {
|
|
705
|
+
tsType = "boolean[]";
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
tsType = "any[]";
|
|
709
|
+
}
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
case "object":
|
|
713
|
+
tsType = "Record<string, any>";
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
const isOptional = modifiers.includes(".optional()") || modifiers.includes(".nullable()");
|
|
717
|
+
const isAutoGenerated = modifiers.includes(".autoGenerated()");
|
|
718
|
+
const isCreatedNow = modifiers.includes(".createdNow()");
|
|
719
|
+
const isUpdateNow = modifiers.includes(".updateNow()");
|
|
720
|
+
const isDefault = modifiers.includes(".default(");
|
|
721
|
+
// Calculate modifier flags
|
|
722
|
+
let modFlags = 0;
|
|
723
|
+
if (isCreatedNow)
|
|
724
|
+
modFlags |= (1 << 0); // CreatedNow
|
|
725
|
+
if (isUpdateNow)
|
|
726
|
+
modFlags |= (1 << 1); // UpdateNow
|
|
727
|
+
if (isAutoGenerated)
|
|
728
|
+
modFlags |= (1 << 2); // AutoGenerated
|
|
729
|
+
if (modifiers.includes(".searchable()"))
|
|
730
|
+
modFlags |= (1 << 3); // Searchable
|
|
731
|
+
if (isDefault)
|
|
732
|
+
modFlags |= (1 << 4); // HasDefault
|
|
733
|
+
if (modifiers.includes(".unique()") || modifiers.includes(".applyUnique()"))
|
|
734
|
+
modFlags |= (1 << 5); // Unique
|
|
735
|
+
if (modifiers.includes(".index()") || modifiers.includes(".applyIndex()"))
|
|
736
|
+
modFlags |= (1 << 6); // Indexed
|
|
737
|
+
if (isOptional)
|
|
738
|
+
modFlags |= (1 << 7); // Nullable
|
|
739
|
+
tables[tableName].fieldMetadata[fieldName] = {
|
|
740
|
+
modifiers: modFlags,
|
|
741
|
+
type: tsType,
|
|
742
|
+
optional: isOptional
|
|
743
|
+
};
|
|
744
|
+
// Add to base DataModel type
|
|
745
|
+
tables[tableName].fields.push(` ${fieldName}${isOptional ? "?" : ""}: ${tsType};`);
|
|
746
|
+
// Add to CreateInput if not auto-generated and not a system field
|
|
747
|
+
if (!isAutoGenerated &&
|
|
748
|
+
fieldName !== "createdAt" &&
|
|
749
|
+
fieldName !== "updatedAt" &&
|
|
750
|
+
fieldName !== "_id" &&
|
|
751
|
+
fieldName !== "_collection") {
|
|
752
|
+
tables[tableName].createFields.push(` ${fieldName}?: ${tsType};`);
|
|
753
|
+
}
|
|
754
|
+
// Add to UpdateInput if not auto-generated and not read-only
|
|
755
|
+
if (!isAutoGenerated &&
|
|
756
|
+
fieldName !== "createdAt" &&
|
|
757
|
+
fieldName !== "_id" &&
|
|
758
|
+
fieldName !== "_collection") {
|
|
759
|
+
tables[tableName].updateFields.push(` ${fieldName}?: ${tsType};`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
// Parse relationships from options
|
|
763
|
+
if (options) {
|
|
764
|
+
const relBlockMatch = options.match(/relationships:\s*{([\s\S]+?)}/);
|
|
765
|
+
if (relBlockMatch) {
|
|
766
|
+
const relContent = relBlockMatch[1];
|
|
767
|
+
const relMatches = relContent.matchAll(/(\w+):\s*(hasMany|belongsTo|hasOne)\(["'](\w+)["']/g);
|
|
768
|
+
for (const relMatch of relMatches) {
|
|
769
|
+
const relName = relMatch[1];
|
|
770
|
+
const relType = relMatch[2];
|
|
771
|
+
const targetTable = relMatch[3];
|
|
772
|
+
let tsType = "any";
|
|
773
|
+
if (relType === "hasMany") {
|
|
774
|
+
tsType = `DataModel["${targetTable}"][]`;
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
tsType = `DataModel["${targetTable}"] | null`;
|
|
778
|
+
}
|
|
779
|
+
// Relationships are always optional in the DataModel as they need to be loaded
|
|
780
|
+
tables[tableName].fields.push(` ${relName}?: ${tsType};`);
|
|
781
|
+
console.log(` Found relationship: ${relName} -> ${targetTable} (${relType})`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (tables[tableName].fields.length === 0) {
|
|
786
|
+
console.log(` No fields found for table ${tableName}. Raw fields content:\n${fields}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// Generate DataModel type
|
|
790
|
+
const tableTypes = Object.entries(tables)
|
|
791
|
+
.map(([name, data]) => {
|
|
792
|
+
return ` ${name}: {\n${data.fields.join("\n")}\n _id: string;\n _collection: "${name}";\n };`;
|
|
793
|
+
})
|
|
794
|
+
.join("\n");
|
|
795
|
+
const dataModel = `export type DataModel = {\n${tableTypes}\n};`;
|
|
796
|
+
// Generate Input Types
|
|
797
|
+
const inputTypesList = [];
|
|
798
|
+
for (const [name, data] of Object.entries(tables)) {
|
|
799
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
800
|
+
inputTypesList.push(`export type ${pascalName}CreateInput = {\n${data.createFields.length > 0 ? data.createFields.join("\n") : " // All fields are auto-generated"}\n};`);
|
|
801
|
+
inputTypesList.push(`export type ${pascalName}UpdateInput = {\n${data.updateFields.length > 0 ? data.updateFields.join("\n") : " // All fields are read-only"}\n};`);
|
|
802
|
+
}
|
|
803
|
+
const inputTypes = `// ============================================================================
|
|
804
|
+
// PRISMA-STYLE INPUT TYPES
|
|
805
|
+
// ============================================================================
|
|
806
|
+
|
|
807
|
+
${inputTypesList.join("\n\n")}
|
|
808
|
+
|
|
809
|
+
export type GeneratedInputModels = {
|
|
810
|
+
${Object.keys(tables).map(name => {
|
|
811
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
812
|
+
return ` ${name}: {
|
|
813
|
+
create: ${pascalName}CreateInput;
|
|
814
|
+
update: ${pascalName}UpdateInput;
|
|
815
|
+
};`;
|
|
816
|
+
}).join("\n")}
|
|
817
|
+
};
|
|
818
|
+
`;
|
|
819
|
+
// Generate Relationship Types
|
|
820
|
+
const relationshipTypesList = [];
|
|
821
|
+
for (const [name, data] of Object.entries(tables)) {
|
|
822
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
823
|
+
// Extract relationship fields from the base type
|
|
824
|
+
const relationshipFields = data.fields.filter(f => f.includes("DataModel["));
|
|
825
|
+
if (relationshipFields.length > 0) {
|
|
826
|
+
relationshipTypesList.push(`export type ${pascalName}WithRelations = DataModel["${name}"] & {\n${relationshipFields.join("\n")}\n};`);
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
relationshipTypesList.push(`export type ${pascalName}WithRelations = DataModel["${name}"];`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const relationshipTypes = `// ============================================================================
|
|
833
|
+
// RELATIONSHIP TYPES
|
|
834
|
+
// ============================================================================
|
|
835
|
+
|
|
836
|
+
${relationshipTypesList.join("\n\n")}
|
|
837
|
+
`;
|
|
838
|
+
return { dataModel, inputTypes, relationshipTypes };
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Scan schema folder for all .ts files
|
|
842
|
+
*/
|
|
843
|
+
async scanSchemaFolder(folderPath) {
|
|
844
|
+
const files = [];
|
|
845
|
+
// Check if folder exists
|
|
846
|
+
try {
|
|
847
|
+
const entryNames = fs.readdirSync(folderPath);
|
|
848
|
+
for (const name of entryNames) {
|
|
849
|
+
if (!name.endsWith(".ts"))
|
|
850
|
+
continue;
|
|
851
|
+
if (name === "index.ts")
|
|
852
|
+
continue;
|
|
853
|
+
const filePath = join(folderPath, name);
|
|
854
|
+
// Skip directories
|
|
855
|
+
const stat = fs.statSync(filePath);
|
|
856
|
+
if (stat.isDirectory())
|
|
857
|
+
continue;
|
|
858
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
859
|
+
files.push({ filePath, content });
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
// Folder doesn't exist
|
|
864
|
+
}
|
|
865
|
+
return files;
|
|
866
|
+
}
|
|
867
|
+
async generateApiTypes(typeInfos) {
|
|
868
|
+
let output = `// This file is auto-generated by Archlast CLI\n`;
|
|
869
|
+
output += `// Do not edit manually\n\n`;
|
|
870
|
+
output += `import type { FunctionReference } from '@archlast/client';\n\n`;
|
|
871
|
+
// Generate DataModel type from schema
|
|
872
|
+
const { dataModel: dataModelType, inputTypes, relationshipTypes } = await this.generateDataModelFromSchema();
|
|
873
|
+
output += `// Data model types\n${dataModelType}\n\n`;
|
|
874
|
+
// Generate ApiDefinition
|
|
875
|
+
output += `export type ApiDefinition = {\n`;
|
|
876
|
+
for (const info of typeInfos) {
|
|
877
|
+
const fullName = `${info.moduleName}.${info.name}`;
|
|
878
|
+
output += ` "${fullName}": {\n`;
|
|
879
|
+
output += ` type: "${info.type}";\n`;
|
|
880
|
+
output += ` args: ${info.argsType};\n`;
|
|
881
|
+
output += ` return: ${info.returnType};\n`;
|
|
882
|
+
if (info.auth) {
|
|
883
|
+
output += ` auth: "${info.auth}";\n`;
|
|
884
|
+
}
|
|
885
|
+
output += ` };\n`;
|
|
886
|
+
}
|
|
887
|
+
output += `};\n\n`;
|
|
888
|
+
// Generate api object
|
|
889
|
+
output += `export const api = {\n`;
|
|
890
|
+
const groupedByModule = typeInfos.reduce((acc, info) => {
|
|
891
|
+
if (!acc[info.moduleName])
|
|
892
|
+
acc[info.moduleName] = [];
|
|
893
|
+
acc[info.moduleName].push(info);
|
|
894
|
+
return acc;
|
|
895
|
+
}, {});
|
|
896
|
+
for (const [moduleName, funcs] of Object.entries(groupedByModule)) {
|
|
897
|
+
// Quote module name if it contains special characters
|
|
898
|
+
const quotedModuleName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(moduleName)
|
|
899
|
+
? moduleName
|
|
900
|
+
: `"${moduleName}"`;
|
|
901
|
+
output += ` ${quotedModuleName}: {\n`;
|
|
902
|
+
for (const func of funcs) {
|
|
903
|
+
const fullName = `${moduleName}.${func.name}`;
|
|
904
|
+
const authStr = func.auth ? `, _auth: '${func.auth}'` : "";
|
|
905
|
+
output += ` ${func.name}: { _name: '${fullName}', _type: '${func.type}', _args: undefined as any, _return: undefined as any${authStr} } as FunctionReference<'${func.type}', '${fullName}', ${func.argsType}, ${func.returnType}>,\n`;
|
|
906
|
+
}
|
|
907
|
+
output += ` },\n`;
|
|
908
|
+
}
|
|
909
|
+
output += `} as const;\n\n`;
|
|
910
|
+
output += `export type Api = typeof api;\n`;
|
|
911
|
+
return output;
|
|
912
|
+
}
|
|
913
|
+
async generateRpcTypes(rpcTypeInfos) {
|
|
914
|
+
let output = `// This file is auto-generated by Archlast CLI\n`;
|
|
915
|
+
output += `// Do not edit manually\n\n`;
|
|
916
|
+
output += `// tRPC Procedure Type Definitions\n`;
|
|
917
|
+
output += `// This file defines the input/output types for all RPC procedures\n`;
|
|
918
|
+
output += `// Use with @trpc/client for fully typed API calls\n\n`;
|
|
919
|
+
// Import only DataModel - no tRPC dependencies
|
|
920
|
+
output += `import type { DataModel } from './server';\n\n`;
|
|
921
|
+
// Group procedures by module name
|
|
922
|
+
const groupedByModule = rpcTypeInfos.reduce((acc, info) => {
|
|
923
|
+
if (!acc[info.moduleName])
|
|
924
|
+
acc[info.moduleName] = [];
|
|
925
|
+
acc[info.moduleName].push(info);
|
|
926
|
+
return acc;
|
|
927
|
+
}, {});
|
|
928
|
+
// Generate typescript types for each procedure
|
|
929
|
+
output += `// ============================================\n`;
|
|
930
|
+
output += `// Procedure Input/Output Types\n`;
|
|
931
|
+
output += `// ============================================\n\n`;
|
|
932
|
+
for (const [moduleName, procs] of Object.entries(groupedByModule)) {
|
|
933
|
+
// Convert hyphenated names to camelCase
|
|
934
|
+
const safeModuleName = moduleName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
935
|
+
const pascalModuleName = safeModuleName.charAt(0).toUpperCase() + safeModuleName.slice(1);
|
|
936
|
+
output += `// ${pascalModuleName} Module\n`;
|
|
937
|
+
for (const proc of procs) {
|
|
938
|
+
output += `export type ${proc.name}Input = ${proc.argsType};\n`;
|
|
939
|
+
output += `export type ${proc.name}Output = ${proc.returnType};\n`;
|
|
940
|
+
}
|
|
941
|
+
output += `\n`;
|
|
942
|
+
}
|
|
943
|
+
// Generate a combined namespace for all procedures
|
|
944
|
+
output += `// ============================================\n`;
|
|
945
|
+
output += `// All Procedures Types\n`;
|
|
946
|
+
output += `// ============================================\n\n`;
|
|
947
|
+
output += `export interface RpcProcedures {\n`;
|
|
948
|
+
for (const [moduleName, procs] of Object.entries(groupedByModule)) {
|
|
949
|
+
const safeModuleName = moduleName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
950
|
+
output += ` ${safeModuleName}: {\n`;
|
|
951
|
+
for (const proc of procs) {
|
|
952
|
+
output += ` ${proc.name}: {\n`;
|
|
953
|
+
output += ` input: ${proc.argsType};\n`;
|
|
954
|
+
output += ` output: ${proc.returnType};\n`;
|
|
955
|
+
output += ` };\n`;
|
|
956
|
+
}
|
|
957
|
+
output += ` };\n`;
|
|
958
|
+
}
|
|
959
|
+
output += `}\n\n`;
|
|
960
|
+
// Export list of all procedure names
|
|
961
|
+
output += `// Procedure names for reference\n`;
|
|
962
|
+
output += `export const PROCEDURES = {\n`;
|
|
963
|
+
for (const [moduleName, procs] of Object.entries(groupedByModule)) {
|
|
964
|
+
const safeModuleName = moduleName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
965
|
+
output += ` ${safeModuleName}: [\n`;
|
|
966
|
+
for (const proc of procs) {
|
|
967
|
+
output += ` "${proc.name}",\n`;
|
|
968
|
+
}
|
|
969
|
+
output += ` ],\n`;
|
|
970
|
+
}
|
|
971
|
+
output += `} as const;\n`;
|
|
972
|
+
return output;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Generate runtime tRPC router definition file
|
|
976
|
+
* This creates a real t.router() that imports and wraps the actual RPC procedures
|
|
977
|
+
*/
|
|
978
|
+
async generateTrpcRouter(rpcTypeInfos) {
|
|
979
|
+
let output = `// This file is auto-generated by Archlast CLI\n`;
|
|
980
|
+
output += `// Do not edit manually\n\n`;
|
|
981
|
+
output += `// tRPC Router Runtime Definition\n`;
|
|
982
|
+
output += `// This file creates a real tRPC router that wraps the RPC procedures\n`;
|
|
983
|
+
output += `// Client: Import type { AppRouter } for full type safety\n\n`;
|
|
984
|
+
// Import tRPC and actual RpcCtx type from @archlast/server
|
|
985
|
+
output += `import { initTRPC } from "@trpc/server";\n`;
|
|
986
|
+
output += `import type { RpcCtx } from "@archlast/server/functions/types";\n\n`;
|
|
987
|
+
// Group procedures by import path (to avoid duplicate imports)
|
|
988
|
+
const procsByImportPath = rpcTypeInfos.reduce((acc, info) => {
|
|
989
|
+
if (!acc[info.importPath])
|
|
990
|
+
acc[info.importPath] = [];
|
|
991
|
+
acc[info.importPath].push(info);
|
|
992
|
+
return acc;
|
|
993
|
+
}, {});
|
|
994
|
+
// Generate imports for each source file
|
|
995
|
+
output += `// ============================================\n`;
|
|
996
|
+
output += `// Import RPC Procedures\n`;
|
|
997
|
+
output += `// ============================================\n\n`;
|
|
998
|
+
for (const [importPath, procs] of Object.entries(procsByImportPath)) {
|
|
999
|
+
const exportNames = procs.map((p) => p.name).join(", ");
|
|
1000
|
+
output += `import { ${exportNames} } from "${importPath}";\n`;
|
|
1001
|
+
}
|
|
1002
|
+
output += `\n`;
|
|
1003
|
+
// Initialize tRPC with proper context type
|
|
1004
|
+
output += `// ============================================\n`;
|
|
1005
|
+
output += `// Initialize tRPC\n`;
|
|
1006
|
+
output += `// ============================================\n\n`;
|
|
1007
|
+
output += `const t = initTRPC.context<RpcCtx>().create();\n\n`;
|
|
1008
|
+
// Build the router
|
|
1009
|
+
output += `// ============================================\n`;
|
|
1010
|
+
output += `// Build Runtime Router\n`;
|
|
1011
|
+
output += `// ============================================\n\n`;
|
|
1012
|
+
output += `/**\n`;
|
|
1013
|
+
output += ` * tRPC Router\n`;
|
|
1014
|
+
output += ` * Wraps the RPC procedures in tRPC's t.procedure API\n`;
|
|
1015
|
+
output += ` * Client: Use createTRPCClient<typeof appRouter> for type-safe calls\n`;
|
|
1016
|
+
output += ` */\n`;
|
|
1017
|
+
output += `export const appRouter = t.router({\n`;
|
|
1018
|
+
// Group procedures by module name for nested router structure
|
|
1019
|
+
const groupedByModule = rpcTypeInfos.reduce((acc, info) => {
|
|
1020
|
+
if (!acc[info.moduleName])
|
|
1021
|
+
acc[info.moduleName] = [];
|
|
1022
|
+
acc[info.moduleName].push(info);
|
|
1023
|
+
return acc;
|
|
1024
|
+
}, {});
|
|
1025
|
+
for (const [moduleName, procs] of Object.entries(groupedByModule)) {
|
|
1026
|
+
// Convert hyphenated names to camelCase
|
|
1027
|
+
const safeModuleName = moduleName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1028
|
+
output += ` ${safeModuleName}: t.router({\n`;
|
|
1029
|
+
for (const proc of procs) {
|
|
1030
|
+
// Generate the procedure wrapper
|
|
1031
|
+
// We access the procedure's args and handler from the imported object
|
|
1032
|
+
output += ` ${proc.name}: t.procedure`;
|
|
1033
|
+
// Add input validation if args exist (use non-null assertion since args is optional in RpcDefinition)
|
|
1034
|
+
if (proc.hasArgs) {
|
|
1035
|
+
output += `.input(${proc.name}.args!)`;
|
|
1036
|
+
}
|
|
1037
|
+
// Add query or mutation
|
|
1038
|
+
if (proc.procedureType === "query") {
|
|
1039
|
+
output += `.query(async ({ ctx, input }) => {\n`;
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
output += `.mutation(async ({ ctx, input }) => {\n`;
|
|
1043
|
+
}
|
|
1044
|
+
// Call the original handler
|
|
1045
|
+
output += ` return ${proc.name}.handler(ctx, input);\n`;
|
|
1046
|
+
output += ` }),\n`;
|
|
1047
|
+
}
|
|
1048
|
+
output += ` }),\n`;
|
|
1049
|
+
}
|
|
1050
|
+
output += `});\n\n`;
|
|
1051
|
+
// Export the AppRouter type
|
|
1052
|
+
output += `// ============================================\n`;
|
|
1053
|
+
output += `// Export Router Type\n`;
|
|
1054
|
+
output += `// ============================================\n\n`;
|
|
1055
|
+
output += `export type AppRouter = typeof appRouter;\n\n`;
|
|
1056
|
+
// Export tRPC instance for server use
|
|
1057
|
+
output += `export { t };\n`;
|
|
1058
|
+
return output;
|
|
1059
|
+
}
|
|
1060
|
+
async generateServerTypes() {
|
|
1061
|
+
const { dataModel, inputTypes, relationshipTypes } = await this.generateDataModelFromSchema();
|
|
1062
|
+
return `// This file is auto-generated by Archlast CLI
|
|
1063
|
+
// Do not edit manually
|
|
1064
|
+
|
|
1065
|
+
import "reflect-metadata";
|
|
1066
|
+
import { z } from "zod";
|
|
1067
|
+
import type { FunctionReference } from "@archlast/client";
|
|
1068
|
+
import type { GenericDatabaseReader, GenericDatabaseWriter, RelationsMap, Document } from "@archlast/server/db/interfaces";
|
|
1069
|
+
import type { AuthContext } from "@archlast/server/auth/interfaces";
|
|
1070
|
+
import type { IQueryable, IMutableQueryable, IDbSet, IChangeTracker } from "@archlast/server/repository/interfaces";
|
|
1071
|
+
import type { DbContext, EntityEntry, EntityState, IRepository } from "@archlast/server/repository/factory";
|
|
1072
|
+
import type { SchedulerService } from "@archlast/server/jobs/scheduler";
|
|
1073
|
+
import type { JobQueue } from "@archlast/server/jobs/queue";
|
|
1074
|
+
import type { Storage } from "@archlast/server/storage/types";
|
|
1075
|
+
import type { LogService } from "@archlast/server/logging/logger";
|
|
1076
|
+
import { WebhookGuard } from "@archlast/server/webhook/definition";
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
// ============================================================================
|
|
1080
|
+
// DATA MODEL
|
|
1081
|
+
// ============================================================================
|
|
1082
|
+
|
|
1083
|
+
${dataModel}
|
|
1084
|
+
|
|
1085
|
+
// Re-export validator
|
|
1086
|
+
export { v } from "@archlast/server/schema/validators";
|
|
1087
|
+
|
|
1088
|
+
${inputTypes}
|
|
1089
|
+
|
|
1090
|
+
${relationshipTypes}
|
|
1091
|
+
|
|
1092
|
+
// ============================================================================
|
|
1093
|
+
// DI DECORATORS (inline implementations)
|
|
1094
|
+
// ============================================================================
|
|
1095
|
+
|
|
1096
|
+
export interface InjectableOptions {
|
|
1097
|
+
provide: string;
|
|
1098
|
+
useFactory?: string;
|
|
1099
|
+
scope?: "singleton" | "transient";
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
export function Injectable(options?: InjectableOptions): ClassDecorator {
|
|
1103
|
+
return function <TFunction extends Function>(target: TFunction): TFunction {
|
|
1104
|
+
// Store metadata on the constructor itself
|
|
1105
|
+
(target as any).__injectable__ = options || { provide: target.name };
|
|
1106
|
+
return target;
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
export function Factory(token: string): ClassDecorator {
|
|
1111
|
+
return function <TFunction extends Function>(target: TFunction): TFunction {
|
|
1112
|
+
// Store metadata on the constructor itself
|
|
1113
|
+
(target as any).__factory__ = token;
|
|
1114
|
+
return target;
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ============================================================================
|
|
1119
|
+
// RE-EXPORTS (webhook guards from package)
|
|
1120
|
+
// ============================================================================
|
|
1121
|
+
|
|
1122
|
+
// Import and re-export webhook guards from the server package
|
|
1123
|
+
// Note: When deployed, these will be available from @archlast/server/webhook/guard
|
|
1124
|
+
export { createWebhookGuard, createHmacGuard } from "@archlast/server/webhook/guard";
|
|
1125
|
+
|
|
1126
|
+
// ============================================================================
|
|
1127
|
+
// CONTEXT TYPES
|
|
1128
|
+
// ============================================================================
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* DI methods available in function contexts
|
|
1132
|
+
*/
|
|
1133
|
+
export interface DIContext {
|
|
1134
|
+
/**
|
|
1135
|
+
* Resolve a singleton service by token
|
|
1136
|
+
* @example ctx.service<IUserService>("UserService")
|
|
1137
|
+
*/
|
|
1138
|
+
service<T>(token: string): T;
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Resolve a factory implementation by token and implementation key
|
|
1142
|
+
* @example ctx.factory<IEmailProvider>("EmailProvider", "Resend")
|
|
1143
|
+
*/
|
|
1144
|
+
factory<T>(token: string, implementation?: string): T;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Server context contains all server-wide dependencies
|
|
1149
|
+
*/
|
|
1150
|
+
export interface ServerContext extends DIContext {
|
|
1151
|
+
auth: AuthContext;
|
|
1152
|
+
db: GenericDatabaseWriter<DataModel>;
|
|
1153
|
+
/** Uncached database client for system functions */
|
|
1154
|
+
rawDb: GenericDatabaseWriter<DataModel>;
|
|
1155
|
+
/** Repository with EF Core DbContext pattern - supports change tracking and SaveChangesAsync() */
|
|
1156
|
+
repository: IRepository<DataModel, GeneratedInputModels>;
|
|
1157
|
+
storage: Storage;
|
|
1158
|
+
logger: LogService;
|
|
1159
|
+
scheduler: SchedulerService;
|
|
1160
|
+
jobQueue: JobQueue;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Query context - read-only database operations
|
|
1165
|
+
*/
|
|
1166
|
+
export interface QueryCtx extends DIContext {
|
|
1167
|
+
auth: AuthContext;
|
|
1168
|
+
db: GenericDatabaseReader<DataModel>;
|
|
1169
|
+
/** Uncached database client for system functions */
|
|
1170
|
+
rawDb: GenericDatabaseReader<DataModel>;
|
|
1171
|
+
/** Repository with EF Core DbContext pattern - query-only access but with FindAsync, Find, Entry methods */
|
|
1172
|
+
repository: IRepository<DataModel, GeneratedInputModels>;
|
|
1173
|
+
storage: Storage;
|
|
1174
|
+
logger: LogService;
|
|
1175
|
+
scheduler: SchedulerService;
|
|
1176
|
+
jobQueue: JobQueue;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Mutation context - read/write database operations
|
|
1181
|
+
*/
|
|
1182
|
+
export interface MutationCtx extends DIContext {
|
|
1183
|
+
auth: AuthContext;
|
|
1184
|
+
db: GenericDatabaseWriter<DataModel>;
|
|
1185
|
+
/** Uncached database client for system functions that need fresh data */
|
|
1186
|
+
rawDb: GenericDatabaseWriter<DataModel>;
|
|
1187
|
+
/** Repository with EF Core DbContext pattern - supports change tracking and SaveChangesAsync() with input types */
|
|
1188
|
+
repository: IRepository<DataModel, GeneratedInputModels>;
|
|
1189
|
+
storage: Storage;
|
|
1190
|
+
logger: LogService;
|
|
1191
|
+
scheduler: SchedulerService;
|
|
1192
|
+
jobQueue: JobQueue;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Action context - full read/write database access for side effects and external APIs
|
|
1197
|
+
*/
|
|
1198
|
+
export interface ActionCtx extends DIContext {
|
|
1199
|
+
auth: AuthContext;
|
|
1200
|
+
db: GenericDatabaseWriter<DataModel>;
|
|
1201
|
+
/** Uncached database client for system functions that need fresh data */
|
|
1202
|
+
rawDb: GenericDatabaseWriter<DataModel>;
|
|
1203
|
+
/** Repository with EF Core DbContext pattern - supports change tracking and SaveChangesAsync() with input types */
|
|
1204
|
+
repository: IRepository<DataModel, GeneratedInputModels>;
|
|
1205
|
+
storage: Storage;
|
|
1206
|
+
logger: LogService;
|
|
1207
|
+
scheduler: SchedulerService;
|
|
1208
|
+
jobQueue: JobQueue;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Service context - request-scoped context for services
|
|
1213
|
+
*/
|
|
1214
|
+
export interface ServiceContext extends DIContext {
|
|
1215
|
+
auth: AuthContext;
|
|
1216
|
+
db: GenericDatabaseWriter<DataModel>;
|
|
1217
|
+
/** Uncached database client for system functions that need fresh data */
|
|
1218
|
+
rawDb: GenericDatabaseWriter<DataModel>;
|
|
1219
|
+
/** Repository with EF Core DbContext pattern - supports change tracking and SaveChangesAsync() with input types */
|
|
1220
|
+
repository: IRepository<DataModel, GeneratedInputModels>;
|
|
1221
|
+
storage: Storage;
|
|
1222
|
+
logger: LogService;
|
|
1223
|
+
scheduler: SchedulerService;
|
|
1224
|
+
jobQueue: JobQueue;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* HTTP request object with path parameters and query params
|
|
1229
|
+
*/
|
|
1230
|
+
export interface HttpRequest {
|
|
1231
|
+
raw: Request;
|
|
1232
|
+
method: string;
|
|
1233
|
+
path: string;
|
|
1234
|
+
headers: Headers;
|
|
1235
|
+
params: Record<string, string>;
|
|
1236
|
+
query: Record<string, string>;
|
|
1237
|
+
json<T = unknown>(): Promise<T>;
|
|
1238
|
+
text(): Promise<string>;
|
|
1239
|
+
formData(): Promise<FormData>;
|
|
1240
|
+
blob(): Promise<Blob>;
|
|
1241
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* HTTP context - includes server context and request
|
|
1246
|
+
*/
|
|
1247
|
+
export interface HttpCtx extends DIContext {
|
|
1248
|
+
auth: AuthContext;
|
|
1249
|
+
db: GenericDatabaseWriter<DataModel>;
|
|
1250
|
+
repository: { [K in keyof DataModel]: IMutableQueryable<DataModel[K]> };
|
|
1251
|
+
storage: Storage;
|
|
1252
|
+
logger: LogService;
|
|
1253
|
+
scheduler: SchedulerService;
|
|
1254
|
+
jobQueue: JobQueue;
|
|
1255
|
+
server: ServerContext;
|
|
1256
|
+
req: HttpRequest;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Webhook context - similar to MutationCtx
|
|
1261
|
+
*/
|
|
1262
|
+
export interface WebhookCtx extends DIContext {
|
|
1263
|
+
auth: AuthContext;
|
|
1264
|
+
db: GenericDatabaseWriter<DataModel>;
|
|
1265
|
+
repository: { [K in keyof DataModel]: IMutableQueryable<DataModel[K]> };
|
|
1266
|
+
storage: Storage;
|
|
1267
|
+
logger: LogService;
|
|
1268
|
+
scheduler: SchedulerService;
|
|
1269
|
+
jobQueue: JobQueue;
|
|
1270
|
+
server: ServerContext;
|
|
1271
|
+
req: Request;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* RPC (tRPC-style) context - For type-safe public API procedures
|
|
1276
|
+
* Similar to QueryCtx/MutationCtx but designed for external RPC calls
|
|
1277
|
+
*/
|
|
1278
|
+
export interface RpcCtx<
|
|
1279
|
+
DataModel extends Record<string, Document> = any,
|
|
1280
|
+
Relations extends RelationsMap<DataModel> = RelationsMap<DataModel>,
|
|
1281
|
+
> extends DIContext {
|
|
1282
|
+
auth: AuthContext;
|
|
1283
|
+
db: GenericDatabaseWriter<DataModel, Relations>;
|
|
1284
|
+
repository: { [K in keyof DataModel]: IMutableQueryable<DataModel[K]> };
|
|
1285
|
+
storage: Storage;
|
|
1286
|
+
logger: LogService;
|
|
1287
|
+
scheduler: SchedulerService;
|
|
1288
|
+
jobQueue: JobQueue;
|
|
1289
|
+
server: ServerContext;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
export type QueryHandler<Args, Return> = (ctx: QueryCtx, args: Args) => Promise<Return>;
|
|
1293
|
+
export type MutationHandler<Args, Return> = (ctx: MutationCtx, args: Args) => Promise<Return>;
|
|
1294
|
+
export type ActionHandler<Args, Return> = (ctx: ActionCtx, args: Args) => Promise<Return>;
|
|
1295
|
+
export type HttpHandler<Args, Return> = (ctx: HttpCtx, args: Args) => Promise<Return> | Return;
|
|
1296
|
+
export type WebhookHandler<Args, Return> = (ctx: WebhookCtx, args: Args) => Promise<Return> | Return;
|
|
1297
|
+
|
|
1298
|
+
export type FunctionType = "query" | "mutation" | "action" | "http" | "webhook";
|
|
1299
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Auth mode determines authentication requirements for a function
|
|
1303
|
+
* - "required" (default): Authentication is required (session or API key)
|
|
1304
|
+
* - "optional": Authentication is optional (ctx.auth may be unauthenticated)
|
|
1305
|
+
* - "public": No authentication required (function is publicly accessible)
|
|
1306
|
+
*/
|
|
1307
|
+
export type AuthMode = "required" | "optional" | "public";
|
|
1308
|
+
|
|
1309
|
+
export interface FunctionDefinition {
|
|
1310
|
+
type: FunctionType;
|
|
1311
|
+
handler: Function;
|
|
1312
|
+
args?: z.ZodType<any>;
|
|
1313
|
+
name?: string; // Assigned at registration
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Auth mode - defaults to "required"
|
|
1317
|
+
*/
|
|
1318
|
+
auth?: AuthMode;
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Required permissions - checked before handler execution
|
|
1322
|
+
* Example: ["tasks.read", "tasks.write"]
|
|
1323
|
+
*/
|
|
1324
|
+
permissions?: string[];
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Allowed scopes for API key auth
|
|
1328
|
+
* If not specified, function is accessible to all auth types
|
|
1329
|
+
*/
|
|
1330
|
+
scopes?: ("query" | "mutation" | "action")[];
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* HTTP-specific function definition
|
|
1335
|
+
*/
|
|
1336
|
+
export interface HttpDefinition<Args = unknown, Return = unknown> {
|
|
1337
|
+
type: "http";
|
|
1338
|
+
method: HttpMethod;
|
|
1339
|
+
path: string;
|
|
1340
|
+
handler: HttpHandler<Args, Return>;
|
|
1341
|
+
auth?: AuthMode;
|
|
1342
|
+
permissions?: string[];
|
|
1343
|
+
middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Webhook-specific function definition
|
|
1348
|
+
*/
|
|
1349
|
+
export interface WebhookDefinition<Event = unknown, Return = unknown> {
|
|
1350
|
+
type: "webhook";
|
|
1351
|
+
method: HttpMethod;
|
|
1352
|
+
path: string;
|
|
1353
|
+
handler: WebhookHandler<Event, Return>;
|
|
1354
|
+
eventSchema?: z.ZodType<Event>;
|
|
1355
|
+
guards?: WebhookGuard[];
|
|
1356
|
+
auth?: AuthMode;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Overload for config object with args (explicit z.ZodType)
|
|
1360
|
+
export function query<Args extends z.ZodType<any, any, any>, Return>(config: {
|
|
1361
|
+
args: Args;
|
|
1362
|
+
handler: QueryHandler<z.infer<Args>, Return>;
|
|
1363
|
+
auth?: AuthMode;
|
|
1364
|
+
permissions?: string[];
|
|
1365
|
+
scopes?: ("query" | "mutation" | "action")[];
|
|
1366
|
+
}): FunctionDefinition;
|
|
1367
|
+
// Overload for config object with args (z.ZodRawShape)
|
|
1368
|
+
export function query<Args extends z.ZodRawShape, Return>(config: {
|
|
1369
|
+
args: Args;
|
|
1370
|
+
handler: QueryHandler<z.infer<z.ZodObject<Args>>, Return>;
|
|
1371
|
+
auth?: AuthMode;
|
|
1372
|
+
permissions?: string[];
|
|
1373
|
+
scopes?: ("query" | "mutation" | "action")[];
|
|
1374
|
+
}): FunctionDefinition;
|
|
1375
|
+
// Overload for config object without args
|
|
1376
|
+
export function query<Return>(config: {
|
|
1377
|
+
handler: QueryHandler<Record<string, never>, Return>;
|
|
1378
|
+
auth?: AuthMode;
|
|
1379
|
+
permissions?: string[];
|
|
1380
|
+
scopes?: ("query" | "mutation" | "action")[];
|
|
1381
|
+
}): FunctionDefinition;
|
|
1382
|
+
// Overload for simple handler (no args)
|
|
1383
|
+
export function query<Return>(
|
|
1384
|
+
handler: QueryHandler<Record<string, never>, Return>
|
|
1385
|
+
): FunctionDefinition;
|
|
1386
|
+
export function query(configOrHandler: any): FunctionDefinition {
|
|
1387
|
+
if (typeof configOrHandler === "function") {
|
|
1388
|
+
return {
|
|
1389
|
+
type: "query",
|
|
1390
|
+
handler: configOrHandler,
|
|
1391
|
+
auth: "required", // default
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
// If args is already a ZodType (z.object, etc.), use it directly; otherwise wrap with z.object()
|
|
1395
|
+
const args = configOrHandler.args && typeof configOrHandler.args === "object" && "parse" in configOrHandler.args
|
|
1396
|
+
? configOrHandler.args
|
|
1397
|
+
: configOrHandler.args
|
|
1398
|
+
? z.object(configOrHandler.args)
|
|
1399
|
+
: undefined;
|
|
1400
|
+
|
|
1401
|
+
return {
|
|
1402
|
+
type: "query",
|
|
1403
|
+
handler: configOrHandler.handler,
|
|
1404
|
+
args,
|
|
1405
|
+
auth: configOrHandler.auth || "required",
|
|
1406
|
+
permissions: configOrHandler.permissions,
|
|
1407
|
+
scopes: configOrHandler.scopes,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Overload for config object with args (explicit z.ZodType)
|
|
1412
|
+
export function mutation<Args extends z.ZodType<any, any, any>, Return>(config: {
|
|
1413
|
+
args: Args;
|
|
1414
|
+
handler: MutationHandler<z.infer<Args>, Return>;
|
|
1415
|
+
auth?: AuthMode;
|
|
1416
|
+
permissions?: string[];
|
|
1417
|
+
scopes?: ("query" | "mutation" | "action")[];
|
|
1418
|
+
}): FunctionDefinition;
|
|
1419
|
+
// Overload for config object with args (z.ZodRawShape)
|
|
1420
|
+
export function mutation<Args extends z.ZodRawShape, Return>(config: {
|
|
1421
|
+
args: Args;
|
|
1422
|
+
handler: MutationHandler<z.infer<z.ZodObject<Args>>, Return>;
|
|
1423
|
+
auth?: AuthMode;
|
|
1424
|
+
permissions?: string[];
|
|
1425
|
+
scopes?: ("query" | "mutation" | "action")[];
|
|
1426
|
+
}): FunctionDefinition;
|
|
1427
|
+
// Overload for config object without args
|
|
1428
|
+
export function mutation<Return>(config: {
|
|
1429
|
+
handler: MutationHandler<Record<string, never>, Return>;
|
|
1430
|
+
auth?: AuthMode;
|
|
1431
|
+
permissions?: string[];
|
|
1432
|
+
scopes?: ("query" | "mutation" | "action")[];
|
|
1433
|
+
}): FunctionDefinition;
|
|
1434
|
+
// Overload for simple handler (no args)
|
|
1435
|
+
export function mutation<Return>(
|
|
1436
|
+
handler: MutationHandler<Record<string, never>, Return>
|
|
1437
|
+
): FunctionDefinition;
|
|
1438
|
+
export function mutation(configOrHandler: any): FunctionDefinition {
|
|
1439
|
+
if (typeof configOrHandler === "function") {
|
|
1440
|
+
return {
|
|
1441
|
+
type: "mutation",
|
|
1442
|
+
handler: configOrHandler,
|
|
1443
|
+
auth: "required", // default
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
// If args is already a ZodType (z.object, etc.), use it directly; otherwise wrap with z.object()
|
|
1447
|
+
const args = configOrHandler.args && typeof configOrHandler.args === "object" && "parse" in configOrHandler.args
|
|
1448
|
+
? configOrHandler.args
|
|
1449
|
+
: configOrHandler.args
|
|
1450
|
+
? z.object(configOrHandler.args)
|
|
1451
|
+
: undefined;
|
|
1452
|
+
|
|
1453
|
+
return {
|
|
1454
|
+
type: "mutation",
|
|
1455
|
+
handler: configOrHandler.handler,
|
|
1456
|
+
args,
|
|
1457
|
+
auth: configOrHandler.auth || "required",
|
|
1458
|
+
permissions: configOrHandler.permissions,
|
|
1459
|
+
scopes: configOrHandler.scopes,
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
export function action<Args = unknown, Return = unknown>(
|
|
1464
|
+
handlerOrConfig:
|
|
1465
|
+
| ActionHandler<Args, Return>
|
|
1466
|
+
| {
|
|
1467
|
+
handler: ActionHandler<Args, Return>;
|
|
1468
|
+
auth?: AuthMode;
|
|
1469
|
+
permissions?: string[];
|
|
1470
|
+
scopes?: ("query" | "mutation" | "action")[];
|
|
1471
|
+
}
|
|
1472
|
+
): FunctionDefinition {
|
|
1473
|
+
if (typeof handlerOrConfig === "function") {
|
|
1474
|
+
return {
|
|
1475
|
+
type: "action",
|
|
1476
|
+
handler: handlerOrConfig,
|
|
1477
|
+
auth: "required", // default
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
type: "action",
|
|
1482
|
+
handler: handlerOrConfig.handler,
|
|
1483
|
+
auth: handlerOrConfig.auth || "required",
|
|
1484
|
+
permissions: handlerOrConfig.permissions,
|
|
1485
|
+
scopes: handlerOrConfig.scopes,
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* HTTP endpoint builders
|
|
1491
|
+
*/
|
|
1492
|
+
export const http = {
|
|
1493
|
+
get: <Args = unknown, Return = Response>(config: {
|
|
1494
|
+
path: string;
|
|
1495
|
+
handler: HttpHandler<Args, Return>;
|
|
1496
|
+
auth?: AuthMode;
|
|
1497
|
+
permissions?: string[];
|
|
1498
|
+
middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
|
|
1499
|
+
}): HttpDefinition<Args, Return> => ({
|
|
1500
|
+
type: "http",
|
|
1501
|
+
method: "GET",
|
|
1502
|
+
...config,
|
|
1503
|
+
}),
|
|
1504
|
+
|
|
1505
|
+
post: <Args = unknown, Return = Response>(config: {
|
|
1506
|
+
path: string;
|
|
1507
|
+
handler: HttpHandler<Args, Return>;
|
|
1508
|
+
auth?: AuthMode;
|
|
1509
|
+
permissions?: string[];
|
|
1510
|
+
middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
|
|
1511
|
+
}): HttpDefinition<Args, Return> => ({
|
|
1512
|
+
type: "http",
|
|
1513
|
+
method: "POST",
|
|
1514
|
+
...config,
|
|
1515
|
+
}),
|
|
1516
|
+
|
|
1517
|
+
put: <Args = unknown, Return = Response>(config: {
|
|
1518
|
+
path: string;
|
|
1519
|
+
handler: HttpHandler<Args, Return>;
|
|
1520
|
+
auth?: AuthMode;
|
|
1521
|
+
permissions?: string[];
|
|
1522
|
+
middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
|
|
1523
|
+
}): HttpDefinition<Args, Return> => ({
|
|
1524
|
+
type: "http",
|
|
1525
|
+
method: "PUT",
|
|
1526
|
+
...config,
|
|
1527
|
+
}),
|
|
1528
|
+
|
|
1529
|
+
delete: <Args = unknown, Return = Response>(config: {
|
|
1530
|
+
path: string;
|
|
1531
|
+
handler: HttpHandler<Args, Return>;
|
|
1532
|
+
auth?: AuthMode;
|
|
1533
|
+
permissions?: string[];
|
|
1534
|
+
middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
|
|
1535
|
+
}): HttpDefinition<Args, Return> => ({
|
|
1536
|
+
type: "http",
|
|
1537
|
+
method: "DELETE",
|
|
1538
|
+
...config,
|
|
1539
|
+
}),
|
|
1540
|
+
|
|
1541
|
+
patch: <Args = unknown, Return = Response>(config: {
|
|
1542
|
+
path: string;
|
|
1543
|
+
handler: HttpHandler<Args, Return>;
|
|
1544
|
+
auth?: AuthMode;
|
|
1545
|
+
permissions?: string[];
|
|
1546
|
+
middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
|
|
1547
|
+
}): HttpDefinition<Args, Return> => ({
|
|
1548
|
+
type: "http",
|
|
1549
|
+
method: "PATCH",
|
|
1550
|
+
...config,
|
|
1551
|
+
}),
|
|
1552
|
+
|
|
1553
|
+
head: <Args = unknown, Return = Response>(config: {
|
|
1554
|
+
path: string;
|
|
1555
|
+
handler: HttpHandler<Args, Return>;
|
|
1556
|
+
auth?: AuthMode;
|
|
1557
|
+
permissions?: string[];
|
|
1558
|
+
middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
|
|
1559
|
+
}): HttpDefinition<Args, Return> => ({
|
|
1560
|
+
type: "http",
|
|
1561
|
+
method: "HEAD",
|
|
1562
|
+
...config,
|
|
1563
|
+
}),
|
|
1564
|
+
|
|
1565
|
+
options: <Args = unknown, Return = Response>(config: {
|
|
1566
|
+
path: string;
|
|
1567
|
+
handler: HttpHandler<Args, Return>;
|
|
1568
|
+
auth?: AuthMode;
|
|
1569
|
+
permissions?: string[];
|
|
1570
|
+
middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
|
|
1571
|
+
}): HttpDefinition<Args, Return> => ({
|
|
1572
|
+
type: "http",
|
|
1573
|
+
method: "OPTIONS",
|
|
1574
|
+
...config,
|
|
1575
|
+
}),
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Webhook builder
|
|
1580
|
+
*/
|
|
1581
|
+
export function webhook<Event = unknown, Return = unknown>(config: {
|
|
1582
|
+
path: string;
|
|
1583
|
+
handler: WebhookHandler<Event, Return>;
|
|
1584
|
+
eventSchema?: z.ZodType<Event>;
|
|
1585
|
+
guards?: WebhookGuard[];
|
|
1586
|
+
auth?: AuthMode;
|
|
1587
|
+
method?: HttpMethod;
|
|
1588
|
+
}): WebhookDefinition<Event, Return> {
|
|
1589
|
+
return {
|
|
1590
|
+
type: "webhook",
|
|
1591
|
+
method: config.method ?? "POST",
|
|
1592
|
+
path: config.path,
|
|
1593
|
+
handler: config.handler,
|
|
1594
|
+
eventSchema: config.eventSchema,
|
|
1595
|
+
guards: config.guards ?? [],
|
|
1596
|
+
auth: config.auth ?? "public", // Webhooks are typically public with guard verification
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* Type guards
|
|
1602
|
+
*/
|
|
1603
|
+
export function isHttpDefinition(value: any): value is HttpDefinition {
|
|
1604
|
+
return value && typeof value === "object" && value.type === "http";
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
export function isWebhookDefinition(value: any): value is WebhookDefinition {
|
|
1608
|
+
return value && typeof value === "object" && value.type === "webhook";
|
|
1609
|
+
}
|
|
1610
|
+
`;
|
|
1611
|
+
}
|
|
1612
|
+
generateIndexExport() {
|
|
1613
|
+
return `// This file is auto-generated by Archlast CLI
|
|
1614
|
+
// Do not edit manually
|
|
1615
|
+
|
|
1616
|
+
export { api } from './api';
|
|
1617
|
+
export type { Api } from './api';
|
|
1618
|
+
`;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Generate CRUD handlers for all collections in the schema
|
|
1622
|
+
* Creates _generated/crud/ directory with individual collection files
|
|
1623
|
+
*/
|
|
1624
|
+
async generateCrudHandlers(generatedDir) {
|
|
1625
|
+
const crudDir = path.join(generatedDir, "crud");
|
|
1626
|
+
// Ensure crud directory exists
|
|
1627
|
+
if (!fs.existsSync(crudDir)) {
|
|
1628
|
+
fs.mkdirSync(crudDir, { recursive: true });
|
|
1629
|
+
}
|
|
1630
|
+
// Get schema tables
|
|
1631
|
+
const { dataModel, inputTypes } = await this.generateDataModelFromSchema();
|
|
1632
|
+
// Extract table names from the DataModel type
|
|
1633
|
+
const tables = this.extractTableNames(dataModel);
|
|
1634
|
+
if (tables.length === 0) {
|
|
1635
|
+
console.log(" No tables found for CRUD generation");
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
console.log(` Generating CRUD handlers for ${tables.length} collections`);
|
|
1639
|
+
// Generate individual collection files
|
|
1640
|
+
for (const tableName of tables) {
|
|
1641
|
+
const handlerCode = this.generateCrudHandlerForTable(tableName);
|
|
1642
|
+
const filePath = path.join(crudDir, `${tableName}.ts`);
|
|
1643
|
+
fs.writeFileSync(filePath, handlerCode, "utf-8");
|
|
1644
|
+
}
|
|
1645
|
+
// Generate index.ts that re-exports all handlers
|
|
1646
|
+
const indexCode = this.generateCrudIndex(tables);
|
|
1647
|
+
const indexPath = path.join(crudDir, "index.ts");
|
|
1648
|
+
fs.writeFileSync(indexPath, indexCode, "utf-8");
|
|
1649
|
+
console.log(` CRUD handlers generated in _generated/crud/`);
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Extract table names from the DataModel type string
|
|
1653
|
+
*/
|
|
1654
|
+
extractTableNames(dataModel) {
|
|
1655
|
+
// Match table names from DataModel type definition
|
|
1656
|
+
// Pattern: ` tableName: {` or ` "tableName": {`
|
|
1657
|
+
const matches = dataModel.matchAll(/(?:\s*)(?:["']?)(\w+)(?:["']?)\s*:\s*{/g);
|
|
1658
|
+
const tables = [];
|
|
1659
|
+
for (const match of matches) {
|
|
1660
|
+
const tableName = match[1];
|
|
1661
|
+
// Skip system fields and type definitions
|
|
1662
|
+
if (tableName !== "_id" &&
|
|
1663
|
+
tableName !== "_collection" &&
|
|
1664
|
+
tableName !== "DataModel" &&
|
|
1665
|
+
tableName !== "export" &&
|
|
1666
|
+
tableName !== "type") {
|
|
1667
|
+
tables.push(tableName);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return tables;
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Generate CRUD handler code for a single table (public for CLI command)
|
|
1674
|
+
*/
|
|
1675
|
+
generateCrudHandlerForTable(tableName) {
|
|
1676
|
+
const pascalName = tableName.charAt(0).toUpperCase() + tableName.slice(1);
|
|
1677
|
+
const camelName = tableName;
|
|
1678
|
+
return `// Auto-generated CRUD handlers for "${tableName}"
|
|
1679
|
+
// This file is auto-generated by Archlast CLI - do not edit manually
|
|
1680
|
+
|
|
1681
|
+
import { http } from "../server";
|
|
1682
|
+
import type { DataModel } from "../server";
|
|
1683
|
+
|
|
1684
|
+
const collection = "${tableName}" as const;
|
|
1685
|
+
|
|
1686
|
+
// Type for the collection document
|
|
1687
|
+
type ${pascalName}Doc = DataModel["${tableName}"];
|
|
1688
|
+
// Type for creating a document (excludes auto-generated fields)
|
|
1689
|
+
type ${pascalName}Create = Omit<${pascalName}Doc, "_id" | "_collection">;
|
|
1690
|
+
// Type for updating a document (all fields optional)
|
|
1691
|
+
type ${pascalName}Update = Partial<${pascalName}Create>;
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* GET /${tableName}
|
|
1695
|
+
* List all ${tableName} records with optional pagination
|
|
1696
|
+
*/
|
|
1697
|
+
export const list${pascalName} = http.get({
|
|
1698
|
+
path: "/${tableName}",
|
|
1699
|
+
auth: "public",
|
|
1700
|
+
handler: async (ctx) => {
|
|
1701
|
+
// Query params are already parsed into ctx.req.query
|
|
1702
|
+
const limitStr = ctx.req.query.limit;
|
|
1703
|
+
const offsetStr = ctx.req.query.offset;
|
|
1704
|
+
const limit = limitStr ? Number(limitStr) : 50;
|
|
1705
|
+
const offset = offsetStr ? Number(offsetStr) : 0;
|
|
1706
|
+
|
|
1707
|
+
// Use .take() and .skip() for pagination
|
|
1708
|
+
const result = await ctx.db.query(collection)
|
|
1709
|
+
.take(limit)
|
|
1710
|
+
.skip(offset)
|
|
1711
|
+
.findMany();
|
|
1712
|
+
|
|
1713
|
+
return result;
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* GET /${tableName}/:id
|
|
1719
|
+
* Get a single ${tableName} record by ID
|
|
1720
|
+
*/
|
|
1721
|
+
export const get${pascalName} = http.get({
|
|
1722
|
+
path: "/${tableName}/:id",
|
|
1723
|
+
auth: "public",
|
|
1724
|
+
handler: async (ctx) => {
|
|
1725
|
+
// Path params are available in ctx.req.params
|
|
1726
|
+
const id = ctx.req.params.id;
|
|
1727
|
+
const result = await ctx.db.get(collection, id);
|
|
1728
|
+
|
|
1729
|
+
if (!result) {
|
|
1730
|
+
return { error: "${tableName} not found" };
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
return result;
|
|
1734
|
+
},
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* POST /${tableName}
|
|
1739
|
+
* Create a new ${tableName} record
|
|
1740
|
+
*/
|
|
1741
|
+
export const create${pascalName} = http.post({
|
|
1742
|
+
path: "/${tableName}",
|
|
1743
|
+
auth: "public",
|
|
1744
|
+
handler: async (ctx) => {
|
|
1745
|
+
// Parse JSON body from the request with proper type
|
|
1746
|
+
const data = await ctx.req.json<${pascalName}Create>();
|
|
1747
|
+
|
|
1748
|
+
const result = await ctx.db.insert(collection, data);
|
|
1749
|
+
|
|
1750
|
+
return result;
|
|
1751
|
+
},
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* PATCH /${tableName}/:id
|
|
1756
|
+
* Update a ${tableName} record
|
|
1757
|
+
*/
|
|
1758
|
+
export const update${pascalName} = http.patch({
|
|
1759
|
+
path: "/${tableName}/:id",
|
|
1760
|
+
auth: "public",
|
|
1761
|
+
handler: async (ctx) => {
|
|
1762
|
+
const id = ctx.req.params.id;
|
|
1763
|
+
const data = await ctx.req.json<${pascalName}Update>();
|
|
1764
|
+
|
|
1765
|
+
const existing = await ctx.db.get(collection, id);
|
|
1766
|
+
if (!existing) {
|
|
1767
|
+
return { error: "${tableName} not found" };
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const result = await ctx.db.update(collection, id, data);
|
|
1771
|
+
return result;
|
|
1772
|
+
},
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
/**
|
|
1776
|
+
* DELETE /${tableName}/:id
|
|
1777
|
+
* Delete a ${tableName} record
|
|
1778
|
+
*/
|
|
1779
|
+
export const delete${pascalName} = http.delete({
|
|
1780
|
+
path: "/${tableName}/:id",
|
|
1781
|
+
auth: "public",
|
|
1782
|
+
handler: async (ctx) => {
|
|
1783
|
+
const id = ctx.req.params.id;
|
|
1784
|
+
|
|
1785
|
+
const existing = await ctx.db.get(collection, id);
|
|
1786
|
+
if (!existing) {
|
|
1787
|
+
return { error: "${tableName} not found" };
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
await ctx.db.delete(collection, id);
|
|
1791
|
+
return { success: true };
|
|
1792
|
+
},
|
|
1793
|
+
});
|
|
1794
|
+
`;
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Generate the index.ts file for CRUD handlers
|
|
1798
|
+
*/
|
|
1799
|
+
generateCrudIndex(tables) {
|
|
1800
|
+
const exports = tables.map(tableName => {
|
|
1801
|
+
const pascalName = tableName.charAt(0).toUpperCase() + tableName.slice(1);
|
|
1802
|
+
return `export * from "./${tableName}";`;
|
|
1803
|
+
}).join("\n");
|
|
1804
|
+
return `// Auto-generated CRUD exports
|
|
1805
|
+
// This file is auto-generated by Archlast CLI - do not edit manually
|
|
1806
|
+
|
|
1807
|
+
${exports}
|
|
1808
|
+
|
|
1809
|
+
// Re-export all handlers as a grouped object
|
|
1810
|
+
import * as tasksHandlers from "./tasks";
|
|
1811
|
+
import * as usersHandlers from "./users";
|
|
1812
|
+
|
|
1813
|
+
// Add more collections as needed...
|
|
1814
|
+
`;
|
|
1815
|
+
}
|
|
1816
|
+
}
|