@getstrata/core 2.2.25 → 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 +6 -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/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/validation/rules.js +22 -18
- package/dist/index.js +40 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 2.2.25
|
|
4
10
|
|
|
5
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.
|
|
@@ -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 };
|
|
@@ -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
|
}
|
|
@@ -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.");
|