@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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
import { createRouter, defineRoute } from "@donkeylabs/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ListCronHandler } from "./handlers/list";
|
|
5
|
+
|
|
6
|
+
const router = createRouter("api");
|
|
7
|
+
|
|
8
|
+
router.route("cron.list").typed(
|
|
9
|
+
defineRoute({
|
|
10
|
+
output: z.object({
|
|
11
|
+
tasks: z.array(z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
expression: z.string(),
|
|
15
|
+
enabled: z.boolean(),
|
|
16
|
+
lastRun: z.string().optional(),
|
|
17
|
+
nextRun: z.string().optional()
|
|
18
|
+
}))
|
|
19
|
+
}),
|
|
20
|
+
handle: ListCronHandler,
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export default router;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Handler, AppContext } from "$lib/api";
|
|
3
|
+
import type { Routes } from "$lib/api";
|
|
4
|
+
|
|
5
|
+
export class EmitEventHandler implements Handler<Routes.Api.Events.Emit> {
|
|
6
|
+
ctx: AppContext;
|
|
7
|
+
|
|
8
|
+
constructor(ctx: AppContext) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
handle(input: Routes.Api.Events.Emit.Input): Routes.Api.Events.Emit.Output {
|
|
13
|
+
return this.ctx.plugins.demo.emitEvent(input.event!, input.data);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
import { createRouter, defineRoute } from "@donkeylabs/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { EmitEventHandler } from "./handlers/emit";
|
|
5
|
+
|
|
6
|
+
const router = createRouter("api");
|
|
7
|
+
|
|
8
|
+
router.route("events.emit").typed(
|
|
9
|
+
defineRoute({
|
|
10
|
+
input: z.object({
|
|
11
|
+
event: z.string().default("demo.test"),
|
|
12
|
+
data: z.any().default({ test: true })
|
|
13
|
+
}),
|
|
14
|
+
output: z.object({ success: z.boolean() }),
|
|
15
|
+
handle: EmitEventHandler,
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export default router;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Export all route modules
|
|
2
|
+
export { default as counterRoutes } from "./counter";
|
|
3
|
+
export { default as cacheRoutes } from "./cache";
|
|
4
|
+
export { default as jobsRoutes } from "./jobs";
|
|
5
|
+
export { default as cronRoutes } from "./cron";
|
|
6
|
+
export { default as ratelimitRoutes } from "./ratelimit";
|
|
7
|
+
export { default as eventsRoutes } from "./events";
|
|
8
|
+
export { default as sseRoutes } from "./sse";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Handler, AppContext } from "$lib/api";
|
|
3
|
+
import type { Routes } from "$lib/api";
|
|
4
|
+
|
|
5
|
+
export class EnqueueJobHandler implements Handler<Routes.Api.Jobs.Enqueue> {
|
|
6
|
+
ctx: AppContext;
|
|
7
|
+
|
|
8
|
+
constructor(ctx: AppContext) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
handle(input: Routes.Api.Jobs.Enqueue.Input): Routes.Api.Jobs.Enqueue.Output {
|
|
13
|
+
return this.ctx.plugins.demo.enqueueJob(input.name!, input.data, input.delay);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Handler, AppContext } from "$lib/api";
|
|
3
|
+
import type { Routes } from "$lib/api";
|
|
4
|
+
|
|
5
|
+
export class StatsJobHandler implements Handler<Routes.Api.Jobs.Stats> {
|
|
6
|
+
ctx: AppContext;
|
|
7
|
+
|
|
8
|
+
constructor(ctx: AppContext) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
handle(input: Routes.Api.Jobs.Stats.Input): Routes.Api.Jobs.Stats.Output {
|
|
13
|
+
return this.ctx.plugins.demo.getJobStats();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
import { createRouter, defineRoute } from "@donkeylabs/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { EnqueueJobHandler } from "./handlers/enqueue";
|
|
5
|
+
import { StatsJobHandler } from "./handlers/stats";
|
|
6
|
+
|
|
7
|
+
const router = createRouter("api");
|
|
8
|
+
|
|
9
|
+
router.route("jobs.enqueue").typed(
|
|
10
|
+
defineRoute({
|
|
11
|
+
input: z.object({
|
|
12
|
+
name: z.string().default("demo-job"),
|
|
13
|
+
data: z.any().default({}),
|
|
14
|
+
delay: z.number().optional()
|
|
15
|
+
}),
|
|
16
|
+
output: z.object({ jobId: z.string() }),
|
|
17
|
+
handle: EnqueueJobHandler,
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
router.route("jobs.stats").typed(
|
|
22
|
+
defineRoute({
|
|
23
|
+
output: z.object({ pending: z.number(), running: z.number(), completed: z.number() }),
|
|
24
|
+
handle: StatsJobHandler,
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export default router;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Handler, AppContext } from "$lib/api";
|
|
3
|
+
import type { Routes } from "$lib/api";
|
|
4
|
+
|
|
5
|
+
export class CheckRateLimitHandler implements Handler<Routes.Api.Ratelimit.Check> {
|
|
6
|
+
ctx: AppContext;
|
|
7
|
+
|
|
8
|
+
constructor(ctx: AppContext) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
handle(input: Routes.Api.Ratelimit.Check.Input): Routes.Api.Ratelimit.Check.Output {
|
|
13
|
+
return this.ctx.plugins.demo.checkRateLimit(input.key!, input.limit!, input.window!);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Handler, AppContext } from "$lib/api";
|
|
3
|
+
import type { Routes } from "$lib/api";
|
|
4
|
+
|
|
5
|
+
export class ResetRateLimitHandler implements Handler<Routes.Api.Ratelimit.Reset> {
|
|
6
|
+
ctx: AppContext;
|
|
7
|
+
|
|
8
|
+
constructor(ctx: AppContext) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
handle(input: Routes.Api.Ratelimit.Reset.Input): Routes.Api.Ratelimit.Reset.Output {
|
|
13
|
+
return this.ctx.plugins.demo.resetRateLimit(input.key!);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
import { createRouter, defineRoute } from "@donkeylabs/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { CheckRateLimitHandler } from "./handlers/check";
|
|
5
|
+
import { ResetRateLimitHandler } from "./handlers/reset";
|
|
6
|
+
|
|
7
|
+
const router = createRouter("api");
|
|
8
|
+
|
|
9
|
+
router.route("ratelimit.check").typed(
|
|
10
|
+
defineRoute({
|
|
11
|
+
input: z.object({
|
|
12
|
+
key: z.string().default("demo"),
|
|
13
|
+
limit: z.number().default(5),
|
|
14
|
+
window: z.number().default(60000)
|
|
15
|
+
}),
|
|
16
|
+
output: z.object({ allowed: z.boolean(), remaining: z.number(), resetAt: z.date() }),
|
|
17
|
+
handle: CheckRateLimitHandler,
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
router.route("ratelimit.reset").typed(
|
|
22
|
+
defineRoute({
|
|
23
|
+
input: z.object({ key: z.string().default("demo") }),
|
|
24
|
+
output: z.object({ success: z.boolean() }),
|
|
25
|
+
handle: ResetRateLimitHandler,
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export default router;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Handler, AppContext } from "$lib/api";
|
|
3
|
+
import type { Routes } from "$lib/api";
|
|
4
|
+
|
|
5
|
+
export class BroadcastSseHandler implements Handler<Routes.Api.Sse.Broadcast> {
|
|
6
|
+
ctx: AppContext;
|
|
7
|
+
|
|
8
|
+
constructor(ctx: AppContext) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
handle(input: Routes.Api.Sse.Broadcast.Input): Routes.Api.Sse.Broadcast.Output {
|
|
13
|
+
return this.ctx.plugins.demo.broadcast(input.channel!, input.event!, input.data);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Handler, AppContext } from "$lib/api";
|
|
3
|
+
import type { Routes } from "$lib/api";
|
|
4
|
+
|
|
5
|
+
export class ClientsSseHandler implements Handler<Routes.Api.Sse.Clients> {
|
|
6
|
+
ctx: AppContext;
|
|
7
|
+
|
|
8
|
+
constructor(ctx: AppContext) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
handle(input: Routes.Api.Sse.Clients.Input): Routes.Api.Sse.Clients.Output {
|
|
13
|
+
return this.ctx.plugins.demo.getSSEClients();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
import { createRouter, defineRoute } from "@donkeylabs/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { BroadcastSseHandler } from "./handlers/broadcast";
|
|
5
|
+
import { ClientsSseHandler } from "./handlers/clients";
|
|
6
|
+
|
|
7
|
+
const router = createRouter("api");
|
|
8
|
+
|
|
9
|
+
router.route("sse.broadcast").typed(
|
|
10
|
+
defineRoute({
|
|
11
|
+
input: z.object({
|
|
12
|
+
channel: z.string().default("events"),
|
|
13
|
+
event: z.string().default("manual"),
|
|
14
|
+
data: z.any()
|
|
15
|
+
}),
|
|
16
|
+
output: z.object({ success: z.boolean(), recipients: z.number() }),
|
|
17
|
+
handle: BroadcastSseHandler,
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
router.route("sse.clients").typed(
|
|
22
|
+
defineRoute({
|
|
23
|
+
output: z.object({ total: z.number(), byChannel: z.number() }),
|
|
24
|
+
handle: ClientsSseHandler,
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export default router;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import adapter from '@donkeylabs/adapter-sveltekit';
|
|
2
2
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import type { Config } from '@sveltejs/kit';
|
|
5
|
+
|
|
6
|
+
const config: Config = {
|
|
6
7
|
preprocess: vitePreprocess(),
|
|
7
8
|
|
|
8
9
|
kit: {
|
|
9
|
-
adapter: adapter(
|
|
10
|
-
serverEntry: './src/server/index.ts',
|
|
11
|
-
}),
|
|
10
|
+
adapter: adapter(),
|
|
12
11
|
alias: {
|
|
13
12
|
$server: '.@donkeylabs/server',
|
|
14
13
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
|
-
"rewriteRelativeImportExtensions": true,
|
|
5
4
|
"allowJs": true,
|
|
6
5
|
"checkJs": true,
|
|
7
6
|
"esModuleInterop": true,
|
|
@@ -13,13 +12,9 @@
|
|
|
13
12
|
"moduleResolution": "bundler"
|
|
14
13
|
},
|
|
15
14
|
"include": [
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
".svelte-kit/types/**/$types.d.ts",
|
|
19
|
-
"vite.config.ts",
|
|
20
|
-
"src/**/*.js",
|
|
15
|
+
".@donkeylabs/server/**/*.ts",
|
|
16
|
+
".@donkeylabs/server/**/*.d.ts",
|
|
21
17
|
"src/**/*.ts",
|
|
22
|
-
"src/**/*.svelte"
|
|
23
|
-
".@donkeylabs/server/**/*.d.ts"
|
|
18
|
+
"src/**/*.svelte"
|
|
24
19
|
]
|
|
25
|
-
}
|
|
20
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { sveltekit } from '@sveltejs/kit/vite';
|
|
2
2
|
import tailwindcss from '@tailwindcss/vite';
|
|
3
3
|
import { defineConfig } from 'vite';
|
|
4
|
+
import { donkeylabsDev } from '@donkeylabs/adapter-sveltekit/vite';
|
|
4
5
|
|
|
5
6
|
export default defineConfig({
|
|
6
|
-
plugins: [tailwindcss(), sveltekit()]
|
|
7
|
+
plugins: [donkeylabsDev(), tailwindcss(), sveltekit()]
|
|
7
8
|
});
|
|
@@ -1,144 +0,0 @@
|
|
|
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
|
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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>;
|
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
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();
|