duron 0.3.0-beta.1 → 0.3.0-beta.11
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/dist/action-job.d.ts +31 -0
- package/dist/action-job.d.ts.map +1 -1
- package/dist/action-job.js +68 -7
- package/dist/action-manager.d.ts +42 -0
- package/dist/action-manager.d.ts.map +1 -1
- package/dist/action-manager.js +61 -0
- package/dist/action.d.ts +144 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +133 -2
- package/dist/adapters/adapter.d.ts +359 -0
- package/dist/adapters/adapter.d.ts.map +1 -1
- package/dist/adapters/adapter.js +208 -0
- package/dist/adapters/postgres/base.d.ts +166 -0
- package/dist/adapters/postgres/base.d.ts.map +1 -1
- package/dist/adapters/postgres/base.js +273 -19
- package/dist/adapters/postgres/pglite.d.ts +37 -0
- package/dist/adapters/postgres/pglite.d.ts.map +1 -1
- package/dist/adapters/postgres/pglite.js +38 -0
- package/dist/adapters/postgres/postgres.d.ts +35 -0
- package/dist/adapters/postgres/postgres.d.ts.map +1 -1
- package/dist/adapters/postgres/postgres.js +42 -0
- package/dist/adapters/postgres/schema.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.js +14 -2
- package/dist/adapters/schemas.d.ts +9 -0
- package/dist/adapters/schemas.d.ts.map +1 -1
- package/dist/adapters/schemas.js +73 -1
- package/dist/client.d.ts +249 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +413 -3
- package/dist/constants.js +6 -0
- package/dist/errors.d.ts +166 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +189 -19
- package/dist/server.d.ts +44 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +56 -0
- package/dist/step-manager.d.ts +84 -0
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +354 -14
- package/dist/telemetry/adapter.d.ts +344 -0
- package/dist/telemetry/adapter.d.ts.map +1 -1
- package/dist/telemetry/adapter.js +151 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +1 -0
- package/dist/telemetry/local.d.ts +50 -1
- package/dist/telemetry/local.d.ts.map +1 -1
- package/dist/telemetry/local.js +165 -0
- package/dist/telemetry/noop.d.ts +12 -1
- package/dist/telemetry/noop.d.ts.map +1 -1
- package/dist/telemetry/noop.js +70 -0
- package/dist/telemetry/opentelemetry.d.ts +25 -1
- package/dist/telemetry/opentelemetry.d.ts.map +1 -1
- package/dist/telemetry/opentelemetry.js +149 -0
- package/dist/utils/p-retry.d.ts +5 -0
- package/dist/utils/p-retry.d.ts.map +1 -1
- package/dist/utils/p-retry.js +8 -0
- package/dist/utils/wait-for-abort.d.ts +1 -0
- package/dist/utils/wait-for-abort.d.ts.map +1 -1
- package/dist/utils/wait-for-abort.js +1 -0
- package/migrations/postgres/{20251203223656_conscious_johnny_blaze → 20260119153838_flimsy_thor_girl}/migration.sql +29 -2
- package/migrations/postgres/{20260118202533_wealthy_mysterio → 20260119153838_flimsy_thor_girl}/snapshot.json +5 -5
- package/package.json +1 -1
- package/src/action-job.ts +14 -7
- package/src/action.ts +23 -13
- package/src/adapters/postgres/base.ts +45 -19
- package/src/adapters/postgres/schema.ts +5 -2
- package/src/adapters/schemas.ts +11 -1
- package/src/client.ts +187 -8
- package/src/errors.ts +141 -30
- package/src/step-manager.ts +171 -10
- package/src/telemetry/adapter.ts +174 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/local.ts +93 -0
- package/src/telemetry/noop.ts +46 -0
- package/src/telemetry/opentelemetry.ts +145 -2
- package/migrations/postgres/20251203223656_conscious_johnny_blaze/snapshot.json +0 -941
- package/migrations/postgres/20260117231749_clumsy_penance/migration.sql +0 -3
- package/migrations/postgres/20260117231749_clumsy_penance/snapshot.json +0 -988
- package/migrations/postgres/20260118202533_wealthy_mysterio/migration.sql +0 -24
|
@@ -8,6 +8,14 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
8
8
|
tables;
|
|
9
9
|
schema = 'duron';
|
|
10
10
|
migrateOnStart = true;
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Constructor
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Create a new PostgresAdapter instance.
|
|
16
|
+
*
|
|
17
|
+
* @param options - Configuration options for the PostgreSQL adapter
|
|
18
|
+
*/
|
|
11
19
|
constructor(options) {
|
|
12
20
|
super();
|
|
13
21
|
this.connection = options.connection;
|
|
@@ -16,9 +24,21 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
16
24
|
this.tables = createSchema(this.schema);
|
|
17
25
|
this._initDb();
|
|
18
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Initialize the database connection and Drizzle instance.
|
|
29
|
+
*/
|
|
19
30
|
_initDb() {
|
|
20
31
|
throw new Error('Not implemented');
|
|
21
32
|
}
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Lifecycle Methods
|
|
35
|
+
// ============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Start the adapter.
|
|
38
|
+
* Runs migrations if enabled and sets up database listeners.
|
|
39
|
+
*
|
|
40
|
+
* @returns Promise resolving to `true` if started successfully, `false` otherwise
|
|
41
|
+
*/
|
|
22
42
|
async _start() {
|
|
23
43
|
await this._listen(`ping-${this.id}`, async (payload) => {
|
|
24
44
|
const fromClientId = JSON.parse(payload).fromClientId;
|
|
@@ -38,7 +58,16 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
38
58
|
});
|
|
39
59
|
}
|
|
40
60
|
async _stop() {
|
|
61
|
+
// do nothing
|
|
41
62
|
}
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Job Methods
|
|
65
|
+
// ============================================================================
|
|
66
|
+
/**
|
|
67
|
+
* Internal method to create a new job in the database.
|
|
68
|
+
*
|
|
69
|
+
* @returns Promise resolving to the job ID, or `null` if creation failed
|
|
70
|
+
*/
|
|
42
71
|
async _createJob({ queue, groupKey, input, timeoutMs, checksum, concurrencyLimit }) {
|
|
43
72
|
const [result] = await this.db
|
|
44
73
|
.insert(this.tables.jobsTable)
|
|
@@ -57,6 +86,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
57
86
|
}
|
|
58
87
|
return result.id;
|
|
59
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Internal method to mark a job as completed.
|
|
91
|
+
*
|
|
92
|
+
* @returns Promise resolving to `true` if completed, `false` otherwise
|
|
93
|
+
*/
|
|
60
94
|
async _completeJob({ jobId, output }) {
|
|
61
95
|
const result = await this.db
|
|
62
96
|
.update(this.tables.jobsTable)
|
|
@@ -70,6 +104,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
70
104
|
.returning({ id: this.tables.jobsTable.id });
|
|
71
105
|
return result.length > 0;
|
|
72
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Internal method to mark a job as failed.
|
|
109
|
+
*
|
|
110
|
+
* @returns Promise resolving to `true` if failed, `false` otherwise
|
|
111
|
+
*/
|
|
73
112
|
async _failJob({ jobId, error }) {
|
|
74
113
|
const result = await this.db
|
|
75
114
|
.update(this.tables.jobsTable)
|
|
@@ -83,6 +122,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
83
122
|
.returning({ id: this.tables.jobsTable.id });
|
|
84
123
|
return result.length > 0;
|
|
85
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Internal method to cancel a job.
|
|
127
|
+
*
|
|
128
|
+
* @returns Promise resolving to `true` if cancelled, `false` otherwise
|
|
129
|
+
*/
|
|
86
130
|
async _cancelJob({ jobId }) {
|
|
87
131
|
const result = await this.db
|
|
88
132
|
.update(this.tables.jobsTable)
|
|
@@ -95,7 +139,14 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
95
139
|
.returning({ id: this.tables.jobsTable.id });
|
|
96
140
|
return result.length > 0;
|
|
97
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Internal method to retry a completed, cancelled, or failed job by creating a copy of it with status 'created' and cleared output/error.
|
|
144
|
+
* Uses SELECT FOR UPDATE to prevent concurrent retries from creating duplicate jobs.
|
|
145
|
+
*
|
|
146
|
+
* @returns Promise resolving to the job ID, or `null` if creation failed
|
|
147
|
+
*/
|
|
98
148
|
async _retryJob({ jobId }) {
|
|
149
|
+
// Use a single atomic query with FOR UPDATE lock to prevent race conditions
|
|
99
150
|
const result = this._map(await this.db.execute(sql `
|
|
100
151
|
WITH locked_source AS (
|
|
101
152
|
-- Lock the source job row to prevent concurrent retries
|
|
@@ -169,6 +220,25 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
169
220
|
}
|
|
170
221
|
return result[0].id;
|
|
171
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Internal method to time travel a job to restart from a specific step.
|
|
225
|
+
* The job must be in completed, failed, or cancelled status.
|
|
226
|
+
* Resets the job and ancestor steps to active status, deletes subsequent steps,
|
|
227
|
+
* and preserves completed parallel siblings.
|
|
228
|
+
*
|
|
229
|
+
* Algorithm:
|
|
230
|
+
* 1. Validate job is in terminal state (completed/failed/cancelled)
|
|
231
|
+
* 2. Find the target step and all its ancestors (using parent_step_id)
|
|
232
|
+
* 3. Determine which steps to keep:
|
|
233
|
+
* - Steps completed BEFORE the target step (by created_at)
|
|
234
|
+
* - Branch siblings that are completed (independent)
|
|
235
|
+
* 4. Delete steps that should not be kept
|
|
236
|
+
* 5. Reset ancestor steps to active status (they need to re-run)
|
|
237
|
+
* 6. Reset the target step to active status
|
|
238
|
+
* 7. Reset job to created status
|
|
239
|
+
*
|
|
240
|
+
* @returns Promise resolving to `true` if time travel succeeded, `false` otherwise
|
|
241
|
+
*/
|
|
172
242
|
async _timeTravelJob({ jobId, stepId }) {
|
|
173
243
|
const result = this._map(await this.db.execute(sql `
|
|
174
244
|
WITH RECURSIVE
|
|
@@ -336,16 +406,29 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
336
406
|
`));
|
|
337
407
|
return result.length > 0 && result[0].success === true;
|
|
338
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Internal method to delete a job by its ID.
|
|
411
|
+
* Active jobs cannot be deleted.
|
|
412
|
+
*
|
|
413
|
+
* @returns Promise resolving to `true` if deleted, `false` otherwise
|
|
414
|
+
*/
|
|
339
415
|
async _deleteJob({ jobId }) {
|
|
340
416
|
const result = await this.db
|
|
341
417
|
.delete(this.tables.jobsTable)
|
|
342
418
|
.where(and(eq(this.tables.jobsTable.id, jobId), ne(this.tables.jobsTable.status, JOB_STATUS_ACTIVE)))
|
|
343
419
|
.returning({ id: this.tables.jobsTable.id });
|
|
420
|
+
// Also delete associated steps
|
|
344
421
|
if (result.length > 0) {
|
|
345
422
|
await this.db.delete(this.tables.jobStepsTable).where(eq(this.tables.jobStepsTable.job_id, jobId));
|
|
346
423
|
}
|
|
347
424
|
return result.length > 0;
|
|
348
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Internal method to delete multiple jobs using the same filters as getJobs.
|
|
428
|
+
* Active jobs cannot be deleted and will be excluded from deletion.
|
|
429
|
+
*
|
|
430
|
+
* @returns Promise resolving to the number of jobs deleted
|
|
431
|
+
*/
|
|
349
432
|
async _deleteJobs(options) {
|
|
350
433
|
const jobsTable = this.tables.jobsTable;
|
|
351
434
|
const filters = options?.filters ?? {};
|
|
@@ -353,6 +436,13 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
353
436
|
const result = await this.db.delete(jobsTable).where(where).returning({ id: jobsTable.id });
|
|
354
437
|
return result.length;
|
|
355
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Internal method to fetch jobs from the database respecting concurrency limits per group.
|
|
441
|
+
* Uses the concurrency limit from the latest job created for each groupKey.
|
|
442
|
+
* Uses advisory locks to ensure thread-safe job fetching.
|
|
443
|
+
*
|
|
444
|
+
* @returns Promise resolving to an array of fetched jobs
|
|
445
|
+
*/
|
|
356
446
|
async _fetch({ batch }) {
|
|
357
447
|
const result = this._map(await this.db.execute(sql `
|
|
358
448
|
WITH group_concurrency AS (
|
|
@@ -464,6 +554,12 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
464
554
|
`));
|
|
465
555
|
return result;
|
|
466
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* Internal method to recover stuck jobs (jobs that were active but the process that owned them is no longer running).
|
|
559
|
+
* In multi-process mode, pings other processes to check if they're alive before recovering their jobs.
|
|
560
|
+
*
|
|
561
|
+
* @returns Promise resolving to the number of jobs recovered
|
|
562
|
+
*/
|
|
467
563
|
async _recoverJobs(options) {
|
|
468
564
|
const { checksums, multiProcessMode = false, processTimeout = 5_000 } = options;
|
|
469
565
|
const unresponsiveClientIds = [this.id];
|
|
@@ -527,6 +623,14 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
527
623
|
}
|
|
528
624
|
return 0;
|
|
529
625
|
}
|
|
626
|
+
// ============================================================================
|
|
627
|
+
// Step Methods
|
|
628
|
+
// ============================================================================
|
|
629
|
+
/**
|
|
630
|
+
* Internal method to create or recover a job step by creating or resetting a step record in the database.
|
|
631
|
+
*
|
|
632
|
+
* @returns Promise resolving to the step, or `null` if creation failed
|
|
633
|
+
*/
|
|
530
634
|
async _createOrRecoverJobStep({ jobId, name, timeoutMs, retriesLimit, parentStepId, parallel = false, }) {
|
|
531
635
|
const [result] = this._map(await this.db.execute(sql `
|
|
532
636
|
WITH job_check AS (
|
|
@@ -539,7 +643,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
539
643
|
step_existed AS (
|
|
540
644
|
SELECT EXISTS(
|
|
541
645
|
SELECT 1 FROM ${this.tables.jobStepsTable} s
|
|
542
|
-
WHERE s.job_id = ${jobId}
|
|
646
|
+
WHERE s.job_id = ${jobId}
|
|
647
|
+
AND s.name = ${name}
|
|
648
|
+
AND s.parent_step_id IS NOT DISTINCT FROM ${parentStepId}
|
|
543
649
|
) AS existed
|
|
544
650
|
),
|
|
545
651
|
upserted_step AS (
|
|
@@ -569,7 +675,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
569
675
|
0,
|
|
570
676
|
NULL
|
|
571
677
|
WHERE EXISTS (SELECT 1 FROM job_check)
|
|
572
|
-
ON CONFLICT (job_id, name) DO UPDATE
|
|
678
|
+
ON CONFLICT (job_id, name, parent_step_id) DO UPDATE
|
|
573
679
|
SET
|
|
574
680
|
timeout_ms = ${timeoutMs},
|
|
575
681
|
expires_at = now() + interval '${sql.raw(timeoutMs.toString())} milliseconds',
|
|
@@ -609,6 +715,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
609
715
|
INNER JOIN job_check jc ON s.job_id = jc.id
|
|
610
716
|
WHERE s.job_id = ${jobId}
|
|
611
717
|
AND s.name = ${name}
|
|
718
|
+
AND s.parent_step_id IS NOT DISTINCT FROM ${parentStepId}
|
|
612
719
|
AND NOT EXISTS (SELECT 1 FROM final_upserted)
|
|
613
720
|
)
|
|
614
721
|
SELECT * FROM final_upserted
|
|
@@ -621,6 +728,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
621
728
|
}
|
|
622
729
|
return result;
|
|
623
730
|
}
|
|
731
|
+
/**
|
|
732
|
+
* Internal method to mark a job step as completed.
|
|
733
|
+
*
|
|
734
|
+
* @returns Promise resolving to `true` if completed, `false` otherwise
|
|
735
|
+
*/
|
|
624
736
|
async _completeJobStep({ stepId, output }) {
|
|
625
737
|
const result = await this.db
|
|
626
738
|
.update(this.tables.jobStepsTable)
|
|
@@ -635,6 +747,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
635
747
|
.returning({ id: this.tables.jobStepsTable.id });
|
|
636
748
|
return result.length > 0;
|
|
637
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Internal method to mark a job step as failed.
|
|
752
|
+
*
|
|
753
|
+
* @returns Promise resolving to `true` if failed, `false` otherwise
|
|
754
|
+
*/
|
|
638
755
|
async _failJobStep({ stepId, error }) {
|
|
639
756
|
const result = await this.db
|
|
640
757
|
.update(this.tables.jobStepsTable)
|
|
@@ -649,6 +766,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
649
766
|
.returning({ id: this.tables.jobStepsTable.id });
|
|
650
767
|
return result.length > 0;
|
|
651
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* Internal method to delay a job step.
|
|
771
|
+
*
|
|
772
|
+
* @returns Promise resolving to `true` if delayed, `false` otherwise
|
|
773
|
+
*/
|
|
652
774
|
async _delayJobStep({ stepId, delayMs, error }) {
|
|
653
775
|
const jobStepsTable = this.tables.jobStepsTable;
|
|
654
776
|
const jobsTable = this.tables.jobsTable;
|
|
@@ -673,6 +795,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
673
795
|
.returning({ id: jobStepsTable.id });
|
|
674
796
|
return result.length > 0;
|
|
675
797
|
}
|
|
798
|
+
/**
|
|
799
|
+
* Internal method to cancel a job step.
|
|
800
|
+
*
|
|
801
|
+
* @returns Promise resolving to `true` if cancelled, `false` otherwise
|
|
802
|
+
*/
|
|
676
803
|
async _cancelJobStep({ stepId }) {
|
|
677
804
|
const result = await this.db
|
|
678
805
|
.update(this.tables.jobStepsTable)
|
|
@@ -686,30 +813,51 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
686
813
|
.returning({ id: this.tables.jobStepsTable.id });
|
|
687
814
|
return result.length > 0;
|
|
688
815
|
}
|
|
816
|
+
// ============================================================================
|
|
817
|
+
// Query Methods
|
|
818
|
+
// ============================================================================
|
|
819
|
+
/**
|
|
820
|
+
* Internal method to get a job by its ID. Does not include step information.
|
|
821
|
+
*/
|
|
689
822
|
async _getJobById(jobId) {
|
|
823
|
+
const jobsTable = this.tables.jobsTable;
|
|
824
|
+
// Calculate duration as a SQL expression (finishedAt - startedAt in milliseconds)
|
|
825
|
+
const durationMs = sql `
|
|
826
|
+
CASE
|
|
827
|
+
WHEN ${jobsTable.started_at} IS NOT NULL AND ${jobsTable.finished_at} IS NOT NULL
|
|
828
|
+
THEN EXTRACT(EPOCH FROM (${jobsTable.finished_at} - ${jobsTable.started_at})) * 1000
|
|
829
|
+
ELSE NULL
|
|
830
|
+
END
|
|
831
|
+
`.as('duration_ms');
|
|
690
832
|
const [job] = await this.db
|
|
691
833
|
.select({
|
|
692
|
-
id:
|
|
693
|
-
actionName:
|
|
694
|
-
groupKey:
|
|
695
|
-
input:
|
|
696
|
-
output:
|
|
697
|
-
error:
|
|
698
|
-
status:
|
|
699
|
-
timeoutMs:
|
|
700
|
-
expiresAt:
|
|
701
|
-
startedAt:
|
|
702
|
-
finishedAt:
|
|
703
|
-
createdAt:
|
|
704
|
-
updatedAt:
|
|
705
|
-
concurrencyLimit:
|
|
706
|
-
clientId:
|
|
834
|
+
id: jobsTable.id,
|
|
835
|
+
actionName: jobsTable.action_name,
|
|
836
|
+
groupKey: jobsTable.group_key,
|
|
837
|
+
input: jobsTable.input,
|
|
838
|
+
output: jobsTable.output,
|
|
839
|
+
error: jobsTable.error,
|
|
840
|
+
status: jobsTable.status,
|
|
841
|
+
timeoutMs: jobsTable.timeout_ms,
|
|
842
|
+
expiresAt: jobsTable.expires_at,
|
|
843
|
+
startedAt: jobsTable.started_at,
|
|
844
|
+
finishedAt: jobsTable.finished_at,
|
|
845
|
+
createdAt: jobsTable.created_at,
|
|
846
|
+
updatedAt: jobsTable.updated_at,
|
|
847
|
+
concurrencyLimit: jobsTable.concurrency_limit,
|
|
848
|
+
clientId: jobsTable.client_id,
|
|
849
|
+
durationMs,
|
|
707
850
|
})
|
|
708
|
-
.from(
|
|
709
|
-
.where(eq(
|
|
851
|
+
.from(jobsTable)
|
|
852
|
+
.where(eq(jobsTable.id, jobId))
|
|
710
853
|
.limit(1);
|
|
711
854
|
return job ?? null;
|
|
712
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* Internal method to get all steps for a job with optional fuzzy search.
|
|
858
|
+
* Steps are always ordered by created_at ASC.
|
|
859
|
+
* Steps do not include output data.
|
|
860
|
+
*/
|
|
713
861
|
async _getJobSteps(options) {
|
|
714
862
|
const { jobId, search } = options;
|
|
715
863
|
const jobStepsTable = this.tables.jobStepsTable;
|
|
@@ -753,6 +901,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
753
901
|
}
|
|
754
902
|
const jobsTable = this.tables.jobsTable;
|
|
755
903
|
const fuzzySearch = filters.search?.trim();
|
|
904
|
+
// Build WHERE clause parts using postgres template literals
|
|
756
905
|
return and(filters.status
|
|
757
906
|
? inArray(jobsTable.status, Array.isArray(filters.status) ? filters.status : [filters.status])
|
|
758
907
|
: undefined, filters.actionName
|
|
@@ -785,6 +934,10 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
785
934
|
? this.#buildJsonbWhereConditions(filters.outputFilter, jobsTable.output)
|
|
786
935
|
: []));
|
|
787
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Internal method to get jobs with pagination, filtering, and sorting.
|
|
939
|
+
* Does not include step information or job output.
|
|
940
|
+
*/
|
|
788
941
|
async _getJobs(options) {
|
|
789
942
|
const jobsTable = this.tables.jobsTable;
|
|
790
943
|
const page = options?.page ?? 1;
|
|
@@ -793,6 +946,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
793
946
|
const sortInput = options?.sort ?? { field: 'startedAt', order: 'desc' };
|
|
794
947
|
const sorts = Array.isArray(sortInput) ? sortInput : [sortInput];
|
|
795
948
|
const where = this._buildJobsWhereClause(filters);
|
|
949
|
+
// Get total count
|
|
796
950
|
const total = await this.db.$count(jobsTable, where);
|
|
797
951
|
if (!total) {
|
|
798
952
|
return {
|
|
@@ -802,6 +956,14 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
802
956
|
pageSize,
|
|
803
957
|
};
|
|
804
958
|
}
|
|
959
|
+
// Calculate duration as a SQL expression (finishedAt - startedAt in milliseconds)
|
|
960
|
+
const durationMs = sql `
|
|
961
|
+
CASE
|
|
962
|
+
WHEN ${jobsTable.started_at} IS NOT NULL AND ${jobsTable.finished_at} IS NOT NULL
|
|
963
|
+
THEN EXTRACT(EPOCH FROM (${jobsTable.finished_at} - ${jobsTable.started_at})) * 1000
|
|
964
|
+
ELSE NULL
|
|
965
|
+
END
|
|
966
|
+
`.as('duration_ms');
|
|
805
967
|
const sortFieldMap = {
|
|
806
968
|
createdAt: jobsTable.created_at,
|
|
807
969
|
startedAt: jobsTable.started_at,
|
|
@@ -809,6 +971,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
809
971
|
status: jobsTable.status,
|
|
810
972
|
actionName: jobsTable.action_name,
|
|
811
973
|
expiresAt: jobsTable.expires_at,
|
|
974
|
+
duration: durationMs,
|
|
812
975
|
};
|
|
813
976
|
const jobs = await this.db
|
|
814
977
|
.select({
|
|
@@ -827,6 +990,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
827
990
|
updatedAt: jobsTable.updated_at,
|
|
828
991
|
concurrencyLimit: jobsTable.concurrency_limit,
|
|
829
992
|
clientId: jobsTable.client_id,
|
|
993
|
+
durationMs,
|
|
830
994
|
})
|
|
831
995
|
.from(jobsTable)
|
|
832
996
|
.where(where)
|
|
@@ -850,6 +1014,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
850
1014
|
pageSize,
|
|
851
1015
|
};
|
|
852
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Internal method to get a step by its ID with all information.
|
|
1019
|
+
*/
|
|
853
1020
|
async _getJobStepById(stepId) {
|
|
854
1021
|
const [step] = await this.db
|
|
855
1022
|
.select({
|
|
@@ -877,6 +1044,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
877
1044
|
.limit(1);
|
|
878
1045
|
return step ?? null;
|
|
879
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Internal method to get job status and updatedAt timestamp.
|
|
1049
|
+
*/
|
|
880
1050
|
async _getJobStatus(jobId) {
|
|
881
1051
|
const [job] = await this.db
|
|
882
1052
|
.select({
|
|
@@ -888,6 +1058,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
888
1058
|
.limit(1);
|
|
889
1059
|
return job ?? null;
|
|
890
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Internal method to get job step status and updatedAt timestamp.
|
|
1063
|
+
*/
|
|
891
1064
|
async _getJobStepStatus(stepId) {
|
|
892
1065
|
const [step] = await this.db
|
|
893
1066
|
.select({
|
|
@@ -899,6 +1072,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
899
1072
|
.limit(1);
|
|
900
1073
|
return step ?? null;
|
|
901
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Internal method to get action statistics including counts and last job created date.
|
|
1077
|
+
*/
|
|
902
1078
|
async _getActions() {
|
|
903
1079
|
const actionStats = this.db.$with('action_stats').as(this.db
|
|
904
1080
|
.select({
|
|
@@ -930,6 +1106,12 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
930
1106
|
})),
|
|
931
1107
|
};
|
|
932
1108
|
}
|
|
1109
|
+
// ============================================================================
|
|
1110
|
+
// Metrics Methods
|
|
1111
|
+
// ============================================================================
|
|
1112
|
+
/**
|
|
1113
|
+
* Internal method to insert multiple metric records in a single batch.
|
|
1114
|
+
*/
|
|
933
1115
|
async _insertMetrics(metrics) {
|
|
934
1116
|
if (metrics.length === 0) {
|
|
935
1117
|
return 0;
|
|
@@ -948,10 +1130,15 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
948
1130
|
.returning({ id: this.tables.metricsTable.id });
|
|
949
1131
|
return result.length;
|
|
950
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Internal method to get metrics for a job or step.
|
|
1135
|
+
*/
|
|
951
1136
|
async _getMetrics(options) {
|
|
952
1137
|
const metricsTable = this.tables.metricsTable;
|
|
953
1138
|
const filters = options.filters ?? {};
|
|
1139
|
+
// Build WHERE clause
|
|
954
1140
|
const where = this._buildMetricsWhereClause(options.jobId, options.stepId, filters);
|
|
1141
|
+
// Build sort
|
|
955
1142
|
const sortInput = options.sort ?? { field: 'timestamp', order: 'desc' };
|
|
956
1143
|
const sortFieldMap = {
|
|
957
1144
|
name: metricsTable.name,
|
|
@@ -959,6 +1146,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
959
1146
|
timestamp: metricsTable.timestamp,
|
|
960
1147
|
createdAt: metricsTable.created_at,
|
|
961
1148
|
};
|
|
1149
|
+
// Get total count
|
|
962
1150
|
const total = await this.db.$count(metricsTable, where);
|
|
963
1151
|
if (!total) {
|
|
964
1152
|
return {
|
|
@@ -988,6 +1176,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
988
1176
|
total,
|
|
989
1177
|
};
|
|
990
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Internal method to delete all metrics for a job.
|
|
1181
|
+
*/
|
|
991
1182
|
async _deleteMetrics(options) {
|
|
992
1183
|
const result = await this.db
|
|
993
1184
|
.delete(this.tables.metricsTable)
|
|
@@ -995,6 +1186,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
995
1186
|
.returning({ id: this.tables.metricsTable.id });
|
|
996
1187
|
return result.length;
|
|
997
1188
|
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Build WHERE clause for metrics queries.
|
|
1191
|
+
*/
|
|
998
1192
|
_buildMetricsWhereClause(jobId, stepId, filters) {
|
|
999
1193
|
const metricsTable = this.tables.metricsTable;
|
|
1000
1194
|
return and(jobId ? eq(metricsTable.job_id, jobId) : undefined, stepId ? eq(metricsTable.step_id, stepId) : undefined, filters?.name
|
|
@@ -1009,6 +1203,25 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1009
1203
|
? this.#buildJsonbWhereConditions(filters.attributesFilter, metricsTable.attributes)
|
|
1010
1204
|
: []));
|
|
1011
1205
|
}
|
|
1206
|
+
// ============================================================================
|
|
1207
|
+
// Private Methods
|
|
1208
|
+
// ============================================================================
|
|
1209
|
+
/**
|
|
1210
|
+
* Build WHERE conditions for JSONB filter using individual property checks.
|
|
1211
|
+
* Each property becomes a separate condition using ->> operator and ILIKE for case-insensitive matching.
|
|
1212
|
+
* Supports nested properties via dot notation and arrays.
|
|
1213
|
+
*
|
|
1214
|
+
* Example:
|
|
1215
|
+
* { "email": "tincho@gmail", "address.name": "nicolas", "products": ["chicle"] }
|
|
1216
|
+
* Generates:
|
|
1217
|
+
* input ->> 'email' ILIKE '%tincho@gmail%'
|
|
1218
|
+
* AND input ->> 'address' ->> 'name' ILIKE '%nicolas%'
|
|
1219
|
+
* AND EXISTS (SELECT 1 FROM jsonb_array_elements_text(input -> 'products') AS elem WHERE LOWER(elem) ILIKE LOWER('%chicle%'))
|
|
1220
|
+
*
|
|
1221
|
+
* @param filter - Flat record with dot-notation keys (e.g., { "email": "test", "address.name": "value", "products": ["chicle"] })
|
|
1222
|
+
* @param jsonbColumn - The JSONB column name
|
|
1223
|
+
* @returns Array of SQL conditions
|
|
1224
|
+
*/
|
|
1012
1225
|
#buildJsonbWhereConditions(filter, jsonbColumn) {
|
|
1013
1226
|
const conditions = [];
|
|
1014
1227
|
for (const [key, value] of Object.entries(filter)) {
|
|
@@ -1016,11 +1229,16 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1016
1229
|
if (parts.length === 0) {
|
|
1017
1230
|
continue;
|
|
1018
1231
|
}
|
|
1232
|
+
// Build the JSONB path expression step by step
|
|
1233
|
+
// For "address.name": input -> 'address' ->> 'name' (-> for intermediate, ->> for final)
|
|
1234
|
+
// For "email": input ->> 'email' (->> for single level)
|
|
1019
1235
|
let jsonbPath = sql `${jsonbColumn}`;
|
|
1020
1236
|
if (parts.length === 1) {
|
|
1237
|
+
// Single level: use ->> directly
|
|
1021
1238
|
jsonbPath = sql `${jsonbPath} ->> ${parts[0]}`;
|
|
1022
1239
|
}
|
|
1023
1240
|
else {
|
|
1241
|
+
// Nested: use -> for intermediate steps, ->> for final step
|
|
1024
1242
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
1025
1243
|
const part = parts[i];
|
|
1026
1244
|
if (part) {
|
|
@@ -1032,9 +1250,12 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1032
1250
|
jsonbPath = sql `${jsonbPath} ->> ${lastPart}`;
|
|
1033
1251
|
}
|
|
1034
1252
|
}
|
|
1253
|
+
// Handle array values - check if JSONB array contains at least one of the values
|
|
1035
1254
|
if (Array.isArray(value)) {
|
|
1255
|
+
// Build condition: check if any element in the JSONB array matches any value in the filter array
|
|
1036
1256
|
const arrayValueConditions = value.map((arrayValue) => {
|
|
1037
1257
|
const arrayValueStr = String(arrayValue);
|
|
1258
|
+
// Get the array from JSONB: input -> 'products'
|
|
1038
1259
|
let arrayPath = sql `${jsonbColumn}`;
|
|
1039
1260
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
1040
1261
|
const part = parts[i];
|
|
@@ -1046,6 +1267,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1046
1267
|
if (lastPart) {
|
|
1047
1268
|
arrayPath = sql `${arrayPath} -> ${lastPart}`;
|
|
1048
1269
|
}
|
|
1270
|
+
// Check if the JSONB array contains the value (case-insensitive for strings)
|
|
1049
1271
|
if (typeof arrayValue === 'string') {
|
|
1050
1272
|
return sql `EXISTS (
|
|
1051
1273
|
SELECT 1
|
|
@@ -1054,30 +1276,62 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1054
1276
|
)`;
|
|
1055
1277
|
}
|
|
1056
1278
|
else {
|
|
1279
|
+
// For non-string values, use exact containment
|
|
1057
1280
|
return sql `${arrayPath} @> ${sql.raw(JSON.stringify([arrayValue]))}::jsonb`;
|
|
1058
1281
|
}
|
|
1059
1282
|
});
|
|
1283
|
+
// Combine array conditions with OR (at least one must match)
|
|
1060
1284
|
if (arrayValueConditions.length > 0) {
|
|
1061
1285
|
conditions.push(arrayValueConditions.reduce((acc, condition, idx) => (idx === 0 ? condition : sql `${acc} OR ${condition}`)));
|
|
1062
1286
|
}
|
|
1063
1287
|
}
|
|
1064
1288
|
else if (typeof value === 'string') {
|
|
1289
|
+
// String values: use ILIKE for case-insensitive partial matching
|
|
1065
1290
|
conditions.push(sql `COALESCE(${jsonbPath}, '') ILIKE ${`%${value}%`}`);
|
|
1066
1291
|
}
|
|
1067
1292
|
else {
|
|
1293
|
+
// Non-string, non-array values: use exact match
|
|
1294
|
+
// Convert JSONB value to text for comparison
|
|
1068
1295
|
conditions.push(sql `${jsonbPath}::text = ${String(value)}`);
|
|
1069
1296
|
}
|
|
1070
1297
|
}
|
|
1071
1298
|
return conditions;
|
|
1072
1299
|
}
|
|
1300
|
+
// ============================================================================
|
|
1301
|
+
// Protected Methods
|
|
1302
|
+
// ============================================================================
|
|
1303
|
+
/**
|
|
1304
|
+
* Send a PostgreSQL notification.
|
|
1305
|
+
*
|
|
1306
|
+
* @param event - The event name
|
|
1307
|
+
* @param data - The data to send
|
|
1308
|
+
* @returns Promise resolving to `void`
|
|
1309
|
+
*/
|
|
1073
1310
|
async _notify(_event, _data) {
|
|
1311
|
+
// do nothing
|
|
1074
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Listen for PostgreSQL notifications.
|
|
1315
|
+
*
|
|
1316
|
+
* @param event - The event name to listen for
|
|
1317
|
+
* @param callback - Callback function to handle notifications
|
|
1318
|
+
* @returns Promise resolving to an object with an `unlisten` function
|
|
1319
|
+
*/
|
|
1075
1320
|
async _listen(_event, _callback) {
|
|
1321
|
+
// do nothing
|
|
1076
1322
|
return {
|
|
1077
1323
|
unlisten: () => {
|
|
1324
|
+
// do nothing
|
|
1078
1325
|
},
|
|
1079
1326
|
};
|
|
1080
1327
|
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Map database query results to the expected format.
|
|
1330
|
+
* Can be overridden by subclasses to handle different result formats.
|
|
1331
|
+
*
|
|
1332
|
+
* @param result - The raw database query result
|
|
1333
|
+
* @returns The mapped result
|
|
1334
|
+
*/
|
|
1081
1335
|
_map(result) {
|
|
1082
1336
|
return result;
|
|
1083
1337
|
}
|
|
@@ -3,12 +3,49 @@ import { type AdapterOptions, PostgresBaseAdapter } from './base.js';
|
|
|
3
3
|
import type createSchema from './schema.js';
|
|
4
4
|
type Schema = ReturnType<typeof createSchema>;
|
|
5
5
|
export type DB = ReturnType<typeof drizzle<Schema>>;
|
|
6
|
+
/**
|
|
7
|
+
* PGLite adapter implementation for Duron.
|
|
8
|
+
* Extends PostgresAdapter to work with PGLite (in-memory PostgreSQL).
|
|
9
|
+
*
|
|
10
|
+
* @template Options - The adapter options type
|
|
11
|
+
*/
|
|
6
12
|
export declare class PGLiteAdapter extends PostgresBaseAdapter<DB, string | undefined> {
|
|
13
|
+
/**
|
|
14
|
+
* Start the adapter.
|
|
15
|
+
* Runs migrations if enabled and sets up database listeners.
|
|
16
|
+
*
|
|
17
|
+
* @returns Promise resolving to `true` if started successfully, `false` otherwise
|
|
18
|
+
*/
|
|
7
19
|
protected _start(): Promise<void>;
|
|
8
20
|
protected _stop(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Map database query results to the expected format.
|
|
23
|
+
* PGLite returns results in a `rows` property, so we extract that.
|
|
24
|
+
*
|
|
25
|
+
* @param result - The raw database query result
|
|
26
|
+
* @returns The mapped result (result.rows)
|
|
27
|
+
*/
|
|
9
28
|
protected _map(result: any): any;
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the PGLite database connection.
|
|
31
|
+
* Creates a new Drizzle instance without connection options.
|
|
32
|
+
*/
|
|
10
33
|
protected _initDb(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Send a PGLite notification.
|
|
36
|
+
*
|
|
37
|
+
* @param event - The event name
|
|
38
|
+
* @param data - The data to send
|
|
39
|
+
* @returns Promise resolving to `void`
|
|
40
|
+
*/
|
|
11
41
|
protected _notify(event: string, data: any): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Listen for PGLite notifications.
|
|
44
|
+
*
|
|
45
|
+
* @param event - The event name to listen for
|
|
46
|
+
* @param callback - Callback function to handle notifications
|
|
47
|
+
* @returns Promise resolving to an object with an `unlisten` function
|
|
48
|
+
*/
|
|
12
49
|
protected _listen(event: string, callback: (payload: string) => void): Promise<{
|
|
13
50
|
unlisten: () => void;
|
|
14
51
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pglite.d.ts","sourceRoot":"","sources":["../../../src/adapters/postgres/pglite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AAG5C,OAAO,EAAE,KAAK,cAAc,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AACpE,OAAO,KAAK,YAAY,MAAM,aAAa,CAAA;AAE3C,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAA;AAE7C,MAAM,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"pglite.d.ts","sourceRoot":"","sources":["../../../src/adapters/postgres/pglite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AAG5C,OAAO,EAAE,KAAK,cAAc,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AACpE,OAAO,KAAK,YAAY,MAAM,aAAa,CAAA;AAE3C,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAA;AAE7C,MAAM,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;AAEnD;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5E;;;;;OAKG;cACsB,MAAM;cAYN,KAAK;IAI9B;;;;;;OAMG;cACgB,IAAI,CAAC,MAAM,EAAE,GAAG;IAInC;;;OAGG;cACgB,OAAO;IAa1B;;;;;;OAMG;cACsB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzE;;;;;;OAMG;cACsB,OAAO,CAC9B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAClC,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CASrC;AAED,eAAO,MAAM,aAAa,GAAI,SAAS,cAAc,CAAC,MAAM,GAAG,SAAS,CAAC,kBAExE,CAAA"}
|