@axiom-lattice/pg-stores 1.0.2 → 1.0.3

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.
@@ -0,0 +1,591 @@
1
+ /**
2
+ * PostgreSQL implementation of ScheduleStorage
3
+ *
4
+ * Provides persistent storage for scheduled tasks
5
+ * Data survives service restarts
6
+ */
7
+
8
+ import { Pool, PoolConfig } from "pg";
9
+ import {
10
+ ScheduleStorage,
11
+ ScheduledTaskDefinition,
12
+ ScheduledTaskStatus,
13
+ ScheduleExecutionType,
14
+ } from "@axiom-lattice/protocols";
15
+ import { MigrationManager } from "../migrations/migration";
16
+ import { createScheduledTasksTable } from "../migrations/schedule_migrations";
17
+
18
+ /**
19
+ * PostgreSQL ScheduleStorage options
20
+ */
21
+ export interface PostgreSQLScheduleStorageOptions {
22
+ /**
23
+ * PostgreSQL connection pool configuration
24
+ * Can be a connection string or PoolConfig object
25
+ */
26
+ poolConfig: string | PoolConfig;
27
+
28
+ /**
29
+ * Whether to run migrations automatically on initialization
30
+ * @default true
31
+ */
32
+ autoMigrate?: boolean;
33
+ }
34
+
35
+ /**
36
+ * Database row type for scheduled tasks
37
+ */
38
+ interface ScheduledTaskRow {
39
+ task_id: string;
40
+ task_type: string;
41
+ payload: string | Record<string, any>;
42
+ assistant_id: string | null;
43
+ thread_id: string | null;
44
+ execution_type: string;
45
+ execute_at: Date | null;
46
+ delay_ms: number | null;
47
+ cron_expression: string | null;
48
+ timezone: string | null;
49
+ next_run_at: Date | null;
50
+ last_run_at: Date | null;
51
+ status: string;
52
+ run_count: number;
53
+ max_runs: number | null;
54
+ retry_count: number;
55
+ max_retries: number;
56
+ last_error: string | null;
57
+ created_at: Date;
58
+ updated_at: Date;
59
+ expires_at: Date | null;
60
+ metadata: string | Record<string, any> | null;
61
+ }
62
+
63
+ /**
64
+ * PostgreSQL implementation of ScheduleStorage
65
+ */
66
+ export class PostgreSQLScheduleStorage implements ScheduleStorage {
67
+ private pool: Pool;
68
+ private migrationManager: MigrationManager;
69
+ private initialized: boolean = false;
70
+
71
+ constructor(options: PostgreSQLScheduleStorageOptions) {
72
+ // Create Pool from config
73
+ if (typeof options.poolConfig === "string") {
74
+ this.pool = new Pool({ connectionString: options.poolConfig });
75
+ } else {
76
+ this.pool = new Pool(options.poolConfig);
77
+ }
78
+
79
+ this.migrationManager = new MigrationManager(this.pool);
80
+ this.migrationManager.register(createScheduledTasksTable);
81
+
82
+ // Auto-migrate by default
83
+ if (options.autoMigrate !== false) {
84
+ this.initialize().catch((error) => {
85
+ console.error("Failed to initialize PostgreSQLScheduleStorage:", error);
86
+ throw error;
87
+ });
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Dispose resources and close the connection pool
93
+ */
94
+ async dispose(): Promise<void> {
95
+ if (this.pool) {
96
+ await this.pool.end();
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Initialize the store and run migrations
102
+ */
103
+ async initialize(): Promise<void> {
104
+ if (this.initialized) {
105
+ return;
106
+ }
107
+
108
+ await this.migrationManager.migrate();
109
+ this.initialized = true;
110
+ }
111
+
112
+ /**
113
+ * Ensure store is initialized
114
+ */
115
+ private async ensureInitialized(): Promise<void> {
116
+ if (!this.initialized) {
117
+ await this.initialize();
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Save a new task
123
+ */
124
+ async save(task: ScheduledTaskDefinition): Promise<void> {
125
+ await this.ensureInitialized();
126
+
127
+ await this.pool.query(
128
+ `
129
+ INSERT INTO lattice_scheduled_tasks (
130
+ task_id, task_type, payload, assistant_id, thread_id, execution_type,
131
+ execute_at, delay_ms, cron_expression, timezone,
132
+ next_run_at, last_run_at, status, run_count, max_runs,
133
+ retry_count, max_retries, last_error,
134
+ created_at, updated_at, expires_at, metadata
135
+ ) VALUES (
136
+ $1, $2, $3, $4, $5, $6,
137
+ $7, $8, $9, $10,
138
+ $11, $12, $13, $14, $15,
139
+ $16, $17, $18,
140
+ $19, $20, $21, $22
141
+ )
142
+ ON CONFLICT (task_id) DO UPDATE SET
143
+ task_type = EXCLUDED.task_type,
144
+ payload = EXCLUDED.payload,
145
+ assistant_id = EXCLUDED.assistant_id,
146
+ thread_id = EXCLUDED.thread_id,
147
+ execution_type = EXCLUDED.execution_type,
148
+ execute_at = EXCLUDED.execute_at,
149
+ delay_ms = EXCLUDED.delay_ms,
150
+ cron_expression = EXCLUDED.cron_expression,
151
+ timezone = EXCLUDED.timezone,
152
+ next_run_at = EXCLUDED.next_run_at,
153
+ last_run_at = EXCLUDED.last_run_at,
154
+ status = EXCLUDED.status,
155
+ run_count = EXCLUDED.run_count,
156
+ max_runs = EXCLUDED.max_runs,
157
+ retry_count = EXCLUDED.retry_count,
158
+ max_retries = EXCLUDED.max_retries,
159
+ last_error = EXCLUDED.last_error,
160
+ updated_at = EXCLUDED.updated_at,
161
+ expires_at = EXCLUDED.expires_at,
162
+ metadata = EXCLUDED.metadata
163
+ `,
164
+ [
165
+ task.taskId,
166
+ task.taskType,
167
+ JSON.stringify(task.payload),
168
+ task.assistantId ?? null,
169
+ task.threadId ?? null,
170
+ task.executionType,
171
+ task.executeAt ? new Date(task.executeAt) : null,
172
+ task.delayMs ?? null,
173
+ task.cronExpression ?? null,
174
+ task.timezone ?? null,
175
+ task.nextRunAt ? new Date(task.nextRunAt) : null,
176
+ task.lastRunAt ? new Date(task.lastRunAt) : null,
177
+ task.status,
178
+ task.runCount,
179
+ task.maxRuns ?? null,
180
+ task.retryCount,
181
+ task.maxRetries,
182
+ task.lastError ?? null,
183
+ new Date(task.createdAt),
184
+ new Date(task.updatedAt),
185
+ task.expiresAt ? new Date(task.expiresAt) : null,
186
+ task.metadata ? JSON.stringify(task.metadata) : null,
187
+ ]
188
+ );
189
+ }
190
+
191
+ /**
192
+ * Get task by ID
193
+ */
194
+ async get(taskId: string): Promise<ScheduledTaskDefinition | null> {
195
+ await this.ensureInitialized();
196
+
197
+ const result = await this.pool.query<ScheduledTaskRow>(
198
+ `SELECT * FROM lattice_scheduled_tasks WHERE task_id = $1`,
199
+ [taskId]
200
+ );
201
+
202
+ if (result.rows.length === 0) {
203
+ return null;
204
+ }
205
+
206
+ return this.mapRowToTask(result.rows[0]);
207
+ }
208
+
209
+ /**
210
+ * Update task
211
+ */
212
+ async update(
213
+ taskId: string,
214
+ updates: Partial<ScheduledTaskDefinition>
215
+ ): Promise<void> {
216
+ await this.ensureInitialized();
217
+
218
+ const setClauses: string[] = [];
219
+ const values: any[] = [];
220
+ let paramIndex = 1;
221
+
222
+ // Build dynamic UPDATE query
223
+ if (updates.taskType !== undefined) {
224
+ setClauses.push(`task_type = $${paramIndex++}`);
225
+ values.push(updates.taskType);
226
+ }
227
+ if (updates.payload !== undefined) {
228
+ setClauses.push(`payload = $${paramIndex++}`);
229
+ values.push(JSON.stringify(updates.payload));
230
+ }
231
+ if (updates.assistantId !== undefined) {
232
+ setClauses.push(`assistant_id = $${paramIndex++}`);
233
+ values.push(updates.assistantId);
234
+ }
235
+ if (updates.threadId !== undefined) {
236
+ setClauses.push(`thread_id = $${paramIndex++}`);
237
+ values.push(updates.threadId);
238
+ }
239
+ if (updates.executionType !== undefined) {
240
+ setClauses.push(`execution_type = $${paramIndex++}`);
241
+ values.push(updates.executionType);
242
+ }
243
+ if (updates.executeAt !== undefined) {
244
+ setClauses.push(`execute_at = $${paramIndex++}`);
245
+ values.push(updates.executeAt ? new Date(updates.executeAt) : null);
246
+ }
247
+ if (updates.delayMs !== undefined) {
248
+ setClauses.push(`delay_ms = $${paramIndex++}`);
249
+ values.push(updates.delayMs);
250
+ }
251
+ if (updates.cronExpression !== undefined) {
252
+ setClauses.push(`cron_expression = $${paramIndex++}`);
253
+ values.push(updates.cronExpression);
254
+ }
255
+ if (updates.timezone !== undefined) {
256
+ setClauses.push(`timezone = $${paramIndex++}`);
257
+ values.push(updates.timezone);
258
+ }
259
+ if (updates.nextRunAt !== undefined) {
260
+ setClauses.push(`next_run_at = $${paramIndex++}`);
261
+ values.push(updates.nextRunAt ? new Date(updates.nextRunAt) : null);
262
+ }
263
+ if (updates.lastRunAt !== undefined) {
264
+ setClauses.push(`last_run_at = $${paramIndex++}`);
265
+ values.push(updates.lastRunAt ? new Date(updates.lastRunAt) : null);
266
+ }
267
+ if (updates.status !== undefined) {
268
+ setClauses.push(`status = $${paramIndex++}`);
269
+ values.push(updates.status);
270
+ }
271
+ if (updates.runCount !== undefined) {
272
+ setClauses.push(`run_count = $${paramIndex++}`);
273
+ values.push(updates.runCount);
274
+ }
275
+ if (updates.maxRuns !== undefined) {
276
+ setClauses.push(`max_runs = $${paramIndex++}`);
277
+ values.push(updates.maxRuns);
278
+ }
279
+ if (updates.retryCount !== undefined) {
280
+ setClauses.push(`retry_count = $${paramIndex++}`);
281
+ values.push(updates.retryCount);
282
+ }
283
+ if (updates.maxRetries !== undefined) {
284
+ setClauses.push(`max_retries = $${paramIndex++}`);
285
+ values.push(updates.maxRetries);
286
+ }
287
+ if (updates.lastError !== undefined) {
288
+ setClauses.push(`last_error = $${paramIndex++}`);
289
+ values.push(updates.lastError);
290
+ }
291
+ if (updates.expiresAt !== undefined) {
292
+ setClauses.push(`expires_at = $${paramIndex++}`);
293
+ values.push(updates.expiresAt ? new Date(updates.expiresAt) : null);
294
+ }
295
+ if (updates.metadata !== undefined) {
296
+ setClauses.push(`metadata = $${paramIndex++}`);
297
+ values.push(updates.metadata ? JSON.stringify(updates.metadata) : null);
298
+ }
299
+
300
+ // Always update updated_at
301
+ setClauses.push(`updated_at = $${paramIndex++}`);
302
+ values.push(new Date());
303
+
304
+ // Add taskId as the last parameter
305
+ values.push(taskId);
306
+
307
+ if (setClauses.length === 0) {
308
+ return;
309
+ }
310
+
311
+ await this.pool.query(
312
+ `UPDATE lattice_scheduled_tasks SET ${setClauses.join(
313
+ ", "
314
+ )} WHERE task_id = $${paramIndex}`,
315
+ values
316
+ );
317
+ }
318
+
319
+ /**
320
+ * Delete task
321
+ */
322
+ async delete(taskId: string): Promise<void> {
323
+ await this.ensureInitialized();
324
+
325
+ await this.pool.query(
326
+ `DELETE FROM lattice_scheduled_tasks WHERE task_id = $1`,
327
+ [taskId]
328
+ );
329
+ }
330
+
331
+ /**
332
+ * Get all active tasks (pending or paused)
333
+ */
334
+ async getActiveTasks(): Promise<ScheduledTaskDefinition[]> {
335
+ await this.ensureInitialized();
336
+
337
+ const result = await this.pool.query<ScheduledTaskRow>(
338
+ `SELECT * FROM lattice_scheduled_tasks WHERE status IN ($1, $2) ORDER BY created_at ASC`,
339
+ [ScheduledTaskStatus.PENDING, ScheduledTaskStatus.PAUSED]
340
+ );
341
+
342
+ return result.rows.map((row) => this.mapRowToTask(row));
343
+ }
344
+
345
+ /**
346
+ * Get tasks by type
347
+ */
348
+ async getTasksByType(taskType: string): Promise<ScheduledTaskDefinition[]> {
349
+ await this.ensureInitialized();
350
+
351
+ const result = await this.pool.query<ScheduledTaskRow>(
352
+ `SELECT * FROM lattice_scheduled_tasks WHERE task_type = $1 ORDER BY created_at DESC`,
353
+ [taskType]
354
+ );
355
+
356
+ return result.rows.map((row) => this.mapRowToTask(row));
357
+ }
358
+
359
+ /**
360
+ * Get tasks by status
361
+ */
362
+ async getTasksByStatus(
363
+ status: ScheduledTaskStatus
364
+ ): Promise<ScheduledTaskDefinition[]> {
365
+ await this.ensureInitialized();
366
+
367
+ const result = await this.pool.query<ScheduledTaskRow>(
368
+ `SELECT * FROM lattice_scheduled_tasks WHERE status = $1 ORDER BY created_at DESC`,
369
+ [status]
370
+ );
371
+
372
+ return result.rows.map((row) => this.mapRowToTask(row));
373
+ }
374
+
375
+ /**
376
+ * Get tasks by execution type
377
+ */
378
+ async getTasksByExecutionType(
379
+ executionType: ScheduleExecutionType
380
+ ): Promise<ScheduledTaskDefinition[]> {
381
+ await this.ensureInitialized();
382
+
383
+ const result = await this.pool.query<ScheduledTaskRow>(
384
+ `SELECT * FROM lattice_scheduled_tasks WHERE execution_type = $1 ORDER BY created_at DESC`,
385
+ [executionType]
386
+ );
387
+
388
+ return result.rows.map((row) => this.mapRowToTask(row));
389
+ }
390
+
391
+ /**
392
+ * Get tasks by assistant ID
393
+ */
394
+ async getTasksByAssistantId(
395
+ assistantId: string
396
+ ): Promise<ScheduledTaskDefinition[]> {
397
+ await this.ensureInitialized();
398
+
399
+ const result = await this.pool.query<ScheduledTaskRow>(
400
+ `SELECT * FROM lattice_scheduled_tasks WHERE assistant_id = $1 ORDER BY created_at DESC`,
401
+ [assistantId]
402
+ );
403
+
404
+ return result.rows.map((row) => this.mapRowToTask(row));
405
+ }
406
+
407
+ /**
408
+ * Get tasks by thread ID
409
+ */
410
+ async getTasksByThreadId(
411
+ threadId: string
412
+ ): Promise<ScheduledTaskDefinition[]> {
413
+ await this.ensureInitialized();
414
+
415
+ const result = await this.pool.query<ScheduledTaskRow>(
416
+ `SELECT * FROM lattice_scheduled_tasks WHERE thread_id = $1 ORDER BY created_at DESC`,
417
+ [threadId]
418
+ );
419
+
420
+ return result.rows.map((row) => this.mapRowToTask(row));
421
+ }
422
+
423
+ /**
424
+ * Get all tasks with optional filters
425
+ */
426
+ async getAllTasks(filters?: {
427
+ status?: ScheduledTaskStatus;
428
+ executionType?: ScheduleExecutionType;
429
+ taskType?: string;
430
+ assistantId?: string;
431
+ threadId?: string;
432
+ limit?: number;
433
+ offset?: number;
434
+ }): Promise<ScheduledTaskDefinition[]> {
435
+ await this.ensureInitialized();
436
+
437
+ const whereClauses: string[] = [];
438
+ const values: any[] = [];
439
+ let paramIndex = 1;
440
+
441
+ if (filters?.status !== undefined) {
442
+ whereClauses.push(`status = $${paramIndex++}`);
443
+ values.push(filters.status);
444
+ }
445
+ if (filters?.executionType !== undefined) {
446
+ whereClauses.push(`execution_type = $${paramIndex++}`);
447
+ values.push(filters.executionType);
448
+ }
449
+ if (filters?.taskType !== undefined) {
450
+ whereClauses.push(`task_type = $${paramIndex++}`);
451
+ values.push(filters.taskType);
452
+ }
453
+ if (filters?.assistantId !== undefined) {
454
+ whereClauses.push(`assistant_id = $${paramIndex++}`);
455
+ values.push(filters.assistantId);
456
+ }
457
+ if (filters?.threadId !== undefined) {
458
+ whereClauses.push(`thread_id = $${paramIndex++}`);
459
+ values.push(filters.threadId);
460
+ }
461
+
462
+ let query = `SELECT * FROM lattice_scheduled_tasks`;
463
+ if (whereClauses.length > 0) {
464
+ query += ` WHERE ${whereClauses.join(" AND ")}`;
465
+ }
466
+ query += ` ORDER BY created_at DESC`;
467
+
468
+ if (filters?.limit !== undefined) {
469
+ query += ` LIMIT $${paramIndex++}`;
470
+ values.push(filters.limit);
471
+ }
472
+ if (filters?.offset !== undefined) {
473
+ query += ` OFFSET $${paramIndex++}`;
474
+ values.push(filters.offset);
475
+ }
476
+
477
+ const result = await this.pool.query<ScheduledTaskRow>(query, values);
478
+
479
+ return result.rows.map((row) => this.mapRowToTask(row));
480
+ }
481
+
482
+ /**
483
+ * Count tasks with optional filters
484
+ */
485
+ async countTasks(filters?: {
486
+ status?: ScheduledTaskStatus;
487
+ executionType?: ScheduleExecutionType;
488
+ taskType?: string;
489
+ assistantId?: string;
490
+ threadId?: string;
491
+ }): Promise<number> {
492
+ await this.ensureInitialized();
493
+
494
+ const whereClauses: string[] = [];
495
+ const values: any[] = [];
496
+ let paramIndex = 1;
497
+
498
+ if (filters?.status !== undefined) {
499
+ whereClauses.push(`status = $${paramIndex++}`);
500
+ values.push(filters.status);
501
+ }
502
+ if (filters?.executionType !== undefined) {
503
+ whereClauses.push(`execution_type = $${paramIndex++}`);
504
+ values.push(filters.executionType);
505
+ }
506
+ if (filters?.taskType !== undefined) {
507
+ whereClauses.push(`task_type = $${paramIndex++}`);
508
+ values.push(filters.taskType);
509
+ }
510
+ if (filters?.assistantId !== undefined) {
511
+ whereClauses.push(`assistant_id = $${paramIndex++}`);
512
+ values.push(filters.assistantId);
513
+ }
514
+ if (filters?.threadId !== undefined) {
515
+ whereClauses.push(`thread_id = $${paramIndex++}`);
516
+ values.push(filters.threadId);
517
+ }
518
+
519
+ let query = `SELECT COUNT(*) as count FROM lattice_scheduled_tasks`;
520
+ if (whereClauses.length > 0) {
521
+ query += ` WHERE ${whereClauses.join(" AND ")}`;
522
+ }
523
+
524
+ const result = await this.pool.query<{ count: string }>(query, values);
525
+
526
+ return parseInt(result.rows[0].count, 10);
527
+ }
528
+
529
+ /**
530
+ * Delete completed/cancelled/failed tasks older than specified time
531
+ */
532
+ async deleteOldTasks(olderThanMs: number): Promise<number> {
533
+ await this.ensureInitialized();
534
+
535
+ const cutoff = new Date(Date.now() - olderThanMs);
536
+
537
+ const result = await this.pool.query(
538
+ `
539
+ DELETE FROM lattice_scheduled_tasks
540
+ WHERE status IN ($1, $2, $3)
541
+ AND updated_at < $4
542
+ `,
543
+ [
544
+ ScheduledTaskStatus.COMPLETED,
545
+ ScheduledTaskStatus.CANCELLED,
546
+ ScheduledTaskStatus.FAILED,
547
+ cutoff,
548
+ ]
549
+ );
550
+
551
+ return result.rowCount ?? 0;
552
+ }
553
+
554
+ /**
555
+ * Map database row to ScheduledTaskDefinition
556
+ */
557
+ private mapRowToTask(row: ScheduledTaskRow): ScheduledTaskDefinition {
558
+ return {
559
+ taskId: row.task_id,
560
+ taskType: row.task_type,
561
+ payload:
562
+ typeof row.payload === "string"
563
+ ? JSON.parse(row.payload)
564
+ : row.payload || {},
565
+ assistantId: row.assistant_id ?? undefined,
566
+ threadId: row.thread_id ?? undefined,
567
+ executionType: row.execution_type as ScheduleExecutionType,
568
+ executeAt: row.execute_at ? row.execute_at.getTime() : undefined,
569
+ delayMs: row.delay_ms ?? undefined,
570
+ cronExpression: row.cron_expression ?? undefined,
571
+ timezone: row.timezone ?? undefined,
572
+ nextRunAt: row.next_run_at ? row.next_run_at.getTime() : undefined,
573
+ lastRunAt: row.last_run_at ? row.last_run_at.getTime() : undefined,
574
+ status: row.status as ScheduledTaskStatus,
575
+ runCount: row.run_count,
576
+ maxRuns: row.max_runs ?? undefined,
577
+ retryCount: row.retry_count,
578
+ maxRetries: row.max_retries,
579
+ lastError: row.last_error ?? undefined,
580
+ createdAt: row.created_at.getTime(),
581
+ updatedAt: row.updated_at.getTime(),
582
+ expiresAt: row.expires_at ? row.expires_at.getTime() : undefined,
583
+ metadata:
584
+ row.metadata === null
585
+ ? undefined
586
+ : typeof row.metadata === "string"
587
+ ? JSON.parse(row.metadata)
588
+ : row.metadata,
589
+ };
590
+ }
591
+ }