@yunsoft/yuncms-core 0.1.24 → 0.1.25
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/package.json +1 -1
- package/src/query.js +3 -0
- package/src/services/items-service.js +7 -2
- package/src/transaction.js +54 -7
package/package.json
CHANGED
package/src/query.js
CHANGED
|
@@ -47,6 +47,9 @@ function normalizeDelimited(value, label, { maxItems }) {
|
|
|
47
47
|
|
|
48
48
|
function normalizeInteger(value, fallback, { label, min, max }) {
|
|
49
49
|
if (value == null || value === '') return fallback;
|
|
50
|
+
if (typeof value === 'object' || typeof value === 'boolean') {
|
|
51
|
+
throw queryError(`${label} must be an integer between ${min} and ${max}`, label);
|
|
52
|
+
}
|
|
50
53
|
const number = Number(value);
|
|
51
54
|
if (!Number.isInteger(number) || number < min || number > max) {
|
|
52
55
|
throw queryError(`${label} must be an integer between ${min} and ${max}`, label);
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from '../query.js';
|
|
15
15
|
import { SchemaCache } from '../schema.js';
|
|
16
16
|
import { isSystemManagedField, systemMutationEntries } from '../system-fields.js';
|
|
17
|
-
import { withTransaction } from '../transaction.js';
|
|
17
|
+
import { dispatchAfterCommit, transactionContext, withTransaction } from '../transaction.js';
|
|
18
18
|
import { BaseService } from './base-service.js';
|
|
19
19
|
import { PermissionsService } from './permissions-service.js';
|
|
20
20
|
|
|
@@ -139,6 +139,8 @@ export class ItemsService extends BaseService {
|
|
|
139
139
|
accountability: this.accountability,
|
|
140
140
|
collection: this.collection,
|
|
141
141
|
requestId: this.requestId,
|
|
142
|
+
database: this.database,
|
|
143
|
+
transaction: transactionContext(this.database),
|
|
142
144
|
...extra,
|
|
143
145
|
};
|
|
144
146
|
}
|
|
@@ -150,7 +152,10 @@ export class ItemsService extends BaseService {
|
|
|
150
152
|
|
|
151
153
|
async actionMutation(event, payload, context = {}) {
|
|
152
154
|
if (!this.emitter) return;
|
|
153
|
-
|
|
155
|
+
const eventContext = this.hookContext(context);
|
|
156
|
+
const eventPayload = eventContext.transaction ? structuredClone(payload) : payload;
|
|
157
|
+
await dispatchAfterCommit(this.database, (database, transaction) =>
|
|
158
|
+
this.emitter.action(event, eventPayload, { ...eventContext, database, transaction }));
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
async getCollectionSchema(database = this.database) {
|
package/src/transaction.js
CHANGED
|
@@ -5,6 +5,33 @@ const ISOLATION_LEVELS = new Set([
|
|
|
5
5
|
'SERIALIZABLE',
|
|
6
6
|
]);
|
|
7
7
|
|
|
8
|
+
const transactions = new WeakMap();
|
|
9
|
+
|
|
10
|
+
export function transactionContext(database) {
|
|
11
|
+
return transactions.has(database) ? Object.freeze({ managed: true, state: 'active' }) : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function dispatchAfterCommit(database, action) {
|
|
15
|
+
const transaction = transactions.get(database);
|
|
16
|
+
if (transaction) {
|
|
17
|
+
transaction.actions.push(action);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
await action(database, null);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateOperation(database, operation) {
|
|
24
|
+
if (!database) throw new Error('Database connection is required');
|
|
25
|
+
if (typeof operation !== 'function') throw new Error('Transaction operation is required');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function joinTransaction(connection, operation, options) {
|
|
29
|
+
if (options.isolationLevel && String(options.isolationLevel).toUpperCase() !== transactions.get(connection).isolationLevel) {
|
|
30
|
+
throw new Error('A nested transaction cannot change the outer isolation level');
|
|
31
|
+
}
|
|
32
|
+
return operation(connection);
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
async function setIsolationLevel(connection, isolationLevel) {
|
|
9
36
|
if (!isolationLevel) return;
|
|
10
37
|
const level = String(isolationLevel).toUpperCase();
|
|
@@ -14,17 +41,16 @@ async function setIsolationLevel(connection, isolationLevel) {
|
|
|
14
41
|
await connection.query(`SET TRANSACTION ISOLATION LEVEL ${level}`);
|
|
15
42
|
}
|
|
16
43
|
|
|
17
|
-
|
|
18
|
-
if (!connection) throw new Error('Database connection is required');
|
|
19
|
-
if (typeof operation !== 'function') throw new Error('Transaction operation is required');
|
|
20
|
-
|
|
44
|
+
async function executeTransaction(connection, operation, options) {
|
|
21
45
|
await setIsolationLevel(connection, options.isolationLevel);
|
|
22
46
|
await connection.beginTransaction();
|
|
47
|
+
const transaction = { actions: [], isolationLevel: options.isolationLevel ? String(options.isolationLevel).toUpperCase() : null };
|
|
48
|
+
transactions.set(connection, transaction);
|
|
23
49
|
|
|
24
50
|
try {
|
|
25
51
|
const result = await operation(connection);
|
|
26
52
|
await connection.commit();
|
|
27
|
-
return result;
|
|
53
|
+
return { result, actions: transaction.actions };
|
|
28
54
|
} catch (error) {
|
|
29
55
|
try {
|
|
30
56
|
await connection.rollback();
|
|
@@ -32,15 +58,36 @@ export async function withConnectionTransaction(connection, operation, options =
|
|
|
32
58
|
error.rollbackError = rollbackError;
|
|
33
59
|
}
|
|
34
60
|
throw error;
|
|
61
|
+
} finally {
|
|
62
|
+
transactions.delete(connection);
|
|
35
63
|
}
|
|
36
64
|
}
|
|
37
65
|
|
|
66
|
+
async function finishActions(committed, database) {
|
|
67
|
+
const context = Object.freeze({ managed: true, state: 'committed' });
|
|
68
|
+
for (const action of committed.actions) await action(database, context);
|
|
69
|
+
return committed.result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function withConnectionTransaction(connection, operation, options = {}) {
|
|
73
|
+
validateOperation(connection, operation);
|
|
74
|
+
if (transactions.has(connection)) return joinTransaction(connection, operation, options);
|
|
75
|
+
const committed = await executeTransaction(connection, operation, options);
|
|
76
|
+
// This connection belongs to the caller and remains usable for post-commit actions.
|
|
77
|
+
return finishActions(committed, connection);
|
|
78
|
+
}
|
|
79
|
+
|
|
38
80
|
export async function withTransaction(pool, operation, options = {}) {
|
|
81
|
+
validateOperation(pool, operation);
|
|
82
|
+
if (transactions.has(pool)) return joinTransaction(pool, operation, options);
|
|
83
|
+
if (typeof pool.getConnection !== 'function') return withConnectionTransaction(pool, operation, options);
|
|
39
84
|
const connection = await pool.getConnection();
|
|
40
|
-
|
|
85
|
+
let committed;
|
|
41
86
|
try {
|
|
42
|
-
|
|
87
|
+
committed = await executeTransaction(connection, operation, options);
|
|
43
88
|
} finally {
|
|
44
89
|
connection.release();
|
|
45
90
|
}
|
|
91
|
+
// Return the borrowed connection before hooks acquire connections from the pool.
|
|
92
|
+
return finishActions(committed, pool);
|
|
46
93
|
}
|