@open-mercato/core 0.6.6-develop.6304.1.4cf2b975cb → 0.6.6-develop.6309.1.983aeec27a
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/dist/modules/customers/data/guards.js +10 -1
- package/dist/modules/customers/data/guards.js.map +2 -2
- package/dist/modules/sales/commands/configuration.js +8 -8
- package/dist/modules/sales/commands/configuration.js.map +2 -2
- package/dist/modules/sales/commands/documentAddresses.js +8 -7
- package/dist/modules/sales/commands/documentAddresses.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +120 -42
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/notes.js +8 -8
- package/dist/modules/sales/commands/notes.js.map +2 -2
- package/dist/modules/sales/commands/payments.js +16 -10
- package/dist/modules/sales/commands/payments.js.map +2 -2
- package/dist/modules/sales/commands/shipments.js +7 -7
- package/dist/modules/sales/commands/shipments.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/data/guards.ts +29 -0
- package/src/modules/sales/commands/configuration.ts +8 -8
- package/src/modules/sales/commands/documentAddresses.ts +8 -7
- package/src/modules/sales/commands/documents.ts +120 -42
- package/src/modules/sales/commands/notes.ts +8 -8
- package/src/modules/sales/commands/payments.ts +13 -18
- package/src/modules/sales/commands/shipments.ts +11 -7
|
@@ -12,6 +12,13 @@ function normalizeIsoToken(raw) {
|
|
|
12
12
|
if (!Number.isFinite(ms)) return null;
|
|
13
13
|
return new Date(ms).toISOString();
|
|
14
14
|
}
|
|
15
|
+
const RECORD_LOCK_RESOLUTION_HEADER_NAME = "x-om-record-lock-resolution";
|
|
16
|
+
const RECORD_LOCK_OVERRIDE_RESOLUTIONS = /* @__PURE__ */ new Set(["accept_mine", "merged"]);
|
|
17
|
+
function isAuthorizedRecordLockOverride(headers) {
|
|
18
|
+
const raw = headers.get(RECORD_LOCK_RESOLUTION_HEADER_NAME);
|
|
19
|
+
if (typeof raw !== "string") return false;
|
|
20
|
+
return RECORD_LOCK_OVERRIDE_RESOLUTIONS.has(raw.trim().toLowerCase());
|
|
21
|
+
}
|
|
15
22
|
const optimisticLockGuard = {
|
|
16
23
|
id: "customers.optimistic-lock",
|
|
17
24
|
targetEntity: "*",
|
|
@@ -20,6 +27,7 @@ const optimisticLockGuard = {
|
|
|
20
27
|
async validate(input) {
|
|
21
28
|
const config = parseOptimisticLockEnv(process.env[OPTIMISTIC_LOCK_ENV_VAR]);
|
|
22
29
|
if (config.mode === "off") return { ok: true };
|
|
30
|
+
if (isAuthorizedRecordLockOverride(input.requestHeaders)) return { ok: true };
|
|
23
31
|
const enabled = config.mode === "all" || config.entities.has(input.resourceKind.toLowerCase());
|
|
24
32
|
if (!enabled) return { ok: true };
|
|
25
33
|
const readers = getAllOptimisticLockReaders();
|
|
@@ -61,6 +69,7 @@ const optimisticLockGuard = {
|
|
|
61
69
|
};
|
|
62
70
|
const guards = [optimisticLockGuard];
|
|
63
71
|
export {
|
|
64
|
-
guards
|
|
72
|
+
guards,
|
|
73
|
+
isAuthorizedRecordLockOverride
|
|
65
74
|
};
|
|
66
75
|
//# sourceMappingURL=guards.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/data/guards.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { MutationGuard } from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { parseOptimisticLockEnv } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport {\n OPTIMISTIC_LOCK_CONFLICT_CODE,\n OPTIMISTIC_LOCK_CONFLICT_ERROR,\n OPTIMISTIC_LOCK_ENV_VAR,\n OPTIMISTIC_LOCK_HEADER_NAME,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\n\nfunction normalizeIsoToken(raw: string): string | null {\n const ms = Date.parse(raw)\n if (!Number.isFinite(ms)) return null\n return new Date(ms).toISOString()\n}\n\nconst optimisticLockGuard: MutationGuard = {\n id: 'customers.optimistic-lock',\n targetEntity: '*',\n operations: ['update', 'delete'],\n priority: 100,\n async validate(input) {\n const config = parseOptimisticLockEnv(process.env[OPTIMISTIC_LOCK_ENV_VAR])\n if (config.mode === 'off') return { ok: true }\n const enabled = config.mode === 'all' || config.entities.has(input.resourceKind.toLowerCase())\n if (!enabled) return { ok: true }\n const readers = getAllOptimisticLockReaders()\n const reader = readers[input.resourceKind]\n if (!reader) return { ok: true }\n const expectedRaw = input.requestHeaders.get(OPTIMISTIC_LOCK_HEADER_NAME)\n if (!expectedRaw || expectedRaw.trim().length === 0) return { ok: true }\n const expectedIso = normalizeIsoToken(expectedRaw.trim())\n if (!expectedIso) return { ok: true }\n if (!input.resourceId) return { ok: true }\n const container = await createRequestContainer()\n let em: EntityManager\n try {\n em = container.resolve('em') as EntityManager\n } catch {\n return { ok: true }\n }\n const currentRaw = await reader(em, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n })\n if (currentRaw == null) return { ok: true }\n const currentIso = normalizeIsoToken(currentRaw)\n if (currentIso == null) return { ok: true }\n if (currentIso === expectedIso) return { ok: true }\n return {\n ok: false,\n status: 409,\n body: {\n error: OPTIMISTIC_LOCK_CONFLICT_ERROR,\n code: OPTIMISTIC_LOCK_CONFLICT_CODE,\n currentUpdatedAt: currentIso,\n expectedUpdatedAt: expectedIso,\n },\n }\n },\n}\n\nexport const guards: MutationGuard[] = [optimisticLockGuard]\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AACvC,SAAS,mCAAmC;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,kBAAkB,KAA4B;AACrD,QAAM,KAAK,KAAK,MAAM,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO;AACjC,SAAO,IAAI,KAAK,EAAE,EAAE,YAAY;AAClC;AAEA,MAAM,sBAAqC;AAAA,EACzC,IAAI;AAAA,EACJ,cAAc;AAAA,EACd,YAAY,CAAC,UAAU,QAAQ;AAAA,EAC/B,UAAU;AAAA,EACV,MAAM,SAAS,OAAO;AACpB,UAAM,SAAS,uBAAuB,QAAQ,IAAI,uBAAuB,CAAC;AAC1E,QAAI,OAAO,SAAS,MAAO,QAAO,EAAE,IAAI,KAAK;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { MutationGuard } from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { parseOptimisticLockEnv } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport {\n OPTIMISTIC_LOCK_CONFLICT_CODE,\n OPTIMISTIC_LOCK_CONFLICT_ERROR,\n OPTIMISTIC_LOCK_ENV_VAR,\n OPTIMISTIC_LOCK_HEADER_NAME,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\n\nfunction normalizeIsoToken(raw: string): string | null {\n const ms = Date.parse(raw)\n if (!Number.isFinite(ms)) return null\n return new Date(ms).toISOString()\n}\n\n/**\n * HTTP header carrying the enterprise `record_locks` conflict resolution the\n * client chose in the merge dialog. This guard only reads the header value off\n * the request (no enterprise import) \u2014 it is the integration contract between\n * the record-lock layer and this universal optimistic-lock floor.\n */\nconst RECORD_LOCK_RESOLUTION_HEADER_NAME = 'x-om-record-lock-resolution'\n\n/**\n * Resolutions that express an explicit, privileged intent to OVERWRITE the\n * incoming concurrent write (\"Keep mine\" / merged). When the request asserts\n * one of these, the record-lock guard (which runs first, at priority 0) has\n * already authorized the override against `canOverrideIncoming`, so the\n * stale-`updated_at` floor MUST defer to it \u2014 otherwise it rejects exactly the\n * overwrite the user just authorized, looping the merge dialog on a fresh 409\n * (issue #3601). `accept_incoming` is intentionally NOT here: that path reloads\n * with a fresh `updated_at` and must still pass through the floor normally.\n */\nconst RECORD_LOCK_OVERRIDE_RESOLUTIONS: ReadonlySet<string> = new Set(['accept_mine', 'merged'])\n\nexport function isAuthorizedRecordLockOverride(headers: Headers): boolean {\n const raw = headers.get(RECORD_LOCK_RESOLUTION_HEADER_NAME)\n if (typeof raw !== 'string') return false\n return RECORD_LOCK_OVERRIDE_RESOLUTIONS.has(raw.trim().toLowerCase())\n}\n\nconst optimisticLockGuard: MutationGuard = {\n id: 'customers.optimistic-lock',\n targetEntity: '*',\n operations: ['update', 'delete'],\n priority: 100,\n async validate(input) {\n const config = parseOptimisticLockEnv(process.env[OPTIMISTIC_LOCK_ENV_VAR])\n if (config.mode === 'off') return { ok: true }\n // A privileged record-lock \"Keep mine\" override deliberately overwrites the\n // concurrent write; the floor must not block it on the now-stale timestamp.\n if (isAuthorizedRecordLockOverride(input.requestHeaders)) return { ok: true }\n const enabled = config.mode === 'all' || config.entities.has(input.resourceKind.toLowerCase())\n if (!enabled) return { ok: true }\n const readers = getAllOptimisticLockReaders()\n const reader = readers[input.resourceKind]\n if (!reader) return { ok: true }\n const expectedRaw = input.requestHeaders.get(OPTIMISTIC_LOCK_HEADER_NAME)\n if (!expectedRaw || expectedRaw.trim().length === 0) return { ok: true }\n const expectedIso = normalizeIsoToken(expectedRaw.trim())\n if (!expectedIso) return { ok: true }\n if (!input.resourceId) return { ok: true }\n const container = await createRequestContainer()\n let em: EntityManager\n try {\n em = container.resolve('em') as EntityManager\n } catch {\n return { ok: true }\n }\n const currentRaw = await reader(em, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n })\n if (currentRaw == null) return { ok: true }\n const currentIso = normalizeIsoToken(currentRaw)\n if (currentIso == null) return { ok: true }\n if (currentIso === expectedIso) return { ok: true }\n return {\n ok: false,\n status: 409,\n body: {\n error: OPTIMISTIC_LOCK_CONFLICT_ERROR,\n code: OPTIMISTIC_LOCK_CONFLICT_CODE,\n currentUpdatedAt: currentIso,\n expectedUpdatedAt: expectedIso,\n },\n }\n },\n}\n\nexport const guards: MutationGuard[] = [optimisticLockGuard]\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AACvC,SAAS,mCAAmC;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,kBAAkB,KAA4B;AACrD,QAAM,KAAK,KAAK,MAAM,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO;AACjC,SAAO,IAAI,KAAK,EAAE,EAAE,YAAY;AAClC;AAQA,MAAM,qCAAqC;AAY3C,MAAM,mCAAwD,oBAAI,IAAI,CAAC,eAAe,QAAQ,CAAC;AAExF,SAAS,+BAA+B,SAA2B;AACxE,QAAM,MAAM,QAAQ,IAAI,kCAAkC;AAC1D,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,SAAO,iCAAiC,IAAI,IAAI,KAAK,EAAE,YAAY,CAAC;AACtE;AAEA,MAAM,sBAAqC;AAAA,EACzC,IAAI;AAAA,EACJ,cAAc;AAAA,EACd,YAAY,CAAC,UAAU,QAAQ;AAAA,EAC/B,UAAU;AAAA,EACV,MAAM,SAAS,OAAO;AACpB,UAAM,SAAS,uBAAuB,QAAQ,IAAI,uBAAuB,CAAC;AAC1E,QAAI,OAAO,SAAS,MAAO,QAAO,EAAE,IAAI,KAAK;AAG7C,QAAI,+BAA+B,MAAM,cAAc,EAAG,QAAO,EAAE,IAAI,KAAK;AAC5E,UAAM,UAAU,OAAO,SAAS,SAAS,OAAO,SAAS,IAAI,MAAM,aAAa,YAAY,CAAC;AAC7F,QAAI,CAAC,QAAS,QAAO,EAAE,IAAI,KAAK;AAChC,UAAM,UAAU,4BAA4B;AAC5C,UAAM,SAAS,QAAQ,MAAM,YAAY;AACzC,QAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,KAAK;AAC/B,UAAM,cAAc,MAAM,eAAe,IAAI,2BAA2B;AACxE,QAAI,CAAC,eAAe,YAAY,KAAK,EAAE,WAAW,EAAG,QAAO,EAAE,IAAI,KAAK;AACvE,UAAM,cAAc,kBAAkB,YAAY,KAAK,CAAC;AACxD,QAAI,CAAC,YAAa,QAAO,EAAE,IAAI,KAAK;AACpC,QAAI,CAAC,MAAM,WAAY,QAAO,EAAE,IAAI,KAAK;AACzC,UAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAI;AACJ,QAAI;AACF,WAAK,UAAU,QAAQ,IAAI;AAAA,IAC7B,QAAQ;AACN,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AACA,UAAM,aAAa,MAAM,OAAO,IAAI;AAAA,MAClC,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM;AAAA,MAClB,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,IAC1C,CAAC;AACD,QAAI,cAAc,KAAM,QAAO,EAAE,IAAI,KAAK;AAC1C,UAAM,aAAa,kBAAkB,UAAU;AAC/C,QAAI,cAAc,KAAM,QAAO,EAAE,IAAI,KAAK;AAC1C,QAAI,eAAe,YAAa,QAAO,EAAE,IAAI,KAAK;AAClD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,kBAAkB;AAAA,QAClB,mBAAmB;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,SAA0B,CAAC,mBAAmB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -71,7 +71,7 @@ const channelCrudIndexer = {
|
|
|
71
71
|
entityType: E.sales.sales_channel
|
|
72
72
|
};
|
|
73
73
|
async function loadChannelSnapshot(em, id) {
|
|
74
|
-
const channel = await em
|
|
74
|
+
const channel = await findOneWithDecryption(em, SalesChannel, { id, deletedAt: null }, {});
|
|
75
75
|
if (!channel) return null;
|
|
76
76
|
const custom = await loadCustomFieldSnapshot(em, {
|
|
77
77
|
entityId: E.sales.sales_channel,
|
|
@@ -504,7 +504,7 @@ const createChannelCommand = {
|
|
|
504
504
|
const after = payload?.after;
|
|
505
505
|
if (!after) return;
|
|
506
506
|
const em = ctx.container.resolve("em").fork();
|
|
507
|
-
const record = await em
|
|
507
|
+
const record = await findOneWithDecryption(em, SalesChannel, { id: after.id }, {}, { tenantId: after.tenantId, organizationId: after.organizationId });
|
|
508
508
|
if (!record) return;
|
|
509
509
|
ensureTenantScope(ctx, record.tenantId);
|
|
510
510
|
ensureOrganizationScope(ctx, record.organizationId);
|
|
@@ -578,7 +578,7 @@ const updateChannelCommand = {
|
|
|
578
578
|
async execute(rawInput, ctx) {
|
|
579
579
|
const { parsed, custom } = parseWithCustomFields(channelUpdateSchema, rawInput);
|
|
580
580
|
const em = ctx.container.resolve("em").fork();
|
|
581
|
-
const record = await em
|
|
581
|
+
const record = await findOneWithDecryption(em, SalesChannel, { id: parsed.id, deletedAt: null }, {}, { tenantId: parsed.tenantId, organizationId: parsed.organizationId });
|
|
582
582
|
if (!record) throw new CrudHttpError(404, { error: "Channel not found" });
|
|
583
583
|
const dataEngine = ctx.container.resolve("dataEngine");
|
|
584
584
|
const scope = resolveScopeFromUpdate(record, parsed, ctx);
|
|
@@ -670,7 +670,7 @@ const updateChannelCommand = {
|
|
|
670
670
|
const before = payload?.before;
|
|
671
671
|
if (!before) return;
|
|
672
672
|
const em = ctx.container.resolve("em").fork();
|
|
673
|
-
let record = await em
|
|
673
|
+
let record = await findOneWithDecryption(em, SalesChannel, { id: before.id }, {}, { tenantId: before.tenantId, organizationId: before.organizationId });
|
|
674
674
|
if (!record) {
|
|
675
675
|
record = em.create(SalesChannel, channelSeedFromSnapshot(before));
|
|
676
676
|
em.persist(record);
|
|
@@ -721,7 +721,7 @@ const deleteChannelCommand = {
|
|
|
721
721
|
async execute(input, ctx) {
|
|
722
722
|
const id = requireId(input, "Channel id is required");
|
|
723
723
|
const em = ctx.container.resolve("em").fork();
|
|
724
|
-
const record = await em
|
|
724
|
+
const record = await findOneWithDecryption(em, SalesChannel, { id }, {});
|
|
725
725
|
if (!record) throw new CrudHttpError(404, { error: "Channel not found" });
|
|
726
726
|
ensureTenantScope(ctx, record.tenantId);
|
|
727
727
|
ensureOrganizationScope(ctx, record.organizationId);
|
|
@@ -765,7 +765,7 @@ const deleteChannelCommand = {
|
|
|
765
765
|
const before = payload?.before;
|
|
766
766
|
if (!before) return;
|
|
767
767
|
const em = ctx.container.resolve("em").fork();
|
|
768
|
-
let record = await em
|
|
768
|
+
let record = await findOneWithDecryption(em, SalesChannel, { id: before.id }, {}, { tenantId: before.tenantId, organizationId: before.organizationId });
|
|
769
769
|
if (!record) {
|
|
770
770
|
record = em.create(SalesChannel, channelSeedFromSnapshot(before));
|
|
771
771
|
em.persist(record);
|
|
@@ -1615,7 +1615,7 @@ const createTaxRateCommand = {
|
|
|
1615
1615
|
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
1616
1616
|
const em = ctx.container.resolve("em").fork();
|
|
1617
1617
|
if (parsed.channelId) {
|
|
1618
|
-
const channel = await em
|
|
1618
|
+
const channel = await findOneWithDecryption(em, SalesChannel, { id: parsed.channelId, deletedAt: null }, {}, { tenantId: parsed.tenantId, organizationId: parsed.organizationId });
|
|
1619
1619
|
const channelInScope = assertFound(channel, "Channel not found for tax rate");
|
|
1620
1620
|
ensureSameScope(channelInScope, parsed.organizationId, parsed.tenantId);
|
|
1621
1621
|
}
|
|
@@ -1745,7 +1745,7 @@ const updateTaxRateCommand = {
|
|
|
1745
1745
|
record.organizationId = scope.organizationId;
|
|
1746
1746
|
record.tenantId = scope.tenantId;
|
|
1747
1747
|
if (parsed.channelId !== void 0 && parsed.channelId !== null) {
|
|
1748
|
-
const channel = await em
|
|
1748
|
+
const channel = await findOneWithDecryption(em, SalesChannel, { id: parsed.channelId, deletedAt: null }, {}, { tenantId: record.tenantId, organizationId: record.organizationId });
|
|
1749
1749
|
const channelInScope = assertFound(channel, "Channel not found for tax rate");
|
|
1750
1750
|
ensureSameScope(channelInScope, record.organizationId, record.tenantId);
|
|
1751
1751
|
}
|