duron 0.3.0-beta.10 → 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 +59 -0
- 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 +143 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +131 -0
- 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 +268 -17
- 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.js +11 -1
- 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 +224 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +311 -1
- package/dist/constants.js +6 -0
- package/dist/errors.d.ts +119 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +111 -0
- 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 +83 -0
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +237 -5
- package/dist/telemetry/adapter.d.ts +322 -0
- package/dist/telemetry/adapter.d.ts.map +1 -1
- package/dist/telemetry/adapter.js +145 -0
- package/dist/telemetry/index.js +1 -0
- package/dist/telemetry/local.d.ts +48 -0
- package/dist/telemetry/local.d.ts.map +1 -1
- package/dist/telemetry/local.js +102 -0
- package/dist/telemetry/noop.d.ts +10 -0
- package/dist/telemetry/noop.d.ts.map +1 -1
- package/dist/telemetry/noop.js +43 -0
- package/dist/telemetry/opentelemetry.d.ts +23 -0
- package/dist/telemetry/opentelemetry.d.ts.map +1 -1
- package/dist/telemetry/opentelemetry.js +39 -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/package.json +1 -1
- package/src/adapters/postgres/base.ts +40 -17
- package/src/adapters/schemas.ts +11 -1
|
@@ -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 (
|
|
@@ -624,6 +728,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
624
728
|
}
|
|
625
729
|
return result;
|
|
626
730
|
}
|
|
731
|
+
/**
|
|
732
|
+
* Internal method to mark a job step as completed.
|
|
733
|
+
*
|
|
734
|
+
* @returns Promise resolving to `true` if completed, `false` otherwise
|
|
735
|
+
*/
|
|
627
736
|
async _completeJobStep({ stepId, output }) {
|
|
628
737
|
const result = await this.db
|
|
629
738
|
.update(this.tables.jobStepsTable)
|
|
@@ -638,6 +747,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
638
747
|
.returning({ id: this.tables.jobStepsTable.id });
|
|
639
748
|
return result.length > 0;
|
|
640
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Internal method to mark a job step as failed.
|
|
752
|
+
*
|
|
753
|
+
* @returns Promise resolving to `true` if failed, `false` otherwise
|
|
754
|
+
*/
|
|
641
755
|
async _failJobStep({ stepId, error }) {
|
|
642
756
|
const result = await this.db
|
|
643
757
|
.update(this.tables.jobStepsTable)
|
|
@@ -652,6 +766,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
652
766
|
.returning({ id: this.tables.jobStepsTable.id });
|
|
653
767
|
return result.length > 0;
|
|
654
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* Internal method to delay a job step.
|
|
771
|
+
*
|
|
772
|
+
* @returns Promise resolving to `true` if delayed, `false` otherwise
|
|
773
|
+
*/
|
|
655
774
|
async _delayJobStep({ stepId, delayMs, error }) {
|
|
656
775
|
const jobStepsTable = this.tables.jobStepsTable;
|
|
657
776
|
const jobsTable = this.tables.jobsTable;
|
|
@@ -676,6 +795,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
676
795
|
.returning({ id: jobStepsTable.id });
|
|
677
796
|
return result.length > 0;
|
|
678
797
|
}
|
|
798
|
+
/**
|
|
799
|
+
* Internal method to cancel a job step.
|
|
800
|
+
*
|
|
801
|
+
* @returns Promise resolving to `true` if cancelled, `false` otherwise
|
|
802
|
+
*/
|
|
679
803
|
async _cancelJobStep({ stepId }) {
|
|
680
804
|
const result = await this.db
|
|
681
805
|
.update(this.tables.jobStepsTable)
|
|
@@ -689,30 +813,51 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
689
813
|
.returning({ id: this.tables.jobStepsTable.id });
|
|
690
814
|
return result.length > 0;
|
|
691
815
|
}
|
|
816
|
+
// ============================================================================
|
|
817
|
+
// Query Methods
|
|
818
|
+
// ============================================================================
|
|
819
|
+
/**
|
|
820
|
+
* Internal method to get a job by its ID. Does not include step information.
|
|
821
|
+
*/
|
|
692
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');
|
|
693
832
|
const [job] = await this.db
|
|
694
833
|
.select({
|
|
695
|
-
id:
|
|
696
|
-
actionName:
|
|
697
|
-
groupKey:
|
|
698
|
-
input:
|
|
699
|
-
output:
|
|
700
|
-
error:
|
|
701
|
-
status:
|
|
702
|
-
timeoutMs:
|
|
703
|
-
expiresAt:
|
|
704
|
-
startedAt:
|
|
705
|
-
finishedAt:
|
|
706
|
-
createdAt:
|
|
707
|
-
updatedAt:
|
|
708
|
-
concurrencyLimit:
|
|
709
|
-
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,
|
|
710
850
|
})
|
|
711
|
-
.from(
|
|
712
|
-
.where(eq(
|
|
851
|
+
.from(jobsTable)
|
|
852
|
+
.where(eq(jobsTable.id, jobId))
|
|
713
853
|
.limit(1);
|
|
714
854
|
return job ?? null;
|
|
715
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
|
+
*/
|
|
716
861
|
async _getJobSteps(options) {
|
|
717
862
|
const { jobId, search } = options;
|
|
718
863
|
const jobStepsTable = this.tables.jobStepsTable;
|
|
@@ -756,6 +901,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
756
901
|
}
|
|
757
902
|
const jobsTable = this.tables.jobsTable;
|
|
758
903
|
const fuzzySearch = filters.search?.trim();
|
|
904
|
+
// Build WHERE clause parts using postgres template literals
|
|
759
905
|
return and(filters.status
|
|
760
906
|
? inArray(jobsTable.status, Array.isArray(filters.status) ? filters.status : [filters.status])
|
|
761
907
|
: undefined, filters.actionName
|
|
@@ -788,6 +934,10 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
788
934
|
? this.#buildJsonbWhereConditions(filters.outputFilter, jobsTable.output)
|
|
789
935
|
: []));
|
|
790
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Internal method to get jobs with pagination, filtering, and sorting.
|
|
939
|
+
* Does not include step information or job output.
|
|
940
|
+
*/
|
|
791
941
|
async _getJobs(options) {
|
|
792
942
|
const jobsTable = this.tables.jobsTable;
|
|
793
943
|
const page = options?.page ?? 1;
|
|
@@ -796,6 +946,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
796
946
|
const sortInput = options?.sort ?? { field: 'startedAt', order: 'desc' };
|
|
797
947
|
const sorts = Array.isArray(sortInput) ? sortInput : [sortInput];
|
|
798
948
|
const where = this._buildJobsWhereClause(filters);
|
|
949
|
+
// Get total count
|
|
799
950
|
const total = await this.db.$count(jobsTable, where);
|
|
800
951
|
if (!total) {
|
|
801
952
|
return {
|
|
@@ -805,6 +956,14 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
805
956
|
pageSize,
|
|
806
957
|
};
|
|
807
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');
|
|
808
967
|
const sortFieldMap = {
|
|
809
968
|
createdAt: jobsTable.created_at,
|
|
810
969
|
startedAt: jobsTable.started_at,
|
|
@@ -812,6 +971,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
812
971
|
status: jobsTable.status,
|
|
813
972
|
actionName: jobsTable.action_name,
|
|
814
973
|
expiresAt: jobsTable.expires_at,
|
|
974
|
+
duration: durationMs,
|
|
815
975
|
};
|
|
816
976
|
const jobs = await this.db
|
|
817
977
|
.select({
|
|
@@ -830,6 +990,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
830
990
|
updatedAt: jobsTable.updated_at,
|
|
831
991
|
concurrencyLimit: jobsTable.concurrency_limit,
|
|
832
992
|
clientId: jobsTable.client_id,
|
|
993
|
+
durationMs,
|
|
833
994
|
})
|
|
834
995
|
.from(jobsTable)
|
|
835
996
|
.where(where)
|
|
@@ -853,6 +1014,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
853
1014
|
pageSize,
|
|
854
1015
|
};
|
|
855
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Internal method to get a step by its ID with all information.
|
|
1019
|
+
*/
|
|
856
1020
|
async _getJobStepById(stepId) {
|
|
857
1021
|
const [step] = await this.db
|
|
858
1022
|
.select({
|
|
@@ -880,6 +1044,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
880
1044
|
.limit(1);
|
|
881
1045
|
return step ?? null;
|
|
882
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Internal method to get job status and updatedAt timestamp.
|
|
1049
|
+
*/
|
|
883
1050
|
async _getJobStatus(jobId) {
|
|
884
1051
|
const [job] = await this.db
|
|
885
1052
|
.select({
|
|
@@ -891,6 +1058,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
891
1058
|
.limit(1);
|
|
892
1059
|
return job ?? null;
|
|
893
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Internal method to get job step status and updatedAt timestamp.
|
|
1063
|
+
*/
|
|
894
1064
|
async _getJobStepStatus(stepId) {
|
|
895
1065
|
const [step] = await this.db
|
|
896
1066
|
.select({
|
|
@@ -902,6 +1072,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
902
1072
|
.limit(1);
|
|
903
1073
|
return step ?? null;
|
|
904
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Internal method to get action statistics including counts and last job created date.
|
|
1077
|
+
*/
|
|
905
1078
|
async _getActions() {
|
|
906
1079
|
const actionStats = this.db.$with('action_stats').as(this.db
|
|
907
1080
|
.select({
|
|
@@ -933,6 +1106,12 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
933
1106
|
})),
|
|
934
1107
|
};
|
|
935
1108
|
}
|
|
1109
|
+
// ============================================================================
|
|
1110
|
+
// Metrics Methods
|
|
1111
|
+
// ============================================================================
|
|
1112
|
+
/**
|
|
1113
|
+
* Internal method to insert multiple metric records in a single batch.
|
|
1114
|
+
*/
|
|
936
1115
|
async _insertMetrics(metrics) {
|
|
937
1116
|
if (metrics.length === 0) {
|
|
938
1117
|
return 0;
|
|
@@ -951,10 +1130,15 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
951
1130
|
.returning({ id: this.tables.metricsTable.id });
|
|
952
1131
|
return result.length;
|
|
953
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Internal method to get metrics for a job or step.
|
|
1135
|
+
*/
|
|
954
1136
|
async _getMetrics(options) {
|
|
955
1137
|
const metricsTable = this.tables.metricsTable;
|
|
956
1138
|
const filters = options.filters ?? {};
|
|
1139
|
+
// Build WHERE clause
|
|
957
1140
|
const where = this._buildMetricsWhereClause(options.jobId, options.stepId, filters);
|
|
1141
|
+
// Build sort
|
|
958
1142
|
const sortInput = options.sort ?? { field: 'timestamp', order: 'desc' };
|
|
959
1143
|
const sortFieldMap = {
|
|
960
1144
|
name: metricsTable.name,
|
|
@@ -962,6 +1146,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
962
1146
|
timestamp: metricsTable.timestamp,
|
|
963
1147
|
createdAt: metricsTable.created_at,
|
|
964
1148
|
};
|
|
1149
|
+
// Get total count
|
|
965
1150
|
const total = await this.db.$count(metricsTable, where);
|
|
966
1151
|
if (!total) {
|
|
967
1152
|
return {
|
|
@@ -991,6 +1176,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
991
1176
|
total,
|
|
992
1177
|
};
|
|
993
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Internal method to delete all metrics for a job.
|
|
1181
|
+
*/
|
|
994
1182
|
async _deleteMetrics(options) {
|
|
995
1183
|
const result = await this.db
|
|
996
1184
|
.delete(this.tables.metricsTable)
|
|
@@ -998,6 +1186,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
998
1186
|
.returning({ id: this.tables.metricsTable.id });
|
|
999
1187
|
return result.length;
|
|
1000
1188
|
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Build WHERE clause for metrics queries.
|
|
1191
|
+
*/
|
|
1001
1192
|
_buildMetricsWhereClause(jobId, stepId, filters) {
|
|
1002
1193
|
const metricsTable = this.tables.metricsTable;
|
|
1003
1194
|
return and(jobId ? eq(metricsTable.job_id, jobId) : undefined, stepId ? eq(metricsTable.step_id, stepId) : undefined, filters?.name
|
|
@@ -1012,6 +1203,25 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1012
1203
|
? this.#buildJsonbWhereConditions(filters.attributesFilter, metricsTable.attributes)
|
|
1013
1204
|
: []));
|
|
1014
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
|
+
*/
|
|
1015
1225
|
#buildJsonbWhereConditions(filter, jsonbColumn) {
|
|
1016
1226
|
const conditions = [];
|
|
1017
1227
|
for (const [key, value] of Object.entries(filter)) {
|
|
@@ -1019,11 +1229,16 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1019
1229
|
if (parts.length === 0) {
|
|
1020
1230
|
continue;
|
|
1021
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)
|
|
1022
1235
|
let jsonbPath = sql `${jsonbColumn}`;
|
|
1023
1236
|
if (parts.length === 1) {
|
|
1237
|
+
// Single level: use ->> directly
|
|
1024
1238
|
jsonbPath = sql `${jsonbPath} ->> ${parts[0]}`;
|
|
1025
1239
|
}
|
|
1026
1240
|
else {
|
|
1241
|
+
// Nested: use -> for intermediate steps, ->> for final step
|
|
1027
1242
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
1028
1243
|
const part = parts[i];
|
|
1029
1244
|
if (part) {
|
|
@@ -1035,9 +1250,12 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1035
1250
|
jsonbPath = sql `${jsonbPath} ->> ${lastPart}`;
|
|
1036
1251
|
}
|
|
1037
1252
|
}
|
|
1253
|
+
// Handle array values - check if JSONB array contains at least one of the values
|
|
1038
1254
|
if (Array.isArray(value)) {
|
|
1255
|
+
// Build condition: check if any element in the JSONB array matches any value in the filter array
|
|
1039
1256
|
const arrayValueConditions = value.map((arrayValue) => {
|
|
1040
1257
|
const arrayValueStr = String(arrayValue);
|
|
1258
|
+
// Get the array from JSONB: input -> 'products'
|
|
1041
1259
|
let arrayPath = sql `${jsonbColumn}`;
|
|
1042
1260
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
1043
1261
|
const part = parts[i];
|
|
@@ -1049,6 +1267,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1049
1267
|
if (lastPart) {
|
|
1050
1268
|
arrayPath = sql `${arrayPath} -> ${lastPart}`;
|
|
1051
1269
|
}
|
|
1270
|
+
// Check if the JSONB array contains the value (case-insensitive for strings)
|
|
1052
1271
|
if (typeof arrayValue === 'string') {
|
|
1053
1272
|
return sql `EXISTS (
|
|
1054
1273
|
SELECT 1
|
|
@@ -1057,30 +1276,62 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1057
1276
|
)`;
|
|
1058
1277
|
}
|
|
1059
1278
|
else {
|
|
1279
|
+
// For non-string values, use exact containment
|
|
1060
1280
|
return sql `${arrayPath} @> ${sql.raw(JSON.stringify([arrayValue]))}::jsonb`;
|
|
1061
1281
|
}
|
|
1062
1282
|
});
|
|
1283
|
+
// Combine array conditions with OR (at least one must match)
|
|
1063
1284
|
if (arrayValueConditions.length > 0) {
|
|
1064
1285
|
conditions.push(arrayValueConditions.reduce((acc, condition, idx) => (idx === 0 ? condition : sql `${acc} OR ${condition}`)));
|
|
1065
1286
|
}
|
|
1066
1287
|
}
|
|
1067
1288
|
else if (typeof value === 'string') {
|
|
1289
|
+
// String values: use ILIKE for case-insensitive partial matching
|
|
1068
1290
|
conditions.push(sql `COALESCE(${jsonbPath}, '') ILIKE ${`%${value}%`}`);
|
|
1069
1291
|
}
|
|
1070
1292
|
else {
|
|
1293
|
+
// Non-string, non-array values: use exact match
|
|
1294
|
+
// Convert JSONB value to text for comparison
|
|
1071
1295
|
conditions.push(sql `${jsonbPath}::text = ${String(value)}`);
|
|
1072
1296
|
}
|
|
1073
1297
|
}
|
|
1074
1298
|
return conditions;
|
|
1075
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
|
+
*/
|
|
1076
1310
|
async _notify(_event, _data) {
|
|
1311
|
+
// do nothing
|
|
1077
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
|
+
*/
|
|
1078
1320
|
async _listen(_event, _callback) {
|
|
1321
|
+
// do nothing
|
|
1079
1322
|
return {
|
|
1080
1323
|
unlisten: () => {
|
|
1324
|
+
// do nothing
|
|
1081
1325
|
},
|
|
1082
1326
|
};
|
|
1083
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
|
+
*/
|
|
1084
1335
|
_map(result) {
|
|
1085
1336
|
return result;
|
|
1086
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"}
|