duron 0.3.0-beta.9 → 0.3.0
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 +33 -2
- package/dist/action-job.d.ts.map +1 -1
- package/dist/action-job.js +88 -23
- package/dist/action-manager.d.ts +44 -2
- package/dist/action-manager.d.ts.map +1 -1
- package/dist/action-manager.js +64 -3
- package/dist/action.d.ts +388 -7
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +44 -23
- package/dist/adapters/adapter.d.ts +365 -8
- package/dist/adapters/adapter.d.ts.map +1 -1
- package/dist/adapters/adapter.js +221 -15
- package/dist/adapters/postgres/base.d.ts +184 -6
- package/dist/adapters/postgres/base.d.ts.map +1 -1
- package/dist/adapters/postgres/base.js +436 -75
- 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 +150 -37
- package/dist/adapters/postgres/schema.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.d.ts +151 -38
- package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.js +2 -2
- package/dist/adapters/postgres/schema.js +60 -23
- package/dist/adapters/schemas.d.ts +124 -80
- package/dist/adapters/schemas.d.ts.map +1 -1
- package/dist/adapters/schemas.js +139 -26
- package/dist/client.d.ts +426 -22
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +370 -20
- package/dist/constants.js +6 -0
- package/dist/errors.d.ts +140 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +152 -9
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/server.d.ts +99 -37
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +84 -25
- package/dist/step-manager.d.ts +111 -4
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +403 -75
- package/dist/telemetry/index.d.ts +1 -4
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +2 -4
- package/dist/telemetry/local-span-exporter.d.ts +56 -0
- package/dist/telemetry/local-span-exporter.d.ts.map +1 -0
- package/dist/telemetry/local-span-exporter.js +118 -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/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/migration.sql +32 -20
- package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/snapshot.json +241 -66
- package/package.json +42 -26
- package/src/action-job.ts +33 -29
- package/src/action-manager.ts +5 -5
- package/src/action.ts +317 -149
- package/src/adapters/adapter.ts +54 -54
- package/src/adapters/postgres/base.ts +266 -86
- package/src/adapters/postgres/schema.default.ts +2 -2
- package/src/adapters/postgres/schema.ts +52 -24
- package/src/adapters/schemas.ts +91 -36
- package/src/client.ts +322 -68
- package/src/errors.ts +84 -12
- package/src/index.ts +2 -0
- package/src/server.ts +39 -37
- package/src/step-manager.ts +246 -95
- package/src/telemetry/index.ts +2 -20
- package/src/telemetry/local-span-exporter.ts +148 -0
- package/dist/telemetry/adapter.d.ts +0 -107
- package/dist/telemetry/adapter.d.ts.map +0 -1
- package/dist/telemetry/adapter.js +0 -134
- package/dist/telemetry/local.d.ts +0 -22
- package/dist/telemetry/local.d.ts.map +0 -1
- package/dist/telemetry/local.js +0 -243
- package/dist/telemetry/noop.d.ts +0 -17
- package/dist/telemetry/noop.d.ts.map +0 -1
- package/dist/telemetry/noop.js +0 -66
- package/dist/telemetry/opentelemetry.d.ts +0 -25
- package/dist/telemetry/opentelemetry.d.ts.map +0 -1
- package/dist/telemetry/opentelemetry.js +0 -312
- package/src/telemetry/adapter.ts +0 -642
- package/src/telemetry/local.ts +0 -429
- package/src/telemetry/noop.ts +0 -141
- package/src/telemetry/opentelemetry.ts +0 -453
|
@@ -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,18 +58,29 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
38
58
|
});
|
|
39
59
|
}
|
|
40
60
|
async _stop() {
|
|
61
|
+
// do nothing
|
|
41
62
|
}
|
|
42
|
-
|
|
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
|
+
*/
|
|
71
|
+
async _createJob({ queue, groupKey, input, timeoutMs, checksum, concurrencyLimit, concurrencyStepLimit, description, }) {
|
|
43
72
|
const [result] = await this.db
|
|
44
73
|
.insert(this.tables.jobsTable)
|
|
45
74
|
.values({
|
|
46
75
|
action_name: queue,
|
|
47
76
|
group_key: groupKey,
|
|
77
|
+
description: description ?? null,
|
|
48
78
|
checksum,
|
|
49
79
|
input,
|
|
50
80
|
status: JOB_STATUS_CREATED,
|
|
51
81
|
timeout_ms: timeoutMs,
|
|
52
82
|
concurrency_limit: concurrencyLimit,
|
|
83
|
+
concurrency_step_limit: concurrencyStepLimit,
|
|
53
84
|
})
|
|
54
85
|
.returning({ id: this.tables.jobsTable.id });
|
|
55
86
|
if (!result) {
|
|
@@ -57,6 +88,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
57
88
|
}
|
|
58
89
|
return result.id;
|
|
59
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Internal method to mark a job as completed.
|
|
93
|
+
*
|
|
94
|
+
* @returns Promise resolving to `true` if completed, `false` otherwise
|
|
95
|
+
*/
|
|
60
96
|
async _completeJob({ jobId, output }) {
|
|
61
97
|
const result = await this.db
|
|
62
98
|
.update(this.tables.jobsTable)
|
|
@@ -70,6 +106,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
70
106
|
.returning({ id: this.tables.jobsTable.id });
|
|
71
107
|
return result.length > 0;
|
|
72
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Internal method to mark a job as failed.
|
|
111
|
+
*
|
|
112
|
+
* @returns Promise resolving to `true` if failed, `false` otherwise
|
|
113
|
+
*/
|
|
73
114
|
async _failJob({ jobId, error }) {
|
|
74
115
|
const result = await this.db
|
|
75
116
|
.update(this.tables.jobsTable)
|
|
@@ -83,6 +124,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
83
124
|
.returning({ id: this.tables.jobsTable.id });
|
|
84
125
|
return result.length > 0;
|
|
85
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Internal method to cancel a job.
|
|
129
|
+
*
|
|
130
|
+
* @returns Promise resolving to `true` if cancelled, `false` otherwise
|
|
131
|
+
*/
|
|
86
132
|
async _cancelJob({ jobId }) {
|
|
87
133
|
const result = await this.db
|
|
88
134
|
.update(this.tables.jobsTable)
|
|
@@ -95,18 +141,27 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
95
141
|
.returning({ id: this.tables.jobsTable.id });
|
|
96
142
|
return result.length > 0;
|
|
97
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Internal method to retry a completed, cancelled, or failed job by creating a copy of it with status 'created' and cleared output/error.
|
|
146
|
+
* Uses SELECT FOR UPDATE to prevent concurrent retries from creating duplicate jobs.
|
|
147
|
+
*
|
|
148
|
+
* @returns Promise resolving to the job ID, or `null` if creation failed
|
|
149
|
+
*/
|
|
98
150
|
async _retryJob({ jobId }) {
|
|
151
|
+
// Use a single atomic query with FOR UPDATE lock to prevent race conditions
|
|
99
152
|
const result = this._map(await this.db.execute(sql `
|
|
100
153
|
WITH locked_source AS (
|
|
101
154
|
-- Lock the source job row to prevent concurrent retries
|
|
102
155
|
SELECT
|
|
103
156
|
j.action_name,
|
|
104
157
|
j.group_key,
|
|
158
|
+
j.description,
|
|
105
159
|
j.checksum,
|
|
106
160
|
j.input,
|
|
107
161
|
j.timeout_ms,
|
|
108
162
|
j.created_at,
|
|
109
|
-
j.concurrency_limit
|
|
163
|
+
j.concurrency_limit,
|
|
164
|
+
j.concurrency_step_limit
|
|
110
165
|
FROM ${this.tables.jobsTable} j
|
|
111
166
|
WHERE j.id = ${jobId}
|
|
112
167
|
AND j.status IN (${JOB_STATUS_COMPLETED}, ${JOB_STATUS_CANCELLED}, ${JOB_STATUS_FAILED})
|
|
@@ -131,15 +186,18 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
131
186
|
INSERT INTO ${this.tables.jobsTable} (
|
|
132
187
|
action_name,
|
|
133
188
|
group_key,
|
|
189
|
+
description,
|
|
134
190
|
checksum,
|
|
135
191
|
input,
|
|
136
192
|
status,
|
|
137
193
|
timeout_ms,
|
|
138
|
-
concurrency_limit
|
|
194
|
+
concurrency_limit,
|
|
195
|
+
concurrency_step_limit
|
|
139
196
|
)
|
|
140
197
|
SELECT
|
|
141
198
|
ls.action_name,
|
|
142
199
|
ls.group_key,
|
|
200
|
+
ls.description,
|
|
143
201
|
ls.checksum,
|
|
144
202
|
ls.input,
|
|
145
203
|
${JOB_STATUS_CREATED},
|
|
@@ -155,7 +213,8 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
155
213
|
LIMIT 1
|
|
156
214
|
),
|
|
157
215
|
ls.concurrency_limit
|
|
158
|
-
)
|
|
216
|
+
),
|
|
217
|
+
ls.concurrency_step_limit
|
|
159
218
|
FROM locked_source ls
|
|
160
219
|
WHERE NOT EXISTS (SELECT 1 FROM existing_retry)
|
|
161
220
|
RETURNING id
|
|
@@ -169,6 +228,25 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
169
228
|
}
|
|
170
229
|
return result[0].id;
|
|
171
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Internal method to time travel a job to restart from a specific step.
|
|
233
|
+
* The job must be in completed, failed, or cancelled status.
|
|
234
|
+
* Resets the job and ancestor steps to active status, deletes subsequent steps,
|
|
235
|
+
* and preserves completed parallel siblings.
|
|
236
|
+
*
|
|
237
|
+
* Algorithm:
|
|
238
|
+
* 1. Validate job is in terminal state (completed/failed/cancelled)
|
|
239
|
+
* 2. Find the target step and all its ancestors (using parent_step_id)
|
|
240
|
+
* 3. Determine which steps to keep:
|
|
241
|
+
* - Steps completed BEFORE the target step (by created_at)
|
|
242
|
+
* - Branch siblings that are completed (independent)
|
|
243
|
+
* 4. Delete steps that should not be kept
|
|
244
|
+
* 5. Reset ancestor steps to active status (they need to re-run)
|
|
245
|
+
* 6. Reset the target step to active status
|
|
246
|
+
* 7. Reset job to created status
|
|
247
|
+
*
|
|
248
|
+
* @returns Promise resolving to `true` if time travel succeeded, `false` otherwise
|
|
249
|
+
*/
|
|
172
250
|
async _timeTravelJob({ jobId, stepId }) {
|
|
173
251
|
const result = this._map(await this.db.execute(sql `
|
|
174
252
|
WITH RECURSIVE
|
|
@@ -336,16 +414,29 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
336
414
|
`));
|
|
337
415
|
return result.length > 0 && result[0].success === true;
|
|
338
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Internal method to delete a job by its ID.
|
|
419
|
+
* Active jobs cannot be deleted.
|
|
420
|
+
*
|
|
421
|
+
* @returns Promise resolving to `true` if deleted, `false` otherwise
|
|
422
|
+
*/
|
|
339
423
|
async _deleteJob({ jobId }) {
|
|
340
424
|
const result = await this.db
|
|
341
425
|
.delete(this.tables.jobsTable)
|
|
342
426
|
.where(and(eq(this.tables.jobsTable.id, jobId), ne(this.tables.jobsTable.status, JOB_STATUS_ACTIVE)))
|
|
343
427
|
.returning({ id: this.tables.jobsTable.id });
|
|
428
|
+
// Also delete associated steps
|
|
344
429
|
if (result.length > 0) {
|
|
345
430
|
await this.db.delete(this.tables.jobStepsTable).where(eq(this.tables.jobStepsTable.job_id, jobId));
|
|
346
431
|
}
|
|
347
432
|
return result.length > 0;
|
|
348
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Internal method to delete multiple jobs using the same filters as getJobs.
|
|
436
|
+
* Active jobs cannot be deleted and will be excluded from deletion.
|
|
437
|
+
*
|
|
438
|
+
* @returns Promise resolving to the number of jobs deleted
|
|
439
|
+
*/
|
|
349
440
|
async _deleteJobs(options) {
|
|
350
441
|
const jobsTable = this.tables.jobsTable;
|
|
351
442
|
const filters = options?.filters ?? {};
|
|
@@ -353,6 +444,13 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
353
444
|
const result = await this.db.delete(jobsTable).where(where).returning({ id: jobsTable.id });
|
|
354
445
|
return result.length;
|
|
355
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Internal method to fetch jobs from the database respecting concurrency limits per group.
|
|
449
|
+
* Uses the concurrency limit from the latest job created for each groupKey.
|
|
450
|
+
* Uses advisory locks to ensure thread-safe job fetching.
|
|
451
|
+
*
|
|
452
|
+
* @returns Promise resolving to an array of fetched jobs
|
|
453
|
+
*/
|
|
356
454
|
async _fetch({ batch }) {
|
|
357
455
|
const result = this._map(await this.db.execute(sql `
|
|
358
456
|
WITH group_concurrency AS (
|
|
@@ -450,6 +548,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
450
548
|
j.id,
|
|
451
549
|
j.action_name as "actionName",
|
|
452
550
|
j.group_key as "groupKey",
|
|
551
|
+
j.description,
|
|
453
552
|
j.input,
|
|
454
553
|
j.output,
|
|
455
554
|
j.error,
|
|
@@ -460,10 +559,17 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
460
559
|
j.finished_at as "finishedAt",
|
|
461
560
|
j.created_at as "createdAt",
|
|
462
561
|
j.updated_at as "updatedAt",
|
|
463
|
-
j.concurrency_limit as "concurrencyLimit"
|
|
562
|
+
j.concurrency_limit as "concurrencyLimit",
|
|
563
|
+
j.concurrency_step_limit as "concurrencyStepLimit"
|
|
464
564
|
`));
|
|
465
565
|
return result;
|
|
466
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* Internal method to recover stuck jobs (jobs that were active but the process that owned them is no longer running).
|
|
569
|
+
* In multi-process mode, pings other processes to check if they're alive before recovering their jobs.
|
|
570
|
+
*
|
|
571
|
+
* @returns Promise resolving to the number of jobs recovered
|
|
572
|
+
*/
|
|
467
573
|
async _recoverJobs(options) {
|
|
468
574
|
const { checksums, multiProcessMode = false, processTimeout = 5_000 } = options;
|
|
469
575
|
const unresponsiveClientIds = [this.id];
|
|
@@ -527,6 +633,14 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
527
633
|
}
|
|
528
634
|
return 0;
|
|
529
635
|
}
|
|
636
|
+
// ============================================================================
|
|
637
|
+
// Step Methods
|
|
638
|
+
// ============================================================================
|
|
639
|
+
/**
|
|
640
|
+
* Internal method to create or recover a job step by creating or resetting a step record in the database.
|
|
641
|
+
*
|
|
642
|
+
* @returns Promise resolving to the step, or `null` if creation failed
|
|
643
|
+
*/
|
|
530
644
|
async _createOrRecoverJobStep({ jobId, name, timeoutMs, retriesLimit, parentStepId, parallel = false, }) {
|
|
531
645
|
const [result] = this._map(await this.db.execute(sql `
|
|
532
646
|
WITH job_check AS (
|
|
@@ -539,7 +653,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
539
653
|
step_existed AS (
|
|
540
654
|
SELECT EXISTS(
|
|
541
655
|
SELECT 1 FROM ${this.tables.jobStepsTable} s
|
|
542
|
-
WHERE s.job_id = ${jobId}
|
|
656
|
+
WHERE s.job_id = ${jobId}
|
|
543
657
|
AND s.name = ${name}
|
|
544
658
|
AND s.parent_step_id IS NOT DISTINCT FROM ${parentStepId}
|
|
545
659
|
) AS existed
|
|
@@ -624,6 +738,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
624
738
|
}
|
|
625
739
|
return result;
|
|
626
740
|
}
|
|
741
|
+
/**
|
|
742
|
+
* Internal method to mark a job step as completed.
|
|
743
|
+
*
|
|
744
|
+
* @returns Promise resolving to `true` if completed, `false` otherwise
|
|
745
|
+
*/
|
|
627
746
|
async _completeJobStep({ stepId, output }) {
|
|
628
747
|
const result = await this.db
|
|
629
748
|
.update(this.tables.jobStepsTable)
|
|
@@ -638,6 +757,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
638
757
|
.returning({ id: this.tables.jobStepsTable.id });
|
|
639
758
|
return result.length > 0;
|
|
640
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* Internal method to mark a job step as failed.
|
|
762
|
+
*
|
|
763
|
+
* @returns Promise resolving to `true` if failed, `false` otherwise
|
|
764
|
+
*/
|
|
641
765
|
async _failJobStep({ stepId, error }) {
|
|
642
766
|
const result = await this.db
|
|
643
767
|
.update(this.tables.jobStepsTable)
|
|
@@ -652,6 +776,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
652
776
|
.returning({ id: this.tables.jobStepsTable.id });
|
|
653
777
|
return result.length > 0;
|
|
654
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* Internal method to delay a job step.
|
|
781
|
+
*
|
|
782
|
+
* @returns Promise resolving to `true` if delayed, `false` otherwise
|
|
783
|
+
*/
|
|
655
784
|
async _delayJobStep({ stepId, delayMs, error }) {
|
|
656
785
|
const jobStepsTable = this.tables.jobStepsTable;
|
|
657
786
|
const jobsTable = this.tables.jobsTable;
|
|
@@ -676,6 +805,11 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
676
805
|
.returning({ id: jobStepsTable.id });
|
|
677
806
|
return result.length > 0;
|
|
678
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* Internal method to cancel a job step.
|
|
810
|
+
*
|
|
811
|
+
* @returns Promise resolving to `true` if cancelled, `false` otherwise
|
|
812
|
+
*/
|
|
679
813
|
async _cancelJobStep({ stepId }) {
|
|
680
814
|
const result = await this.db
|
|
681
815
|
.update(this.tables.jobStepsTable)
|
|
@@ -689,30 +823,53 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
689
823
|
.returning({ id: this.tables.jobStepsTable.id });
|
|
690
824
|
return result.length > 0;
|
|
691
825
|
}
|
|
826
|
+
// ============================================================================
|
|
827
|
+
// Query Methods
|
|
828
|
+
// ============================================================================
|
|
829
|
+
/**
|
|
830
|
+
* Internal method to get a job by its ID. Does not include step information.
|
|
831
|
+
*/
|
|
692
832
|
async _getJobById(jobId) {
|
|
833
|
+
const jobsTable = this.tables.jobsTable;
|
|
834
|
+
// Calculate duration as a SQL expression (finishedAt - startedAt in milliseconds)
|
|
835
|
+
const durationMs = sql `
|
|
836
|
+
CASE
|
|
837
|
+
WHEN ${jobsTable.started_at} IS NOT NULL AND ${jobsTable.finished_at} IS NOT NULL
|
|
838
|
+
THEN EXTRACT(EPOCH FROM (${jobsTable.finished_at} - ${jobsTable.started_at})) * 1000
|
|
839
|
+
ELSE NULL
|
|
840
|
+
END
|
|
841
|
+
`.as('duration_ms');
|
|
693
842
|
const [job] = await this.db
|
|
694
843
|
.select({
|
|
695
|
-
id:
|
|
696
|
-
actionName:
|
|
697
|
-
groupKey:
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
844
|
+
id: jobsTable.id,
|
|
845
|
+
actionName: jobsTable.action_name,
|
|
846
|
+
groupKey: jobsTable.group_key,
|
|
847
|
+
description: jobsTable.description,
|
|
848
|
+
input: jobsTable.input,
|
|
849
|
+
output: jobsTable.output,
|
|
850
|
+
error: jobsTable.error,
|
|
851
|
+
status: jobsTable.status,
|
|
852
|
+
timeoutMs: jobsTable.timeout_ms,
|
|
853
|
+
expiresAt: jobsTable.expires_at,
|
|
854
|
+
startedAt: jobsTable.started_at,
|
|
855
|
+
finishedAt: jobsTable.finished_at,
|
|
856
|
+
createdAt: jobsTable.created_at,
|
|
857
|
+
updatedAt: jobsTable.updated_at,
|
|
858
|
+
concurrencyLimit: jobsTable.concurrency_limit,
|
|
859
|
+
concurrencyStepLimit: jobsTable.concurrency_step_limit,
|
|
860
|
+
clientId: jobsTable.client_id,
|
|
861
|
+
durationMs,
|
|
710
862
|
})
|
|
711
|
-
.from(
|
|
712
|
-
.where(eq(
|
|
863
|
+
.from(jobsTable)
|
|
864
|
+
.where(eq(jobsTable.id, jobId))
|
|
713
865
|
.limit(1);
|
|
714
866
|
return job ?? null;
|
|
715
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Internal method to get all steps for a job with optional fuzzy search.
|
|
870
|
+
* Steps are always ordered by created_at ASC.
|
|
871
|
+
* Steps do not include output data.
|
|
872
|
+
*/
|
|
716
873
|
async _getJobSteps(options) {
|
|
717
874
|
const { jobId, search } = options;
|
|
718
875
|
const jobStepsTable = this.tables.jobStepsTable;
|
|
@@ -756,6 +913,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
756
913
|
}
|
|
757
914
|
const jobsTable = this.tables.jobsTable;
|
|
758
915
|
const fuzzySearch = filters.search?.trim();
|
|
916
|
+
// Build WHERE clause parts using postgres template literals
|
|
759
917
|
return and(filters.status
|
|
760
918
|
? inArray(jobsTable.status, Array.isArray(filters.status) ? filters.status : [filters.status])
|
|
761
919
|
: undefined, filters.actionName
|
|
@@ -766,7 +924,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
766
924
|
? ilike(jobsTable.group_key, `%${filters.groupKey}%`)
|
|
767
925
|
: undefined, filters.clientId
|
|
768
926
|
? inArray(jobsTable.client_id, Array.isArray(filters.clientId) ? filters.clientId : [filters.clientId])
|
|
769
|
-
: undefined, filters.createdAt && Array.isArray(filters.createdAt)
|
|
927
|
+
: undefined, filters.description ? ilike(jobsTable.description, `%${filters.description}%`) : undefined, filters.createdAt && Array.isArray(filters.createdAt)
|
|
770
928
|
? between(sql `date_trunc('second', ${jobsTable.created_at})`, filters.createdAt[0].toISOString(), filters.createdAt[1].toISOString())
|
|
771
929
|
: undefined, filters.createdAt && !Array.isArray(filters.createdAt)
|
|
772
930
|
? gte(sql `date_trunc('second', ${jobsTable.created_at})`, filters.createdAt.toISOString())
|
|
@@ -781,13 +939,17 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
781
939
|
: undefined, filters.updatedAfter
|
|
782
940
|
? sql `date_trunc('milliseconds', ${jobsTable.updated_at}) > ${filters.updatedAfter.toISOString()}::timestamptz`
|
|
783
941
|
: undefined, fuzzySearch && fuzzySearch.length > 0
|
|
784
|
-
? or(ilike(jobsTable.action_name, `%${fuzzySearch}%`), ilike(jobsTable.group_key, `%${fuzzySearch}%`), ilike(jobsTable.client_id, `%${fuzzySearch}%`), sql `${jobsTable.id}::text ilike ${`%${fuzzySearch}%`}`, sql `to_tsvector('english', ${jobsTable.input}::text) @@ plainto_tsquery('english', ${fuzzySearch})`, sql `to_tsvector('english', ${jobsTable.output}::text) @@ plainto_tsquery('english', ${fuzzySearch})`)
|
|
942
|
+
? or(ilike(jobsTable.action_name, `%${fuzzySearch}%`), ilike(jobsTable.group_key, `%${fuzzySearch}%`), ilike(jobsTable.description, `%${fuzzySearch}%`), ilike(jobsTable.client_id, `%${fuzzySearch}%`), sql `${jobsTable.id}::text ilike ${`%${fuzzySearch}%`}`, sql `to_tsvector('english', ${jobsTable.input}::text) @@ plainto_tsquery('english', ${fuzzySearch})`, sql `to_tsvector('english', ${jobsTable.output}::text) @@ plainto_tsquery('english', ${fuzzySearch})`)
|
|
785
943
|
: undefined, ...(filters.inputFilter && Object.keys(filters.inputFilter).length > 0
|
|
786
944
|
? this.#buildJsonbWhereConditions(filters.inputFilter, jobsTable.input)
|
|
787
945
|
: []), ...(filters.outputFilter && Object.keys(filters.outputFilter).length > 0
|
|
788
946
|
? this.#buildJsonbWhereConditions(filters.outputFilter, jobsTable.output)
|
|
789
947
|
: []));
|
|
790
948
|
}
|
|
949
|
+
/**
|
|
950
|
+
* Internal method to get jobs with pagination, filtering, and sorting.
|
|
951
|
+
* Does not include step information or job output.
|
|
952
|
+
*/
|
|
791
953
|
async _getJobs(options) {
|
|
792
954
|
const jobsTable = this.tables.jobsTable;
|
|
793
955
|
const page = options?.page ?? 1;
|
|
@@ -796,6 +958,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
796
958
|
const sortInput = options?.sort ?? { field: 'startedAt', order: 'desc' };
|
|
797
959
|
const sorts = Array.isArray(sortInput) ? sortInput : [sortInput];
|
|
798
960
|
const where = this._buildJobsWhereClause(filters);
|
|
961
|
+
// Get total count
|
|
799
962
|
const total = await this.db.$count(jobsTable, where);
|
|
800
963
|
if (!total) {
|
|
801
964
|
return {
|
|
@@ -805,6 +968,14 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
805
968
|
pageSize,
|
|
806
969
|
};
|
|
807
970
|
}
|
|
971
|
+
// Calculate duration as a SQL expression (finishedAt - startedAt in milliseconds)
|
|
972
|
+
const durationMs = sql `
|
|
973
|
+
CASE
|
|
974
|
+
WHEN ${jobsTable.started_at} IS NOT NULL AND ${jobsTable.finished_at} IS NOT NULL
|
|
975
|
+
THEN EXTRACT(EPOCH FROM (${jobsTable.finished_at} - ${jobsTable.started_at})) * 1000
|
|
976
|
+
ELSE NULL
|
|
977
|
+
END
|
|
978
|
+
`.as('duration_ms');
|
|
808
979
|
const sortFieldMap = {
|
|
809
980
|
createdAt: jobsTable.created_at,
|
|
810
981
|
startedAt: jobsTable.started_at,
|
|
@@ -812,12 +983,15 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
812
983
|
status: jobsTable.status,
|
|
813
984
|
actionName: jobsTable.action_name,
|
|
814
985
|
expiresAt: jobsTable.expires_at,
|
|
986
|
+
duration: durationMs,
|
|
987
|
+
description: jobsTable.description,
|
|
815
988
|
};
|
|
816
989
|
const jobs = await this.db
|
|
817
990
|
.select({
|
|
818
991
|
id: jobsTable.id,
|
|
819
992
|
actionName: jobsTable.action_name,
|
|
820
993
|
groupKey: jobsTable.group_key,
|
|
994
|
+
description: jobsTable.description,
|
|
821
995
|
input: jobsTable.input,
|
|
822
996
|
output: jobsTable.output,
|
|
823
997
|
error: jobsTable.error,
|
|
@@ -829,7 +1003,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
829
1003
|
createdAt: jobsTable.created_at,
|
|
830
1004
|
updatedAt: jobsTable.updated_at,
|
|
831
1005
|
concurrencyLimit: jobsTable.concurrency_limit,
|
|
1006
|
+
concurrencyStepLimit: jobsTable.concurrency_step_limit,
|
|
832
1007
|
clientId: jobsTable.client_id,
|
|
1008
|
+
durationMs,
|
|
833
1009
|
})
|
|
834
1010
|
.from(jobsTable)
|
|
835
1011
|
.where(where)
|
|
@@ -853,6 +1029,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
853
1029
|
pageSize,
|
|
854
1030
|
};
|
|
855
1031
|
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Internal method to get a step by its ID with all information.
|
|
1034
|
+
*/
|
|
856
1035
|
async _getJobStepById(stepId) {
|
|
857
1036
|
const [step] = await this.db
|
|
858
1037
|
.select({
|
|
@@ -880,6 +1059,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
880
1059
|
.limit(1);
|
|
881
1060
|
return step ?? null;
|
|
882
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Internal method to get job status and updatedAt timestamp.
|
|
1064
|
+
*/
|
|
883
1065
|
async _getJobStatus(jobId) {
|
|
884
1066
|
const [job] = await this.db
|
|
885
1067
|
.select({
|
|
@@ -891,6 +1073,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
891
1073
|
.limit(1);
|
|
892
1074
|
return job ?? null;
|
|
893
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Internal method to get job step status and updatedAt timestamp.
|
|
1078
|
+
*/
|
|
894
1079
|
async _getJobStepStatus(stepId) {
|
|
895
1080
|
const [step] = await this.db
|
|
896
1081
|
.select({
|
|
@@ -902,6 +1087,9 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
902
1087
|
.limit(1);
|
|
903
1088
|
return step ?? null;
|
|
904
1089
|
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Internal method to get action statistics including counts and last job created date.
|
|
1092
|
+
*/
|
|
905
1093
|
async _getActions() {
|
|
906
1094
|
const actionStats = this.db.$with('action_stats').as(this.db
|
|
907
1095
|
.select({
|
|
@@ -933,85 +1121,217 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
933
1121
|
})),
|
|
934
1122
|
};
|
|
935
1123
|
}
|
|
936
|
-
|
|
937
|
-
|
|
1124
|
+
// ============================================================================
|
|
1125
|
+
// Metrics Methods
|
|
1126
|
+
// ============================================================================
|
|
1127
|
+
/**
|
|
1128
|
+
* Internal method to insert multiple span records in a single batch.
|
|
1129
|
+
*/
|
|
1130
|
+
async _insertSpans(spans) {
|
|
1131
|
+
if (spans.length === 0) {
|
|
938
1132
|
return 0;
|
|
939
1133
|
}
|
|
940
|
-
const values =
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1134
|
+
const values = spans.map((s) => ({
|
|
1135
|
+
trace_id: s.traceId,
|
|
1136
|
+
span_id: s.spanId,
|
|
1137
|
+
parent_span_id: s.parentSpanId,
|
|
1138
|
+
job_id: s.jobId,
|
|
1139
|
+
step_id: s.stepId,
|
|
1140
|
+
name: s.name,
|
|
1141
|
+
kind: s.kind,
|
|
1142
|
+
start_time_unix_nano: s.startTimeUnixNano,
|
|
1143
|
+
end_time_unix_nano: s.endTimeUnixNano,
|
|
1144
|
+
status_code: s.statusCode,
|
|
1145
|
+
status_message: s.statusMessage,
|
|
1146
|
+
attributes: s.attributes ?? {},
|
|
1147
|
+
events: s.events ?? [],
|
|
947
1148
|
}));
|
|
948
1149
|
const result = await this.db
|
|
949
|
-
.insert(this.tables.
|
|
1150
|
+
.insert(this.tables.spansTable)
|
|
950
1151
|
.values(values)
|
|
951
|
-
.returning({ id: this.tables.
|
|
1152
|
+
.returning({ id: this.tables.spansTable.id });
|
|
952
1153
|
return result.length;
|
|
953
1154
|
}
|
|
954
|
-
|
|
955
|
-
|
|
1155
|
+
/**
|
|
1156
|
+
* Internal method to get spans for a job or step.
|
|
1157
|
+
* For step queries, uses a recursive CTE to find all descendant spans.
|
|
1158
|
+
*/
|
|
1159
|
+
async _getSpans(options) {
|
|
1160
|
+
const spansTable = this.tables.spansTable;
|
|
956
1161
|
const filters = options.filters ?? {};
|
|
957
|
-
|
|
958
|
-
const sortInput = options.sort ?? { field: '
|
|
1162
|
+
// Build sort
|
|
1163
|
+
const sortInput = options.sort ?? { field: 'startTimeUnixNano', order: 'asc' };
|
|
959
1164
|
const sortFieldMap = {
|
|
960
|
-
name:
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
createdAt: metricsTable.created_at,
|
|
1165
|
+
name: 'name',
|
|
1166
|
+
startTimeUnixNano: 'start_time_unix_nano',
|
|
1167
|
+
endTimeUnixNano: 'end_time_unix_nano',
|
|
964
1168
|
};
|
|
965
|
-
const
|
|
1169
|
+
const sortField = sortFieldMap[sortInput.field];
|
|
1170
|
+
const sortOrder = sortInput.order === 'asc' ? 'ASC' : 'DESC';
|
|
1171
|
+
// For step queries, use a recursive CTE to get descendant spans
|
|
1172
|
+
if (options.stepId) {
|
|
1173
|
+
return this._getStepSpansRecursive(options.stepId, sortField, sortOrder, filters);
|
|
1174
|
+
}
|
|
1175
|
+
// Build WHERE clause for job queries
|
|
1176
|
+
const where = this._buildSpansWhereClause(options.jobId, undefined, filters);
|
|
1177
|
+
// Get total count
|
|
1178
|
+
const total = await this.db.$count(spansTable, where);
|
|
966
1179
|
if (!total) {
|
|
967
1180
|
return {
|
|
968
|
-
|
|
1181
|
+
spans: [],
|
|
969
1182
|
total: 0,
|
|
970
1183
|
};
|
|
971
1184
|
}
|
|
972
|
-
const
|
|
973
|
-
const orderByClause = sortInput.order === 'asc'
|
|
974
|
-
|
|
1185
|
+
const sortFieldColumn = sortFieldMap[sortInput.field];
|
|
1186
|
+
const orderByClause = sortInput.order === 'asc'
|
|
1187
|
+
? asc(spansTable[sortFieldColumn])
|
|
1188
|
+
: desc(spansTable[sortFieldColumn]);
|
|
1189
|
+
const rows = await this.db
|
|
975
1190
|
.select({
|
|
976
|
-
id:
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1191
|
+
id: spansTable.id,
|
|
1192
|
+
traceId: spansTable.trace_id,
|
|
1193
|
+
spanId: spansTable.span_id,
|
|
1194
|
+
parentSpanId: spansTable.parent_span_id,
|
|
1195
|
+
jobId: spansTable.job_id,
|
|
1196
|
+
stepId: spansTable.step_id,
|
|
1197
|
+
name: spansTable.name,
|
|
1198
|
+
kind: spansTable.kind,
|
|
1199
|
+
startTimeUnixNano: spansTable.start_time_unix_nano,
|
|
1200
|
+
endTimeUnixNano: spansTable.end_time_unix_nano,
|
|
1201
|
+
statusCode: spansTable.status_code,
|
|
1202
|
+
statusMessage: spansTable.status_message,
|
|
1203
|
+
attributes: spansTable.attributes,
|
|
1204
|
+
events: spansTable.events,
|
|
985
1205
|
})
|
|
986
|
-
.from(
|
|
1206
|
+
.from(spansTable)
|
|
987
1207
|
.where(where)
|
|
988
1208
|
.orderBy(orderByClause);
|
|
1209
|
+
// Cast kind and statusCode to proper types, convert BigInt to string for JSON serialization
|
|
1210
|
+
const spans = rows.map((row) => ({
|
|
1211
|
+
...row,
|
|
1212
|
+
kind: row.kind,
|
|
1213
|
+
statusCode: row.statusCode,
|
|
1214
|
+
// Convert BigInt to string for JSON serialization
|
|
1215
|
+
startTimeUnixNano: row.startTimeUnixNano?.toString() ?? null,
|
|
1216
|
+
endTimeUnixNano: row.endTimeUnixNano?.toString() ?? null,
|
|
1217
|
+
}));
|
|
989
1218
|
return {
|
|
990
|
-
|
|
1219
|
+
spans,
|
|
991
1220
|
total,
|
|
992
1221
|
};
|
|
993
1222
|
}
|
|
994
|
-
|
|
1223
|
+
/**
|
|
1224
|
+
* Get spans for a step using a recursive CTE to traverse the span hierarchy.
|
|
1225
|
+
* This returns the step's span and all its descendant spans (children, grandchildren, etc.)
|
|
1226
|
+
*/
|
|
1227
|
+
async _getStepSpansRecursive(stepId, sortField, sortOrder, _filters) {
|
|
1228
|
+
const schemaName = this.schema;
|
|
1229
|
+
// Use a recursive CTE to find all descendant spans
|
|
1230
|
+
// 1. Base case: find the span with step_id = stepId
|
|
1231
|
+
// 2. Recursive case: find all spans where parent_span_id = span_id of a span we've already found
|
|
1232
|
+
const query = sql `
|
|
1233
|
+
WITH RECURSIVE span_tree AS (
|
|
1234
|
+
-- Base case: the span(s) for the step
|
|
1235
|
+
SELECT * FROM ${sql.identifier(schemaName)}.spans WHERE step_id = ${stepId}::uuid
|
|
1236
|
+
UNION ALL
|
|
1237
|
+
-- Recursive case: children of spans we've found
|
|
1238
|
+
SELECT s.* FROM ${sql.identifier(schemaName)}.spans s
|
|
1239
|
+
INNER JOIN span_tree st ON s.parent_span_id = st.span_id
|
|
1240
|
+
)
|
|
1241
|
+
SELECT
|
|
1242
|
+
id,
|
|
1243
|
+
trace_id as "traceId",
|
|
1244
|
+
span_id as "spanId",
|
|
1245
|
+
parent_span_id as "parentSpanId",
|
|
1246
|
+
job_id as "jobId",
|
|
1247
|
+
step_id as "stepId",
|
|
1248
|
+
name,
|
|
1249
|
+
kind,
|
|
1250
|
+
start_time_unix_nano as "startTimeUnixNano",
|
|
1251
|
+
end_time_unix_nano as "endTimeUnixNano",
|
|
1252
|
+
status_code as "statusCode",
|
|
1253
|
+
status_message as "statusMessage",
|
|
1254
|
+
attributes,
|
|
1255
|
+
events
|
|
1256
|
+
FROM span_tree
|
|
1257
|
+
ORDER BY ${sql.identifier(sortField)} ${sql.raw(sortOrder)}
|
|
1258
|
+
`;
|
|
1259
|
+
// Raw SQL returns numeric types as strings, so we type them as such
|
|
1260
|
+
const rows = (await this.db.execute(query));
|
|
1261
|
+
// Convert types: raw SQL returns numeric types as strings
|
|
1262
|
+
const spans = rows.map((row) => ({
|
|
1263
|
+
...row,
|
|
1264
|
+
// Convert id to number (bigserial comes as string from raw SQL)
|
|
1265
|
+
id: typeof row.id === 'string' ? Number.parseInt(row.id, 10) : row.id,
|
|
1266
|
+
// Convert kind and statusCode to proper types
|
|
1267
|
+
kind: (typeof row.kind === 'string' ? Number.parseInt(row.kind, 10) : row.kind),
|
|
1268
|
+
statusCode: (typeof row.statusCode === 'string' ? Number.parseInt(row.statusCode, 10) : row.statusCode),
|
|
1269
|
+
// Convert BigInt to string for JSON serialization
|
|
1270
|
+
startTimeUnixNano: row.startTimeUnixNano?.toString() ?? null,
|
|
1271
|
+
endTimeUnixNano: row.endTimeUnixNano?.toString() ?? null,
|
|
1272
|
+
}));
|
|
1273
|
+
return {
|
|
1274
|
+
spans,
|
|
1275
|
+
total: spans.length,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Internal method to delete all spans for a job.
|
|
1280
|
+
*/
|
|
1281
|
+
async _deleteSpans(options) {
|
|
995
1282
|
const result = await this.db
|
|
996
|
-
.delete(this.tables.
|
|
997
|
-
.where(eq(this.tables.
|
|
998
|
-
.returning({ id: this.tables.
|
|
1283
|
+
.delete(this.tables.spansTable)
|
|
1284
|
+
.where(eq(this.tables.spansTable.job_id, options.jobId))
|
|
1285
|
+
.returning({ id: this.tables.spansTable.id });
|
|
999
1286
|
return result.length;
|
|
1000
1287
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1288
|
+
/**
|
|
1289
|
+
* Build WHERE clause for spans queries (used for job queries only).
|
|
1290
|
+
* When querying by jobId, we find all spans that share the same trace_id
|
|
1291
|
+
* as spans with that job. This includes spans from external libraries that
|
|
1292
|
+
* don't have the duron.job.id attribute but are part of the same trace.
|
|
1293
|
+
*
|
|
1294
|
+
* Note: Step queries are handled separately by _getStepSpansRecursive using
|
|
1295
|
+
* a recursive CTE to traverse the span hierarchy.
|
|
1296
|
+
*/
|
|
1297
|
+
_buildSpansWhereClause(jobId, _stepId, filters) {
|
|
1298
|
+
const spansTable = this.tables.spansTable;
|
|
1299
|
+
// Build condition for finding spans by trace_id (includes external spans)
|
|
1300
|
+
let traceCondition;
|
|
1301
|
+
if (jobId) {
|
|
1302
|
+
// Find all spans that share a trace_id with any span that has this job_id
|
|
1303
|
+
// This includes external spans (like from AI SDK) that don't have duron.job.id
|
|
1304
|
+
traceCondition = inArray(spansTable.trace_id, this.db.select({ traceId: spansTable.trace_id }).from(spansTable).where(eq(spansTable.job_id, jobId)));
|
|
1305
|
+
}
|
|
1306
|
+
return and(traceCondition, filters?.name
|
|
1004
1307
|
? Array.isArray(filters.name)
|
|
1005
|
-
? or(...filters.name.map((n) => ilike(
|
|
1006
|
-
: ilike(
|
|
1007
|
-
: undefined, filters?.
|
|
1008
|
-
? inArray(
|
|
1009
|
-
: undefined, filters?.
|
|
1010
|
-
?
|
|
1011
|
-
: undefined, ...(filters?.attributesFilter && Object.keys(filters.attributesFilter).length > 0
|
|
1012
|
-
? this.#buildJsonbWhereConditions(filters.attributesFilter, metricsTable.attributes)
|
|
1308
|
+
? or(...filters.name.map((n) => ilike(spansTable.name, `%${n}%`)))
|
|
1309
|
+
: ilike(spansTable.name, `%${filters.name}%`)
|
|
1310
|
+
: undefined, filters?.kind ? inArray(spansTable.kind, Array.isArray(filters.kind) ? filters.kind : [filters.kind]) : undefined, filters?.statusCode
|
|
1311
|
+
? inArray(spansTable.status_code, Array.isArray(filters.statusCode) ? filters.statusCode : [filters.statusCode])
|
|
1312
|
+
: undefined, filters?.traceId ? eq(spansTable.trace_id, filters.traceId) : undefined, ...(filters?.attributesFilter && Object.keys(filters.attributesFilter).length > 0
|
|
1313
|
+
? this.#buildJsonbWhereConditions(filters.attributesFilter, spansTable.attributes)
|
|
1013
1314
|
: []));
|
|
1014
1315
|
}
|
|
1316
|
+
// ============================================================================
|
|
1317
|
+
// Private Methods
|
|
1318
|
+
// ============================================================================
|
|
1319
|
+
/**
|
|
1320
|
+
* Build WHERE conditions for JSONB filter using individual property checks.
|
|
1321
|
+
* Each property becomes a separate condition using ->> operator and ILIKE for case-insensitive matching.
|
|
1322
|
+
* Supports nested properties via dot notation and arrays.
|
|
1323
|
+
*
|
|
1324
|
+
* Example:
|
|
1325
|
+
* { "email": "tincho@gmail", "address.name": "nicolas", "products": ["chicle"] }
|
|
1326
|
+
* Generates:
|
|
1327
|
+
* input ->> 'email' ILIKE '%tincho@gmail%'
|
|
1328
|
+
* AND input ->> 'address' ->> 'name' ILIKE '%nicolas%'
|
|
1329
|
+
* AND EXISTS (SELECT 1 FROM jsonb_array_elements_text(input -> 'products') AS elem WHERE LOWER(elem) ILIKE LOWER('%chicle%'))
|
|
1330
|
+
*
|
|
1331
|
+
* @param filter - Flat record with dot-notation keys (e.g., { "email": "test", "address.name": "value", "products": ["chicle"] })
|
|
1332
|
+
* @param jsonbColumn - The JSONB column name
|
|
1333
|
+
* @returns Array of SQL conditions
|
|
1334
|
+
*/
|
|
1015
1335
|
#buildJsonbWhereConditions(filter, jsonbColumn) {
|
|
1016
1336
|
const conditions = [];
|
|
1017
1337
|
for (const [key, value] of Object.entries(filter)) {
|
|
@@ -1019,11 +1339,16 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1019
1339
|
if (parts.length === 0) {
|
|
1020
1340
|
continue;
|
|
1021
1341
|
}
|
|
1342
|
+
// Build the JSONB path expression step by step
|
|
1343
|
+
// For "address.name": input -> 'address' ->> 'name' (-> for intermediate, ->> for final)
|
|
1344
|
+
// For "email": input ->> 'email' (->> for single level)
|
|
1022
1345
|
let jsonbPath = sql `${jsonbColumn}`;
|
|
1023
1346
|
if (parts.length === 1) {
|
|
1347
|
+
// Single level: use ->> directly
|
|
1024
1348
|
jsonbPath = sql `${jsonbPath} ->> ${parts[0]}`;
|
|
1025
1349
|
}
|
|
1026
1350
|
else {
|
|
1351
|
+
// Nested: use -> for intermediate steps, ->> for final step
|
|
1027
1352
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
1028
1353
|
const part = parts[i];
|
|
1029
1354
|
if (part) {
|
|
@@ -1035,9 +1360,12 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1035
1360
|
jsonbPath = sql `${jsonbPath} ->> ${lastPart}`;
|
|
1036
1361
|
}
|
|
1037
1362
|
}
|
|
1363
|
+
// Handle array values - check if JSONB array contains at least one of the values
|
|
1038
1364
|
if (Array.isArray(value)) {
|
|
1365
|
+
// Build condition: check if any element in the JSONB array matches any value in the filter array
|
|
1039
1366
|
const arrayValueConditions = value.map((arrayValue) => {
|
|
1040
1367
|
const arrayValueStr = String(arrayValue);
|
|
1368
|
+
// Get the array from JSONB: input -> 'products'
|
|
1041
1369
|
let arrayPath = sql `${jsonbColumn}`;
|
|
1042
1370
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
1043
1371
|
const part = parts[i];
|
|
@@ -1049,6 +1377,7 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1049
1377
|
if (lastPart) {
|
|
1050
1378
|
arrayPath = sql `${arrayPath} -> ${lastPart}`;
|
|
1051
1379
|
}
|
|
1380
|
+
// Check if the JSONB array contains the value (case-insensitive for strings)
|
|
1052
1381
|
if (typeof arrayValue === 'string') {
|
|
1053
1382
|
return sql `EXISTS (
|
|
1054
1383
|
SELECT 1
|
|
@@ -1057,30 +1386,62 @@ export class PostgresBaseAdapter extends Adapter {
|
|
|
1057
1386
|
)`;
|
|
1058
1387
|
}
|
|
1059
1388
|
else {
|
|
1389
|
+
// For non-string values, use exact containment
|
|
1060
1390
|
return sql `${arrayPath} @> ${sql.raw(JSON.stringify([arrayValue]))}::jsonb`;
|
|
1061
1391
|
}
|
|
1062
1392
|
});
|
|
1393
|
+
// Combine array conditions with OR (at least one must match)
|
|
1063
1394
|
if (arrayValueConditions.length > 0) {
|
|
1064
1395
|
conditions.push(arrayValueConditions.reduce((acc, condition, idx) => (idx === 0 ? condition : sql `${acc} OR ${condition}`)));
|
|
1065
1396
|
}
|
|
1066
1397
|
}
|
|
1067
1398
|
else if (typeof value === 'string') {
|
|
1399
|
+
// String values: use ILIKE for case-insensitive partial matching
|
|
1068
1400
|
conditions.push(sql `COALESCE(${jsonbPath}, '') ILIKE ${`%${value}%`}`);
|
|
1069
1401
|
}
|
|
1070
1402
|
else {
|
|
1403
|
+
// Non-string, non-array values: use exact match
|
|
1404
|
+
// Convert JSONB value to text for comparison
|
|
1071
1405
|
conditions.push(sql `${jsonbPath}::text = ${String(value)}`);
|
|
1072
1406
|
}
|
|
1073
1407
|
}
|
|
1074
1408
|
return conditions;
|
|
1075
1409
|
}
|
|
1410
|
+
// ============================================================================
|
|
1411
|
+
// Protected Methods
|
|
1412
|
+
// ============================================================================
|
|
1413
|
+
/**
|
|
1414
|
+
* Send a PostgreSQL notification.
|
|
1415
|
+
*
|
|
1416
|
+
* @param event - The event name
|
|
1417
|
+
* @param data - The data to send
|
|
1418
|
+
* @returns Promise resolving to `void`
|
|
1419
|
+
*/
|
|
1076
1420
|
async _notify(_event, _data) {
|
|
1421
|
+
// do nothing
|
|
1077
1422
|
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Listen for PostgreSQL notifications.
|
|
1425
|
+
*
|
|
1426
|
+
* @param event - The event name to listen for
|
|
1427
|
+
* @param callback - Callback function to handle notifications
|
|
1428
|
+
* @returns Promise resolving to an object with an `unlisten` function
|
|
1429
|
+
*/
|
|
1078
1430
|
async _listen(_event, _callback) {
|
|
1431
|
+
// do nothing
|
|
1079
1432
|
return {
|
|
1080
1433
|
unlisten: () => {
|
|
1434
|
+
// do nothing
|
|
1081
1435
|
},
|
|
1082
1436
|
};
|
|
1083
1437
|
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Map database query results to the expected format.
|
|
1440
|
+
* Can be overridden by subclasses to handle different result formats.
|
|
1441
|
+
*
|
|
1442
|
+
* @param result - The raw database query result
|
|
1443
|
+
* @returns The mapped result
|
|
1444
|
+
*/
|
|
1084
1445
|
_map(result) {
|
|
1085
1446
|
return result;
|
|
1086
1447
|
}
|