@open-mercato/shared 0.6.5-develop.4516.1.88e6ab71a9 → 0.6.5-develop.4544.1.71c003c861
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/commands/flush.js +23 -1
- package/dist/lib/commands/flush.js.map +2 -2
- package/dist/lib/crud/factory.js +16 -0
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/optimistic-lock-command.js +109 -0
- package/dist/lib/crud/optimistic-lock-command.js.map +7 -0
- package/dist/lib/crud/optimistic-lock-headers.js +15 -0
- package/dist/lib/crud/optimistic-lock-headers.js.map +7 -0
- package/dist/lib/crud/optimistic-lock-store.js +52 -0
- package/dist/lib/crud/optimistic-lock-store.js.map +7 -0
- package/dist/lib/crud/optimistic-lock.js +172 -0
- package/dist/lib/crud/optimistic-lock.js.map +7 -0
- package/dist/lib/di/container.js +18 -2
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/commands/__tests__/flush.test.ts +110 -9
- package/src/lib/commands/flush.ts +79 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +106 -0
- package/src/lib/crud/__tests__/optimistic-lock-command.test.ts +425 -0
- package/src/lib/crud/__tests__/optimistic-lock-store.test.ts +194 -0
- package/src/lib/crud/__tests__/optimistic-lock.test.ts +526 -0
- package/src/lib/crud/factory.ts +23 -0
- package/src/lib/crud/optimistic-lock-command.ts +305 -0
- package/src/lib/crud/optimistic-lock-headers.ts +30 -0
- package/src/lib/crud/optimistic-lock-store.ts +87 -0
- package/src/lib/crud/optimistic-lock.ts +379 -0
- package/src/lib/di/container.ts +17 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 220 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -1,10 +1,32 @@
|
|
|
1
|
+
async function flushPendingChangesGuard(em, label) {
|
|
2
|
+
let pendingCount = -1;
|
|
3
|
+
try {
|
|
4
|
+
const uow = typeof em.getUnitOfWork === "function" ? em.getUnitOfWork() : void 0;
|
|
5
|
+
if (uow && typeof uow.computeChangeSets === "function" && typeof uow.getChangeSets === "function") {
|
|
6
|
+
uow.computeChangeSets();
|
|
7
|
+
pendingCount = uow.getChangeSets().length;
|
|
8
|
+
}
|
|
9
|
+
} catch {
|
|
10
|
+
pendingCount = -1;
|
|
11
|
+
}
|
|
12
|
+
if (pendingCount > 0) {
|
|
13
|
+
await em.flush();
|
|
14
|
+
if (process.env.NODE_ENV !== "production") {
|
|
15
|
+
const where = label ? ` (${label})` : "";
|
|
16
|
+
console.warn(
|
|
17
|
+
`[withAtomicFlush]${where}: ${pendingCount} pending change-set(s) remained at the commit boundary and were flushed defensively. A phase mutated a managed entity after its flush boundary \u2014 split the mutation and any dependent read/sync into separate phases so the change is never at risk of being dropped.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
1
22
|
async function withAtomicFlush(em, phases, options) {
|
|
2
23
|
if (phases.length === 0) return;
|
|
3
24
|
const runPhasesAndFlush = async () => {
|
|
4
25
|
for (const phase of phases) {
|
|
5
26
|
await phase();
|
|
27
|
+
await em.flush();
|
|
6
28
|
}
|
|
7
|
-
await em
|
|
29
|
+
await flushPendingChangesGuard(em, options?.label);
|
|
8
30
|
};
|
|
9
31
|
if (!options?.transaction) {
|
|
10
32
|
await runPhasesAndFlush();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/commands/flush.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { IsolationLevel } from '@mikro-orm/core'\n\n/**\n * Options controlling how {@link withAtomicFlush} executes its phases.\n */\nexport type AtomicFlushOptions = {\n /**\n * When true, the whole sequence runs inside a database transaction for\n * all-or-nothing semantics. Default: false (a single `em.flush()` commits\n * all phases at the end \u2014 no transaction).\n */\n transaction?: boolean\n /**\n * Optional transaction isolation level, forwarded to `em.begin()`. Only\n * honoured when this call opens a new top-level transaction (i.e.\n * `transaction: true` and the EntityManager is not already inside a\n * transaction). Ignored when joining an ambient transaction.\n */\n isolationLevel?: IsolationLevel\n /**\n * Optional label for diagnostics.
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { IsolationLevel } from '@mikro-orm/core'\n\n/**\n * Options controlling how {@link withAtomicFlush} executes its phases.\n */\nexport type AtomicFlushOptions = {\n /**\n * When true, the whole sequence runs inside a database transaction for\n * all-or-nothing semantics. Default: false (a single `em.flush()` commits\n * all phases at the end \u2014 no transaction).\n */\n transaction?: boolean\n /**\n * Optional transaction isolation level, forwarded to `em.begin()`. Only\n * honoured when this call opens a new top-level transaction (i.e.\n * `transaction: true` and the EntityManager is not already inside a\n * transaction). Ignored when joining an ambient transaction.\n */\n isolationLevel?: IsolationLevel\n /**\n * Optional label for diagnostics. Surfaced in the commit-boundary guard's\n * dev warning when a pending change set had to be flushed defensively, so the\n * offending command is identifiable.\n */\n label?: string\n}\n\ntype UnitOfWorkProbe = {\n computeChangeSets?: () => void\n getChangeSets?: () => ReadonlyArray<unknown>\n}\n\ntype FlushGuardEntityManager = {\n flush: () => Promise<void>\n getUnitOfWork?: () => UnitOfWorkProbe | undefined\n}\n\n/**\n * Commit-boundary safety net.\n *\n * After every phase has run and flushed, this asserts the UnitOfWork holds NO\n * pending change sets before the transaction commits. If it still does \u2014 a phase\n * mutated a managed entity AFTER its own per-phase flush boundary (the exact\n * shape that silently drops a scalar UPDATE under MikroORM v7) \u2014 the guard\n * flushes those changes defensively so the write can never be lost, and warns in\n * non-production so the latent ordering bug gets fixed at the source.\n *\n * Detection is best-effort and fail-safe: it only issues the extra flush when it\n * can PROVE the UnitOfWork is dirty (`computeChangeSets()` \u2192 `getChangeSets()`\n * non-empty). On EntityManagers that don't expose a UnitOfWork (partial/mock EMs\n * in unit tests) it does nothing \u2014 the per-phase flushes already ran \u2014 so it\n * never double-flushes a clean unit of work and never changes flush counts for\n * callers that were already correct.\n */\nasync function flushPendingChangesGuard(\n em: FlushGuardEntityManager,\n label?: string,\n): Promise<void> {\n let pendingCount = -1\n try {\n const uow = typeof em.getUnitOfWork === 'function' ? em.getUnitOfWork() : undefined\n if (uow && typeof uow.computeChangeSets === 'function' && typeof uow.getChangeSets === 'function') {\n uow.computeChangeSets()\n pendingCount = uow.getChangeSets().length\n }\n } catch {\n // Probing the UnitOfWork must never break a command; fall back to \"unknown\".\n pendingCount = -1\n }\n\n if (pendingCount > 0) {\n await em.flush()\n if (process.env.NODE_ENV !== 'production') {\n const where = label ? ` (${label})` : ''\n console.warn(\n `[withAtomicFlush]${where}: ${pendingCount} pending change-set(s) remained at the commit boundary and were flushed defensively. ` +\n 'A phase mutated a managed entity after its flush boundary \u2014 split the mutation and any dependent read/sync into separate phases so the change is never at risk of being dropped.',\n )\n }\n }\n}\n\n/**\n * Wraps multiple mutation phases in a single atomic flush.\n *\n * Prevents partial commits when a command mutates entities across\n * multiple phases (e.g., scalar mutations + relation syncs).\n * Each phase function runs sequentially; a single `em.flush()`\n * commits all changes at the end on the same `EntityManager` the\n * phases mutate, so closures over `em` stay valid.\n *\n * When `options.transaction` is true, the whole sequence runs\n * inside a database transaction for all-or-nothing semantics.\n *\n * ## Re-entrancy / composability\n *\n * `withAtomicFlush({ transaction: true })` is safe to nest. If the\n * supplied `EntityManager` is **already inside a transaction**, this\n * call does NOT open a second one (raw `em.begin()` would clobber the\n * active `#transactionContext` and orphan the outer transaction \u2014 unlike\n * `em.transactional()`, MikroORM's `em.begin()` does not check\n * `isInTransaction()`). Instead it joins the ambient transaction: the\n * phases run and flush within it, and the outermost caller owns the final\n * `commit()` / `rollback()`. A phase error therefore rolls back the entire\n * enclosing transaction (all-or-nothing across the whole nest).\n *\n * This mirrors the contract every command relies on: each command forks\n * the request `EntityManager` first, so the common case opens a fresh\n * top-level transaction; nesting only happens when one transactional unit\n * is composed inside another on the same `em`.\n *\n * When `phases` is empty the call is a true no-op \u2014 no flush,\n * no transaction. Callers that need an explicit commit should\n * pass at least one phase.\n *\n * Keep side-effect emissions (`emitCrudSideEffects` etc.) OUTSIDE\n * the `withAtomicFlush` block \u2014 they should only fire after commit.\n */\nexport async function withAtomicFlush(\n em: EntityManager,\n phases: Array<() => void | Promise<void>>,\n options?: AtomicFlushOptions,\n): Promise<void> {\n if (phases.length === 0) return\n\n // SPEC-018: the phases ARE flush boundaries \u2014 flush AFTER EACH phase, not once\n // at the end. A phase's scalar mutations must be persisted before the NEXT\n // phase runs any query (em.find / findOne / nativeUpdate / a sync helper);\n // otherwise the interleaved read resets MikroORM v7's identity-map changeset\n // and the pending scalar UPDATE is silently dropped (the #2453 family). This\n // is the framework-level guarantee that lets commands keep mutations and the\n // reads that depend on them in separate phases without hand-rolled flushes.\n //\n // Atomicity is preserved: when `transaction: true` (or an ambient transaction\n // is joined), each `em.flush()` only emits SQL inside the open transaction \u2014\n // the single commit/rollback below still spans every phase, so a later-phase\n // failure rolls back all earlier phases. Without a transaction the helper\n // keeps its documented \"each phase flushes independently\" behavior.\n //\n // Commit-boundary guarantee: after the last phase flush, `flushPendingChangesGuard`\n // re-checks the UnitOfWork and flushes once more if ANY pending change set remains\n // (a phase mutated state after its boundary). The transaction therefore can never\n // commit with unflushed work \u2014 if a per-phase flush was missed \"for some reason\",\n // the guard catches it inside the same transaction and warns in dev.\n const runPhasesAndFlush = async () => {\n for (const phase of phases) {\n await phase()\n await em.flush()\n }\n await flushPendingChangesGuard(em as unknown as FlushGuardEntityManager, options?.label)\n }\n\n if (!options?.transaction) {\n await runPhasesAndFlush()\n return\n }\n\n // Re-entrancy guard: never open a nested transaction with raw begin/commit.\n // If a transaction is already active on this EntityManager, join it \u2014 the\n // outermost caller owns commit/rollback. A phase error propagates and rolls\n // back the whole enclosing transaction.\n //\n // Guard the probe: real MikroORM EntityManagers always implement\n // `isInTransaction()`, but partial / mock EMs may not. A missing method is\n // treated as \"not in a transaction\", so this call opens its own top-level\n // transaction via the begin/commit path below (which those EMs do support).\n const isInTransaction = (em as { isInTransaction?: () => boolean }).isInTransaction\n if (typeof isInTransaction === 'function' && isInTransaction.call(em)) {\n await runPhasesAndFlush()\n return\n }\n\n await em.begin(options.isolationLevel ? { isolationLevel: options.isolationLevel } : undefined)\n try {\n await runPhasesAndFlush()\n await em.commit()\n } catch (err) {\n try {\n await em.rollback()\n } catch {\n // rollback failure should not mask the original error; intentionally swallowed\n }\n throw err\n }\n}\n"],
|
|
5
|
+
"mappings": "AAuDA,eAAe,yBACb,IACA,OACe;AACf,MAAI,eAAe;AACnB,MAAI;AACF,UAAM,MAAM,OAAO,GAAG,kBAAkB,aAAa,GAAG,cAAc,IAAI;AAC1E,QAAI,OAAO,OAAO,IAAI,sBAAsB,cAAc,OAAO,IAAI,kBAAkB,YAAY;AACjG,UAAI,kBAAkB;AACtB,qBAAe,IAAI,cAAc,EAAE;AAAA,IACrC;AAAA,EACF,QAAQ;AAEN,mBAAe;AAAA,EACjB;AAEA,MAAI,eAAe,GAAG;AACpB,UAAM,GAAG,MAAM;AACf,QAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,YAAM,QAAQ,QAAQ,KAAK,KAAK,MAAM;AACtC,cAAQ;AAAA,QACN,oBAAoB,KAAK,KAAK,YAAY;AAAA,MAE5C;AAAA,IACF;AAAA,EACF;AACF;AAsCA,eAAsB,gBACpB,IACA,QACA,SACe;AACf,MAAI,OAAO,WAAW,EAAG;AAqBzB,QAAM,oBAAoB,YAAY;AACpC,eAAW,SAAS,QAAQ;AAC1B,YAAM,MAAM;AACZ,YAAM,GAAG,MAAM;AAAA,IACjB;AACA,UAAM,yBAAyB,IAA0C,SAAS,KAAK;AAAA,EACzF;AAEA,MAAI,CAAC,SAAS,aAAa;AACzB,UAAM,kBAAkB;AACxB;AAAA,EACF;AAWA,QAAM,kBAAmB,GAA2C;AACpE,MAAI,OAAO,oBAAoB,cAAc,gBAAgB,KAAK,EAAE,GAAG;AACrE,UAAM,kBAAkB;AACxB;AAAA,EACF;AAEA,QAAM,GAAG,MAAM,QAAQ,iBAAiB,EAAE,gBAAgB,QAAQ,eAAe,IAAI,MAAS;AAC9F,MAAI;AACF,UAAM,kBAAkB;AACxB,UAAM,GAAG,OAAO;AAAA,EAClB,SAAS,KAAK;AACZ,QAAI;AACF,YAAM,GAAG,SAAS;AAAA,IACpB,QAAQ;AAAA,IAER;AACA,UAAM;AAAA,EACR;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/crud/factory.js
CHANGED
|
@@ -48,6 +48,8 @@ import { runApiInterceptorsAfter, runApiInterceptorsBefore } from "./interceptor
|
|
|
48
48
|
import { mergeIdFilter, parseIdsParam } from "./ids.js";
|
|
49
49
|
import { mergeAdvancedFilters } from "./advanced-filter-integration.js";
|
|
50
50
|
import { parseExtensionHeaders } from "../umes/extension-headers.js";
|
|
51
|
+
import { createGenericOptimisticLockReader } from "./optimistic-lock.js";
|
|
52
|
+
import { registerOptimisticLockReaderIfAbsent } from "./optimistic-lock-store.js";
|
|
51
53
|
function resolveSortParams(queryParams) {
|
|
52
54
|
const rawSortField = queryParams.sortField ?? queryParams.sort ?? "id";
|
|
53
55
|
const rawSortDir = queryParams.sortDir ?? queryParams.order ?? "asc";
|
|
@@ -586,6 +588,20 @@ function makeCrudRoute(opts) {
|
|
|
586
588
|
const resourceKind = resourceInfo.primary;
|
|
587
589
|
const resourceAliases = resourceInfo.aliases;
|
|
588
590
|
const resourceTargets = expandResourceAliases(resourceKind, resourceAliases);
|
|
591
|
+
if (ormCfg.entity && resourceKind && resourceKind !== "resource") {
|
|
592
|
+
const genericReader = createGenericOptimisticLockReader({
|
|
593
|
+
entity: ormCfg.entity,
|
|
594
|
+
idField: ormCfg.idField ?? "id",
|
|
595
|
+
tenantField: ormCfg.tenantField,
|
|
596
|
+
orgField: ormCfg.orgField,
|
|
597
|
+
softDeleteField: ormCfg.softDeleteField
|
|
598
|
+
});
|
|
599
|
+
const keysToRegister = { [resourceKind]: genericReader };
|
|
600
|
+
for (const alias of resourceAliases) {
|
|
601
|
+
if (alias && alias !== resourceKind) keysToRegister[alias] = genericReader;
|
|
602
|
+
}
|
|
603
|
+
registerOptimisticLockReaderIfAbsent(keysToRegister);
|
|
604
|
+
}
|
|
589
605
|
const defaultIdentifierResolver = (entity, _action) => {
|
|
590
606
|
const id = normalizeIdentifierValue(entity[ormCfg.idField]);
|
|
591
607
|
const orgId = ormCfg.orgField ? normalizeIdentifierValue(entity[ormCfg.orgField]) : null;
|