@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 +1 -1
- package/src/core/logs-adapter-kysely.ts +7 -0
- package/src/core/logs.ts +3 -0
- package/src/core/migrations/workflows/002_add_metadata_column.ts +41 -7
- package/src/core/processes.ts +7 -3
- package/src/core/watchdog-executor.ts +58 -20
- package/src/core.ts +18 -3
- package/src/server.ts +11 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
+
}
|
package/src/core/processes.ts
CHANGED
|
@@ -170,8 +170,8 @@ export interface SpawnOptions {
|
|
|
170
170
|
// ============================================
|
|
171
171
|
|
|
172
172
|
export interface ProcessesConfig {
|
|
173
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
66
|
-
): Kysely<any
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
547
|
+
const table = this.core.db.schema
|
|
548
548
|
.createTable("__donkeylabs_migrations__")
|
|
549
|
-
.ifNotExists()
|
|
550
|
-
|
|
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
|
|
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 {
|