@donkeylabs/server 2.0.34 → 2.0.37

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.0.34",
3
+ "version": "2.0.37",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -49,6 +49,8 @@ const LOG_LEVEL_VALUES: Record<LogLevel, number> = {
49
49
  export interface KyselyLogsAdapterConfig {
50
50
  /** Database file path (default: ".donkeylabs/logs.db") */
51
51
  dbPath?: string;
52
+ /** Use an existing Kysely instance */
53
+ db?: Kysely<Database>;
52
54
  }
53
55
 
54
56
  // ============================================
@@ -61,6 +63,11 @@ export class KyselyLogsAdapter implements LogsAdapter {
61
63
  private ensureTablePromise: Promise<void> | null = null;
62
64
 
63
65
  constructor(config: KyselyLogsAdapterConfig = {}) {
66
+ if (config.db) {
67
+ this.db = config.db;
68
+ return;
69
+ }
70
+
64
71
  const dbPath = config.dbPath ?? ".donkeylabs/logs.db";
65
72
 
66
73
  // Ensure directory exists
package/src/core/logs.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { Events } from "./events";
10
10
  import type { LogLevel } from "./logger";
11
+ import type { Kysely } from "kysely";
11
12
 
12
13
  // ============================================
13
14
  // Types
@@ -72,6 +73,8 @@ export interface LogsConfig {
72
73
  maxBufferSize?: number;
73
74
  /** Database path (default: ".donkeylabs/logs.db") */
74
75
  dbPath?: string;
76
+ /** Use an existing Kysely instance for logs storage */
77
+ db?: Kysely<any>;
75
78
  }
76
79
 
77
80
  // ============================================
@@ -7,13 +7,8 @@
7
7
  import { sql, type Kysely } from "kysely";
8
8
 
9
9
  export async function up(db: Kysely<any>): Promise<void> {
10
- // SQLite doesn't support IF NOT EXISTS for ALTER TABLE ADD COLUMN
11
- // Check if column exists first
12
- const tableInfo = await sql<{ name: string }>`
13
- PRAGMA table_info(__donkeylabs_workflow_instances__)
14
- `.execute(db);
15
-
16
- const hasMetadataColumn = tableInfo.rows.some((row) => row.name === "metadata");
10
+ const dialect = getDialectName(db);
11
+ const hasMetadataColumn = await columnExists(db, dialect);
17
12
 
18
13
  if (!hasMetadataColumn) {
19
14
  await sql`
@@ -26,3 +21,42 @@ export async function down(db: Kysely<any>): Promise<void> {
26
21
  // SQLite doesn't support DROP COLUMN directly
27
22
  // In practice, we don't need to remove it - the column can stay
28
23
  }
24
+
25
+ async function columnExists(db: Kysely<any>, dialect: string): Promise<boolean> {
26
+ if (dialect.includes("postgres")) {
27
+ const result = await sql<{ name: string }>`
28
+ SELECT column_name as name
29
+ FROM information_schema.columns
30
+ WHERE table_name = '__donkeylabs_workflow_instances__'
31
+ AND column_name = 'metadata'
32
+ AND table_schema = current_schema()
33
+ `.execute(db);
34
+ return result.rows.length > 0;
35
+ }
36
+
37
+ if (dialect.includes("mysql")) {
38
+ const result = await sql<{ name: string }>`
39
+ SELECT column_name as name
40
+ FROM information_schema.columns
41
+ WHERE table_name = '__donkeylabs_workflow_instances__'
42
+ AND column_name = 'metadata'
43
+ AND table_schema = database()
44
+ `.execute(db);
45
+ return result.rows.length > 0;
46
+ }
47
+
48
+ const tableInfo = await sql<{ name: string }>`
49
+ PRAGMA table_info(__donkeylabs_workflow_instances__)
50
+ `.execute(db);
51
+
52
+ return tableInfo.rows.some((row) => row.name === "metadata");
53
+ }
54
+
55
+ function getDialectName(db: Kysely<any>): string {
56
+ try {
57
+ const adapter = (db as any).getExecutor?.().adapter;
58
+ return adapter?.constructor?.name?.toLowerCase() ?? "";
59
+ } catch {
60
+ return "";
61
+ }
62
+ }
@@ -170,8 +170,8 @@ export interface SpawnOptions {
170
170
  // ============================================
171
171
 
172
172
  export interface ProcessesConfig {
173
- /** SQLite adapter configuration */
174
- adapter?: SqliteProcessAdapterConfig;
173
+ /** Adapter instance or sqlite adapter configuration */
174
+ adapter?: ProcessAdapter | SqliteProcessAdapterConfig;
175
175
  /** Socket server configuration */
176
176
  socket?: ProcessSocketConfig;
177
177
  /** Events service for emitting process events */
@@ -278,7 +278,11 @@ export class ProcessesImpl implements Processes {
278
278
  private isShuttingDown = false;
279
279
 
280
280
  constructor(config: ProcessesConfig = {}) {
281
- this.adapter = new SqliteProcessAdapter(config.adapter);
281
+ if (config.adapter && typeof (config.adapter as ProcessAdapter).get === "function") {
282
+ this.adapter = config.adapter as ProcessAdapter;
283
+ } else {
284
+ this.adapter = new SqliteProcessAdapter(config.adapter as SqliteProcessAdapterConfig | undefined);
285
+ }
282
286
  this.events = config.events;
283
287
  this.heartbeatCheckInterval = config.heartbeatCheckInterval ?? 10000;
284
288
  this.autoRecoverOrphans = config.autoRecoverOrphans ?? true;
@@ -1,10 +1,11 @@
1
- import { Kysely } from "kysely";
1
+ import { Kysely, PostgresDialect, MysqlDialect } from "kysely";
2
2
  import { BunSqliteDialect } from "kysely-bun-sqlite";
3
3
  import Database from "bun:sqlite";
4
4
  import { ProcessClient } from "./process-client";
5
5
  import { KyselyWorkflowAdapter } from "./workflow-adapter-kysely";
6
6
  import { KyselyJobAdapter } from "./job-adapter-kysely";
7
7
  import { SqliteProcessAdapter } from "./process-adapter-sqlite";
8
+ import { KyselyProcessAdapter } from "./process-adapter-kysely";
8
9
  import { WatchdogRunner, type WatchdogRunnerConfig } from "./watchdog-runner";
9
10
 
10
11
  interface WatchdogConfig extends WatchdogRunnerConfig {
@@ -17,6 +18,10 @@ interface WatchdogConfig extends WatchdogRunnerConfig {
17
18
  synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
18
19
  journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
19
20
  };
21
+ database?: {
22
+ type: "sqlite" | "postgres" | "mysql";
23
+ connectionString: string;
24
+ };
20
25
  }
21
26
 
22
27
  const raw = process.env.DONKEYLABS_WATCHDOG_CONFIG;
@@ -27,19 +32,21 @@ if (!raw) {
27
32
  const config: WatchdogConfig = JSON.parse(raw);
28
33
  const client = await ProcessClient.connect();
29
34
 
30
- const workflowAdapter = config.workflows?.dbPath
31
- ? new KyselyWorkflowAdapter(createDb(config.workflows.dbPath, config.sqlitePragmas), {
35
+ const workflowAdapter = config.workflows?.dbPath || config.database
36
+ ? new KyselyWorkflowAdapter(await createDb(config.workflows?.dbPath, config), {
32
37
  cleanupDays: 0,
33
38
  })
34
39
  : undefined;
35
- const jobAdapter = config.jobs?.dbPath
36
- ? new KyselyJobAdapter(createDb(config.jobs.dbPath, config.sqlitePragmas), {
40
+ const jobAdapter = config.jobs?.dbPath || config.database
41
+ ? new KyselyJobAdapter(await createDb(config.jobs?.dbPath, config), {
37
42
  cleanupDays: 0,
38
43
  })
39
44
  : undefined;
40
45
  const processAdapter = config.processes?.dbPath
41
46
  ? new SqliteProcessAdapter({ path: config.processes.dbPath, cleanupDays: 0 })
42
- : undefined;
47
+ : config.database
48
+ ? new KyselyProcessAdapter(await createDb(undefined, config))
49
+ : undefined;
43
50
 
44
51
  const runner = new WatchdogRunner(config, {
45
52
  workflowsAdapter: workflowAdapter,
@@ -60,21 +67,52 @@ process.on("SIGTERM", async () => {
60
67
  client.disconnect();
61
68
  });
62
69
 
63
- function createDb(
64
- dbPath: string,
65
- pragmas?: { busyTimeout?: number; synchronous?: string; journalMode?: string }
66
- ): Kysely<any> {
67
- const sqlite = new Database(dbPath);
68
- const busyTimeout = pragmas?.busyTimeout ?? 5000;
69
- sqlite.run(`PRAGMA busy_timeout = ${busyTimeout}`);
70
- if (pragmas?.journalMode) {
71
- sqlite.run(`PRAGMA journal_mode = ${pragmas.journalMode}`);
70
+ async function createDb(
71
+ dbPath: string | undefined,
72
+ config: WatchdogConfig
73
+ ): Promise<Kysely<any>> {
74
+ const dbConfig = config.database;
75
+
76
+ if (!dbConfig || dbConfig.type === "sqlite") {
77
+ const sqlitePath = dbConfig?.connectionString ?? dbPath;
78
+ if (!sqlitePath) {
79
+ throw new Error("SQLite dbPath or connectionString is required for watchdog");
80
+ }
81
+ const sqlite = new Database(sqlitePath);
82
+ const pragmas = config.sqlitePragmas;
83
+ const busyTimeout = pragmas?.busyTimeout ?? 5000;
84
+ sqlite.run(`PRAGMA busy_timeout = ${busyTimeout}`);
85
+ if (pragmas?.journalMode) {
86
+ sqlite.run(`PRAGMA journal_mode = ${pragmas.journalMode}`);
87
+ }
88
+ if (pragmas?.synchronous) {
89
+ sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous}`);
90
+ }
91
+
92
+ return new Kysely<any>({
93
+ dialect: new BunSqliteDialect({ database: sqlite }),
94
+ });
72
95
  }
73
- if (pragmas?.synchronous) {
74
- sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous}`);
96
+
97
+ if (dbConfig.type === "postgres") {
98
+ // @ts-ignore optional dependency
99
+ const { Pool: PGPool } = await import("pg");
100
+ return new Kysely<any>({
101
+ dialect: new PostgresDialect({
102
+ pool: new PGPool({ connectionString: dbConfig.connectionString }),
103
+ }),
104
+ });
105
+ }
106
+
107
+ if (dbConfig.type === "mysql") {
108
+ // @ts-ignore optional dependency
109
+ const { createPool: createMySQLPool } = await import("mysql2");
110
+ return new Kysely<any>({
111
+ dialect: new MysqlDialect({
112
+ pool: createMySQLPool(dbConfig.connectionString),
113
+ }),
114
+ });
75
115
  }
76
116
 
77
- return new Kysely<any>({
78
- dialect: new BunSqliteDialect({ database: sqlite }),
79
- });
117
+ throw new Error(`Unsupported database type: ${dbConfig.type}`);
80
118
  }
package/src/core.ts CHANGED
@@ -544,10 +544,15 @@ export class PluginManager {
544
544
  * This table tracks which migrations have been applied for each plugin.
545
545
  */
546
546
  private async ensureMigrationsTable(): Promise<void> {
547
- await this.core.db.schema
547
+ const table = this.core.db.schema
548
548
  .createTable("__donkeylabs_migrations__")
549
- .ifNotExists()
550
- .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
549
+ .ifNotExists();
550
+
551
+ const withId = this.isPostgresDialect()
552
+ ? table.addColumn("id", "serial", (col) => col.primaryKey())
553
+ : table.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement());
554
+
555
+ await withId
551
556
  .addColumn("plugin_name", "text", (col) => col.notNull())
552
557
  .addColumn("migration_name", "text", (col) => col.notNull())
553
558
  .addColumn("executed_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
@@ -559,6 +564,16 @@ export class PluginManager {
559
564
  ON __donkeylabs_migrations__(plugin_name, migration_name)`.execute(this.core.db);
560
565
  }
561
566
 
567
+ private isPostgresDialect(): boolean {
568
+ try {
569
+ const adapter = (this.core.db as any).getExecutor?.().adapter;
570
+ const name = adapter?.constructor?.name?.toLowerCase() ?? "";
571
+ return name.includes("postgres");
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
576
+
562
577
  /**
563
578
  * Checks if a migration has already been applied for a specific plugin.
564
579
  */
package/src/server.ts CHANGED
@@ -266,6 +266,7 @@ export class AppServer {
266
266
  // Create logs service with its own database
267
267
  const logsAdapter = options.logs?.adapter ?? new KyselyLogsAdapter({
268
268
  dbPath: options.logs?.dbPath,
269
+ db: options.logs?.db,
269
270
  });
270
271
  const logs = createLogs({
271
272
  ...options.logs,
@@ -296,6 +297,7 @@ export class AppServer {
296
297
  // Create adapters - use Kysely by default, or legacy SQLite if requested
297
298
  const jobAdapter = options.jobs?.adapter ?? (useLegacy ? undefined : new KyselyJobAdapter(options.db));
298
299
  const workflowAdapter = options.workflows?.adapter ?? (useLegacy ? undefined : new KyselyWorkflowAdapter(options.db));
300
+ const processAdapter = options.processes?.adapter ?? (useLegacy ? undefined : new KyselyProcessAdapter(options.db));
299
301
  const auditAdapter = options.audit?.adapter ?? new KyselyAuditAdapter(options.db);
300
302
 
301
303
  // Jobs can emit events and use Kysely adapter, with logger for scoped logging
@@ -327,6 +329,7 @@ export class AppServer {
327
329
  const processes = createProcesses({
328
330
  ...options.processes,
329
331
  events,
332
+ adapter: processAdapter,
330
333
  useWatchdog: options.watchdog?.enabled ? true : options.processes?.useWatchdog,
331
334
  });
332
335
 
@@ -1107,7 +1110,13 @@ ${factoryFunction}
1107
1110
  const services = this.watchdogConfig.services ?? ["workflows", "jobs", "processes"];
1108
1111
  const workflowsDbPath = this.coreServices.workflows.getDbPath?.();
1109
1112
  const jobsDbPath = (this.options.jobs?.dbPath ?? workflowsDbPath ?? ".donkeylabs/jobs.db") as string;
1110
- const processesDbPath = (this.options.processes?.adapter?.path ?? ".donkeylabs/processes.db") as string;
1113
+ const adapterConfig = this.options.processes?.adapter;
1114
+ let processesDbPath: string | undefined;
1115
+ if (adapterConfig && typeof adapterConfig === "object" && "path" in adapterConfig) {
1116
+ processesDbPath = (adapterConfig as any).path ?? ".donkeylabs/processes.db";
1117
+ } else if (!adapterConfig && !this.options.database) {
1118
+ processesDbPath = ".donkeylabs/processes.db";
1119
+ }
1111
1120
 
1112
1121
  const config = {
1113
1122
  intervalMs: this.watchdogConfig.intervalMs ?? 5000,
@@ -1123,6 +1132,7 @@ ${factoryFunction}
1123
1132
  jobs: jobsDbPath ? { dbPath: jobsDbPath } : undefined,
1124
1133
  processes: processesDbPath ? { dbPath: processesDbPath } : undefined,
1125
1134
  sqlitePragmas: this.options.workflows?.sqlitePragmas,
1135
+ database: this.options.database,
1126
1136
  };
1127
1137
 
1128
1138
  try {