@donkeylabs/cli 0.1.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/LICENSE +21 -0
- package/README.md +141 -0
- package/package.json +51 -0
- package/src/commands/generate.ts +585 -0
- package/src/commands/init.ts +201 -0
- package/src/commands/interactive.ts +223 -0
- package/src/commands/plugin.ts +205 -0
- package/src/index.ts +108 -0
- package/templates/starter/.env.example +3 -0
- package/templates/starter/.gitignore.template +4 -0
- package/templates/starter/CLAUDE.md +144 -0
- package/templates/starter/donkeylabs.config.ts +6 -0
- package/templates/starter/package.json +21 -0
- package/templates/starter/src/client.test.ts +7 -0
- package/templates/starter/src/db.ts +9 -0
- package/templates/starter/src/index.ts +48 -0
- package/templates/starter/src/plugins/stats/index.ts +105 -0
- package/templates/starter/src/routes/health/index.ts +5 -0
- package/templates/starter/src/routes/health/ping/index.ts +13 -0
- package/templates/starter/src/routes/health/ping/models/model.ts +23 -0
- package/templates/starter/src/routes/health/ping/schema.ts +14 -0
- package/templates/starter/src/routes/health/ping/tests/integ.test.ts +20 -0
- package/templates/starter/src/routes/health/ping/tests/unit.test.ts +21 -0
- package/templates/starter/src/test-ctx.ts +24 -0
- package/templates/starter/tsconfig.json +27 -0
- package/templates/sveltekit-app/.env.example +3 -0
- package/templates/sveltekit-app/README.md +103 -0
- package/templates/sveltekit-app/donkeylabs.config.ts +10 -0
- package/templates/sveltekit-app/package.json +36 -0
- package/templates/sveltekit-app/src/app.css +40 -0
- package/templates/sveltekit-app/src/app.html +12 -0
- package/templates/sveltekit-app/src/hooks.server.ts +4 -0
- package/templates/sveltekit-app/src/lib/api.ts +134 -0
- package/templates/sveltekit-app/src/lib/components/ui/badge/badge.svelte +30 -0
- package/templates/sveltekit-app/src/lib/components/ui/badge/index.ts +3 -0
- package/templates/sveltekit-app/src/lib/components/ui/button/button.svelte +48 -0
- package/templates/sveltekit-app/src/lib/components/ui/button/index.ts +9 -0
- package/templates/sveltekit-app/src/lib/components/ui/card/card-content.svelte +18 -0
- package/templates/sveltekit-app/src/lib/components/ui/card/card-description.svelte +18 -0
- package/templates/sveltekit-app/src/lib/components/ui/card/card-footer.svelte +18 -0
- package/templates/sveltekit-app/src/lib/components/ui/card/card-header.svelte +18 -0
- package/templates/sveltekit-app/src/lib/components/ui/card/card-title.svelte +18 -0
- package/templates/sveltekit-app/src/lib/components/ui/card/card.svelte +21 -0
- package/templates/sveltekit-app/src/lib/components/ui/card/index.ts +21 -0
- package/templates/sveltekit-app/src/lib/components/ui/index.ts +4 -0
- package/templates/sveltekit-app/src/lib/components/ui/input/index.ts +2 -0
- package/templates/sveltekit-app/src/lib/components/ui/input/input.svelte +20 -0
- package/templates/sveltekit-app/src/lib/utils/index.ts +6 -0
- package/templates/sveltekit-app/src/routes/+layout.svelte +8 -0
- package/templates/sveltekit-app/src/routes/+page.server.ts +25 -0
- package/templates/sveltekit-app/src/routes/+page.svelte +401 -0
- package/templates/sveltekit-app/src/server/index.ts +263 -0
- package/templates/sveltekit-app/static/robots.txt +3 -0
- package/templates/sveltekit-app/svelte.config.js +18 -0
- package/templates/sveltekit-app/tsconfig.json +25 -0
- package/templates/sveltekit-app/vite.config.ts +7 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* @donkeylabs/cli
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* init Initialize a new project
|
|
7
|
+
* generate Generate types (registry, context, client)
|
|
8
|
+
* plugin Plugin management (create, list)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import pc from "picocolors";
|
|
13
|
+
|
|
14
|
+
const { positionals, values } = parseArgs({
|
|
15
|
+
args: process.argv.slice(2),
|
|
16
|
+
options: {
|
|
17
|
+
help: { type: "boolean", short: "h" },
|
|
18
|
+
version: { type: "boolean", short: "v" },
|
|
19
|
+
type: { type: "string", short: "t" },
|
|
20
|
+
},
|
|
21
|
+
allowPositionals: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const command = positionals[0];
|
|
25
|
+
|
|
26
|
+
function printHelp() {
|
|
27
|
+
console.log(`
|
|
28
|
+
${pc.bold("@donkeylabs/cli")} - CLI for @donkeylabs/server
|
|
29
|
+
|
|
30
|
+
${pc.bold("Usage:")}
|
|
31
|
+
donkeylabs Interactive menu
|
|
32
|
+
donkeylabs <command> [options]
|
|
33
|
+
|
|
34
|
+
${pc.bold("Commands:")}
|
|
35
|
+
${pc.cyan("init")} Initialize a new project
|
|
36
|
+
${pc.cyan("generate")} Generate types (registry, context, client)
|
|
37
|
+
${pc.cyan("plugin")} Plugin management
|
|
38
|
+
|
|
39
|
+
${pc.bold("Options:")}
|
|
40
|
+
-h, --help Show this help message
|
|
41
|
+
-v, --version Show version number
|
|
42
|
+
-t, --type <type> Project type for init (server, sveltekit)
|
|
43
|
+
|
|
44
|
+
${pc.bold("Examples:")}
|
|
45
|
+
donkeylabs # Interactive menu
|
|
46
|
+
donkeylabs init # Interactive project setup
|
|
47
|
+
donkeylabs init --type server # Server-only project
|
|
48
|
+
donkeylabs init --type sveltekit # SvelteKit + adapter project
|
|
49
|
+
donkeylabs generate
|
|
50
|
+
donkeylabs plugin create myPlugin
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printVersion() {
|
|
55
|
+
// Read from package.json
|
|
56
|
+
console.log("0.1.0");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
if (values.help) {
|
|
61
|
+
printHelp();
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (values.version) {
|
|
66
|
+
printVersion();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// No command provided - launch interactive mode
|
|
71
|
+
if (!command) {
|
|
72
|
+
const { interactiveCommand } = await import("./commands/interactive");
|
|
73
|
+
await interactiveCommand();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
switch (command) {
|
|
78
|
+
case "init":
|
|
79
|
+
const { initCommand } = await import("./commands/init");
|
|
80
|
+
const initArgs = [...positionals.slice(1)];
|
|
81
|
+
if (values.type) {
|
|
82
|
+
initArgs.push("--type", values.type);
|
|
83
|
+
}
|
|
84
|
+
await initCommand(initArgs);
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case "generate":
|
|
88
|
+
case "gen":
|
|
89
|
+
const { generateCommand } = await import("./commands/generate");
|
|
90
|
+
await generateCommand(positionals.slice(1));
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case "plugin":
|
|
94
|
+
const { pluginCommand } = await import("./commands/plugin");
|
|
95
|
+
await pluginCommand(positionals.slice(1));
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
default:
|
|
99
|
+
console.error(pc.red(`Unknown command: ${command}`));
|
|
100
|
+
console.log(`Run ${pc.cyan("donkeylabs --help")} for available commands.`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main().catch((error) => {
|
|
106
|
+
console.error(pc.red("Error:"), error.message);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
Built with @donkeylabs/server - a type-safe RPC framework for Bun.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
├── index.ts # Server entry point
|
|
10
|
+
├── db.ts # Database setup
|
|
11
|
+
├── routes/ # Route handlers
|
|
12
|
+
│ └── health/
|
|
13
|
+
│ ├── index.ts # Route definitions
|
|
14
|
+
│ └── ping/
|
|
15
|
+
│ ├── handler.ts
|
|
16
|
+
│ ├── models/
|
|
17
|
+
│ │ └── model.ts # Input/output schemas
|
|
18
|
+
│ └── tests/
|
|
19
|
+
│ ├── unit.test.ts
|
|
20
|
+
│ └── integ.test.ts
|
|
21
|
+
└── plugins/ # Business logic plugins
|
|
22
|
+
└── example/
|
|
23
|
+
├── index.ts # Plugin definition
|
|
24
|
+
├── schema.ts # DB types
|
|
25
|
+
└── migrations/ # SQL migrations
|
|
26
|
+
docs/ # Framework documentation
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
See `src/routes/health/ping/` for a complete example with handler, model, and tests.
|
|
32
|
+
|
|
33
|
+
## Plugins
|
|
34
|
+
|
|
35
|
+
Plugins encapsulate business logic with optional database schemas.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { createPlugin } from "@donkeylabs/server";
|
|
39
|
+
|
|
40
|
+
export const notesPlugin = createPlugin.define({
|
|
41
|
+
name: "notes",
|
|
42
|
+
service: async (ctx) => ({
|
|
43
|
+
async create(title: string) {
|
|
44
|
+
return ctx.db.insertInto("notes").values({ title }).execute();
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
→ See **docs/plugins.md** for schemas, migrations, and dependencies.
|
|
51
|
+
|
|
52
|
+
## Routes
|
|
53
|
+
|
|
54
|
+
Routes are organized by feature in `src/routes/`. Each route has:
|
|
55
|
+
- `handler.ts` - Handler logic
|
|
56
|
+
- `models/model.ts` - Zod schemas for input/output
|
|
57
|
+
- `tests/unit.test.ts` - Unit tests
|
|
58
|
+
- `tests/integ.test.ts` - Integration tests
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// routes/notes/index.ts
|
|
62
|
+
import { createRouter } from "@donkeylabs/server";
|
|
63
|
+
import { createHandler } from "./create/handler";
|
|
64
|
+
|
|
65
|
+
export const notesRouter = createRouter("notes")
|
|
66
|
+
.route("create").typed(createHandler);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
→ See **docs/router.md** and **src/routes/health/** for examples.
|
|
70
|
+
|
|
71
|
+
## Errors
|
|
72
|
+
|
|
73
|
+
Use built-in error factories for proper HTTP responses.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
throw ctx.errors.NotFound("User not found");
|
|
77
|
+
throw ctx.errors.BadRequest("Invalid email");
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
→ See **docs/errors.md** for all error types and custom errors.
|
|
81
|
+
|
|
82
|
+
## Core Services
|
|
83
|
+
|
|
84
|
+
Available via `ctx.core`. **Only use what you need.**
|
|
85
|
+
|
|
86
|
+
| Service | Purpose | Docs |
|
|
87
|
+
|---------|---------|------|
|
|
88
|
+
| `logger` | Structured logging | docs/logger.md |
|
|
89
|
+
| `cache` | In-memory key-value cache | docs/cache.md |
|
|
90
|
+
| `events` | Pub/sub between plugins | docs/events.md |
|
|
91
|
+
| `jobs` | Background job queue | docs/jobs.md |
|
|
92
|
+
| `cron` | Scheduled tasks | docs/cron.md |
|
|
93
|
+
| `sse` | Server-sent events | docs/sse.md |
|
|
94
|
+
| `rateLimiter` | Request rate limiting | docs/rate-limiter.md |
|
|
95
|
+
|
|
96
|
+
→ See **docs/core-services.md** for overview.
|
|
97
|
+
|
|
98
|
+
## Middleware
|
|
99
|
+
|
|
100
|
+
Add authentication, logging, or other cross-cutting concerns.
|
|
101
|
+
|
|
102
|
+
→ See **docs/middleware.md** for usage patterns.
|
|
103
|
+
|
|
104
|
+
## API Client
|
|
105
|
+
|
|
106
|
+
Generate a typed client for your frontend:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
bun run gen:client --output ./frontend/src/lib/api
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
→ See **docs/api-client.md** for client configuration and usage.
|
|
113
|
+
|
|
114
|
+
## Svelte 5 Frontend
|
|
115
|
+
|
|
116
|
+
Build type-safe frontends with Svelte 5 and SvelteKit.
|
|
117
|
+
|
|
118
|
+
```svelte
|
|
119
|
+
<script lang="ts">
|
|
120
|
+
import { api } from "$lib/api";
|
|
121
|
+
let items = $state<Item[]>([]);
|
|
122
|
+
|
|
123
|
+
$effect(() => {
|
|
124
|
+
api.items.list({}).then((r) => items = r.items);
|
|
125
|
+
});
|
|
126
|
+
</script>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
→ See **docs/svelte-frontend.md** for patterns and SSE integration.
|
|
130
|
+
|
|
131
|
+
## CLI Commands
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
bun run dev # Start with hot reload
|
|
135
|
+
bun run test # Run tests
|
|
136
|
+
bun run gen:types # Generate types after adding plugins
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Guidelines
|
|
140
|
+
|
|
141
|
+
- **Keep it simple** - don't add services you don't need
|
|
142
|
+
- **One concern per plugin** - auth, notes, billing as separate plugins
|
|
143
|
+
- **Minimal logging** - log errors and key events, not every call
|
|
144
|
+
- **Read the docs** - check docs/*.md before implementing something complex
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-donkeylabs-app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "bun --watch src/index.ts",
|
|
7
|
+
"start": "bun src/index.ts",
|
|
8
|
+
"test": "bun test",
|
|
9
|
+
"gen:types": "donkeylabs generate"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@donkeylabs/server": "*",
|
|
13
|
+
"kysely": "^0.27.0",
|
|
14
|
+
"kysely-bun-sqlite": "^0.3.0",
|
|
15
|
+
"zod": "^3.24.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@donkeylabs/cli": "*",
|
|
19
|
+
"@types/bun": "latest"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { db } from "./db";
|
|
2
|
+
import { AppServer } from "@donkeylabs/server";
|
|
3
|
+
import { healthRouter } from "./routes/health";
|
|
4
|
+
import { statsPlugin } from "./plugins/stats";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
const server = new AppServer({
|
|
8
|
+
port: Number(process.env.PORT) || 3000,
|
|
9
|
+
db,
|
|
10
|
+
config: { env: process.env.NODE_ENV || "development" },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Register plugins
|
|
14
|
+
server.registerPlugin(statsPlugin);
|
|
15
|
+
|
|
16
|
+
// Register routes with middleware applied at router level
|
|
17
|
+
|
|
18
|
+
const api = server.router("api")
|
|
19
|
+
.middleware
|
|
20
|
+
.timing()
|
|
21
|
+
|
|
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
|
+
|
|
46
|
+
api.router(healthRouter)
|
|
47
|
+
|
|
48
|
+
await server.start();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createPlugin, createMiddleware } from "@donkeylabs/server";
|
|
2
|
+
|
|
3
|
+
export interface RequestStats {
|
|
4
|
+
totalRequests: number;
|
|
5
|
+
avgResponseTime: number;
|
|
6
|
+
minResponseTime: number;
|
|
7
|
+
maxResponseTime: number;
|
|
8
|
+
requestsPerRoute: Map<string, number>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StatsService {
|
|
12
|
+
/** Record a request with its duration */
|
|
13
|
+
recordRequest(route: string, durationMs: number): void;
|
|
14
|
+
/** Get current stats snapshot */
|
|
15
|
+
getStats(): RequestStats;
|
|
16
|
+
/** Reset all stats */
|
|
17
|
+
reset(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const statsPlugin = createPlugin.define({
|
|
21
|
+
name: "stats",
|
|
22
|
+
version: "1.0.0",
|
|
23
|
+
|
|
24
|
+
// Service must come before middleware for TypeScript to infer the Service type
|
|
25
|
+
service: async (ctx): Promise<StatsService> => {
|
|
26
|
+
const logger = ctx.core.logger.child({ plugin: "stats" });
|
|
27
|
+
|
|
28
|
+
// In-memory stats
|
|
29
|
+
let totalRequests = 0;
|
|
30
|
+
let totalTime = 0;
|
|
31
|
+
let minTime = Infinity;
|
|
32
|
+
let maxTime = 0;
|
|
33
|
+
const requestsPerRoute = new Map<string, number>();
|
|
34
|
+
|
|
35
|
+
function getStats(): RequestStats {
|
|
36
|
+
return {
|
|
37
|
+
totalRequests,
|
|
38
|
+
avgResponseTime: totalRequests > 0 ? totalTime / totalRequests : 0,
|
|
39
|
+
minResponseTime: minTime,
|
|
40
|
+
maxResponseTime: maxTime,
|
|
41
|
+
requestsPerRoute: new Map(requestsPerRoute),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.info("Stats plugin initialized");
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
recordRequest(route: string, durationMs: number) {
|
|
49
|
+
totalRequests++;
|
|
50
|
+
totalTime += durationMs;
|
|
51
|
+
minTime = Math.min(minTime, durationMs);
|
|
52
|
+
maxTime = Math.max(maxTime, durationMs);
|
|
53
|
+
requestsPerRoute.set(route, (requestsPerRoute.get(route) ?? 0) + 1);
|
|
54
|
+
|
|
55
|
+
logger.debug("Request recorded", { route, durationMs: durationMs.toFixed(2) });
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
getStats,
|
|
59
|
+
|
|
60
|
+
reset() {
|
|
61
|
+
totalRequests = 0;
|
|
62
|
+
totalTime = 0;
|
|
63
|
+
minTime = Infinity;
|
|
64
|
+
maxTime = 0;
|
|
65
|
+
requestsPerRoute.clear();
|
|
66
|
+
logger.info("Stats reset");
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Middleware - ctx is typed PluginContext, service is typed StatsService
|
|
72
|
+
middleware: (ctx, service) => ({
|
|
73
|
+
/** Timing middleware - records request duration and updates stats */
|
|
74
|
+
timing: createMiddleware(async (req, _reqCtx, next) => {
|
|
75
|
+
const logger = ctx.core.logger;
|
|
76
|
+
const route = new URL(req.url).pathname.slice(1);
|
|
77
|
+
const start = performance.now();
|
|
78
|
+
const response = await next();
|
|
79
|
+
const duration = performance.now() - start;
|
|
80
|
+
|
|
81
|
+
// Use own service to record stats - service is typed!
|
|
82
|
+
service.recordRequest(route, duration);
|
|
83
|
+
logger.info("Request processed", { route, durationMs: duration.toFixed(2) });
|
|
84
|
+
|
|
85
|
+
return response;
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
|
|
89
|
+
// Register crons, events, etc. after service is created
|
|
90
|
+
init: (ctx, service) => {
|
|
91
|
+
const logger = ctx.core.logger.child({ plugin: "stats" });
|
|
92
|
+
|
|
93
|
+
// Log stats every minute
|
|
94
|
+
ctx.core.cron.schedule("* * * * *", () => {
|
|
95
|
+
const stats = service.getStats();
|
|
96
|
+
logger.info("Server stats", {
|
|
97
|
+
requests: stats.totalRequests,
|
|
98
|
+
avgMs: stats.avgResponseTime.toFixed(2),
|
|
99
|
+
minMs: stats.minResponseTime === Infinity ? 0 : stats.minResponseTime.toFixed(2),
|
|
100
|
+
maxMs: stats.maxResponseTime.toFixed(2),
|
|
101
|
+
routes: Object.fromEntries(stats.requestsPerRoute),
|
|
102
|
+
});
|
|
103
|
+
}, { name: "stats-reporter" });
|
|
104
|
+
},
|
|
105
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Route definition with full type inference
|
|
2
|
+
import { createRoute } from "@donkeylabs/server";
|
|
3
|
+
import { Input, Output } from "./schema";
|
|
4
|
+
import { PingModel } from "./models/model";
|
|
5
|
+
|
|
6
|
+
export const pingRoute = createRoute.typed({
|
|
7
|
+
input: Input,
|
|
8
|
+
output: Output,
|
|
9
|
+
handle: async (input, ctx) => {
|
|
10
|
+
const model = new PingModel(ctx);
|
|
11
|
+
return model.handle(input);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// After running `bun run gen:types`, use typed Handler:
|
|
2
|
+
import { Handler } from "@donkeylabs/server";
|
|
3
|
+
import type { Health } from "$server/routes";
|
|
4
|
+
import { AppContext } from "$server/context";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Model class with handler logic.
|
|
8
|
+
*/
|
|
9
|
+
export class PingModel implements Handler<Health.Ping> {
|
|
10
|
+
ctx: AppContext;
|
|
11
|
+
|
|
12
|
+
constructor(ctx: AppContext) {
|
|
13
|
+
this.ctx = ctx;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
handle(input: Health.Ping.Input): Health.Ping.Output {
|
|
17
|
+
return {
|
|
18
|
+
status: "ok",
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
echo: input.echo,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const Input = z.object({
|
|
4
|
+
echo: z.string().optional(),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const Output = z.object({
|
|
8
|
+
status: z.literal("ok"),
|
|
9
|
+
timestamp: z.string(),
|
|
10
|
+
echo: z.string().optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type Input = z.infer<typeof Input>;
|
|
14
|
+
export type Output = z.infer<typeof Output>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import type { Output } from "../models/model";
|
|
3
|
+
|
|
4
|
+
const BASE_URL = "http://localhost:3000";
|
|
5
|
+
|
|
6
|
+
describe("health/ping integration", () => {
|
|
7
|
+
// Note: Server must be running for integration tests
|
|
8
|
+
// Run with: bun run dev & bun test src/routes/health/ping/tests/integ.test.ts
|
|
9
|
+
|
|
10
|
+
it("POST /health.ping returns ok", async () => {
|
|
11
|
+
const res = await fetch(`${BASE_URL}/health.ping`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "Content-Type": "application/json" },
|
|
14
|
+
body: JSON.stringify({}),
|
|
15
|
+
});
|
|
16
|
+
expect(res.ok).toBe(true);
|
|
17
|
+
const data = (await res.json()) as Output;
|
|
18
|
+
expect(data.status).toBe("ok");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { PingModel } from "../models/model";
|
|
3
|
+
import { Input } from "../schema";
|
|
4
|
+
|
|
5
|
+
describe("health/ping model", () => {
|
|
6
|
+
it("returns ok status", () => {
|
|
7
|
+
const input = Input.parse({});
|
|
8
|
+
const ctx = {} as any;
|
|
9
|
+
const model = new PingModel(ctx);
|
|
10
|
+
const result = model.handle(input);
|
|
11
|
+
expect(result.status).toBe("ok");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("echoes input", () => {
|
|
15
|
+
const input = Input.parse({ echo: "hello" });
|
|
16
|
+
const ctx = {} as any;
|
|
17
|
+
const model = new PingModel(ctx);
|
|
18
|
+
const result = model.handle(input);
|
|
19
|
+
expect(result.echo).toBe("hello");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/// <reference path="../.@donkeylabs/server/registry.d.ts" />
|
|
2
|
+
// Test that ctx.plugins.stats is typed correctly
|
|
3
|
+
import type { ServerContext, InferService } from "@donkeylabs/server";
|
|
4
|
+
import { statsPlugin } from "./plugins/stats";
|
|
5
|
+
import type { StatsService } from "./plugins/stats";
|
|
6
|
+
|
|
7
|
+
// Verify InferService works correctly
|
|
8
|
+
type InferredService = InferService<typeof statsPlugin>;
|
|
9
|
+
|
|
10
|
+
// This type assertion verifies that InferredService equals StatsService
|
|
11
|
+
const _typeCheck: InferredService extends StatsService
|
|
12
|
+
? StatsService extends InferredService
|
|
13
|
+
? true
|
|
14
|
+
: false
|
|
15
|
+
: false = true;
|
|
16
|
+
|
|
17
|
+
// Verify ctx.plugins.stats is typed as StatsService
|
|
18
|
+
declare const ctx: ServerContext;
|
|
19
|
+
const stats: StatsService = ctx.plugins.stats; // Should compile without error
|
|
20
|
+
|
|
21
|
+
// These methods should all work
|
|
22
|
+
stats.recordRequest("test", 100);
|
|
23
|
+
stats.getStats();
|
|
24
|
+
stats.reset();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": [
|
|
4
|
+
"ESNext"
|
|
5
|
+
],
|
|
6
|
+
"target": "ESNext",
|
|
7
|
+
"module": "Preserve",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"preserveSymlinks": true,
|
|
14
|
+
"baseUrl": ".",
|
|
15
|
+
"paths": {
|
|
16
|
+
"$server/*": [".@donkeylabs/server/*"],
|
|
17
|
+
"$api": [".@donkeylabs/server/client"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": [
|
|
21
|
+
"src",
|
|
22
|
+
".@donkeylabs/server"
|
|
23
|
+
],
|
|
24
|
+
"exclude": [
|
|
25
|
+
"node_modules"
|
|
26
|
+
]
|
|
27
|
+
}
|