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.
Files changed (60) 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 +59 -0
  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 +143 -0
  8. package/dist/action.d.ts.map +1 -1
  9. package/dist/action.js +131 -0
  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 +268 -17
  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.js +11 -1
  23. package/dist/adapters/schemas.d.ts +9 -0
  24. package/dist/adapters/schemas.d.ts.map +1 -1
  25. package/dist/adapters/schemas.js +73 -1
  26. package/dist/client.d.ts +224 -0
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +311 -1
  29. package/dist/constants.js +6 -0
  30. package/dist/errors.d.ts +119 -0
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js +111 -0
  33. package/dist/server.d.ts +44 -0
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +56 -0
  36. package/dist/step-manager.d.ts +83 -0
  37. package/dist/step-manager.d.ts.map +1 -1
  38. package/dist/step-manager.js +237 -5
  39. package/dist/telemetry/adapter.d.ts +322 -0
  40. package/dist/telemetry/adapter.d.ts.map +1 -1
  41. package/dist/telemetry/adapter.js +145 -0
  42. package/dist/telemetry/index.js +1 -0
  43. package/dist/telemetry/local.d.ts +48 -0
  44. package/dist/telemetry/local.d.ts.map +1 -1
  45. package/dist/telemetry/local.js +102 -0
  46. package/dist/telemetry/noop.d.ts +10 -0
  47. package/dist/telemetry/noop.d.ts.map +1 -1
  48. package/dist/telemetry/noop.js +43 -0
  49. package/dist/telemetry/opentelemetry.d.ts +23 -0
  50. package/dist/telemetry/opentelemetry.d.ts.map +1 -1
  51. package/dist/telemetry/opentelemetry.js +39 -0
  52. package/dist/utils/p-retry.d.ts +5 -0
  53. package/dist/utils/p-retry.d.ts.map +1 -1
  54. package/dist/utils/p-retry.js +8 -0
  55. package/dist/utils/wait-for-abort.d.ts +1 -0
  56. package/dist/utils/wait-for-abort.d.ts.map +1 -1
  57. package/dist/utils/wait-for-abort.js +1 -0
  58. package/package.json +1 -1
  59. package/src/adapters/postgres/base.ts +40 -17
  60. 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: this.tables.jobsTable.id,
696
- actionName: this.tables.jobsTable.action_name,
697
- groupKey: this.tables.jobsTable.group_key,
698
- input: this.tables.jobsTable.input,
699
- output: this.tables.jobsTable.output,
700
- error: this.tables.jobsTable.error,
701
- status: this.tables.jobsTable.status,
702
- timeoutMs: this.tables.jobsTable.timeout_ms,
703
- expiresAt: this.tables.jobsTable.expires_at,
704
- startedAt: this.tables.jobsTable.started_at,
705
- finishedAt: this.tables.jobsTable.finished_at,
706
- createdAt: this.tables.jobsTable.created_at,
707
- updatedAt: this.tables.jobsTable.updated_at,
708
- concurrencyLimit: this.tables.jobsTable.concurrency_limit,
709
- 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,
710
850
  })
711
- .from(this.tables.jobsTable)
712
- .where(eq(this.tables.jobsTable.id, jobId))
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;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"}