@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 +10 -0
- package/dist/core/database/baseRepository.d.ts +5 -0
- package/dist/core/database/query.d.ts +6 -1
- package/dist/core/database/repositoryQuery.d.ts +4 -0
- package/dist/core/events/outbox/index.d.ts +9 -1
- package/dist/core/queue/redisQueue.d.ts +2 -1
- package/dist/core/queue/redisStreams.d.ts +2 -1
- package/dist/core/validation/rules.d.ts +8 -4
- package/dist/entries/database/query.js +15 -0
- package/dist/entries/database/repositoryQuery.js +19 -0
- package/dist/entries/queue/createAppQueue.js +31 -3
- package/dist/entries/queue/publicQueue.js +31 -3
- package/dist/entries/queue/queueMetrics.js +31 -3
- package/dist/entries/queue/redisQueue.js +31 -3
- package/dist/entries/validation/rules.js +22 -18
- package/dist/index.js +71 -3
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|