@open-mercato/shared 0.6.4-develop.4210.1.d412061cfe → 0.6.4-develop.4236.1.9fa6806b34

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.
@@ -20,6 +20,10 @@ import {
20
20
  applyCustomFieldsNormalization,
21
21
  loadCustomFieldDefinitionIndex
22
22
  } from "./custom-fields.js";
23
+ import {
24
+ canReuseCustomFieldDefinitions,
25
+ resolveCfDefIndexOrgCandidates
26
+ } from "./custom-field-definition-index.js";
23
27
  import { serializeExport, normalizeExportFormat, defaultExportFilename, ensureColumns } from "./exporters.js";
24
28
  import { isCrudHttpError } from "./errors.js";
25
29
  import {
@@ -612,7 +616,7 @@ function makeCrudRoute(opts) {
612
616
  }
613
617
  return null;
614
618
  };
615
- const decorateItemsWithCustomFields = async (items, ctx) => {
619
+ const decorateItemsWithCustomFields = async (items, ctx, precomputedDefinitions) => {
616
620
  if (!listCustomFieldDecorator || !Array.isArray(items) || items.length === 0) return items;
617
621
  const entityIds = Array.isArray(listCustomFieldDecorator.entityIds) ? listCustomFieldDecorator.entityIds : [listCustomFieldDecorator.entityIds];
618
622
  if (!entityIds.length) return items;
@@ -627,29 +631,41 @@ function makeCrudRoute(opts) {
627
631
  try {
628
632
  const em = ctx.container.resolve("em");
629
633
  const organizationIds = Array.isArray(ctx.organizationIds) && ctx.organizationIds.length ? ctx.organizationIds : [ctx.selectedOrganizationId ?? null];
630
- let cfDefCache = null;
631
- try {
632
- cfDefCache = ctx.container.resolve("cache");
633
- } catch {
634
- }
635
- const definitionIndex = await loadCustomFieldDefinitionIndex({
636
- em,
637
- entityIds,
638
- tenantId: ctx.auth?.tenantId ?? null,
639
- organizationIds,
640
- cache: cfDefCache ?? null,
641
- requestScope: ctx
634
+ const tenantId = ctx.auth?.tenantId ?? null;
635
+ const reusable = canReuseCustomFieldDefinitions(precomputedDefinitions, {
636
+ entityIds: entityIds.map(String),
637
+ tenantId,
638
+ organizationIds: resolveCfDefIndexOrgCandidates(ctx.organizationIds, ctx.selectedOrganizationId ?? null)
642
639
  });
643
- cfProfiler.mark("definitions_loaded", { definitionCount: definitionIndex.size });
640
+ let definitionIndex;
641
+ if (reusable && precomputedDefinitions) {
642
+ definitionIndex = precomputedDefinitions.index;
643
+ cfProfiler.mark("definitions_reused", { definitionCount: definitionIndex.size });
644
+ } else {
645
+ let cfDefCache = null;
646
+ try {
647
+ cfDefCache = ctx.container.resolve("cache");
648
+ } catch {
649
+ }
650
+ definitionIndex = await loadCustomFieldDefinitionIndex({
651
+ em,
652
+ entityIds,
653
+ tenantId,
654
+ organizationIds,
655
+ cache: cfDefCache ?? null,
656
+ requestScope: ctx
657
+ });
658
+ cfProfiler.mark("definitions_loaded", { definitionCount: definitionIndex.size });
659
+ }
644
660
  const decoratedItems = items.map((raw) => {
645
661
  if (!raw || typeof raw !== "object") return raw;
646
662
  const item = raw;
647
663
  const context = listCustomFieldDecorator.resolveContext ? listCustomFieldDecorator.resolveContext(raw, ctx) ?? {} : {};
648
664
  const organizationId = context.organizationId ?? inferFieldValue(item, ["organization_id", "organizationId"]);
649
- const tenantId = context.tenantId ?? inferFieldValue(item, ["tenant_id", "tenantId"]) ?? ctx.auth?.tenantId ?? null;
665
+ const tenantId2 = context.tenantId ?? inferFieldValue(item, ["tenant_id", "tenantId"]) ?? ctx.auth?.tenantId ?? null;
650
666
  const decorated = decorateRecordWithCustomFields(item, definitionIndex, {
651
667
  organizationId: organizationId ?? null,
652
- tenantId: tenantId ?? null
668
+ tenantId: tenantId2 ?? null
653
669
  });
654
670
  return applyCustomFieldsNormalization(item, decorated, {
655
671
  stripPrefixedKeys: listCustomFieldDecorator.stripPrefixedKeys === true
@@ -1137,7 +1153,7 @@ function makeCrudRoute(opts) {
1137
1153
  const rawItems = res.items || [];
1138
1154
  let transformedItems = rawItems.map((i) => opts.list.transformItem ? opts.list.transformItem(i) : i);
1139
1155
  profiler.mark("transform_complete", { itemCount: transformedItems.length });
1140
- transformedItems = await decorateItemsWithCustomFields(transformedItems, ctx);
1156
+ transformedItems = await decorateItemsWithCustomFields(transformedItems, ctx, res.customFieldDefinitions);
1141
1157
  profiler.mark("custom_fields_complete", { itemCount: transformedItems.length });
1142
1158
  if (opts.list?.entityId && request) {
1143
1159
  try {
@@ -1190,7 +1206,7 @@ function makeCrudRoute(opts) {
1190
1206
  const nextItemsRaw = nextRes.items || [];
1191
1207
  if (!nextItemsRaw.length) break;
1192
1208
  let nextTransformed = nextItemsRaw.map((i) => opts.list.transformItem ? opts.list.transformItem(i) : i);
1193
- nextTransformed = await decorateItemsWithCustomFields(nextTransformed, ctx);
1209
+ nextTransformed = await decorateItemsWithCustomFields(nextTransformed, ctx, nextRes.customFieldDefinitions);
1194
1210
  const nextExportItems = exportFullRequested ? nextItemsRaw.map(normalizeFullRecordForExport) : nextTransformed;
1195
1211
  exportItems.push(...nextExportItems);
1196
1212
  if (nextExportItems.length < exportPageSizeNumber) break;
@@ -1487,9 +1503,9 @@ function makeCrudRoute(opts) {
1487
1503
  if (createLifecycleCmd.beforeEventId && ctx.auth.tenantId) {
1488
1504
  const syncSubs = collectSyncSubscribers(getAllSyncSubscribers(), createLifecycleCmd.beforeEventId);
1489
1505
  if (syncSubs.length) {
1490
- const em = ctx.container.resolve("em");
1506
+ const em2 = ctx.container.resolve("em");
1491
1507
  const syncPayload = buildSyncPayload(
1492
- { eventId: createLifecycleCmd.beforeEventId, entity: createLifecycleCmd.entity, operation: "create", timing: "before", resourceId: null, userId: ctx.auth.sub, organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null, tenantId: ctx.auth.tenantId, em, request },
1508
+ { eventId: createLifecycleCmd.beforeEventId, entity: createLifecycleCmd.entity, operation: "create", timing: "before", resourceId: null, userId: ctx.auth.sub, organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null, tenantId: ctx.auth.tenantId, em: em2, request },
1493
1509
  { payload: input2 && typeof input2 === "object" ? input2 : void 0 }
1494
1510
  );
1495
1511
  const syncResult = await runSyncBeforeEvent(syncSubs, syncPayload, ctx.container);
@@ -1512,9 +1528,9 @@ function makeCrudRoute(opts) {
1512
1528
  if (createLifecycleCmd.afterEventId && ctx.auth.tenantId) {
1513
1529
  const syncAfterSubs = collectSyncSubscribers(getAllSyncSubscribers(), createLifecycleCmd.afterEventId);
1514
1530
  if (syncAfterSubs.length) {
1515
- const em = ctx.container.resolve("em");
1531
+ const em2 = ctx.container.resolve("em");
1516
1532
  const syncPayload = buildSyncPayload(
1517
- { eventId: createLifecycleCmd.afterEventId, entity: createLifecycleCmd.entity, operation: "create", timing: "after", resourceId: result?.id ?? null, userId: ctx.auth.sub, organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null, tenantId: ctx.auth.tenantId, em, request },
1533
+ { eventId: createLifecycleCmd.afterEventId, entity: createLifecycleCmd.entity, operation: "create", timing: "after", resourceId: result?.id ?? null, userId: ctx.auth.sub, organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null, tenantId: ctx.auth.tenantId, em: em2, request },
1518
1534
  { payload: input2 && typeof input2 === "object" ? input2 : void 0 }
1519
1535
  );
1520
1536
  await runSyncAfterEvent(syncAfterSubs, syncPayload, ctx.container);
@@ -1562,9 +1578,9 @@ function makeCrudRoute(opts) {
1562
1578
  if (createLifecycle.beforeEventId && ctx.auth.tenantId) {
1563
1579
  const syncSubs = collectSyncSubscribers(getAllSyncSubscribers(), createLifecycle.beforeEventId);
1564
1580
  if (syncSubs.length) {
1565
- const em = ctx.container.resolve("em");
1581
+ const em2 = ctx.container.resolve("em");
1566
1582
  const syncPayload = buildSyncPayload(
1567
- { eventId: createLifecycle.beforeEventId, entity: createLifecycle.entity, operation: "create", timing: "before", resourceId: null, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em, request },
1583
+ { eventId: createLifecycle.beforeEventId, entity: createLifecycle.entity, operation: "create", timing: "before", resourceId: null, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em: em2, request },
1568
1584
  { payload: input && typeof input === "object" ? input : void 0 }
1569
1585
  );
1570
1586
  const syncResult = await runSyncBeforeEvent(syncSubs, syncPayload, ctx.container);
@@ -1612,21 +1628,26 @@ function makeCrudRoute(opts) {
1612
1628
  if (!ctx.auth.tenantId) return json({ error: "Tenant context is required" }, { status: 400 });
1613
1629
  entityData[ormCfg.tenantField] = ctx.auth.tenantId;
1614
1630
  }
1615
- const entity = await de.createOrmEntity({ entity: ormCfg.entity, data: entityData });
1616
- if (createConfig.customFields && createConfig.customFields.enabled) {
1617
- const cfc = createConfig.customFields;
1618
- const values = cfc.map ? cfc.map(body) : cfc.pickPrefixed ? extractCustomFieldValuesFromPayload(body) : {};
1619
- if (values && Object.keys(values).length > 0) {
1620
- const de2 = ctx.container.resolve("dataEngine");
1621
- await de2.setCustomFields({
1622
- entityId: cfc.entityId,
1623
- recordId: String(entity[ormCfg.idField]),
1624
- organizationId: targetOrgId,
1625
- tenantId: ctx.auth.tenantId,
1626
- values
1627
- });
1631
+ const em = ctx.container.resolve("em");
1632
+ const writeTenantId = ctx.auth.tenantId;
1633
+ const entity = await em.transactional(async () => {
1634
+ const created = await de.createOrmEntity({ entity: ormCfg.entity, data: entityData });
1635
+ if (createConfig.customFields && createConfig.customFields.enabled) {
1636
+ const cfc = createConfig.customFields;
1637
+ const values = cfc.map ? cfc.map(body) : cfc.pickPrefixed ? extractCustomFieldValuesFromPayload(body) : {};
1638
+ if (values && Object.keys(values).length > 0) {
1639
+ const de2 = ctx.container.resolve("dataEngine");
1640
+ await de2.setCustomFields({
1641
+ entityId: cfc.entityId,
1642
+ recordId: String(created[ormCfg.idField]),
1643
+ organizationId: targetOrgId,
1644
+ tenantId: writeTenantId,
1645
+ values
1646
+ });
1647
+ }
1628
1648
  }
1629
- }
1649
+ return created;
1650
+ });
1630
1651
  await opts.hooks?.afterCreate?.(entity, { ...ctx, input });
1631
1652
  const createdEntityId = String(entity[ormCfg.idField]);
1632
1653
  if (createGuardAfterCallbacks?.length && ctx.auth.tenantId) {
@@ -1644,9 +1665,9 @@ function makeCrudRoute(opts) {
1644
1665
  if (createLifecycle.afterEventId && ctx.auth.tenantId) {
1645
1666
  const syncAfterSubs = collectSyncSubscribers(getAllSyncSubscribers(), createLifecycle.afterEventId);
1646
1667
  if (syncAfterSubs.length) {
1647
- const em = ctx.container.resolve("em");
1668
+ const em2 = ctx.container.resolve("em");
1648
1669
  const syncPayload = buildSyncPayload(
1649
- { eventId: createLifecycle.afterEventId, entity: createLifecycle.entity, operation: "create", timing: "after", resourceId: createdEntityId, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em, request },
1670
+ { eventId: createLifecycle.afterEventId, entity: createLifecycle.entity, operation: "create", timing: "after", resourceId: createdEntityId, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em: em2, request },
1650
1671
  { payload: input && typeof input === "object" ? input : void 0, entityData: snapshotEntity(entity) }
1651
1672
  );
1652
1673
  await runSyncAfterEvent(syncAfterSubs, syncPayload, ctx.container);
@@ -1728,13 +1749,13 @@ function makeCrudRoute(opts) {
1728
1749
  if (updateLifecycleCmd.beforeEventId && ctx.auth.tenantId) {
1729
1750
  const syncSubs = collectSyncSubscribers(getAllSyncSubscribers(), updateLifecycleCmd.beforeEventId);
1730
1751
  if (syncSubs.length) {
1731
- const em = ctx.container.resolve("em");
1752
+ const em2 = ctx.container.resolve("em");
1732
1753
  if (candidateId) {
1733
- const prevEntity = await em.findOne(ormCfg.entity, { [ormCfg.idField]: candidateId });
1754
+ const prevEntity = await em2.findOne(ormCfg.entity, { [ormCfg.idField]: candidateId });
1734
1755
  if (prevEntity) cmdUpdatePreviousData = snapshotEntity(prevEntity);
1735
1756
  }
1736
1757
  const syncPayload = buildSyncPayload(
1737
- { eventId: updateLifecycleCmd.beforeEventId, entity: updateLifecycleCmd.entity, operation: "update", timing: "before", resourceId: candidateId ?? null, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em, request },
1758
+ { eventId: updateLifecycleCmd.beforeEventId, entity: updateLifecycleCmd.entity, operation: "update", timing: "before", resourceId: candidateId ?? null, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em: em2, request },
1738
1759
  { payload: input2 && typeof input2 === "object" ? input2 : void 0, previousData: cmdUpdatePreviousData }
1739
1760
  );
1740
1761
  const syncResult = await runSyncBeforeEvent(syncSubs, syncPayload, ctx.container);
@@ -1813,9 +1834,9 @@ function makeCrudRoute(opts) {
1813
1834
  if (updateLifecycleCmd.afterEventId && ctx.auth.tenantId) {
1814
1835
  const syncAfterSubs = collectSyncSubscribers(getAllSyncSubscribers(), updateLifecycleCmd.afterEventId);
1815
1836
  if (syncAfterSubs.length) {
1816
- const em = ctx.container.resolve("em");
1837
+ const em2 = ctx.container.resolve("em");
1817
1838
  const syncPayload = buildSyncPayload(
1818
- { eventId: updateLifecycleCmd.afterEventId, entity: updateLifecycleCmd.entity, operation: "update", timing: "after", resourceId: candidateId ?? null, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em, request },
1839
+ { eventId: updateLifecycleCmd.afterEventId, entity: updateLifecycleCmd.entity, operation: "update", timing: "after", resourceId: candidateId ?? null, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em: em2, request },
1819
1840
  { payload: input2 && typeof input2 === "object" ? input2 : void 0, previousData: cmdUpdatePreviousData }
1820
1841
  );
1821
1842
  await runSyncAfterEvent(syncAfterSubs, syncPayload, ctx.container);
@@ -1843,14 +1864,14 @@ function makeCrudRoute(opts) {
1843
1864
  if (updateLifecycle.beforeEventId && ctx.auth.tenantId) {
1844
1865
  const syncSubs = collectSyncSubscribers(getAllSyncSubscribers(), updateLifecycle.beforeEventId);
1845
1866
  if (syncSubs.length) {
1846
- const em = ctx.container.resolve("em");
1867
+ const em2 = ctx.container.resolve("em");
1847
1868
  const updateCandidateId = input?.id ?? null;
1848
1869
  if (updateCandidateId) {
1849
- const prevEntity = await em.findOne(ormCfg.entity, { [ormCfg.idField]: updateCandidateId });
1870
+ const prevEntity = await em2.findOne(ormCfg.entity, { [ormCfg.idField]: updateCandidateId });
1850
1871
  if (prevEntity) updatePreviousData = snapshotEntity(prevEntity);
1851
1872
  }
1852
1873
  const syncPayload = buildSyncPayload(
1853
- { eventId: updateLifecycle.beforeEventId, entity: updateLifecycle.entity, operation: "update", timing: "before", resourceId: updateCandidateId, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em, request },
1874
+ { eventId: updateLifecycle.beforeEventId, entity: updateLifecycle.entity, operation: "update", timing: "before", resourceId: updateCandidateId, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em: em2, request },
1854
1875
  { payload: input && typeof input === "object" ? input : void 0, previousData: updatePreviousData }
1855
1876
  );
1856
1877
  const syncResult = await runSyncBeforeEvent(syncSubs, syncPayload, ctx.container);
@@ -1903,26 +1924,32 @@ function makeCrudRoute(opts) {
1903
1924
  softDeleteField: ormCfg.softDeleteField
1904
1925
  }
1905
1926
  );
1906
- const entity = await de.updateOrmEntity({
1907
- entity: ormCfg.entity,
1908
- where,
1909
- apply: (e) => updateConfig.applyToEntity(e, input, ctx)
1927
+ const em = ctx.container.resolve("em");
1928
+ const writeTenantId = ctx.auth.tenantId;
1929
+ const entity = await em.transactional(async () => {
1930
+ const updated = await de.updateOrmEntity({
1931
+ entity: ormCfg.entity,
1932
+ where,
1933
+ apply: (e) => updateConfig.applyToEntity(e, input, ctx)
1934
+ });
1935
+ if (!updated) return null;
1936
+ if (updateConfig.customFields && updateConfig.customFields.enabled) {
1937
+ const cfc = updateConfig.customFields;
1938
+ const values = cfc.map ? cfc.map(body) : cfc.pickPrefixed ? extractCustomFieldValuesFromPayload(body) : {};
1939
+ if (values && Object.keys(values).length > 0) {
1940
+ const de2 = ctx.container.resolve("dataEngine");
1941
+ await de2.setCustomFields({
1942
+ entityId: cfc.entityId,
1943
+ recordId: String(updated[ormCfg.idField]),
1944
+ organizationId: targetOrgId,
1945
+ tenantId: writeTenantId,
1946
+ values
1947
+ });
1948
+ }
1949
+ }
1950
+ return updated;
1910
1951
  });
1911
1952
  if (!entity) return json({ error: "Not found" }, { status: 404 });
1912
- if (updateConfig.customFields && updateConfig.customFields.enabled) {
1913
- const cfc = updateConfig.customFields;
1914
- const values = cfc.map ? cfc.map(body) : cfc.pickPrefixed ? extractCustomFieldValuesFromPayload(body) : {};
1915
- if (values && Object.keys(values).length > 0) {
1916
- const de2 = ctx.container.resolve("dataEngine");
1917
- await de2.setCustomFields({
1918
- entityId: cfc.entityId,
1919
- recordId: String(entity[ormCfg.idField]),
1920
- organizationId: targetOrgId,
1921
- tenantId: ctx.auth.tenantId,
1922
- values
1923
- });
1924
- }
1925
- }
1926
1953
  await opts.hooks?.afterUpdate?.(entity, { ...ctx, input });
1927
1954
  if (updateGuardAfterCallbacks.length && ctx.auth.tenantId) {
1928
1955
  await runGuardAfterSuccessCallbacks(updateGuardAfterCallbacks, {
@@ -1939,9 +1966,9 @@ function makeCrudRoute(opts) {
1939
1966
  if (updateLifecycle.afterEventId && ctx.auth.tenantId) {
1940
1967
  const syncAfterSubs = collectSyncSubscribers(getAllSyncSubscribers(), updateLifecycle.afterEventId);
1941
1968
  if (syncAfterSubs.length) {
1942
- const em = ctx.container.resolve("em");
1969
+ const em2 = ctx.container.resolve("em");
1943
1970
  const syncPayload = buildSyncPayload(
1944
- { eventId: updateLifecycle.afterEventId, entity: updateLifecycle.entity, operation: "update", timing: "after", resourceId: id, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em, request },
1971
+ { eventId: updateLifecycle.afterEventId, entity: updateLifecycle.entity, operation: "update", timing: "after", resourceId: id, userId: ctx.auth.sub, organizationId: scopeOrganizationId, tenantId: ctx.auth.tenantId, em: em2, request },
1945
1972
  { payload: input && typeof input === "object" ? input : void 0, previousData: updatePreviousData, entityData: snapshotEntity(entity) }
1946
1973
  );
1947
1974
  await runSyncAfterEvent(syncAfterSubs, syncPayload, ctx.container);