@donkeylabs/cli 0.1.0 → 0.1.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/package.json +1 -1
- package/src/commands/generate.ts +20 -12
- package/src/index.ts +0 -0
- package/templates/starter/package.json +3 -3
- package/templates/sveltekit-app/bun.lock +547 -0
- package/templates/sveltekit-app/donkeylabs.config.ts +1 -0
- package/templates/sveltekit-app/package.json +8 -6
- package/templates/sveltekit-app/scripts/watch-server.ts +55 -0
- package/templates/sveltekit-app/src/lib/api.ts +61 -121
- package/templates/sveltekit-app/src/routes/+page.server.ts +2 -2
- package/templates/sveltekit-app/src/server/index.ts +24 -150
- package/templates/sveltekit-app/src/server/plugins/demo/index.ts +144 -0
- package/templates/sveltekit-app/svelte.config.js +1 -3
- package/templates/sveltekit-app/tsconfig.json +4 -9
- package/templates/sveltekit-app/vite.config.ts +2 -1
|
@@ -3,6 +3,7 @@ import { defineConfig } from "@donkeylabs/server";
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
plugins: ["./src/server/plugins/**/index.ts"],
|
|
5
5
|
outDir: ".@donkeylabs/server",
|
|
6
|
+
entry: "./src/server/index.ts",
|
|
6
7
|
adapter: "@donkeylabs/adapter-sveltekit",
|
|
7
8
|
client: {
|
|
8
9
|
output: "./src/lib/api.ts",
|
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
"version": "0.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "bun --bun vite dev",
|
|
8
|
-
"
|
|
7
|
+
"dev": "bun run gen:types && bun run dev:watch & bun --bun vite dev",
|
|
8
|
+
"dev:watch": "bun --watch --no-clear-screen scripts/watch-server.ts",
|
|
9
|
+
"build": "bun run gen:types && vite build",
|
|
9
10
|
"preview": "bun build/server/entry.js",
|
|
10
11
|
"prepare": "svelte-kit sync || echo ''",
|
|
11
12
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
12
|
-
"gen:types": "donkeylabs generate"
|
|
13
|
+
"gen:types": "donkeylabs generate",
|
|
14
|
+
"cli": "donkeylabs"
|
|
13
15
|
},
|
|
14
16
|
"devDependencies": {
|
|
15
|
-
"@donkeylabs/cli": "*",
|
|
16
17
|
"@sveltejs/kit": "^2.49.1",
|
|
17
18
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
18
19
|
"@tailwindcss/vite": "^4.1.18",
|
|
@@ -23,8 +24,9 @@
|
|
|
23
24
|
"vite": "^7.2.6"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@donkeylabs/
|
|
27
|
-
"@donkeylabs/
|
|
27
|
+
"@donkeylabs/cli": "0.1.1",
|
|
28
|
+
"@donkeylabs/adapter-sveltekit": "0.1.2",
|
|
29
|
+
"@donkeylabs/server": "0.3.1",
|
|
28
30
|
"bits-ui": "^2.15.4",
|
|
29
31
|
"clsx": "^2.1.1",
|
|
30
32
|
"kysely": "^0.27.6",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Watch server files and regenerate types on changes
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
import { exec } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
const serverDir = join(import.meta.dir, "..", "src", "server");
|
|
10
|
+
let isGenerating = false;
|
|
11
|
+
let pendingGenerate = false;
|
|
12
|
+
|
|
13
|
+
async function regenerate() {
|
|
14
|
+
if (isGenerating) {
|
|
15
|
+
pendingGenerate = true;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isGenerating = true;
|
|
20
|
+
console.log("\x1b[36m[watch]\x1b[0m Server files changed, regenerating types...");
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await execAsync("bun run gen:types");
|
|
24
|
+
console.log("\x1b[32m[watch]\x1b[0m Types regenerated successfully");
|
|
25
|
+
} catch (e: any) {
|
|
26
|
+
console.error("\x1b[31m[watch]\x1b[0m Error regenerating types:", e.message);
|
|
27
|
+
} finally {
|
|
28
|
+
isGenerating = false;
|
|
29
|
+
if (pendingGenerate) {
|
|
30
|
+
pendingGenerate = false;
|
|
31
|
+
await regenerate();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Debounce to avoid multiple rapid regenerations
|
|
37
|
+
let debounceTimer: Timer | null = null;
|
|
38
|
+
|
|
39
|
+
function debouncedRegenerate() {
|
|
40
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
41
|
+
debounceTimer = setTimeout(regenerate, 300);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Watch server directory recursively
|
|
45
|
+
watch(serverDir, { recursive: true }, (eventType, filename) => {
|
|
46
|
+
if (!filename) return;
|
|
47
|
+
if (!filename.endsWith(".ts")) return;
|
|
48
|
+
|
|
49
|
+
debouncedRegenerate();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log("\x1b[36m[watch]\x1b[0m Watching src/server/ for changes...");
|
|
53
|
+
|
|
54
|
+
// Keep process alive
|
|
55
|
+
await new Promise(() => {});
|
|
@@ -1,134 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Extends UnifiedApiClientBase to handle SSR direct calls and browser HTTP calls
|
|
4
|
-
*/
|
|
5
|
-
import { UnifiedApiClientBase } from "@donkeylabs/adapter-sveltekit/client";
|
|
6
|
-
|
|
7
|
-
// Route type definitions
|
|
8
|
-
export interface CounterResponse {
|
|
9
|
-
count: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface CacheGetResponse {
|
|
13
|
-
value: any;
|
|
14
|
-
exists: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface CacheKeysResponse {
|
|
18
|
-
keys: string[];
|
|
19
|
-
size: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface JobEnqueueResponse {
|
|
23
|
-
jobId: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface JobStatsResponse {
|
|
27
|
-
pending: number;
|
|
28
|
-
running: number;
|
|
29
|
-
completed: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface CronTask {
|
|
33
|
-
id: string;
|
|
34
|
-
name: string;
|
|
35
|
-
expression: string;
|
|
36
|
-
enabled: boolean;
|
|
37
|
-
lastRun?: string;
|
|
38
|
-
nextRun?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface CronListResponse {
|
|
42
|
-
tasks: CronTask[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface RateLimitResult {
|
|
46
|
-
allowed: boolean;
|
|
47
|
-
remaining: number;
|
|
48
|
-
limit: number;
|
|
49
|
-
resetAt: string;
|
|
50
|
-
retryAfter?: number;
|
|
51
|
-
}
|
|
1
|
+
// Auto-generated by donkeylabs generate
|
|
2
|
+
// DO NOT EDIT MANUALLY
|
|
52
3
|
|
|
53
|
-
|
|
54
|
-
total: number;
|
|
55
|
-
byChannel: number;
|
|
56
|
-
}
|
|
4
|
+
import { UnifiedApiClientBase, type ClientOptions } from "@donkeylabs/adapter-sveltekit/client";
|
|
57
5
|
|
|
58
|
-
/**
|
|
59
|
-
* Typed API client for the demo server
|
|
60
|
-
*/
|
|
61
6
|
export class ApiClient extends UnifiedApiClientBase {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.request
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.request
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
ratelimit = {
|
|
98
|
-
check: (input: { key?: string; limit?: number; window?: number }) =>
|
|
99
|
-
this.request<typeof input, RateLimitResult>("api.ratelimit.check", input),
|
|
100
|
-
reset: (input: { key?: string }) =>
|
|
101
|
-
this.request<typeof input, { success: boolean }>("api.ratelimit.reset", input),
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Events routes
|
|
105
|
-
events = {
|
|
106
|
-
emit: (input: { event?: string; data?: any }) =>
|
|
107
|
-
this.request<typeof input, { success: boolean }>("api.events.emit", input),
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// SSE routes
|
|
111
|
-
sseRoutes = {
|
|
112
|
-
broadcast: (input: { channel?: string; event?: string; data: any }) =>
|
|
113
|
-
this.request<typeof input, { success: boolean }>("api.sse.broadcast", input),
|
|
114
|
-
clients: () =>
|
|
115
|
-
this.request<{}, SSEClientsResponse>("api.sse.clients", {}),
|
|
7
|
+
constructor(options?: ClientOptions) {
|
|
8
|
+
super(options);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
api = {
|
|
12
|
+
counter: {
|
|
13
|
+
get: (input: any) => this.request("api.counter.get", input),
|
|
14
|
+
increment: (input: any) => this.request("api.counter.increment", input),
|
|
15
|
+
decrement: (input: any) => this.request("api.counter.decrement", input),
|
|
16
|
+
reset: (input: any) => this.request("api.counter.reset", input)
|
|
17
|
+
},
|
|
18
|
+
cache: {
|
|
19
|
+
set: (input: any) => this.request("api.cache.set", input),
|
|
20
|
+
get: (input: any) => this.request("api.cache.get", input),
|
|
21
|
+
delete: (input: any) => this.request("api.cache.delete", input),
|
|
22
|
+
keys: (input: any) => this.request("api.cache.keys", input)
|
|
23
|
+
},
|
|
24
|
+
jobs: {
|
|
25
|
+
enqueue: (input: any) => this.request("api.jobs.enqueue", input),
|
|
26
|
+
stats: (input: any) => this.request("api.jobs.stats", input)
|
|
27
|
+
},
|
|
28
|
+
cron: {
|
|
29
|
+
list: (input: any) => this.request("api.cron.list", input)
|
|
30
|
+
},
|
|
31
|
+
ratelimit: {
|
|
32
|
+
check: (input: any) => this.request("api.ratelimit.check", input),
|
|
33
|
+
reset: (input: any) => this.request("api.ratelimit.reset", input)
|
|
34
|
+
},
|
|
35
|
+
events: {
|
|
36
|
+
emit: (input: any) => this.request("api.events.emit", input)
|
|
37
|
+
},
|
|
38
|
+
sse: {
|
|
39
|
+
broadcast: (input: any) => this.request("api.sse.broadcast", input),
|
|
40
|
+
clients: (input: any) => this.request("api.sse.clients", input)
|
|
41
|
+
}
|
|
116
42
|
};
|
|
117
43
|
}
|
|
118
44
|
|
|
119
45
|
/**
|
|
120
46
|
* Create an API client instance
|
|
121
47
|
*
|
|
122
|
-
* @
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
48
|
+
* @param options.locals - Pass SvelteKit locals for SSR direct calls (no HTTP overhead)
|
|
49
|
+
* @param options.baseUrl - Override the base URL for HTTP calls
|
|
50
|
+
*
|
|
51
|
+
* @example SSR usage in +page.server.ts:
|
|
52
|
+
* ```ts
|
|
53
|
+
* export const load = async ({ locals }) => {
|
|
54
|
+
* const api = createApi({ locals });
|
|
55
|
+
* const data = await api.myRoute.get({}); // Direct call, no HTTP!
|
|
56
|
+
* return { data };
|
|
57
|
+
* };
|
|
58
|
+
* ```
|
|
126
59
|
*
|
|
127
|
-
* @example
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
60
|
+
* @example Browser usage in +page.svelte:
|
|
61
|
+
* ```svelte
|
|
62
|
+
* <script>
|
|
63
|
+
* import { createApi } from '$lib/api';
|
|
64
|
+
* const api = createApi(); // HTTP calls
|
|
65
|
+
* let data = $state(null);
|
|
66
|
+
* async function load() {
|
|
67
|
+
* data = await api.myRoute.get({});
|
|
68
|
+
* }
|
|
69
|
+
* </script>
|
|
70
|
+
* ```
|
|
131
71
|
*/
|
|
132
|
-
export function createApi(options?:
|
|
72
|
+
export function createApi(options?: ClientOptions) {
|
|
133
73
|
return new ApiClient(options);
|
|
134
74
|
}
|
|
@@ -8,9 +8,9 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|
|
8
8
|
|
|
9
9
|
try {
|
|
10
10
|
// Direct service call through typed client
|
|
11
|
-
const
|
|
11
|
+
const result = await api.api.counter.get({}) as { count: number };
|
|
12
12
|
return {
|
|
13
|
-
count,
|
|
13
|
+
count: result.count,
|
|
14
14
|
loadedAt: new Date().toISOString(),
|
|
15
15
|
isSSR: true,
|
|
16
16
|
};
|
|
@@ -1,155 +1,24 @@
|
|
|
1
1
|
// Server entry for @donkeylabs/adapter-sveltekit
|
|
2
|
-
import { AppServer,
|
|
2
|
+
import { AppServer, createRouter } from "@donkeylabs/server";
|
|
3
3
|
import { Kysely } from "kysely";
|
|
4
4
|
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import { demoPlugin } from "./plugins/demo";
|
|
7
8
|
|
|
8
9
|
// Simple in-memory database
|
|
9
10
|
const db = new Kysely<{}>({
|
|
10
11
|
dialect: new BunSqliteDialect({ database: new Database(":memory:") }),
|
|
11
12
|
});
|
|
12
13
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"Item shipped",
|
|
19
|
-
"Review submitted",
|
|
20
|
-
"Comment added",
|
|
21
|
-
"File uploaded",
|
|
22
|
-
"Task completed",
|
|
23
|
-
"Alert triggered",
|
|
24
|
-
"Sync finished",
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
// Demo plugin with all core service integrations
|
|
28
|
-
const demoPlugin = createPlugin.define({
|
|
29
|
-
name: "demo",
|
|
30
|
-
service: async (ctx) => {
|
|
31
|
-
let counter = 0;
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
// Counter
|
|
35
|
-
getCounter: () => counter,
|
|
36
|
-
increment: () => ++counter,
|
|
37
|
-
decrement: () => --counter,
|
|
38
|
-
reset: () => { counter = 0; return counter; },
|
|
39
|
-
|
|
40
|
-
// Cache helpers
|
|
41
|
-
cacheSet: async (key: string, value: any, ttl?: number) => {
|
|
42
|
-
await ctx.core.cache.set(key, value, ttl);
|
|
43
|
-
return { success: true };
|
|
44
|
-
},
|
|
45
|
-
cacheGet: async (key: string) => {
|
|
46
|
-
const value = await ctx.core.cache.get(key);
|
|
47
|
-
const exists = await ctx.core.cache.has(key);
|
|
48
|
-
return { value, exists };
|
|
49
|
-
},
|
|
50
|
-
cacheDelete: async (key: string) => {
|
|
51
|
-
await ctx.core.cache.delete(key);
|
|
52
|
-
return { success: true };
|
|
53
|
-
},
|
|
54
|
-
cacheKeys: async () => {
|
|
55
|
-
const keys = await ctx.core.cache.keys();
|
|
56
|
-
return { keys, size: keys.length };
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
// Jobs helpers
|
|
60
|
-
enqueueJob: async (name: string, data: any, delay?: number) => {
|
|
61
|
-
let jobId: string;
|
|
62
|
-
if (delay && delay > 0) {
|
|
63
|
-
const runAt = new Date(Date.now() + delay);
|
|
64
|
-
jobId = await ctx.core.jobs.schedule(name, data, runAt);
|
|
65
|
-
} else {
|
|
66
|
-
jobId = await ctx.core.jobs.enqueue(name, data);
|
|
67
|
-
}
|
|
68
|
-
return { jobId };
|
|
69
|
-
},
|
|
70
|
-
getJobStats: async () => {
|
|
71
|
-
const pending = await ctx.core.jobs.getByName("demo-job", "pending");
|
|
72
|
-
const running = await ctx.core.jobs.getByName("demo-job", "running");
|
|
73
|
-
const completed = await ctx.core.jobs.getByName("demo-job", "completed");
|
|
74
|
-
return {
|
|
75
|
-
pending: pending.length,
|
|
76
|
-
running: running.length,
|
|
77
|
-
completed: completed.length,
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
// Cron helpers
|
|
82
|
-
getCronTasks: () => ctx.core.cron.list().map(t => ({
|
|
83
|
-
id: t.id,
|
|
84
|
-
name: t.name,
|
|
85
|
-
expression: t.expression,
|
|
86
|
-
enabled: t.enabled,
|
|
87
|
-
lastRun: t.lastRun?.toISOString(),
|
|
88
|
-
nextRun: t.nextRun?.toISOString(),
|
|
89
|
-
})),
|
|
90
|
-
|
|
91
|
-
// Rate limiter helpers
|
|
92
|
-
checkRateLimit: async (key: string, limit: number, window: number) => {
|
|
93
|
-
return ctx.core.rateLimiter.check(key, limit, window);
|
|
94
|
-
},
|
|
95
|
-
resetRateLimit: async (key: string) => {
|
|
96
|
-
await ctx.core.rateLimiter.reset(key);
|
|
97
|
-
return { success: true };
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
// Events helpers (internal pub/sub)
|
|
101
|
-
emitEvent: async (event: string, data: any) => {
|
|
102
|
-
await ctx.core.events.emit(event, data);
|
|
103
|
-
return { success: true };
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
// SSE broadcast
|
|
107
|
-
broadcast: (channel: string, event: string, data: any) => {
|
|
108
|
-
ctx.core.sse.broadcast(channel, event, data);
|
|
109
|
-
return { success: true };
|
|
110
|
-
},
|
|
111
|
-
getSSEClients: () => ({
|
|
112
|
-
total: ctx.core.sse.getClients().length,
|
|
113
|
-
byChannel: ctx.core.sse.getClientsByChannel("events").length,
|
|
114
|
-
}),
|
|
115
|
-
};
|
|
116
|
-
},
|
|
117
|
-
init: async (ctx) => {
|
|
118
|
-
// Register job handler for demo
|
|
119
|
-
ctx.core.jobs.register("demo-job", async (data) => {
|
|
120
|
-
ctx.core.logger.info("Demo job executed", { data });
|
|
121
|
-
// Broadcast job completion via SSE
|
|
122
|
-
ctx.core.sse.broadcast("events", "job-completed", {
|
|
123
|
-
id: Date.now(),
|
|
124
|
-
message: `Job completed: ${data.message || "No message"}`,
|
|
125
|
-
timestamp: new Date().toISOString(),
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Schedule cron job to broadcast SSE events every 5 seconds
|
|
130
|
-
ctx.core.cron.schedule("*/5 * * * * *", () => {
|
|
131
|
-
const message = eventMessages[Math.floor(Math.random() * eventMessages.length)];
|
|
132
|
-
ctx.core.sse.broadcast("events", "cron-event", {
|
|
133
|
-
id: Date.now(),
|
|
134
|
-
message,
|
|
135
|
-
timestamp: new Date().toISOString(),
|
|
136
|
-
source: "cron",
|
|
137
|
-
});
|
|
138
|
-
}, { name: "sse-broadcaster" });
|
|
14
|
+
// Create server
|
|
15
|
+
export const server = new AppServer({
|
|
16
|
+
db,
|
|
17
|
+
port: 0, // Port managed by adapter
|
|
18
|
+
});
|
|
139
19
|
|
|
140
|
-
|
|
141
|
-
ctx.core.events.on("demo.*", (data) => {
|
|
142
|
-
ctx.core.sse.broadcast("events", "internal-event", {
|
|
143
|
-
id: Date.now(),
|
|
144
|
-
message: `Internal event: ${JSON.stringify(data)}`,
|
|
145
|
-
timestamp: new Date().toISOString(),
|
|
146
|
-
source: "events",
|
|
147
|
-
});
|
|
148
|
-
});
|
|
20
|
+
server.registerPlugin(demoPlugin);
|
|
149
21
|
|
|
150
|
-
ctx.core.logger.info("Demo plugin initialized with all core services");
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
22
|
|
|
154
23
|
// Create routes
|
|
155
24
|
const api = createRouter("api");
|
|
@@ -202,7 +71,7 @@ api.route("jobs.enqueue").typed({
|
|
|
202
71
|
data: z.any().default({}),
|
|
203
72
|
delay: z.number().optional()
|
|
204
73
|
}),
|
|
205
|
-
handle: async (input, ctx) => ctx.plugins.demo.enqueueJob(input.name
|
|
74
|
+
handle: async (input, ctx) => ctx.plugins.demo.enqueueJob(input.name!, input.data, input.delay),
|
|
206
75
|
});
|
|
207
76
|
|
|
208
77
|
api.route("jobs.stats").typed({
|
|
@@ -221,12 +90,12 @@ api.route("ratelimit.check").typed({
|
|
|
221
90
|
limit: z.number().default(5),
|
|
222
91
|
window: z.number().default(60000)
|
|
223
92
|
}),
|
|
224
|
-
handle: async (input, ctx) => ctx.plugins.demo.checkRateLimit(input.key
|
|
93
|
+
handle: async (input, ctx) => ctx.plugins.demo.checkRateLimit(input.key!, input.limit!, input.window!),
|
|
225
94
|
});
|
|
226
95
|
|
|
227
96
|
api.route("ratelimit.reset").typed({
|
|
228
97
|
input: z.object({ key: z.string().default("demo") }),
|
|
229
|
-
handle: async (input, ctx) => ctx.plugins.demo.resetRateLimit(input.key),
|
|
98
|
+
handle: async (input, ctx) => ctx.plugins.demo.resetRateLimit(input.key!),
|
|
230
99
|
});
|
|
231
100
|
|
|
232
101
|
// Events routes (internal pub/sub)
|
|
@@ -235,7 +104,7 @@ api.route("events.emit").typed({
|
|
|
235
104
|
event: z.string().default("demo.test"),
|
|
236
105
|
data: z.any().default({ test: true })
|
|
237
106
|
}),
|
|
238
|
-
handle: async (input, ctx) => ctx.plugins.demo.emitEvent(input.event
|
|
107
|
+
handle: async (input, ctx) => ctx.plugins.demo.emitEvent(input.event!, input.data),
|
|
239
108
|
});
|
|
240
109
|
|
|
241
110
|
// SSE routes
|
|
@@ -245,19 +114,24 @@ api.route("sse.broadcast").typed({
|
|
|
245
114
|
event: z.string().default("manual"),
|
|
246
115
|
data: z.any()
|
|
247
116
|
}),
|
|
248
|
-
handle: async (input, ctx) => ctx.plugins.demo.broadcast(input.channel
|
|
117
|
+
handle: async (input, ctx) => ctx.plugins.demo.broadcast(input.channel!, input.event!, input.data),
|
|
249
118
|
});
|
|
250
119
|
|
|
251
120
|
api.route("sse.clients").typed({
|
|
252
121
|
handle: async (_input, ctx) => ctx.plugins.demo.getSSEClients(),
|
|
253
122
|
});
|
|
254
123
|
|
|
255
|
-
// Create server
|
|
256
|
-
export const server = new AppServer({
|
|
257
|
-
db,
|
|
258
|
-
port: 0, // Port managed by adapter
|
|
259
|
-
});
|
|
260
124
|
|
|
261
125
|
// Register plugin and routes
|
|
262
|
-
server.registerPlugin(demoPlugin);
|
|
263
126
|
server.use(api);
|
|
127
|
+
|
|
128
|
+
// Handle DONKEYLABS_GENERATE for type generation
|
|
129
|
+
if (process.env.DONKEYLABS_GENERATE === "1") {
|
|
130
|
+
// Extract routes and output as JSON for CLI
|
|
131
|
+
const routes = api.getRoutes().map((r) => ({
|
|
132
|
+
name: r.name,
|
|
133
|
+
handler: r.handler || "typed",
|
|
134
|
+
}));
|
|
135
|
+
console.log(JSON.stringify({ routes }));
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Demo plugin with all core service integrations
|
|
2
|
+
import { createPlugin } from "@donkeylabs/server";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Random event messages for SSE demo
|
|
6
|
+
const eventMessages = [
|
|
7
|
+
"User logged in",
|
|
8
|
+
"New order placed",
|
|
9
|
+
"Payment received",
|
|
10
|
+
"Item shipped",
|
|
11
|
+
"Review submitted",
|
|
12
|
+
"Comment added",
|
|
13
|
+
"File uploaded",
|
|
14
|
+
"Task completed",
|
|
15
|
+
"Alert triggered",
|
|
16
|
+
"Sync finished",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export const demoPlugin = createPlugin.define({
|
|
21
|
+
name: "demo",
|
|
22
|
+
service: async (ctx) => {
|
|
23
|
+
let counter = 0;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
// Counter
|
|
27
|
+
getCounter: () => counter,
|
|
28
|
+
increment: () => ++counter,
|
|
29
|
+
decrement: () => --counter,
|
|
30
|
+
reset: () => { counter = 0; return counter; },
|
|
31
|
+
|
|
32
|
+
// Cache helpers
|
|
33
|
+
cacheSet: async (key: string, value: any, ttl?: number) => {
|
|
34
|
+
await ctx.core.cache.set(key, value, ttl);
|
|
35
|
+
return { success: true };
|
|
36
|
+
},
|
|
37
|
+
cacheGet: async (key: string) => {
|
|
38
|
+
const value = await ctx.core.cache.get(key);
|
|
39
|
+
const exists = await ctx.core.cache.has(key);
|
|
40
|
+
return { value, exists };
|
|
41
|
+
},
|
|
42
|
+
cacheDelete: async (key: string) => {
|
|
43
|
+
await ctx.core.cache.delete(key);
|
|
44
|
+
return { success: true };
|
|
45
|
+
},
|
|
46
|
+
cacheKeys: async () => {
|
|
47
|
+
const keys = await ctx.core.cache.keys();
|
|
48
|
+
return { keys, size: keys.length };
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Jobs helpers
|
|
52
|
+
enqueueJob: async (name: string, data: any, delay?: number) => {
|
|
53
|
+
let jobId: string;
|
|
54
|
+
if (delay && delay > 0) {
|
|
55
|
+
const runAt = new Date(Date.now() + delay);
|
|
56
|
+
jobId = await ctx.core.jobs.schedule(name, data, runAt);
|
|
57
|
+
} else {
|
|
58
|
+
jobId = await ctx.core.jobs.enqueue(name, data);
|
|
59
|
+
}
|
|
60
|
+
return { jobId };
|
|
61
|
+
},
|
|
62
|
+
getJobStats: async () => {
|
|
63
|
+
const pending = await ctx.core.jobs.getByName("demo-job", "pending");
|
|
64
|
+
const running = await ctx.core.jobs.getByName("demo-job", "running");
|
|
65
|
+
const completed = await ctx.core.jobs.getByName("demo-job", "completed");
|
|
66
|
+
return {
|
|
67
|
+
pending: pending.length,
|
|
68
|
+
running: running.length,
|
|
69
|
+
completed: completed.length,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Cron helpers
|
|
74
|
+
getCronTasks: () => ctx.core.cron.list().map(t => ({
|
|
75
|
+
id: t.id,
|
|
76
|
+
name: t.name,
|
|
77
|
+
expression: t.expression,
|
|
78
|
+
enabled: t.enabled,
|
|
79
|
+
lastRun: t.lastRun?.toISOString(),
|
|
80
|
+
nextRun: t.nextRun?.toISOString(),
|
|
81
|
+
})),
|
|
82
|
+
|
|
83
|
+
// Rate limiter helpers
|
|
84
|
+
checkRateLimit: async (key: string, limit: number, window: number) => {
|
|
85
|
+
return ctx.core.rateLimiter.check(key, limit, window);
|
|
86
|
+
},
|
|
87
|
+
resetRateLimit: async (key: string) => {
|
|
88
|
+
await ctx.core.rateLimiter.reset(key);
|
|
89
|
+
return { success: true };
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Events helpers (internal pub/sub)
|
|
93
|
+
emitEvent: async (event: string, data: any) => {
|
|
94
|
+
await ctx.core.events.emit(event, data);
|
|
95
|
+
return { success: true };
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// SSE broadcast
|
|
99
|
+
broadcast: (channel: string, event: string, data: any) => {
|
|
100
|
+
ctx.core.sse.broadcast(channel, event, data);
|
|
101
|
+
return { success: true };
|
|
102
|
+
},
|
|
103
|
+
getSSEClients: () => ({
|
|
104
|
+
total: ctx.core.sse.getClients().length,
|
|
105
|
+
byChannel: ctx.core.sse.getClientsByChannel("events").length,
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
init: async (ctx) => {
|
|
110
|
+
// Register job handler for demo
|
|
111
|
+
ctx.core.jobs.register("demo-job", async (data) => {
|
|
112
|
+
ctx.core.logger.info("Demo job executed", { data });
|
|
113
|
+
// Broadcast job completion via SSE
|
|
114
|
+
ctx.core.sse.broadcast("events", "job-completed", {
|
|
115
|
+
id: Date.now(),
|
|
116
|
+
message: `Job completed: ${data.message || "No message"}`,
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Schedule cron job to broadcast SSE events every 5 seconds
|
|
122
|
+
ctx.core.cron.schedule("*/5 * * * * *", () => {
|
|
123
|
+
const message = eventMessages[Math.floor(Math.random() * eventMessages.length)];
|
|
124
|
+
ctx.core.sse.broadcast("events", "cron-event", {
|
|
125
|
+
id: Date.now(),
|
|
126
|
+
message,
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
source: "cron",
|
|
129
|
+
});
|
|
130
|
+
}, { name: "sse-broadcaster" });
|
|
131
|
+
|
|
132
|
+
// Listen for internal events and broadcast to SSE
|
|
133
|
+
ctx.core.events.on("demo.*", (data) => {
|
|
134
|
+
ctx.core.sse.broadcast("events", "internal-event", {
|
|
135
|
+
id: Date.now(),
|
|
136
|
+
message: `Internal event: ${JSON.stringify(data)}`,
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
source: "events",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
ctx.core.logger.info("Demo plugin initialized with all core services");
|
|
143
|
+
},
|
|
144
|
+
});
|