@getstrata/core 2.2.24 → 2.2.26

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.2.26
4
+
5
+ - Integer validation checks complete safe values instead of accepting numeric prefixes. Whole signed decimal form strings, optional values and existing error messages remain supported; `allowNumericStrings: false` requires JSON numbers. Validated strings are not implicitly cast. Invalid range bounds reject at configuration time.
6
+ - `SqlOutbox.getDelivery(eventId, listenerName)` provides bounded read-only status/attempt inspection under the caller's active connection and RLS scope. It returns no payloads, owner tokens or provider errors, never establishes bypass, and remains an advisory snapshot distinct from atomic replay.
7
+ - Repository and fluent `sumBy(groupColumn, column)` return typed groups with database textual totals, preserving precision without implicit JavaScript number conversion. Bound/protected predicates, soft deletes and transaction/RLS scopes are preserved; unsupported transformations reject explicitly. Native Postgres, MySQL and SQLite regressions cover precision, null groups, rollback and overflow behavior.
8
+
9
+ ## 2.2.25
10
+
11
+ Queue worker run loops recover native Redis connection closure with bounded backoff and a fresh connection. Uncertain Redis mutations are not replayed; retained reservations and retries use normal recovery. Explicit closure remains terminal and non-connection errors remain fatal. Native poll/ack closure regressions cover Streams and list transports.
12
+
3
13
  ## 2.2.24
4
14
 
5
15
  Fix Redis cache recovery after transport failures and command deadlines: retire failed connections, abort active fills, and lazily reconnect for later operations without replaying uncertain commands; preserve permanent explicit closure and shared invalidation fencing.
@@ -78,6 +78,11 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
78
78
  protected countWhere(where?: QueryWhere<TEntity>, options?: Pick<QueryOptions<TEntity>, "withTrashed" | "onlyTrashed" | "joins" | "groupBy">, whereNodes?: readonly WhereNode<TEntity>[]): Promise<number>;
79
79
  protected averageColumn(column: keyof TEntity & string, where?: QueryWhere<TEntity>): Promise<number>;
80
80
  protected averageExpression(expression: string, alias: string, where?: QueryWhere<TEntity>): Promise<number>;
81
+ /** One plain grouped projection; SQL numeric totals remain exact text, never implicit JS numbers. */
82
+ sumBy<K extends keyof TEntity & string>(groupColumn: K, column: keyof TEntity & string, options?: ExtendedQueryOptions<TEntity>): Promise<Array<{
83
+ value: TEntity[K] | null;
84
+ sum: string | null;
85
+ }>>;
81
86
  sum(column: keyof TEntity & string, where?: QueryWhere<TEntity>): Promise<number>;
82
87
  avg(column: keyof TEntity & string, where?: QueryWhere<TEntity>): Promise<number>;
83
88
  min(column: keyof TEntity & string, where?: QueryWhere<TEntity>): Promise<number>;
@@ -36,6 +36,11 @@ declare function buildProjectionQuery<TEntity extends object>(table: TableDefini
36
36
  params: unknown[];
37
37
  };
38
38
  declare function assertSafeProjectionExpression(expression: string): void;
39
+ /** Fixed aliases and textual totals avoid driver-dependent JavaScript precision loss. */
40
+ declare function buildGroupedSumQuery<TEntity extends object, K extends keyof TEntity & string>(table: TableDefinition<TEntity>, groupColumn: K, column: keyof TEntity & string, options?: QueryOptions<TEntity>, whereNodes?: readonly WhereNode<TEntity>[]): {
41
+ text: string;
42
+ params: unknown[];
43
+ };
39
44
  declare function buildGroupedCountQuery<TEntity extends object, K extends keyof TEntity & string>(table: TableDefinition<TEntity>, column: K, where?: QueryWhere<TEntity>, options?: Pick<QueryOptions<TEntity>, "withTrashed" | "onlyTrashed">): {
40
45
  text: string;
41
46
  params: unknown[];
@@ -73,4 +78,4 @@ declare function buildConditionalWriteQuery<TEntity extends object, PrimaryKey e
73
78
  text: string;
74
79
  params: unknown[];
75
80
  };
76
- export { assertSafeProjectionExpression, buildAdvancedWhereClause, buildConditionalWriteQuery, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildIncrementQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildUpsertQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, };
81
+ export { assertSafeProjectionExpression, buildAdvancedWhereClause, buildConditionalWriteQuery, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildGroupedSumQuery, buildIncrementQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildUpsertQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, };
@@ -47,6 +47,10 @@ declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof T
47
47
  private assertNoMutationEagerLoads;
48
48
  get(): Promise<Array<TEntity & LoadedRow>>;
49
49
  first(): Promise<(TEntity & LoadedRow) | null>;
50
+ sumBy<K extends keyof TEntity & string>(groupColumn: K, column: keyof TEntity & string): Promise<Array<{
51
+ value: TEntity[K] | null;
52
+ sum: string | null;
53
+ }>>;
50
54
  count(): Promise<number>;
51
55
  pluck<K extends keyof TEntity & string>(column: K): Promise<Array<TEntity[K]>>;
52
56
  pluck<K extends keyof TEntity & string, KK extends keyof TEntity & string>(column: K, keyBy: KK): Promise<Map<TEntity[KK], TEntity[K]>>;
@@ -13,6 +13,12 @@ interface DurableListener {
13
13
  signal: AbortSignal;
14
14
  }): Promise<void>;
15
15
  }
16
+ type OutboxDeliveryStatus = "pending" | "processing" | "failed" | "completed";
17
+ interface OutboxDeliverySnapshot {
18
+ readonly status: OutboxDeliveryStatus;
19
+ readonly attempts: number;
20
+ readonly maxAttempts: number;
21
+ }
16
22
  interface SqlOutboxOptions {
17
23
  listeners: readonly DurableListener[];
18
24
  leaseMs?: number;
@@ -31,6 +37,8 @@ declare class SqlOutbox {
31
37
  private readonly tenantResolver;
32
38
  constructor(options: SqlOutboxOptions);
33
39
  hasListener(name: string, event?: string): boolean;
40
+ /** Read-only advisory observation: caller owns tenant/RLS scope; no implicit bypass. */
41
+ getDelivery(eventId: string, listenerName: string): Promise<OutboxDeliverySnapshot | null>;
34
42
  publish(name: string, payload: unknown, options?: {
35
43
  id?: string;
36
44
  version?: number;
@@ -53,5 +61,5 @@ declare class SqlOutbox {
53
61
  export type { OutboxMetricState, OutboxMetricsCollector, OutboxMetricsOptions, OutboxMetricsSnapshot, } from "./metrics";
54
62
  export { createOutboxMetricsCollector, renderOutboxMetrics } from "./metrics";
55
63
  export { createOutboxMigration } from "./migration";
56
- export type { DurableEvent, DurableListener, SqlOutboxOptions };
64
+ export type { DurableEvent, DurableListener, OutboxDeliverySnapshot, OutboxDeliveryStatus, SqlOutboxOptions, };
57
65
  export { SqlOutbox };
@@ -27,13 +27,14 @@ declare class RedisQueue implements Queue {
27
27
  dispatch<TPayload extends object>(job: Job<TPayload>, payload: TPayload): Promise<void>;
28
28
  }
29
29
  declare class QueueWorker {
30
+ private readonly redisUrl;
30
31
  private readonly failedJobs;
31
32
  private readonly timeoutSeconds;
32
33
  private readonly queueKeys;
33
34
  private readonly owner;
34
35
  private running;
35
36
  private stopping;
36
- private readonly client;
37
+ private client;
37
38
  constructor(redisUrl: string, failedJobs: FailedJobService, timeoutSeconds?: number, queueKeys?: readonly string[]);
38
39
  requestStop(): void;
39
40
  isRunning(): boolean;
@@ -49,10 +49,11 @@ declare class RedisStreamsQueue implements Queue {
49
49
  cancel(jobId: string): Promise<boolean>;
50
50
  }
51
51
  declare class RedisStreamsWorker {
52
+ private readonly redisUrl;
52
53
  private readonly failedJobs;
53
54
  private readonly timeoutSeconds;
54
55
  private readonly queueKeys;
55
- private readonly client;
56
+ private client;
56
57
  private readonly owner;
57
58
  private readonly cursors;
58
59
  private processing;
@@ -7,11 +7,15 @@ declare function maxLength(maximum: number): ValidationRule;
7
7
  declare function pattern(expression: RegExp): ValidationRule;
8
8
  declare function enumRule<TValue extends string>(allowedValues: readonly TValue[]): ValidationRule;
9
9
  declare function optional(): ValidationRule;
10
- declare function integerRule(): ValidationRule;
10
+ interface IntegerValidationOptions {
11
+ /** Default true preserves whole decimal form strings; false requires JSON numbers. */
12
+ allowNumericStrings?: boolean;
13
+ }
14
+ declare function integerRule(options?: IntegerValidationOptions): ValidationRule;
11
15
  declare function emailRule(): ValidationRule;
12
16
  declare function confirmed(fieldName: string): ValidationRule;
13
- declare function positiveIntegerRule(): ValidationRule;
14
- declare function integerRange(minimum: number, maximum: number): ValidationRule;
17
+ declare function positiveIntegerRule(options?: IntegerValidationOptions): ValidationRule;
18
+ declare function integerRange(minimum: number, maximum: number, options?: IntegerValidationOptions): ValidationRule;
15
19
  declare function validateObject(payload: unknown, schema: ValidationSchema): Record<string, unknown>;
16
- export type { ValidationRule, ValidationSchema };
20
+ export type { IntegerValidationOptions, ValidationRule, ValidationSchema };
17
21
  export { confirmed, emailRule, enumRule, integerRange, integerRule, maxLength, minLength, optional, pattern, positiveIntegerRule, required, stringRule, validateObject, };
@@ -429,6 +429,20 @@ function assertSafeProjectionExpression(expression) {
429
429
  throw new Error(`Unsafe projection expression: ${expression}`);
430
430
  }
431
431
  }
432
+ function buildGroupedSumQuery(table, groupColumn, column, options = {}, whereNodes = options.whereNodes ?? []) {
433
+ if (!table.columns.includes(groupColumn) || !table.columns.includes(column))
434
+ throw new TypeError("Grouped sums require declared columns.");
435
+ for (const key of Object.keys(options))
436
+ if (!["where", "whereNodes", "withTrashed", "onlyTrashed"].includes(key))
437
+ throw new TypeError("Grouped sums do not support joins, selection, ordering, pagination, grouping or locks.");
438
+ const group = qualifyColumn(table.name, groupColumn);
439
+ const total = currentSqlDialect().castToText(`SUM(${qualifyColumn(table.name, column)})`);
440
+ const { clause, params } = buildQueryWhereClause(table, options, whereNodes);
441
+ return {
442
+ text: `SELECT ${group} AS ${quoteIdentifier("value")}, ${total} AS ${quoteIdentifier("sum")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${group} ORDER BY ${group} ASC`,
443
+ params
444
+ };
445
+ }
432
446
  function buildGroupedCountQuery(table, column, where = {}, options = {}) {
433
447
  const qualifiedColumn = qualifyColumn(table.name, column);
434
448
  const { clause, params } = buildQueryWhereClause(table, {
@@ -608,6 +622,7 @@ export {
608
622
  buildCountQuery,
609
623
  buildDeleteByIdQuery,
610
624
  buildGroupedCountQuery,
625
+ buildGroupedSumQuery,
611
626
  buildIncrementQuery,
612
627
  buildInsertQuery,
613
628
  buildJoinClause,
@@ -453,6 +453,20 @@ function assertSafeProjectionExpression(expression) {
453
453
  throw new Error(`Unsafe projection expression: ${expression}`);
454
454
  }
455
455
  }
456
+ function buildGroupedSumQuery(table, groupColumn, column, options = {}, whereNodes = options.whereNodes ?? []) {
457
+ if (!table.columns.includes(groupColumn) || !table.columns.includes(column))
458
+ throw new TypeError("Grouped sums require declared columns.");
459
+ for (const key of Object.keys(options))
460
+ if (!["where", "whereNodes", "withTrashed", "onlyTrashed"].includes(key))
461
+ throw new TypeError("Grouped sums do not support joins, selection, ordering, pagination, grouping or locks.");
462
+ const group = qualifyColumn(table.name, groupColumn);
463
+ const total = currentSqlDialect().castToText(`SUM(${qualifyColumn(table.name, column)})`);
464
+ const { clause, params } = buildQueryWhereClause(table, options, whereNodes);
465
+ return {
466
+ text: `SELECT ${group} AS ${quoteIdentifier("value")}, ${total} AS ${quoteIdentifier("sum")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${group} ORDER BY ${group} ASC`,
467
+ params
468
+ };
469
+ }
456
470
  function buildGroupedCountQuery(table, column, where = {}, options = {}) {
457
471
  const qualifiedColumn = qualifyColumn(table.name, column);
458
472
  const { clause, params } = buildQueryWhereClause(table, {
@@ -1124,6 +1138,11 @@ class RepositoryQuery {
1124
1138
  const attached = await this.attach(rows);
1125
1139
  return attached[0] ?? null;
1126
1140
  }
1141
+ async sumBy(groupColumn, column) {
1142
+ if (this.eagerLoads.length)
1143
+ throw new TypeError("Grouped sums do not support eager loading.");
1144
+ return this.repository.sumBy(groupColumn, column, this.buildOptions());
1145
+ }
1127
1146
  async count() {
1128
1147
  return await this.repository.count(this.buildOptions());
1129
1148
  }
@@ -690,6 +690,7 @@ class RedisStreamsQueue {
690
690
  }
691
691
 
692
692
  class RedisStreamsWorker {
693
+ redisUrl;
693
694
  failedJobs;
694
695
  timeoutSeconds;
695
696
  queueKeys;
@@ -702,6 +703,7 @@ class RedisStreamsWorker {
702
703
  stopping = false;
703
704
  active;
704
705
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = defaultQueueKeys()) {
706
+ this.redisUrl = redisUrl;
705
707
  this.failedJobs = failedJobs;
706
708
  this.timeoutSeconds = timeoutSeconds;
707
709
  this.queueKeys = queueKeys;
@@ -891,8 +893,21 @@ class RedisStreamsWorker {
891
893
  async run() {
892
894
  this.running = true;
893
895
  try {
894
- while (!this.stopping)
895
- await this.processNext();
896
+ while (!this.stopping) {
897
+ try {
898
+ await this.processNext();
899
+ } catch (error) {
900
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
901
+ throw error;
902
+ this.client.close();
903
+ await Bun.sleep(250);
904
+ if (this.stopping)
905
+ return;
906
+ this.client = new RedisClient(this.redisUrl);
907
+ this.initialized = false;
908
+ this.cursors.clear();
909
+ }
910
+ }
896
911
  } finally {
897
912
  this.running = false;
898
913
  }
@@ -1195,6 +1210,7 @@ class RedisQueue {
1195
1210
  }
1196
1211
 
1197
1212
  class QueueWorker {
1213
+ redisUrl;
1198
1214
  failedJobs;
1199
1215
  timeoutSeconds;
1200
1216
  queueKeys;
@@ -1203,6 +1219,7 @@ class QueueWorker {
1203
1219
  stopping = false;
1204
1220
  client;
1205
1221
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = QUEUE_KEYS) {
1222
+ this.redisUrl = redisUrl;
1206
1223
  this.failedJobs = failedJobs;
1207
1224
  this.timeoutSeconds = timeoutSeconds;
1208
1225
  this.queueKeys = queueKeys;
@@ -1283,13 +1300,24 @@ class QueueWorker {
1283
1300
  this.running = true;
1284
1301
  try {
1285
1302
  while (!this.stopping) {
1286
- await this.processNext();
1303
+ try {
1304
+ await this.processNext();
1305
+ } catch (error) {
1306
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
1307
+ throw error;
1308
+ this.client.close();
1309
+ await Bun.sleep(250);
1310
+ if (this.stopping)
1311
+ return;
1312
+ this.client = new RedisClient2(this.redisUrl);
1313
+ }
1287
1314
  }
1288
1315
  } finally {
1289
1316
  this.running = false;
1290
1317
  }
1291
1318
  }
1292
1319
  close() {
1320
+ this.stopping = true;
1293
1321
  this.client.close();
1294
1322
  }
1295
1323
  }
@@ -690,6 +690,7 @@ class RedisStreamsQueue {
690
690
  }
691
691
 
692
692
  class RedisStreamsWorker {
693
+ redisUrl;
693
694
  failedJobs;
694
695
  timeoutSeconds;
695
696
  queueKeys;
@@ -702,6 +703,7 @@ class RedisStreamsWorker {
702
703
  stopping = false;
703
704
  active;
704
705
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = defaultQueueKeys()) {
706
+ this.redisUrl = redisUrl;
705
707
  this.failedJobs = failedJobs;
706
708
  this.timeoutSeconds = timeoutSeconds;
707
709
  this.queueKeys = queueKeys;
@@ -891,8 +893,21 @@ class RedisStreamsWorker {
891
893
  async run() {
892
894
  this.running = true;
893
895
  try {
894
- while (!this.stopping)
895
- await this.processNext();
896
+ while (!this.stopping) {
897
+ try {
898
+ await this.processNext();
899
+ } catch (error) {
900
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
901
+ throw error;
902
+ this.client.close();
903
+ await Bun.sleep(250);
904
+ if (this.stopping)
905
+ return;
906
+ this.client = new RedisClient(this.redisUrl);
907
+ this.initialized = false;
908
+ this.cursors.clear();
909
+ }
910
+ }
896
911
  } finally {
897
912
  this.running = false;
898
913
  }
@@ -1195,6 +1210,7 @@ class RedisQueue {
1195
1210
  }
1196
1211
 
1197
1212
  class QueueWorker {
1213
+ redisUrl;
1198
1214
  failedJobs;
1199
1215
  timeoutSeconds;
1200
1216
  queueKeys;
@@ -1203,6 +1219,7 @@ class QueueWorker {
1203
1219
  stopping = false;
1204
1220
  client;
1205
1221
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = QUEUE_KEYS) {
1222
+ this.redisUrl = redisUrl;
1206
1223
  this.failedJobs = failedJobs;
1207
1224
  this.timeoutSeconds = timeoutSeconds;
1208
1225
  this.queueKeys = queueKeys;
@@ -1283,13 +1300,24 @@ class QueueWorker {
1283
1300
  this.running = true;
1284
1301
  try {
1285
1302
  while (!this.stopping) {
1286
- await this.processNext();
1303
+ try {
1304
+ await this.processNext();
1305
+ } catch (error) {
1306
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
1307
+ throw error;
1308
+ this.client.close();
1309
+ await Bun.sleep(250);
1310
+ if (this.stopping)
1311
+ return;
1312
+ this.client = new RedisClient2(this.redisUrl);
1313
+ }
1287
1314
  }
1288
1315
  } finally {
1289
1316
  this.running = false;
1290
1317
  }
1291
1318
  }
1292
1319
  close() {
1320
+ this.stopping = true;
1293
1321
  this.client.close();
1294
1322
  }
1295
1323
  }
@@ -1569,6 +1569,7 @@ class RedisStreamsQueue {
1569
1569
  }
1570
1570
 
1571
1571
  class RedisStreamsWorker {
1572
+ redisUrl;
1572
1573
  failedJobs;
1573
1574
  timeoutSeconds;
1574
1575
  queueKeys;
@@ -1581,6 +1582,7 @@ class RedisStreamsWorker {
1581
1582
  stopping = false;
1582
1583
  active;
1583
1584
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = defaultQueueKeys()) {
1585
+ this.redisUrl = redisUrl;
1584
1586
  this.failedJobs = failedJobs;
1585
1587
  this.timeoutSeconds = timeoutSeconds;
1586
1588
  this.queueKeys = queueKeys;
@@ -1770,8 +1772,21 @@ class RedisStreamsWorker {
1770
1772
  async run() {
1771
1773
  this.running = true;
1772
1774
  try {
1773
- while (!this.stopping)
1774
- await this.processNext();
1775
+ while (!this.stopping) {
1776
+ try {
1777
+ await this.processNext();
1778
+ } catch (error) {
1779
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
1780
+ throw error;
1781
+ this.client.close();
1782
+ await Bun.sleep(250);
1783
+ if (this.stopping)
1784
+ return;
1785
+ this.client = new RedisClient(this.redisUrl);
1786
+ this.initialized = false;
1787
+ this.cursors.clear();
1788
+ }
1789
+ }
1775
1790
  } finally {
1776
1791
  this.running = false;
1777
1792
  }
@@ -2074,6 +2089,7 @@ class RedisQueue {
2074
2089
  }
2075
2090
 
2076
2091
  class QueueWorker {
2092
+ redisUrl;
2077
2093
  failedJobs;
2078
2094
  timeoutSeconds;
2079
2095
  queueKeys;
@@ -2082,6 +2098,7 @@ class QueueWorker {
2082
2098
  stopping = false;
2083
2099
  client;
2084
2100
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = QUEUE_KEYS) {
2101
+ this.redisUrl = redisUrl;
2085
2102
  this.failedJobs = failedJobs;
2086
2103
  this.timeoutSeconds = timeoutSeconds;
2087
2104
  this.queueKeys = queueKeys;
@@ -2162,13 +2179,24 @@ class QueueWorker {
2162
2179
  this.running = true;
2163
2180
  try {
2164
2181
  while (!this.stopping) {
2165
- await this.processNext();
2182
+ try {
2183
+ await this.processNext();
2184
+ } catch (error) {
2185
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
2186
+ throw error;
2187
+ this.client.close();
2188
+ await Bun.sleep(250);
2189
+ if (this.stopping)
2190
+ return;
2191
+ this.client = new RedisClient2(this.redisUrl);
2192
+ }
2166
2193
  }
2167
2194
  } finally {
2168
2195
  this.running = false;
2169
2196
  }
2170
2197
  }
2171
2198
  close() {
2199
+ this.stopping = true;
2172
2200
  this.client.close();
2173
2201
  }
2174
2202
  }
@@ -622,6 +622,7 @@ class RedisStreamsQueue {
622
622
  }
623
623
 
624
624
  class RedisStreamsWorker {
625
+ redisUrl;
625
626
  failedJobs;
626
627
  timeoutSeconds;
627
628
  queueKeys;
@@ -634,6 +635,7 @@ class RedisStreamsWorker {
634
635
  stopping = false;
635
636
  active;
636
637
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = defaultQueueKeys()) {
638
+ this.redisUrl = redisUrl;
637
639
  this.failedJobs = failedJobs;
638
640
  this.timeoutSeconds = timeoutSeconds;
639
641
  this.queueKeys = queueKeys;
@@ -823,8 +825,21 @@ class RedisStreamsWorker {
823
825
  async run() {
824
826
  this.running = true;
825
827
  try {
826
- while (!this.stopping)
827
- await this.processNext();
828
+ while (!this.stopping) {
829
+ try {
830
+ await this.processNext();
831
+ } catch (error) {
832
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
833
+ throw error;
834
+ this.client.close();
835
+ await Bun.sleep(250);
836
+ if (this.stopping)
837
+ return;
838
+ this.client = new RedisClient(this.redisUrl);
839
+ this.initialized = false;
840
+ this.cursors.clear();
841
+ }
842
+ }
828
843
  } finally {
829
844
  this.running = false;
830
845
  }
@@ -1127,6 +1142,7 @@ class RedisQueue {
1127
1142
  }
1128
1143
 
1129
1144
  class QueueWorker {
1145
+ redisUrl;
1130
1146
  failedJobs;
1131
1147
  timeoutSeconds;
1132
1148
  queueKeys;
@@ -1135,6 +1151,7 @@ class QueueWorker {
1135
1151
  stopping = false;
1136
1152
  client;
1137
1153
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = QUEUE_KEYS) {
1154
+ this.redisUrl = redisUrl;
1138
1155
  this.failedJobs = failedJobs;
1139
1156
  this.timeoutSeconds = timeoutSeconds;
1140
1157
  this.queueKeys = queueKeys;
@@ -1215,13 +1232,24 @@ class QueueWorker {
1215
1232
  this.running = true;
1216
1233
  try {
1217
1234
  while (!this.stopping) {
1218
- await this.processNext();
1235
+ try {
1236
+ await this.processNext();
1237
+ } catch (error) {
1238
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
1239
+ throw error;
1240
+ this.client.close();
1241
+ await Bun.sleep(250);
1242
+ if (this.stopping)
1243
+ return;
1244
+ this.client = new RedisClient2(this.redisUrl);
1245
+ }
1219
1246
  }
1220
1247
  } finally {
1221
1248
  this.running = false;
1222
1249
  }
1223
1250
  }
1224
1251
  close() {
1252
+ this.stopping = true;
1225
1253
  this.client.close();
1226
1254
  }
1227
1255
  }
@@ -69,15 +69,21 @@ function optional() {
69
69
  return;
70
70
  };
71
71
  }
72
- function integerRule() {
72
+ function integerValue(value, options) {
73
+ if (typeof value === "number")
74
+ return Number.isSafeInteger(value) ? value : undefined;
75
+ if (options.allowNumericStrings !== false && typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) {
76
+ const parsed = Number(value.trim());
77
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
78
+ }
79
+ return;
80
+ }
81
+ function integerRule(options = {}) {
73
82
  return (field, value) => {
74
- if (value === undefined || value === null || value === "") {
83
+ if (value === undefined || value === null || value === "")
75
84
  return;
76
- }
77
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
78
- if (!Number.isInteger(parsed)) {
85
+ if (integerValue(value, options) === undefined)
79
86
  return `"${field}" must be an integer.`;
80
- }
81
87
  return;
82
88
  };
83
89
  }
@@ -103,27 +109,25 @@ function confirmed(fieldName) {
103
109
  return;
104
110
  };
105
111
  }
106
- function positiveIntegerRule() {
112
+ function positiveIntegerRule(options = {}) {
107
113
  return (field, value) => {
108
- if (value === undefined || value === null || value === "") {
114
+ if (value === undefined || value === null || value === "")
109
115
  return;
110
- }
111
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
112
- if (!Number.isInteger(parsed) || parsed <= 0) {
116
+ const parsed = integerValue(value, options);
117
+ if (parsed === undefined || parsed <= 0)
113
118
  return `"${field}" must be a positive integer.`;
114
- }
115
119
  return;
116
120
  };
117
121
  }
118
- function integerRange(minimum, maximum) {
122
+ function integerRange(minimum, maximum, options = {}) {
123
+ if (!Number.isSafeInteger(minimum) || !Number.isSafeInteger(maximum) || minimum > maximum)
124
+ throw new TypeError("Integer range bounds must be safe integers in ascending order.");
119
125
  return (field, value) => {
120
- if (value === undefined || value === null || value === "") {
126
+ if (value === undefined || value === null || value === "")
121
127
  return;
122
- }
123
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
124
- if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
128
+ const parsed = integerValue(value, options);
129
+ if (parsed === undefined || parsed < minimum || parsed > maximum)
125
130
  return `"${field}" must be an integer between ${minimum} and ${maximum}.`;
126
- }
127
131
  return;
128
132
  };
129
133
  }
package/dist/index.js CHANGED
@@ -4925,6 +4925,20 @@ function assertSafeProjectionExpression(expression) {
4925
4925
  throw new Error(`Unsafe projection expression: ${expression}`);
4926
4926
  }
4927
4927
  }
4928
+ function buildGroupedSumQuery(table, groupColumn, column, options = {}, whereNodes = options.whereNodes ?? []) {
4929
+ if (!table.columns.includes(groupColumn) || !table.columns.includes(column))
4930
+ throw new TypeError("Grouped sums require declared columns.");
4931
+ for (const key of Object.keys(options))
4932
+ if (!["where", "whereNodes", "withTrashed", "onlyTrashed"].includes(key))
4933
+ throw new TypeError("Grouped sums do not support joins, selection, ordering, pagination, grouping or locks.");
4934
+ const group = qualifyColumn(table.name, groupColumn);
4935
+ const total = currentSqlDialect().castToText(`SUM(${qualifyColumn(table.name, column)})`);
4936
+ const { clause, params } = buildQueryWhereClause(table, options, whereNodes);
4937
+ return {
4938
+ text: `SELECT ${group} AS ${quoteIdentifier("value")}, ${total} AS ${quoteIdentifier("sum")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${group} ORDER BY ${group} ASC`,
4939
+ params
4940
+ };
4941
+ }
4928
4942
  function buildGroupedCountQuery(table, column, where = {}, options = {}) {
4929
4943
  const qualifiedColumn = qualifyColumn(table.name, column);
4930
4944
  const { clause, params } = buildQueryWhereClause(table, {
@@ -5623,6 +5637,11 @@ class RepositoryQuery {
5623
5637
  const attached = await this.attach(rows);
5624
5638
  return attached[0] ?? null;
5625
5639
  }
5640
+ async sumBy(groupColumn, column) {
5641
+ if (this.eagerLoads.length)
5642
+ throw new TypeError("Grouped sums do not support eager loading.");
5643
+ return this.repository.sumBy(groupColumn, column, this.buildOptions());
5644
+ }
5626
5645
  async count() {
5627
5646
  return await this.repository.count(this.buildOptions());
5628
5647
  }
@@ -6235,6 +6254,11 @@ class BaseRepository {
6235
6254
  const [row] = await this.connection.unsafe(text, params);
6236
6255
  return Number(row?.[alias] ?? 0);
6237
6256
  }
6257
+ async sumBy(groupColumn, column, options = {}) {
6258
+ const { whereNodes, ...queryOptions } = options;
6259
+ const { text, params } = buildGroupedSumQuery(this.table, groupColumn, column, queryOptions, whereNodes ?? []);
6260
+ return withDatabaseErrorHandling(() => this.connection.unsafe(text, params));
6261
+ }
6238
6262
  async sum(column, where = {}) {
6239
6263
  return await this.aggregateColumn("SUM", column, where);
6240
6264
  }
@@ -10446,6 +10470,22 @@ class SqlOutbox {
10446
10470
  const listener = this.listeners.get(name);
10447
10471
  return listener !== undefined && (event === undefined || listener.event === event);
10448
10472
  }
10473
+ async getDelivery(eventId, listenerName) {
10474
+ identifier(eventId, 64);
10475
+ identifier(listenerName, 128);
10476
+ const p = placeholders(2);
10477
+ const [row] = await repositoryConnection.unsafe(`SELECT status, attempts, max_attempts FROM strata_outbox_delivery WHERE event_id = ${p[0]} AND listener_name = ${p[1]} LIMIT 1`, [eventId, listenerName]);
10478
+ if (!row)
10479
+ return null;
10480
+ const attempts = Number(row.attempts);
10481
+ const maxAttempts = Number(row.max_attempts);
10482
+ if (!Number.isSafeInteger(attempts) || attempts < 0 || !Number.isSafeInteger(maxAttempts) || maxAttempts < 1)
10483
+ throw new Error("Invalid outbox delivery snapshot.");
10484
+ const status = row.status;
10485
+ if (status !== "pending" && status !== "processing" && status !== "failed" && status !== "completed")
10486
+ throw new Error("Invalid outbox delivery snapshot.");
10487
+ return { status, attempts, maxAttempts };
10488
+ }
10449
10489
  async publish(name, payload, options = {}) {
10450
10490
  if (!hasActiveTransaction())
10451
10491
  throw new Error("Durable publication requires an active business transaction.");
@@ -14821,6 +14861,7 @@ class RedisStreamsQueue {
14821
14861
  }
14822
14862
 
14823
14863
  class RedisStreamsWorker {
14864
+ redisUrl;
14824
14865
  failedJobs;
14825
14866
  timeoutSeconds;
14826
14867
  queueKeys;
@@ -14833,6 +14874,7 @@ class RedisStreamsWorker {
14833
14874
  stopping = false;
14834
14875
  active;
14835
14876
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = defaultQueueKeys()) {
14877
+ this.redisUrl = redisUrl;
14836
14878
  this.failedJobs = failedJobs;
14837
14879
  this.timeoutSeconds = timeoutSeconds;
14838
14880
  this.queueKeys = queueKeys;
@@ -15022,8 +15064,21 @@ class RedisStreamsWorker {
15022
15064
  async run() {
15023
15065
  this.running = true;
15024
15066
  try {
15025
- while (!this.stopping)
15026
- await this.processNext();
15067
+ while (!this.stopping) {
15068
+ try {
15069
+ await this.processNext();
15070
+ } catch (error) {
15071
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
15072
+ throw error;
15073
+ this.client.close();
15074
+ await Bun.sleep(250);
15075
+ if (this.stopping)
15076
+ return;
15077
+ this.client = new RedisClient3(this.redisUrl);
15078
+ this.initialized = false;
15079
+ this.cursors.clear();
15080
+ }
15081
+ }
15027
15082
  } finally {
15028
15083
  this.running = false;
15029
15084
  }
@@ -15249,6 +15304,7 @@ class RedisQueue {
15249
15304
  }
15250
15305
 
15251
15306
  class QueueWorker {
15307
+ redisUrl;
15252
15308
  failedJobs;
15253
15309
  timeoutSeconds;
15254
15310
  queueKeys;
@@ -15257,6 +15313,7 @@ class QueueWorker {
15257
15313
  stopping = false;
15258
15314
  client;
15259
15315
  constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = QUEUE_KEYS) {
15316
+ this.redisUrl = redisUrl;
15260
15317
  this.failedJobs = failedJobs;
15261
15318
  this.timeoutSeconds = timeoutSeconds;
15262
15319
  this.queueKeys = queueKeys;
@@ -15337,13 +15394,24 @@ class QueueWorker {
15337
15394
  this.running = true;
15338
15395
  try {
15339
15396
  while (!this.stopping) {
15340
- await this.processNext();
15397
+ try {
15398
+ await this.processNext();
15399
+ } catch (error) {
15400
+ if (!(error && typeof error === "object" && ("code" in error) && error.code === "ERR_REDIS_CONNECTION_CLOSED"))
15401
+ throw error;
15402
+ this.client.close();
15403
+ await Bun.sleep(250);
15404
+ if (this.stopping)
15405
+ return;
15406
+ this.client = new RedisClient4(this.redisUrl);
15407
+ }
15341
15408
  }
15342
15409
  } finally {
15343
15410
  this.running = false;
15344
15411
  }
15345
15412
  }
15346
15413
  close() {
15414
+ this.stopping = true;
15347
15415
  this.client.close();
15348
15416
  }
15349
15417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "2.2.24",
3
+ "version": "2.2.26",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",