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.
Files changed (80) hide show
  1. package/dist/action-job.d.ts +31 -0
  2. package/dist/action-job.d.ts.map +1 -1
  3. package/dist/action-job.js +68 -7
  4. package/dist/action-manager.d.ts +42 -0
  5. package/dist/action-manager.d.ts.map +1 -1
  6. package/dist/action-manager.js +61 -0
  7. package/dist/action.d.ts +144 -0
  8. package/dist/action.d.ts.map +1 -1
  9. package/dist/action.js +133 -2
  10. package/dist/adapters/adapter.d.ts +359 -0
  11. package/dist/adapters/adapter.d.ts.map +1 -1
  12. package/dist/adapters/adapter.js +208 -0
  13. package/dist/adapters/postgres/base.d.ts +166 -0
  14. package/dist/adapters/postgres/base.d.ts.map +1 -1
  15. package/dist/adapters/postgres/base.js +273 -19
  16. package/dist/adapters/postgres/pglite.d.ts +37 -0
  17. package/dist/adapters/postgres/pglite.d.ts.map +1 -1
  18. package/dist/adapters/postgres/pglite.js +38 -0
  19. package/dist/adapters/postgres/postgres.d.ts +35 -0
  20. package/dist/adapters/postgres/postgres.d.ts.map +1 -1
  21. package/dist/adapters/postgres/postgres.js +42 -0
  22. package/dist/adapters/postgres/schema.d.ts.map +1 -1
  23. package/dist/adapters/postgres/schema.js +14 -2
  24. package/dist/adapters/schemas.d.ts +9 -0
  25. package/dist/adapters/schemas.d.ts.map +1 -1
  26. package/dist/adapters/schemas.js +73 -1
  27. package/dist/client.d.ts +249 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +413 -3
  30. package/dist/constants.js +6 -0
  31. package/dist/errors.d.ts +166 -9
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +189 -19
  34. package/dist/server.d.ts +44 -0
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +56 -0
  37. package/dist/step-manager.d.ts +84 -0
  38. package/dist/step-manager.d.ts.map +1 -1
  39. package/dist/step-manager.js +354 -14
  40. package/dist/telemetry/adapter.d.ts +344 -0
  41. package/dist/telemetry/adapter.d.ts.map +1 -1
  42. package/dist/telemetry/adapter.js +151 -0
  43. package/dist/telemetry/index.d.ts +1 -1
  44. package/dist/telemetry/index.d.ts.map +1 -1
  45. package/dist/telemetry/index.js +1 -0
  46. package/dist/telemetry/local.d.ts +50 -1
  47. package/dist/telemetry/local.d.ts.map +1 -1
  48. package/dist/telemetry/local.js +165 -0
  49. package/dist/telemetry/noop.d.ts +12 -1
  50. package/dist/telemetry/noop.d.ts.map +1 -1
  51. package/dist/telemetry/noop.js +70 -0
  52. package/dist/telemetry/opentelemetry.d.ts +25 -1
  53. package/dist/telemetry/opentelemetry.d.ts.map +1 -1
  54. package/dist/telemetry/opentelemetry.js +149 -0
  55. package/dist/utils/p-retry.d.ts +5 -0
  56. package/dist/utils/p-retry.d.ts.map +1 -1
  57. package/dist/utils/p-retry.js +8 -0
  58. package/dist/utils/wait-for-abort.d.ts +1 -0
  59. package/dist/utils/wait-for-abort.d.ts.map +1 -1
  60. package/dist/utils/wait-for-abort.js +1 -0
  61. package/migrations/postgres/{20251203223656_conscious_johnny_blaze → 20260119153838_flimsy_thor_girl}/migration.sql +29 -2
  62. package/migrations/postgres/{20260118202533_wealthy_mysterio → 20260119153838_flimsy_thor_girl}/snapshot.json +5 -5
  63. package/package.json +1 -1
  64. package/src/action-job.ts +14 -7
  65. package/src/action.ts +23 -13
  66. package/src/adapters/postgres/base.ts +45 -19
  67. package/src/adapters/postgres/schema.ts +5 -2
  68. package/src/adapters/schemas.ts +11 -1
  69. package/src/client.ts +187 -8
  70. package/src/errors.ts +141 -30
  71. package/src/step-manager.ts +171 -10
  72. package/src/telemetry/adapter.ts +174 -0
  73. package/src/telemetry/index.ts +3 -0
  74. package/src/telemetry/local.ts +93 -0
  75. package/src/telemetry/noop.ts +46 -0
  76. package/src/telemetry/opentelemetry.ts +145 -2
  77. package/migrations/postgres/20251203223656_conscious_johnny_blaze/snapshot.json +0 -941
  78. package/migrations/postgres/20260117231749_clumsy_penance/migration.sql +0 -3
  79. package/migrations/postgres/20260117231749_clumsy_penance/snapshot.json +0 -988
  80. 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} AND s.name = ${name}
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: this.tables.jobsTable.id,
693
- actionName: this.tables.jobsTable.action_name,
694
- groupKey: this.tables.jobsTable.group_key,
695
- input: this.tables.jobsTable.input,
696
- output: this.tables.jobsTable.output,
697
- error: this.tables.jobsTable.error,
698
- status: this.tables.jobsTable.status,
699
- timeoutMs: this.tables.jobsTable.timeout_ms,
700
- expiresAt: this.tables.jobsTable.expires_at,
701
- startedAt: this.tables.jobsTable.started_at,
702
- finishedAt: this.tables.jobsTable.finished_at,
703
- createdAt: this.tables.jobsTable.created_at,
704
- updatedAt: this.tables.jobsTable.updated_at,
705
- concurrencyLimit: this.tables.jobsTable.concurrency_limit,
706
- clientId: this.tables.jobsTable.client_id,
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(this.tables.jobsTable)
709
- .where(eq(this.tables.jobsTable.id, jobId))
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;AAQnD,qBAAa,aAAc,SAAQ,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;cAOnD,MAAM;cAYN,KAAK;cAWX,IAAI,CAAC,MAAM,EAAE,GAAG;cAQhB,OAAO;cAoBD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;cAWhD,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"}
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"}