@donkeylabs/cli 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/client/base.ts +481 -0
- package/src/commands/generate.ts +262 -53
- package/src/index.ts +0 -0
- package/templates/starter/package.json +3 -3
- package/templates/starter/src/index.ts +19 -30
- package/templates/starter/src/routes/health/handlers/ping.ts +22 -0
- package/templates/starter/src/routes/health/index.ts +16 -2
- package/templates/sveltekit-app/bun.lock +547 -0
- package/templates/sveltekit-app/donkeylabs.config.ts +2 -0
- package/templates/sveltekit-app/package.json +10 -8
- package/templates/sveltekit-app/scripts/watch-server.ts +55 -0
- package/templates/sveltekit-app/src/lib/api.ts +195 -81
- package/templates/sveltekit-app/src/routes/+page.server.ts +3 -3
- package/templates/sveltekit-app/src/routes/+page.svelte +235 -96
- package/templates/sveltekit-app/src/server/index.ts +29 -247
- package/templates/sveltekit-app/src/server/plugins/demo/index.ts +144 -0
- package/templates/sveltekit-app/src/server/routes/cache/handlers/delete.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/cache/handlers/get.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/cache/handlers/keys.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/cache/handlers/set.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/cache/index.ts +46 -0
- package/templates/sveltekit-app/src/server/routes/counter/handlers/decrement.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/counter/handlers/get.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/counter/handlers/increment.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/counter/handlers/reset.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/counter/index.ts +39 -0
- package/templates/sveltekit-app/src/server/routes/cron/handlers/list.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/cron/index.ts +24 -0
- package/templates/sveltekit-app/src/server/routes/events/handlers/emit.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/events/index.ts +19 -0
- package/templates/sveltekit-app/src/server/routes/index.ts +8 -0
- package/templates/sveltekit-app/src/server/routes/jobs/handlers/enqueue.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/jobs/handlers/stats.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/jobs/index.ts +28 -0
- package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/check.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/reset.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/ratelimit/index.ts +29 -0
- package/templates/sveltekit-app/src/server/routes/sse/handlers/broadcast.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/sse/handlers/clients.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/sse/index.ts +28 -0
- package/templates/sveltekit-app/{svelte.config.js → svelte.config.ts} +4 -5
- package/templates/sveltekit-app/tsconfig.json +4 -9
- package/templates/sveltekit-app/vite.config.ts +2 -1
- package/templates/starter/CLAUDE.md +0 -144
- package/templates/starter/src/client.test.ts +0 -7
- package/templates/starter/src/db.ts +0 -9
- package/templates/starter/src/routes/health/ping/index.ts +0 -13
- package/templates/starter/src/routes/health/ping/models/model.ts +0 -23
- package/templates/starter/src/routes/health/ping/schema.ts +0 -14
- package/templates/starter/src/routes/health/ping/tests/integ.test.ts +0 -20
- package/templates/starter/src/routes/health/ping/tests/unit.test.ts +0 -21
- package/templates/starter/src/test-ctx.ts +0 -24
package/src/commands/generate.ts
CHANGED
|
@@ -84,55 +84,164 @@ interface ExtractedRoute {
|
|
|
84
84
|
handler: string;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
interface RouteInfo {
|
|
88
|
+
name: string;
|
|
89
|
+
prefix: string;
|
|
90
|
+
routeName: string;
|
|
91
|
+
handler: "typed" | "raw" | string;
|
|
92
|
+
inputSource?: string;
|
|
93
|
+
outputSource?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract a balanced block from source code starting at a given position
|
|
98
|
+
*/
|
|
99
|
+
function extractBalancedBlock(source: string, startPos: number, open = "{", close = "}"): string {
|
|
100
|
+
let depth = 0;
|
|
101
|
+
let start = -1;
|
|
102
|
+
let end = -1;
|
|
103
|
+
|
|
104
|
+
for (let i = startPos; i < source.length; i++) {
|
|
105
|
+
if (source[i] === open) {
|
|
106
|
+
if (depth === 0) start = i;
|
|
107
|
+
depth++;
|
|
108
|
+
} else if (source[i] === close) {
|
|
109
|
+
depth--;
|
|
110
|
+
if (depth === 0) {
|
|
111
|
+
end = i;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (start !== -1 && end !== -1) {
|
|
118
|
+
return source.slice(start, end + 1);
|
|
119
|
+
}
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract Zod schema from input/output definition
|
|
125
|
+
*/
|
|
126
|
+
function extractZodSchema(configBlock: string, key: "input" | "output"): string | undefined {
|
|
127
|
+
// Find where the key starts
|
|
128
|
+
const keyPattern = new RegExp(`${key}\\s*:\\s*`);
|
|
129
|
+
const match = configBlock.match(keyPattern);
|
|
130
|
+
if (!match || match.index === undefined) return undefined;
|
|
131
|
+
|
|
132
|
+
const startPos = match.index + match[0].length;
|
|
133
|
+
|
|
134
|
+
// Check if it starts with z.
|
|
135
|
+
const afterKey = configBlock.slice(startPos);
|
|
136
|
+
if (!afterKey.startsWith("z.")) return undefined;
|
|
137
|
+
|
|
138
|
+
// Find the end of the Zod expression by tracking parentheses
|
|
139
|
+
let depth = 0;
|
|
140
|
+
let endPos = 0;
|
|
141
|
+
let inParen = false;
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < afterKey.length; i++) {
|
|
144
|
+
const char = afterKey[i];
|
|
145
|
+
|
|
146
|
+
if (char === "(") {
|
|
147
|
+
depth++;
|
|
148
|
+
inParen = true;
|
|
149
|
+
} else if (char === ")") {
|
|
150
|
+
depth--;
|
|
151
|
+
if (depth === 0 && inParen) {
|
|
152
|
+
endPos = i + 1;
|
|
153
|
+
// Check for chained methods like .optional()
|
|
154
|
+
const rest = afterKey.slice(endPos);
|
|
155
|
+
const chainMatch = rest.match(/^\s*\.\w+\(\)/);
|
|
156
|
+
if (chainMatch) {
|
|
157
|
+
endPos += chainMatch[0].length;
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
} else if (depth === 0 && inParen) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (endPos > 0) {
|
|
167
|
+
return afterKey.slice(0, endPos).trim();
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract routes from a server/router source file by parsing the code
|
|
174
|
+
*/
|
|
175
|
+
async function extractRoutesFromSource(filePath: string): Promise<RouteInfo[]> {
|
|
176
|
+
const fullPath = join(process.cwd(), filePath);
|
|
89
177
|
|
|
90
178
|
if (!existsSync(fullPath)) {
|
|
91
|
-
console.warn(pc.yellow(`Entry file not found: ${
|
|
179
|
+
console.warn(pc.yellow(`Entry file not found: ${filePath}, skipping route extraction`));
|
|
92
180
|
return [];
|
|
93
181
|
}
|
|
94
182
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
env: { ...process.env, DONKEYLABS_GENERATE: "1" },
|
|
98
|
-
stdio: ["inherit", "pipe", "pipe"],
|
|
99
|
-
cwd: process.cwd(),
|
|
100
|
-
});
|
|
183
|
+
const content = await readFile(fullPath, "utf-8");
|
|
184
|
+
const routes: RouteInfo[] = [];
|
|
101
185
|
|
|
102
|
-
|
|
103
|
-
|
|
186
|
+
// Find all createRouter calls with their positions
|
|
187
|
+
const routerPattern = /createRouter\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
188
|
+
const routerPositions: { prefix: string; pos: number }[] = [];
|
|
104
189
|
|
|
105
|
-
|
|
106
|
-
|
|
190
|
+
let routerMatch;
|
|
191
|
+
while ((routerMatch = routerPattern.exec(content)) !== null) {
|
|
192
|
+
routerPositions.push({
|
|
193
|
+
prefix: routerMatch[1] || "",
|
|
194
|
+
pos: routerMatch.index,
|
|
107
195
|
});
|
|
196
|
+
}
|
|
108
197
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
198
|
+
// Sort by position
|
|
199
|
+
routerPositions.sort((a, b) => a.pos - b.pos);
|
|
112
200
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
console.warn(pc.yellow(`Failed to extract routes from server (exit code ${code})`));
|
|
116
|
-
if (stderr) console.warn(pc.dim(stderr));
|
|
117
|
-
resolve([]);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
201
|
+
// Find all route definitions with their positions
|
|
202
|
+
const routePattern = /\.route\s*\(\s*["']([^"']+)["']\s*\)\s*\.(typed|raw|[\w]+)\s*\(/g;
|
|
120
203
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
204
|
+
let routeMatch;
|
|
205
|
+
while ((routeMatch = routePattern.exec(content)) !== null) {
|
|
206
|
+
const routeName = routeMatch[1] || "";
|
|
207
|
+
const handler = routeMatch[2] || "typed";
|
|
208
|
+
const routePos = routeMatch.index;
|
|
209
|
+
|
|
210
|
+
// Find which router this route belongs to (most recent one before this position)
|
|
211
|
+
let currentPrefix = "";
|
|
212
|
+
for (const router of routerPositions) {
|
|
213
|
+
if (router.pos < routePos) {
|
|
214
|
+
currentPrefix = router.prefix;
|
|
215
|
+
} else {
|
|
216
|
+
break;
|
|
128
217
|
}
|
|
129
|
-
}
|
|
218
|
+
}
|
|
130
219
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
220
|
+
// Extract the config block
|
|
221
|
+
const configStartPos = routeMatch.index + routeMatch[0].length - 1; // Position of the (
|
|
222
|
+
const configBlock = extractBalancedBlock(content, configStartPos, "(", ")");
|
|
223
|
+
|
|
224
|
+
// Remove outer parens and the inner braces wrapper
|
|
225
|
+
let innerConfig = configBlock.slice(1, -1).trim(); // Remove ( and )
|
|
226
|
+
if (innerConfig.startsWith("{") && innerConfig.endsWith("}")) {
|
|
227
|
+
innerConfig = innerConfig.slice(1, -1); // Remove { and }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Extract input and output schemas
|
|
231
|
+
const inputSource = extractZodSchema(innerConfig, "input");
|
|
232
|
+
const outputSource = extractZodSchema(innerConfig, "output");
|
|
233
|
+
|
|
234
|
+
routes.push({
|
|
235
|
+
name: currentPrefix ? `${currentPrefix}.${routeName}` : routeName,
|
|
236
|
+
prefix: currentPrefix,
|
|
237
|
+
routeName,
|
|
238
|
+
handler,
|
|
239
|
+
inputSource,
|
|
240
|
+
outputSource,
|
|
134
241
|
});
|
|
135
|
-
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return routes;
|
|
136
245
|
}
|
|
137
246
|
|
|
138
247
|
|
|
@@ -179,6 +288,98 @@ async function findPlugins(
|
|
|
179
288
|
return plugins;
|
|
180
289
|
}
|
|
181
290
|
|
|
291
|
+
async function findRouteFiles(pattern: string): Promise<string[]> {
|
|
292
|
+
const files: string[] = [];
|
|
293
|
+
const baseDir = pattern.includes("**")
|
|
294
|
+
? pattern.split("**")[0] || "."
|
|
295
|
+
: dirname(pattern);
|
|
296
|
+
const fileName = basename(pattern.replace("**/", ""));
|
|
297
|
+
|
|
298
|
+
const targetDir = join(process.cwd(), baseDir);
|
|
299
|
+
if (!existsSync(targetDir)) return files;
|
|
300
|
+
|
|
301
|
+
async function scanDir(dir: string): Promise<void> {
|
|
302
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
303
|
+
for (const entry of entries) {
|
|
304
|
+
const fullPath = join(dir, entry.name);
|
|
305
|
+
if (entry.isDirectory()) {
|
|
306
|
+
await scanDir(fullPath);
|
|
307
|
+
} else if (entry.name === fileName) {
|
|
308
|
+
files.push(relative(process.cwd(), fullPath));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await scanDir(targetDir);
|
|
314
|
+
return files;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Run the server entry file with DONKEYLABS_GENERATE=1 to get typed route metadata
|
|
319
|
+
*/
|
|
320
|
+
async function extractRoutesFromServer(entryPath: string): Promise<RouteInfo[]> {
|
|
321
|
+
const fullPath = join(process.cwd(), entryPath);
|
|
322
|
+
|
|
323
|
+
if (!existsSync(fullPath)) {
|
|
324
|
+
console.warn(pc.yellow(`Entry file not found: ${entryPath}`));
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return new Promise((resolve) => {
|
|
329
|
+
const child = spawn("bun", [fullPath], {
|
|
330
|
+
env: { ...process.env, DONKEYLABS_GENERATE: "1" },
|
|
331
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
332
|
+
cwd: process.cwd(),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
let stdout = "";
|
|
336
|
+
let stderr = "";
|
|
337
|
+
|
|
338
|
+
child.stdout?.on("data", (data) => {
|
|
339
|
+
stdout += data.toString();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
child.stderr?.on("data", (data) => {
|
|
343
|
+
stderr += data.toString();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
child.on("close", (code) => {
|
|
347
|
+
if (code !== 0) {
|
|
348
|
+
console.warn(pc.yellow(`Failed to extract routes from server (exit code ${code})`));
|
|
349
|
+
if (stderr) console.warn(pc.dim(stderr));
|
|
350
|
+
resolve([]);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const result = JSON.parse(stdout.trim());
|
|
356
|
+
// Convert server output to RouteInfo format
|
|
357
|
+
const routes: RouteInfo[] = (result.routes || []).map((r: any) => {
|
|
358
|
+
const parts = r.name.split(".");
|
|
359
|
+
return {
|
|
360
|
+
name: r.name,
|
|
361
|
+
prefix: parts.slice(0, -1).join("."),
|
|
362
|
+
routeName: parts[parts.length - 1] || r.name,
|
|
363
|
+
handler: r.handler || "typed",
|
|
364
|
+
// Server outputs TypeScript strings directly now
|
|
365
|
+
inputSource: r.inputType,
|
|
366
|
+
outputSource: r.outputType,
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
resolve(routes);
|
|
370
|
+
} catch (e) {
|
|
371
|
+
console.warn(pc.yellow("Failed to parse route data from server"));
|
|
372
|
+
resolve([]);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
child.on("error", (err) => {
|
|
377
|
+
console.warn(pc.yellow(`Failed to run entry file: ${err.message}`));
|
|
378
|
+
resolve([]);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
182
383
|
export async function generateCommand(_args: string[]): Promise<void> {
|
|
183
384
|
const config = await loadConfig();
|
|
184
385
|
const outDir = config.outDir || ".@donkeylabs/server";
|
|
@@ -189,7 +390,7 @@ export async function generateCommand(_args: string[]): Promise<void> {
|
|
|
189
390
|
const plugins = await findPlugins(config.plugins);
|
|
190
391
|
const fileRoutes = await findRoutes(config.routes || "./src/routes/**/schema.ts");
|
|
191
392
|
|
|
192
|
-
// Extract routes
|
|
393
|
+
// Extract routes by running the server with DONKEYLABS_GENERATE=1
|
|
193
394
|
const entryPath = config.entry || "./src/index.ts";
|
|
194
395
|
const serverRoutes = await extractRoutesFromServer(entryPath);
|
|
195
396
|
|
|
@@ -206,17 +407,21 @@ export async function generateCommand(_args: string[]): Promise<void> {
|
|
|
206
407
|
// Check if adapter provides a custom generator
|
|
207
408
|
if (config.adapter) {
|
|
208
409
|
try {
|
|
209
|
-
//
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
410
|
+
// Resolve the adapter path from the project's node_modules
|
|
411
|
+
const adapterPath = join(process.cwd(), "node_modules", config.adapter, "src/generator/index.ts");
|
|
412
|
+
if (existsSync(adapterPath)) {
|
|
413
|
+
const adapterModule = await import(adapterPath);
|
|
414
|
+
if (adapterModule.generateClient) {
|
|
415
|
+
await adapterModule.generateClient(config, serverRoutes, clientOutput);
|
|
416
|
+
generated.push(`client (${config.adapter})`);
|
|
417
|
+
console.log(pc.green("Generated:"), generated.map(g => pc.dim(g)).join(", "));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
216
420
|
}
|
|
217
|
-
} catch (e) {
|
|
421
|
+
} catch (e: any) {
|
|
218
422
|
// Adapter doesn't provide generator or import failed, fall back to default
|
|
219
423
|
console.log(pc.dim(`Note: Adapter ${config.adapter} has no custom generator, using default`));
|
|
424
|
+
console.log(pc.dim(`Error: ${e.message}`));
|
|
220
425
|
}
|
|
221
426
|
}
|
|
222
427
|
|
|
@@ -232,10 +437,14 @@ async function generateRegistry(
|
|
|
232
437
|
outPath: string
|
|
233
438
|
) {
|
|
234
439
|
const importLines = plugins
|
|
235
|
-
.map(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
440
|
+
.map((p) => {
|
|
441
|
+
// Calculate relative path from outPath to plugin
|
|
442
|
+
const pluginAbsPath = join(process.cwd(), p.path).replace(/\.ts$/, "");
|
|
443
|
+
const relativePath = relative(outPath, pluginAbsPath);
|
|
444
|
+
// Ensure path starts with ./ or ../
|
|
445
|
+
const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
446
|
+
return `import { ${p.exportName} } from "${importPath}";`;
|
|
447
|
+
})
|
|
239
448
|
.join("\n");
|
|
240
449
|
|
|
241
450
|
const pluginRegistryEntries = plugins
|
|
@@ -383,14 +592,14 @@ export type GlobalContext = AppContext;
|
|
|
383
592
|
}
|
|
384
593
|
|
|
385
594
|
// Route file structure: /src/routes/<namespace>/<route-name>/schema.ts
|
|
386
|
-
interface
|
|
595
|
+
interface SchemaRouteInfo {
|
|
387
596
|
namespace: string;
|
|
388
597
|
name: string;
|
|
389
598
|
schemaPath: string;
|
|
390
599
|
}
|
|
391
600
|
|
|
392
|
-
async function findRoutes(_pattern: string): Promise<
|
|
393
|
-
const routes:
|
|
601
|
+
async function findRoutes(_pattern: string): Promise<SchemaRouteInfo[]> {
|
|
602
|
+
const routes: SchemaRouteInfo[] = [];
|
|
394
603
|
const routesDir = join(process.cwd(), "src/routes");
|
|
395
604
|
|
|
396
605
|
if (!existsSync(routesDir)) {
|
|
@@ -430,9 +639,9 @@ function toPascalCase(str: string): string {
|
|
|
430
639
|
.replace(/^./, (c) => c.toUpperCase());
|
|
431
640
|
}
|
|
432
641
|
|
|
433
|
-
async function generateRouteTypes(routes:
|
|
642
|
+
async function generateRouteTypes(routes: SchemaRouteInfo[], outPath: string): Promise<void> {
|
|
434
643
|
// Group routes by namespace
|
|
435
|
-
const byNamespace = new Map<string,
|
|
644
|
+
const byNamespace = new Map<string, SchemaRouteInfo[]>();
|
|
436
645
|
for (const route of routes) {
|
|
437
646
|
if (!byNamespace.has(route.namespace)) {
|
|
438
647
|
byNamespace.set(route.namespace, []);
|
package/src/index.ts
CHANGED
|
File without changes
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
"gen:types": "donkeylabs generate"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@donkeylabs/server": "
|
|
12
|
+
"@donkeylabs/server": "latest",
|
|
13
13
|
"kysely": "^0.27.0",
|
|
14
14
|
"kysely-bun-sqlite": "^0.3.0",
|
|
15
15
|
"zod": "^3.24.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@donkeylabs/cli": "
|
|
18
|
+
"@donkeylabs/cli": "latest",
|
|
19
19
|
"@types/bun": "latest"
|
|
20
20
|
}
|
|
21
|
-
}
|
|
21
|
+
}
|
|
@@ -1,48 +1,37 @@
|
|
|
1
1
|
import { db } from "./db";
|
|
2
|
-
import { AppServer } from "@donkeylabs/server";
|
|
2
|
+
import { AppServer, createRouter } from "@donkeylabs/server";
|
|
3
3
|
import { healthRouter } from "./routes/health";
|
|
4
4
|
import { statsPlugin } from "./plugins/stats";
|
|
5
|
-
import { z } from "zod";
|
|
6
5
|
|
|
7
6
|
const server = new AppServer({
|
|
8
7
|
port: Number(process.env.PORT) || 3000,
|
|
9
8
|
db,
|
|
10
9
|
config: { env: process.env.NODE_ENV || "development" },
|
|
10
|
+
generateTypes: {
|
|
11
|
+
output: "./.@donkeylabs/server/api.ts",
|
|
12
|
+
baseImport: 'import { ApiClientBase, type ApiClientOptions } from "@donkeylabs/server/client";',
|
|
13
|
+
baseClass: "ApiClientBase",
|
|
14
|
+
constructorSignature: "baseUrl: string, options?: ApiClientOptions",
|
|
15
|
+
constructorBody: "super(baseUrl, options);",
|
|
16
|
+
factoryFunction: `/**
|
|
17
|
+
* Create an API client instance
|
|
18
|
+
* @param baseUrl - The base URL of the API server
|
|
19
|
+
*/
|
|
20
|
+
export function createApi(baseUrl: string, options?: ApiClientOptions) {
|
|
21
|
+
return new ApiClient(baseUrl, options);
|
|
22
|
+
}`,
|
|
23
|
+
},
|
|
11
24
|
});
|
|
12
25
|
|
|
13
26
|
// Register plugins
|
|
14
27
|
server.registerPlugin(statsPlugin);
|
|
15
28
|
|
|
16
|
-
|
|
29
|
+
const api = createRouter("api")
|
|
30
|
+
// Register routes
|
|
31
|
+
api.router(healthRouter);
|
|
17
32
|
|
|
18
|
-
const api = server.router("api")
|
|
19
|
-
.middleware
|
|
20
|
-
.timing()
|
|
21
33
|
|
|
22
|
-
const hello = api.router("hello")
|
|
23
|
-
hello.route("test").typed({
|
|
24
|
-
input: z.string(),
|
|
25
|
-
output: z.string(),
|
|
26
|
-
handle: (input, ctx) => {
|
|
27
|
-
// ctx.plugins.stats should now be typed correctly
|
|
28
|
-
const stats = ctx.plugins.stats;
|
|
29
|
-
return input;
|
|
30
|
-
}
|
|
31
|
-
}).route("ping").typed({
|
|
32
|
-
input: z.string(),
|
|
33
|
-
output: z.string(),
|
|
34
|
-
handle: (input, ctx) => {
|
|
35
|
-
return input;
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
.route("pong").typed({
|
|
39
|
-
input: z.string(),
|
|
40
|
-
output: z.string(),
|
|
41
|
-
handle: (input, ctx) => {
|
|
42
|
-
return input;
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
34
|
|
|
46
|
-
api.router(healthRouter)
|
|
47
35
|
|
|
36
|
+
server.use(api);
|
|
48
37
|
await server.start();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
// Types from generated api.ts (run server once to generate)
|
|
3
|
+
import type { Handler, Routes, AppContext } from "$server/api";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ping Handler
|
|
7
|
+
*/
|
|
8
|
+
export class PingHandler implements Handler<Routes.Api.Health.Ping> {
|
|
9
|
+
ctx: AppContext;
|
|
10
|
+
|
|
11
|
+
constructor(ctx: AppContext) {
|
|
12
|
+
this.ctx = ctx;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
handle(input: Routes.Api.Health.Ping.Input): Routes.Api.Health.Ping.Output {
|
|
16
|
+
return {
|
|
17
|
+
status: "ok",
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
echo: input.echo,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
|
|
1
2
|
import { createRouter } from "@donkeylabs/server";
|
|
2
|
-
import {
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { PingHandler } from "./handlers/ping";
|
|
3
5
|
|
|
4
6
|
export const healthRouter = createRouter("health")
|
|
5
|
-
.route("ping").typed(
|
|
7
|
+
.route("ping").typed({
|
|
8
|
+
input: z.object({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
cool: z.number(),
|
|
11
|
+
echo: z.string().optional(),
|
|
12
|
+
}),
|
|
13
|
+
output: z.object({
|
|
14
|
+
status: z.literal("ok"),
|
|
15
|
+
timestamp: z.string(),
|
|
16
|
+
echo: z.string().optional(),
|
|
17
|
+
}),
|
|
18
|
+
handle: PingHandler,
|
|
19
|
+
});
|