@open-mercato/core 0.8.1-develop.7206.1.45a3cee87f → 0.8.1-develop.7215.1.60786cf836

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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +6 -2
  3. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  4. package/dist/modules/catalog/backend/catalog/products/create/page.js +7 -3
  5. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  6. package/dist/modules/catalog/components/products/ProductUomSection.js +11 -22
  7. package/dist/modules/catalog/components/products/ProductUomSection.js.map +2 -2
  8. package/dist/modules/catalog/components/products/productForm.js +26 -1
  9. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  10. package/dist/modules/configs/lib/upgrade-actions.js +1 -0
  11. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  12. package/dist/modules/configs/services/upgradeActionsService.js +9 -2
  13. package/dist/modules/configs/services/upgradeActionsService.js.map +2 -2
  14. package/dist/modules/entities/lib/definition-scope-where.js +18 -0
  15. package/dist/modules/entities/lib/definition-scope-where.js.map +7 -0
  16. package/dist/modules/entities/lib/definition-scope.js +2 -8
  17. package/dist/modules/entities/lib/definition-scope.js.map +2 -2
  18. package/dist/modules/entities/lib/helpers.js +4 -30
  19. package/dist/modules/entities/lib/helpers.js.map +2 -2
  20. package/dist/modules/entities/lib/scoped-field-defs.js +5 -10
  21. package/dist/modules/entities/lib/scoped-field-defs.js.map +2 -2
  22. package/dist/modules/query_index/lib/engine.js.map +1 -1
  23. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  24. package/dist/modules/wms/components/backend/AdjustInventoryDialog.js +15 -1
  25. package/dist/modules/wms/components/backend/AdjustInventoryDialog.js.map +2 -2
  26. package/package.json +7 -7
  27. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +6 -2
  28. package/src/modules/catalog/backend/catalog/products/create/page.tsx +7 -6
  29. package/src/modules/catalog/components/products/ProductUomSection.tsx +11 -31
  30. package/src/modules/catalog/components/products/productForm.ts +53 -0
  31. package/src/modules/configs/lib/upgrade-actions.ts +7 -0
  32. package/src/modules/configs/services/upgradeActionsService.ts +17 -2
  33. package/src/modules/entities/lib/definition-scope-where.ts +27 -0
  34. package/src/modules/entities/lib/definition-scope.ts +2 -10
  35. package/src/modules/entities/lib/helpers.ts +4 -33
  36. package/src/modules/entities/lib/scoped-field-defs.ts +6 -15
  37. package/src/modules/query_index/lib/engine.ts +4 -4
  38. package/src/modules/query_index/lib/search-tokens.ts +31 -0
  39. package/src/modules/wms/components/backend/AdjustInventoryDialog.tsx +21 -1
@@ -90,6 +90,7 @@ const upgradeActions = [
90
90
  ctaKey: "payment_gateways.upgradeActions.sessionInitializationPrune.cta",
91
91
  successKey: "payment_gateways.upgradeActions.sessionInitializationPrune.success",
92
92
  loadingKey: "payment_gateways.upgradeActions.sessionInitializationPrune.loading",
93
+ requiredModules: ["payment_gateways"],
93
94
  run: async ({ container, tenantId, organizationId }) => {
94
95
  const { registerSessionInitializationPruneSchedule } = await import("@open-mercato/core/modules/payment_gateways/setup");
95
96
  await registerSessionInitializationPruneSchedule(container, { tenantId, organizationId });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/configs/lib/upgrade-actions.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport * as semver from 'semver'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { reconcileAttachmentOrganizations } from '@open-mercato/core/modules/attachments/lib/reconcileOrganization'\n\nconst logger = createLogger('configs').child({ component: 'upgrade-actions' })\n\nexport type UpgradeActionContext = {\n tenantId: string\n organizationId: string\n container: AppContainer\n em: EntityManager\n}\n\nexport type UpgradeActionDefinition = {\n id: string\n version: string\n messageKey: string\n ctaKey: string\n successKey: string\n loadingKey?: string\n run: (ctx: UpgradeActionContext) => Promise<void>\n}\n\n/**\n * Compare two semantic version strings.\n * Uses the semver library for robust version comparison.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n * Throws an error if either version string is invalid.\n */\nexport function compareVersions(a: string, b: string): number {\n const cleanA = semver.valid(semver.coerce(a))\n const cleanB = semver.valid(semver.coerce(b))\n if (!cleanA) {\n throw new Error(`Invalid version string: \"${a}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n if (!cleanB) {\n throw new Error(`Invalid version string: \"${b}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n return semver.compare(cleanA, cleanB)\n}\n\nexport const upgradeActions: UpgradeActionDefinition[] = [\n {\n id: 'attachments.reconcile-organization',\n version: '0.6.6',\n messageKey: 'configs.upgrades.attachmentsOrgReconcile.message',\n ctaKey: 'configs.upgrades.attachmentsOrgReconcile.cta',\n successKey: 'configs.upgrades.attachmentsOrgReconcile.success',\n loadingKey: 'configs.upgrades.attachmentsOrgReconcile.loading',\n async run({ container, em, tenantId }) {\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const report = await reconcileAttachmentOrganizations({ em, queryEngine, tenantId })\n logger.info('attachments organization reconcile completed', {\n tenantId,\n scanned: report.scanned,\n updated: report.updated,\n unresolved: report.unresolved,\n skippedVirtual: report.skippedVirtual,\n })\n },\n },\n {\n id: 'devices.seed-push-token-encryption-map',\n version: '0.6.6',\n messageKey: 'configs.upgrades.devicesPushTokenEncryption.message',\n ctaKey: 'configs.upgrades.devicesPushTokenEncryption.cta',\n successKey: 'configs.upgrades.devicesPushTokenEncryption.success',\n loadingKey: 'configs.upgrades.devicesPushTokenEncryption.loading',\n // Encryption maps are seeded once at tenant creation (`entities seed-encryption`), so a tenant\n // that predates the devices push-token feature has no `devices:user_device` map row. Without it\n // `encryptEntityPayload` no-ops and `push_token` is written as PLAINTEXT (the map is a declaration\n // of which fields to encrypt; the tenant DEK still drives the actual crypto, so seeding it is safe\n // even when encryption is currently disabled \u2014 it only takes effect once encryption is on).\n // Lazy-imported so configs stays decoupled from devices/entities (mirrors the customers action).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ default: devicesEncryptionMaps }, { upsertEncryptionMapSpecs }] = await Promise.all([\n import('@open-mercato/core/modules/devices/encryption'),\n import('@open-mercato/core/modules/entities/cli'),\n ])\n await upsertEncryptionMapSpecs(em, tenantId, organizationId ?? null, devicesEncryptionMaps)\n },\n },\n {\n id: 'customers.seed-interaction-statuses',\n version: '0.6.5',\n messageKey: 'customers.config.upgradeActions.interactionStatuses.message',\n ctaKey: 'customers.config.upgradeActions.interactionStatuses.cta',\n successKey: 'customers.config.upgradeActions.interactionStatuses.success',\n loadingKey: 'customers.config.upgradeActions.interactionStatuses.loading',\n // Existing tenants predate the `interaction_status` dictionary, so their status\n // dropdown is empty until seeded. New tenants get it via customers `seedDefaults`.\n // Lazy-imported so configs stays decoupled from customers (the catalog of actions\n // lives here, but customers code only loads when the action actually runs).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ INTERACTION_STATUS_DEFAULTS }, { ensureDictionaryEntry }] = await Promise.all([\n import('@open-mercato/core/modules/customers/cli'),\n import('@open-mercato/core/modules/customers/commands/shared'),\n ])\n for (const entry of INTERACTION_STATUS_DEFAULTS) {\n await ensureDictionaryEntry(em, {\n tenantId,\n organizationId,\n kind: 'interaction_status',\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n })\n }\n },\n },\n {\n id: 'payment_gateways.register-session-initialization-prune',\n version: '0.6.6',\n messageKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.message',\n ctaKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.cta',\n successKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.success',\n loadingKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.loading',\n run: async ({ container, tenantId, organizationId }) => {\n const { registerSessionInitializationPruneSchedule } = await import(\n '@open-mercato/core/modules/payment_gateways/setup'\n )\n await registerSessionInitializationPruneSchedule(container, { tenantId, organizationId })\n },\n },\n {\n id: 'phone_calls.seed-call-encryption-maps',\n version: '0.7.1',\n messageKey: 'phone_calls.config.upgradeActions.encryptionMaps.message',\n ctaKey: 'phone_calls.config.upgradeActions.encryptionMaps.cta',\n successKey: 'phone_calls.config.upgradeActions.encryptionMaps.success',\n loadingKey: 'phone_calls.config.upgradeActions.encryptionMaps.loading',\n // `Migration20260822120000` backfills these maps, but only for scopes that had active maps when\n // it ran. A tenant that upgraded with encryption disabled and turned it on afterwards is left\n // without a map, so ingest writes call PII as plaintext with nothing to signal it. This is the\n // heal for that case, and the same re-assert path devices exposes for its own map.\n // Lazy-imported so configs stays decoupled from phone_calls/entities (mirrors the devices action).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ default: phoneCallsEncryptionMaps }, { upsertEncryptionMapSpecs }] = await Promise.all([\n import('@open-mercato/core/modules/phone_calls/encryption'),\n import('@open-mercato/core/modules/entities/cli'),\n ])\n await upsertEncryptionMapSpecs(em, tenantId, organizationId ?? null, phoneCallsEncryptionMaps)\n },\n },\n]\n\nexport function actionsUpToVersion(version: string): UpgradeActionDefinition[] {\n return upgradeActions // NOSONAR \u2014 upgradeActions is populated at boot time by modules\n .filter((action) => compareVersions(action.version, version) <= 0)\n .sort((a, b) => compareVersions(a.version, b.version) || a.id.localeCompare(b.id))\n}\n\nexport function findUpgradeAction(actionId: string, maxVersion: string): UpgradeActionDefinition | undefined {\n const matches = actionsUpToVersion(maxVersion).filter((action) => action.id === actionId)\n if (!matches.length) return undefined\n return matches[matches.length - 1]\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport * as semver from 'semver'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { reconcileAttachmentOrganizations } from '@open-mercato/core/modules/attachments/lib/reconcileOrganization'\n\nconst logger = createLogger('configs').child({ component: 'upgrade-actions' })\n\nexport type UpgradeActionContext = {\n tenantId: string\n organizationId: string\n container: AppContainer\n em: EntityManager\n}\n\nexport type UpgradeActionDefinition = {\n id: string\n version: string\n messageKey: string\n ctaKey: string\n successKey: string\n loadingKey?: string\n /**\n * Modules that must be enabled for this action to be offered/executed.\n * Mirrors `InjectionWidgetMetadata.requiredModules` \u2014 omit for actions\n * owned by `configs` itself.\n */\n requiredModules?: string[]\n run: (ctx: UpgradeActionContext) => Promise<void>\n}\n\n/**\n * Compare two semantic version strings.\n * Uses the semver library for robust version comparison.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n * Throws an error if either version string is invalid.\n */\nexport function compareVersions(a: string, b: string): number {\n const cleanA = semver.valid(semver.coerce(a))\n const cleanB = semver.valid(semver.coerce(b))\n if (!cleanA) {\n throw new Error(`Invalid version string: \"${a}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n if (!cleanB) {\n throw new Error(`Invalid version string: \"${b}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n return semver.compare(cleanA, cleanB)\n}\n\nexport const upgradeActions: UpgradeActionDefinition[] = [\n {\n id: 'attachments.reconcile-organization',\n version: '0.6.6',\n messageKey: 'configs.upgrades.attachmentsOrgReconcile.message',\n ctaKey: 'configs.upgrades.attachmentsOrgReconcile.cta',\n successKey: 'configs.upgrades.attachmentsOrgReconcile.success',\n loadingKey: 'configs.upgrades.attachmentsOrgReconcile.loading',\n async run({ container, em, tenantId }) {\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const report = await reconcileAttachmentOrganizations({ em, queryEngine, tenantId })\n logger.info('attachments organization reconcile completed', {\n tenantId,\n scanned: report.scanned,\n updated: report.updated,\n unresolved: report.unresolved,\n skippedVirtual: report.skippedVirtual,\n })\n },\n },\n {\n id: 'devices.seed-push-token-encryption-map',\n version: '0.6.6',\n messageKey: 'configs.upgrades.devicesPushTokenEncryption.message',\n ctaKey: 'configs.upgrades.devicesPushTokenEncryption.cta',\n successKey: 'configs.upgrades.devicesPushTokenEncryption.success',\n loadingKey: 'configs.upgrades.devicesPushTokenEncryption.loading',\n // Encryption maps are seeded once at tenant creation (`entities seed-encryption`), so a tenant\n // that predates the devices push-token feature has no `devices:user_device` map row. Without it\n // `encryptEntityPayload` no-ops and `push_token` is written as PLAINTEXT (the map is a declaration\n // of which fields to encrypt; the tenant DEK still drives the actual crypto, so seeding it is safe\n // even when encryption is currently disabled \u2014 it only takes effect once encryption is on).\n // Lazy-imported so configs stays decoupled from devices/entities (mirrors the customers action).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ default: devicesEncryptionMaps }, { upsertEncryptionMapSpecs }] = await Promise.all([\n import('@open-mercato/core/modules/devices/encryption'),\n import('@open-mercato/core/modules/entities/cli'),\n ])\n await upsertEncryptionMapSpecs(em, tenantId, organizationId ?? null, devicesEncryptionMaps)\n },\n },\n {\n id: 'customers.seed-interaction-statuses',\n version: '0.6.5',\n messageKey: 'customers.config.upgradeActions.interactionStatuses.message',\n ctaKey: 'customers.config.upgradeActions.interactionStatuses.cta',\n successKey: 'customers.config.upgradeActions.interactionStatuses.success',\n loadingKey: 'customers.config.upgradeActions.interactionStatuses.loading',\n // Existing tenants predate the `interaction_status` dictionary, so their status\n // dropdown is empty until seeded. New tenants get it via customers `seedDefaults`.\n // Lazy-imported so configs stays decoupled from customers (the catalog of actions\n // lives here, but customers code only loads when the action actually runs).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ INTERACTION_STATUS_DEFAULTS }, { ensureDictionaryEntry }] = await Promise.all([\n import('@open-mercato/core/modules/customers/cli'),\n import('@open-mercato/core/modules/customers/commands/shared'),\n ])\n for (const entry of INTERACTION_STATUS_DEFAULTS) {\n await ensureDictionaryEntry(em, {\n tenantId,\n organizationId,\n kind: 'interaction_status',\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n })\n }\n },\n },\n {\n id: 'payment_gateways.register-session-initialization-prune',\n version: '0.6.6',\n messageKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.message',\n ctaKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.cta',\n successKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.success',\n loadingKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.loading',\n requiredModules: ['payment_gateways'],\n run: async ({ container, tenantId, organizationId }) => {\n const { registerSessionInitializationPruneSchedule } = await import(\n '@open-mercato/core/modules/payment_gateways/setup'\n )\n await registerSessionInitializationPruneSchedule(container, { tenantId, organizationId })\n },\n },\n {\n id: 'phone_calls.seed-call-encryption-maps',\n version: '0.7.1',\n messageKey: 'phone_calls.config.upgradeActions.encryptionMaps.message',\n ctaKey: 'phone_calls.config.upgradeActions.encryptionMaps.cta',\n successKey: 'phone_calls.config.upgradeActions.encryptionMaps.success',\n loadingKey: 'phone_calls.config.upgradeActions.encryptionMaps.loading',\n // `Migration20260822120000` backfills these maps, but only for scopes that had active maps when\n // it ran. A tenant that upgraded with encryption disabled and turned it on afterwards is left\n // without a map, so ingest writes call PII as plaintext with nothing to signal it. This is the\n // heal for that case, and the same re-assert path devices exposes for its own map.\n // Lazy-imported so configs stays decoupled from phone_calls/entities (mirrors the devices action).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ default: phoneCallsEncryptionMaps }, { upsertEncryptionMapSpecs }] = await Promise.all([\n import('@open-mercato/core/modules/phone_calls/encryption'),\n import('@open-mercato/core/modules/entities/cli'),\n ])\n await upsertEncryptionMapSpecs(em, tenantId, organizationId ?? null, phoneCallsEncryptionMaps)\n },\n },\n]\n\nexport function actionsUpToVersion(version: string): UpgradeActionDefinition[] {\n return upgradeActions // NOSONAR \u2014 upgradeActions is populated at boot time by modules\n .filter((action) => compareVersions(action.version, version) <= 0)\n .sort((a, b) => compareVersions(a.version, b.version) || a.id.localeCompare(b.id))\n}\n\nexport function findUpgradeAction(actionId: string, maxVersion: string): UpgradeActionDefinition | undefined {\n const matches = actionsUpToVersion(maxVersion).filter((action) => action.id === actionId)\n if (!matches.length) return undefined\n return matches[matches.length - 1]\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { UniqueConstraintViolationException } from "@mikro-orm/core";
2
2
  import { appVersion } from "@open-mercato/shared/lib/version";
3
3
  import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
4
+ import { getEnabledModuleIds } from "@open-mercato/shared/security/enabledModulesRegistry";
4
5
  import { UpgradeActionRun } from "../data/entities.js";
5
6
  import { actionsUpToVersion, findUpgradeAction } from "../lib/upgrade-actions.js";
6
7
  import { createLogger } from "@open-mercato/shared/lib/logger";
@@ -11,6 +12,12 @@ function isUpgradeActionsEnabled() {
11
12
  function getCurrentVersion() {
12
13
  return appVersion;
13
14
  }
15
+ function isActionModuleEnabled(definition) {
16
+ const required = definition.requiredModules;
17
+ if (!Array.isArray(required) || required.length === 0) return true;
18
+ const enabledModuleIds = new Set(getEnabledModuleIds());
19
+ return required.every((moduleId) => enabledModuleIds.has(moduleId));
20
+ }
14
21
  async function listPendingUpgradeActions(em, {
15
22
  tenantId,
16
23
  organizationId,
@@ -26,7 +33,7 @@ async function listPendingUpgradeActions(em, {
26
33
  version: { $in: versions }
27
34
  });
28
35
  const completed = new Set(runs.map((run) => `${run.version}::${run.actionId}`));
29
- return definitions.filter((definition) => !completed.has(`${definition.version}::${definition.id}`));
36
+ return definitions.filter((definition) => !completed.has(`${definition.version}::${definition.id}`)).filter(isActionModuleEnabled);
30
37
  }
31
38
  async function executeUpgradeAction(container, {
32
39
  actionId,
@@ -38,7 +45,7 @@ async function executeUpgradeAction(container, {
38
45
  throw new Error("UPGRADE_ACTIONS_DISABLED");
39
46
  }
40
47
  const definition = findUpgradeAction(actionId, version);
41
- if (!definition) {
48
+ if (!definition || !isActionModuleEnabled(definition)) {
42
49
  throw new Error("UPGRADE_ACTION_NOT_AVAILABLE");
43
50
  }
44
51
  const em = container.resolve("em");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/configs/services/upgradeActionsService.ts"],
4
- "sourcesContent": ["import { UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { appVersion } from '@open-mercato/shared/lib/version'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { UpgradeActionRun } from '../data/entities'\nimport { actionsUpToVersion, findUpgradeAction, type UpgradeActionDefinition } from '../lib/upgrade-actions'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('configs')\n\nexport type UpgradeActionStatus = 'completed' | 'already_completed'\n\nexport function isUpgradeActionsEnabled(): boolean {\n return parseBooleanToken(process.env.UPGRADE_ACTIONS_ENABLED ?? '') === true\n}\n\nexport function getCurrentVersion(): string {\n return appVersion\n}\n\nexport async function listPendingUpgradeActions(\n em: EntityManager,\n {\n tenantId,\n organizationId,\n version = appVersion,\n }: { tenantId: string; organizationId: string; version?: string },\n): Promise<UpgradeActionDefinition[]> {\n if (!isUpgradeActionsEnabled()) return []\n const definitions = actionsUpToVersion(version)\n if (!definitions.length) return []\n const versions = Array.from(new Set(definitions.map((definition) => definition.version)))\n const runs = await em.find(UpgradeActionRun, {\n tenantId,\n organizationId,\n version: { $in: versions },\n })\n const completed = new Set(runs.map((run) => `${run.version}::${run.actionId}`))\n return definitions.filter((definition) => !completed.has(`${definition.version}::${definition.id}`))\n}\n\nexport async function executeUpgradeAction(\n container: AwilixContainer,\n {\n actionId,\n tenantId,\n organizationId,\n version = appVersion,\n }: { actionId: string; tenantId: string; organizationId: string; version?: string },\n): Promise<{ action: UpgradeActionDefinition; status: UpgradeActionStatus }> {\n if (!isUpgradeActionsEnabled()) {\n throw new Error('UPGRADE_ACTIONS_DISABLED')\n }\n const definition = findUpgradeAction(actionId, version)\n if (!definition) {\n throw new Error('UPGRADE_ACTION_NOT_AVAILABLE')\n }\n const em = container.resolve<EntityManager>('em')\n const status = await em.transactional(async (tem) => {\n logger.info('Executing upgrade action', {\n component: 'upgrade-actions',\n actionId: definition.id,\n version: definition.version,\n requestedVersion: version,\n tenantId,\n organizationId,\n })\n const alreadyCompleted = await tem.findOne(UpgradeActionRun, {\n actionId: definition.id,\n version: definition.version,\n tenantId,\n organizationId,\n })\n if (alreadyCompleted) return 'already_completed' as const\n await definition.run({ container, em: tem, tenantId, organizationId })\n const record = tem.create(UpgradeActionRun, {\n actionId: definition.id,\n version: definition.version,\n tenantId,\n organizationId,\n })\n tem.persist(record)\n await tem.flush()\n logger.info('Upgrade action completed', { component: 'upgrade-actions', actionId: definition.id, version, tenantId, organizationId })\n return 'completed' as const\n }).catch((error) => {\n logger.error('Upgrade action failed', { component: 'upgrade-actions', actionId, tenantId, organizationId, version, err: error })\n if (error instanceof UniqueConstraintViolationException) {\n return 'already_completed' as const\n }\n throw error\n })\n\n return { action: definition, status }\n}\n"],
5
- "mappings": "AAAA,SAAS,0CAA0C;AAGnD,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,wBAAwB;AACjC,SAAS,oBAAoB,yBAAuD;AACpF,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,SAAS;AAI9B,SAAS,0BAAmC;AACjD,SAAO,kBAAkB,QAAQ,IAAI,2BAA2B,EAAE,MAAM;AAC1E;AAEO,SAAS,oBAA4B;AAC1C,SAAO;AACT;AAEA,eAAsB,0BACpB,IACA;AAAA,EACE;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GACoC;AACpC,MAAI,CAAC,wBAAwB,EAAG,QAAO,CAAC;AACxC,QAAM,cAAc,mBAAmB,OAAO;AAC9C,MAAI,CAAC,YAAY,OAAQ,QAAO,CAAC;AACjC,QAAM,WAAW,MAAM,KAAK,IAAI,IAAI,YAAY,IAAI,CAAC,eAAe,WAAW,OAAO,CAAC,CAAC;AACxF,QAAM,OAAO,MAAM,GAAG,KAAK,kBAAkB;AAAA,IAC3C;AAAA,IACA;AAAA,IACA,SAAS,EAAE,KAAK,SAAS;AAAA,EAC3B,CAAC;AACD,QAAM,YAAY,IAAI,IAAI,KAAK,IAAI,CAAC,QAAQ,GAAG,IAAI,OAAO,KAAK,IAAI,QAAQ,EAAE,CAAC;AAC9E,SAAO,YAAY,OAAO,CAAC,eAAe,CAAC,UAAU,IAAI,GAAG,WAAW,OAAO,KAAK,WAAW,EAAE,EAAE,CAAC;AACrG;AAEA,eAAsB,qBACpB,WACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GAC2E;AAC3E,MAAI,CAAC,wBAAwB,GAAG;AAC9B,UAAM,IAAI,MAAM,0BAA0B;AAAA,EAC5C;AACA,QAAM,aAAa,kBAAkB,UAAU,OAAO;AACtD,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,MAAM,8BAA8B;AAAA,EAChD;AACA,QAAM,KAAK,UAAU,QAAuB,IAAI;AAChD,QAAM,SAAS,MAAM,GAAG,cAAc,OAAO,QAAQ;AACnD,WAAO,KAAK,4BAA4B;AAAA,MACtC,WAAW;AAAA,MACX,UAAU,WAAW;AAAA,MACrB,SAAS,WAAW;AAAA,MACpB,kBAAkB;AAAA,MAClB;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,mBAAmB,MAAM,IAAI,QAAQ,kBAAkB;AAAA,MAC3D,UAAU,WAAW;AAAA,MACrB,SAAS,WAAW;AAAA,MACpB;AAAA,MACA;AAAA,IACF,CAAC;AACD,QAAI,iBAAkB,QAAO;AAC7B,UAAM,WAAW,IAAI,EAAE,WAAW,IAAI,KAAK,UAAU,eAAe,CAAC;AACrE,UAAM,SAAS,IAAI,OAAO,kBAAkB;AAAA,MAC1C,UAAU,WAAW;AAAA,MACrB,SAAS,WAAW;AAAA,MACpB;AAAA,MACA;AAAA,IACF,CAAC;AACD,QAAI,QAAQ,MAAM;AAClB,UAAM,IAAI,MAAM;AAChB,WAAO,KAAK,4BAA4B,EAAE,WAAW,mBAAmB,UAAU,WAAW,IAAI,SAAS,UAAU,eAAe,CAAC;AACpI,WAAO;AAAA,EACT,CAAC,EAAE,MAAM,CAAC,UAAU;AAClB,WAAO,MAAM,yBAAyB,EAAE,WAAW,mBAAmB,UAAU,UAAU,gBAAgB,SAAS,KAAK,MAAM,CAAC;AAC/H,QAAI,iBAAiB,oCAAoC;AACvD,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR,CAAC;AAED,SAAO,EAAE,QAAQ,YAAY,OAAO;AACtC;",
4
+ "sourcesContent": ["import { UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { appVersion } from '@open-mercato/shared/lib/version'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { getEnabledModuleIds } from '@open-mercato/shared/security/enabledModulesRegistry'\nimport { UpgradeActionRun } from '../data/entities'\nimport { actionsUpToVersion, findUpgradeAction, type UpgradeActionDefinition } from '../lib/upgrade-actions'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('configs')\n\nexport type UpgradeActionStatus = 'completed' | 'already_completed'\n\nexport function isUpgradeActionsEnabled(): boolean {\n return parseBooleanToken(process.env.UPGRADE_ACTIONS_ENABLED ?? '') === true\n}\n\nexport function getCurrentVersion(): string {\n return appVersion\n}\n\n/**\n * An action with no `requiredModules` is owned by `configs` itself and is\n * always available. Fails closed when the module registry is empty/unavailable,\n * mirroring `isDocumentEntityRegistryModuleEnabled`.\n */\nfunction isActionModuleEnabled(definition: UpgradeActionDefinition): boolean {\n const required = definition.requiredModules\n if (!Array.isArray(required) || required.length === 0) return true\n const enabledModuleIds = new Set(getEnabledModuleIds())\n return required.every((moduleId) => enabledModuleIds.has(moduleId))\n}\n\nexport async function listPendingUpgradeActions(\n em: EntityManager,\n {\n tenantId,\n organizationId,\n version = appVersion,\n }: { tenantId: string; organizationId: string; version?: string },\n): Promise<UpgradeActionDefinition[]> {\n if (!isUpgradeActionsEnabled()) return []\n const definitions = actionsUpToVersion(version)\n if (!definitions.length) return []\n const versions = Array.from(new Set(definitions.map((definition) => definition.version)))\n const runs = await em.find(UpgradeActionRun, {\n tenantId,\n organizationId,\n version: { $in: versions },\n })\n const completed = new Set(runs.map((run) => `${run.version}::${run.actionId}`))\n return definitions\n .filter((definition) => !completed.has(`${definition.version}::${definition.id}`))\n .filter(isActionModuleEnabled)\n}\n\nexport async function executeUpgradeAction(\n container: AwilixContainer,\n {\n actionId,\n tenantId,\n organizationId,\n version = appVersion,\n }: { actionId: string; tenantId: string; organizationId: string; version?: string },\n): Promise<{ action: UpgradeActionDefinition; status: UpgradeActionStatus }> {\n if (!isUpgradeActionsEnabled()) {\n throw new Error('UPGRADE_ACTIONS_DISABLED')\n }\n const definition = findUpgradeAction(actionId, version)\n if (!definition || !isActionModuleEnabled(definition)) {\n throw new Error('UPGRADE_ACTION_NOT_AVAILABLE')\n }\n const em = container.resolve<EntityManager>('em')\n const status = await em.transactional(async (tem) => {\n logger.info('Executing upgrade action', {\n component: 'upgrade-actions',\n actionId: definition.id,\n version: definition.version,\n requestedVersion: version,\n tenantId,\n organizationId,\n })\n const alreadyCompleted = await tem.findOne(UpgradeActionRun, {\n actionId: definition.id,\n version: definition.version,\n tenantId,\n organizationId,\n })\n if (alreadyCompleted) return 'already_completed' as const\n await definition.run({ container, em: tem, tenantId, organizationId })\n const record = tem.create(UpgradeActionRun, {\n actionId: definition.id,\n version: definition.version,\n tenantId,\n organizationId,\n })\n tem.persist(record)\n await tem.flush()\n logger.info('Upgrade action completed', { component: 'upgrade-actions', actionId: definition.id, version, tenantId, organizationId })\n return 'completed' as const\n }).catch((error) => {\n logger.error('Upgrade action failed', { component: 'upgrade-actions', actionId, tenantId, organizationId, version, err: error })\n if (error instanceof UniqueConstraintViolationException) {\n return 'already_completed' as const\n }\n throw error\n })\n\n return { action: definition, status }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,18 @@
1
+ function createVisibleDefinitionScopeClause(scope) {
2
+ const organizationId = scope.organizationId ?? null;
3
+ const tenantId = scope.tenantId ?? null;
4
+ const organizationCandidates = [{ organizationId: null }];
5
+ if (organizationId !== null) organizationCandidates.unshift({ organizationId });
6
+ const tenantCandidates = [{ tenantId: null }];
7
+ if (tenantId !== null) tenantCandidates.unshift({ tenantId });
8
+ return {
9
+ $and: [
10
+ { $or: organizationCandidates },
11
+ { $or: tenantCandidates }
12
+ ]
13
+ };
14
+ }
15
+ export {
16
+ createVisibleDefinitionScopeClause
17
+ };
18
+ //# sourceMappingURL=definition-scope-where.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/entities/lib/definition-scope-where.ts"],
4
+ "sourcesContent": ["export type DefinitionScopeClauseInput = {\n organizationId?: string | null\n tenantId?: string | null\n}\n\n// SQL `IN (?, NULL)` never matches rows where the column IS NULL, so a scope\n// filter that mixes a concrete id with a literal null has to be expressed as an\n// `$or` of equality and IS NULL. Every query that resolves the definitions\n// visible to a scope \u2014 the write path, the validation path, the single-key\n// lookups \u2014 routes through this builder so they cannot drift apart again (#5919).\nexport function createVisibleDefinitionScopeClause(scope: DefinitionScopeClauseInput) {\n const organizationId = scope.organizationId ?? null\n const tenantId = scope.tenantId ?? null\n\n const organizationCandidates = [{ organizationId: null as string | null }]\n if (organizationId !== null) organizationCandidates.unshift({ organizationId })\n\n const tenantCandidates = [{ tenantId: null as string | null }]\n if (tenantId !== null) tenantCandidates.unshift({ tenantId })\n\n return {\n $and: [\n { $or: organizationCandidates },\n { $or: tenantCandidates },\n ],\n }\n}\n"],
5
+ "mappings": "AAUO,SAAS,mCAAmC,OAAmC;AACpF,QAAM,iBAAiB,MAAM,kBAAkB;AAC/C,QAAM,WAAW,MAAM,YAAY;AAEnC,QAAM,yBAAyB,CAAC,EAAE,gBAAgB,KAAsB,CAAC;AACzE,MAAI,mBAAmB,KAAM,wBAAuB,QAAQ,EAAE,eAAe,CAAC;AAE9E,QAAM,mBAAmB,CAAC,EAAE,UAAU,KAAsB,CAAC;AAC7D,MAAI,aAAa,KAAM,kBAAiB,QAAQ,EAAE,SAAS,CAAC;AAE5D,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,EAAE,KAAK,uBAAuB;AAAA,MAC9B,EAAE,KAAK,iBAAiB;AAAA,IAC1B;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -1,5 +1,6 @@
1
1
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
2
2
  import { CustomFieldDef } from "@open-mercato/core/modules/entities/data/entities";
3
+ import { createVisibleDefinitionScopeClause } from "./definition-scope-where.js";
3
4
  function resolveDefinitionScopeFromOrganizationScope(auth, scope) {
4
5
  const authTenantId = auth.tenantId ?? null;
5
6
  const scopeTenantId = scope.tenantId ?? null;
@@ -26,18 +27,11 @@ function createExactDefinitionWhere(entityId, key, scope) {
26
27
  };
27
28
  }
28
29
  function createVisibleDefinitionWhere(entityId, key, scope, options = {}) {
29
- const organizationCandidates = [{ organizationId: null }];
30
- if (scope.organizationId) organizationCandidates.unshift({ organizationId: scope.organizationId });
31
- const tenantCandidates = [{ tenantId: null }];
32
- if (scope.tenantId) tenantCandidates.unshift({ tenantId: scope.tenantId });
33
30
  return {
34
31
  entityId,
35
32
  key,
36
33
  ...options,
37
- $and: [
38
- { $or: organizationCandidates },
39
- { $or: tenantCandidates }
40
- ]
34
+ ...createVisibleDefinitionScopeClause(scope)
41
35
  };
42
36
  }
43
37
  function definitionTimestamp(def) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/lib/definition-scope.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'\n\ntype AuthScope = {\n tenantId?: string | null\n orgId?: string | null\n}\n\nexport type DefinitionMutationScope = {\n tenantId: string | null\n organizationId: string | null\n}\n\ntype OrganizationScopeLike = {\n tenantId?: string | null\n selectedId?: string | null\n}\n\ntype DefinitionKeySelector = string | { $in: string[] }\n\ntype DefinitionVisibilityOptions = {\n deletedAt?: null | { $ne: null }\n isActive?: boolean\n}\n\ntype DefinitionScopeRecord = {\n tenantId?: string | null\n organizationId?: string | null\n updatedAt?: Date | string | number | null\n createdAt?: Date | string | number | null\n}\n\ntype MutableDefinitionScopeRecord = DefinitionScopeRecord & {\n isActive?: boolean\n deletedAt?: Date | string | number | null\n}\n\nexport function resolveDefinitionScopeFromOrganizationScope(\n auth: AuthScope,\n scope: OrganizationScopeLike,\n): DefinitionMutationScope {\n const authTenantId = auth.tenantId ?? null\n const scopeTenantId = scope.tenantId ?? null\n const tenantMismatch = Boolean(authTenantId && scopeTenantId && authTenantId !== scopeTenantId)\n\n return {\n tenantId: tenantMismatch ? authTenantId : (scopeTenantId ?? authTenantId),\n organizationId: tenantMismatch\n ? (auth.orgId ?? null)\n : (scope.selectedId ?? auth.orgId ?? null),\n }\n}\n\nexport async function resolveDefinitionMutationScope({\n auth,\n container,\n request,\n}: {\n auth: AuthScope\n container: unknown\n request: Request\n}): Promise<DefinitionMutationScope> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request } as any)\n return resolveDefinitionScopeFromOrganizationScope(auth, scope)\n}\n\nexport function createExactDefinitionWhere(\n entityId: string,\n key: DefinitionKeySelector,\n scope: DefinitionMutationScope,\n) {\n return {\n entityId,\n key,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n }\n}\n\nexport function createVisibleDefinitionWhere(\n entityId: string,\n key: DefinitionKeySelector,\n scope: DefinitionMutationScope,\n options: DefinitionVisibilityOptions = {},\n) {\n const organizationCandidates = [{ organizationId: null as string | null }]\n if (scope.organizationId) organizationCandidates.unshift({ organizationId: scope.organizationId })\n\n const tenantCandidates = [{ tenantId: null as string | null }]\n if (scope.tenantId) tenantCandidates.unshift({ tenantId: scope.tenantId })\n\n return {\n entityId,\n key,\n ...options,\n $and: [\n { $or: organizationCandidates },\n { $or: tenantCandidates },\n ],\n }\n}\n\nfunction definitionTimestamp(def: DefinitionScopeRecord) {\n const updatedAt = def?.updatedAt instanceof Date\n ? def.updatedAt.getTime()\n : (def?.updatedAt ? new Date(def.updatedAt).getTime() : 0)\n if (updatedAt) return updatedAt\n return def?.createdAt instanceof Date\n ? def.createdAt.getTime()\n : (def?.createdAt ? new Date(def.createdAt).getTime() : 0)\n}\n\nfunction definitionScopeScore(def: DefinitionScopeRecord) {\n return (def?.tenantId ? 2 : 0) + (def?.organizationId ? 1 : 0)\n}\n\nexport function selectVisibleDefinitionWinner<T extends DefinitionScopeRecord>(definitions: T[]) {\n let winner: T | null = null\n for (const definition of definitions) {\n if (!winner) {\n winner = definition\n continue\n }\n const nextScore = definitionScopeScore(definition)\n const winnerScore = definitionScopeScore(winner)\n if (nextScore > winnerScore) {\n winner = definition\n continue\n }\n if (nextScore < winnerScore) continue\n if (definitionTimestamp(definition) >= definitionTimestamp(winner)) winner = definition\n }\n return winner\n}\n\nexport function markDefinitionTombstoned<T extends MutableDefinitionScopeRecord>(definition: T, now = new Date()) {\n definition.isActive = false\n definition.deletedAt = definition.deletedAt ?? now\n definition.updatedAt = now\n return definition\n}\n\nfunction cloneConfigJson(configJson: unknown) {\n if (configJson == null) return configJson\n return JSON.parse(JSON.stringify(configJson))\n}\n\nexport function createScopedDefinitionTombstone(\n em: Pick<EntityManager, 'create'>,\n source: {\n entityId: string\n key: string\n kind: string\n configJson?: unknown\n },\n scope: DefinitionMutationScope,\n now = new Date(),\n) {\n return em.create(CustomFieldDef, {\n entityId: source.entityId,\n key: source.key,\n kind: source.kind,\n configJson: cloneConfigJson(source.configJson) ?? {},\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: false,\n createdAt: now,\n updatedAt: now,\n deletedAt: now,\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'\nimport { createVisibleDefinitionScopeClause } from './definition-scope-where'\n\ntype AuthScope = {\n tenantId?: string | null\n orgId?: string | null\n}\n\nexport type DefinitionMutationScope = {\n tenantId: string | null\n organizationId: string | null\n}\n\ntype OrganizationScopeLike = {\n tenantId?: string | null\n selectedId?: string | null\n}\n\ntype DefinitionKeySelector = string | { $in: string[] }\n\ntype DefinitionVisibilityOptions = {\n deletedAt?: null | { $ne: null }\n isActive?: boolean\n}\n\ntype DefinitionScopeRecord = {\n tenantId?: string | null\n organizationId?: string | null\n updatedAt?: Date | string | number | null\n createdAt?: Date | string | number | null\n}\n\ntype MutableDefinitionScopeRecord = DefinitionScopeRecord & {\n isActive?: boolean\n deletedAt?: Date | string | number | null\n}\n\nexport function resolveDefinitionScopeFromOrganizationScope(\n auth: AuthScope,\n scope: OrganizationScopeLike,\n): DefinitionMutationScope {\n const authTenantId = auth.tenantId ?? null\n const scopeTenantId = scope.tenantId ?? null\n const tenantMismatch = Boolean(authTenantId && scopeTenantId && authTenantId !== scopeTenantId)\n\n return {\n tenantId: tenantMismatch ? authTenantId : (scopeTenantId ?? authTenantId),\n organizationId: tenantMismatch\n ? (auth.orgId ?? null)\n : (scope.selectedId ?? auth.orgId ?? null),\n }\n}\n\nexport async function resolveDefinitionMutationScope({\n auth,\n container,\n request,\n}: {\n auth: AuthScope\n container: unknown\n request: Request\n}): Promise<DefinitionMutationScope> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request } as any)\n return resolveDefinitionScopeFromOrganizationScope(auth, scope)\n}\n\nexport function createExactDefinitionWhere(\n entityId: string,\n key: DefinitionKeySelector,\n scope: DefinitionMutationScope,\n) {\n return {\n entityId,\n key,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n }\n}\n\nexport function createVisibleDefinitionWhere(\n entityId: string,\n key: DefinitionKeySelector,\n scope: DefinitionMutationScope,\n options: DefinitionVisibilityOptions = {},\n) {\n return {\n entityId,\n key,\n ...options,\n ...createVisibleDefinitionScopeClause(scope),\n }\n}\n\nfunction definitionTimestamp(def: DefinitionScopeRecord) {\n const updatedAt = def?.updatedAt instanceof Date\n ? def.updatedAt.getTime()\n : (def?.updatedAt ? new Date(def.updatedAt).getTime() : 0)\n if (updatedAt) return updatedAt\n return def?.createdAt instanceof Date\n ? def.createdAt.getTime()\n : (def?.createdAt ? new Date(def.createdAt).getTime() : 0)\n}\n\nfunction definitionScopeScore(def: DefinitionScopeRecord) {\n return (def?.tenantId ? 2 : 0) + (def?.organizationId ? 1 : 0)\n}\n\nexport function selectVisibleDefinitionWinner<T extends DefinitionScopeRecord>(definitions: T[]) {\n let winner: T | null = null\n for (const definition of definitions) {\n if (!winner) {\n winner = definition\n continue\n }\n const nextScore = definitionScopeScore(definition)\n const winnerScore = definitionScopeScore(winner)\n if (nextScore > winnerScore) {\n winner = definition\n continue\n }\n if (nextScore < winnerScore) continue\n if (definitionTimestamp(definition) >= definitionTimestamp(winner)) winner = definition\n }\n return winner\n}\n\nexport function markDefinitionTombstoned<T extends MutableDefinitionScopeRecord>(definition: T, now = new Date()) {\n definition.isActive = false\n definition.deletedAt = definition.deletedAt ?? now\n definition.updatedAt = now\n return definition\n}\n\nfunction cloneConfigJson(configJson: unknown) {\n if (configJson == null) return configJson\n return JSON.parse(JSON.stringify(configJson))\n}\n\nexport function createScopedDefinitionTombstone(\n em: Pick<EntityManager, 'create'>,\n source: {\n entityId: string\n key: string\n kind: string\n configJson?: unknown\n },\n scope: DefinitionMutationScope,\n now = new Date(),\n) {\n return em.create(CustomFieldDef, {\n entityId: source.entityId,\n key: source.key,\n kind: source.kind,\n configJson: cloneConfigJson(source.configJson) ?? {},\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: false,\n createdAt: now,\n updatedAt: now,\n deletedAt: now,\n })\n}\n"],
5
+ "mappings": "AACA,SAAS,0CAA0C;AACnD,SAAS,sBAAsB;AAC/B,SAAS,0CAA0C;AAoC5C,SAAS,4CACd,MACA,OACyB;AACzB,QAAM,eAAe,KAAK,YAAY;AACtC,QAAM,gBAAgB,MAAM,YAAY;AACxC,QAAM,iBAAiB,QAAQ,gBAAgB,iBAAiB,iBAAiB,aAAa;AAE9F,SAAO;AAAA,IACL,UAAU,iBAAiB,eAAgB,iBAAiB;AAAA,IAC5D,gBAAgB,iBACX,KAAK,SAAS,OACd,MAAM,cAAc,KAAK,SAAS;AAAA,EACzC;AACF;AAEA,eAAsB,+BAA+B;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AACF,GAIqC;AACnC,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,QAAQ,CAAQ;AAC1F,SAAO,4CAA4C,MAAM,KAAK;AAChE;AAEO,SAAS,2BACd,UACA,KACA,OACA;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,EAClB;AACF;AAEO,SAAS,6BACd,UACA,KACA,OACA,UAAuC,CAAC,GACxC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAG;AAAA,IACH,GAAG,mCAAmC,KAAK;AAAA,EAC7C;AACF;AAEA,SAAS,oBAAoB,KAA4B;AACvD,QAAM,YAAY,KAAK,qBAAqB,OACxC,IAAI,UAAU,QAAQ,IACrB,KAAK,YAAY,IAAI,KAAK,IAAI,SAAS,EAAE,QAAQ,IAAI;AAC1D,MAAI,UAAW,QAAO;AACtB,SAAO,KAAK,qBAAqB,OAC7B,IAAI,UAAU,QAAQ,IACrB,KAAK,YAAY,IAAI,KAAK,IAAI,SAAS,EAAE,QAAQ,IAAI;AAC5D;AAEA,SAAS,qBAAqB,KAA4B;AACxD,UAAQ,KAAK,WAAW,IAAI,MAAM,KAAK,iBAAiB,IAAI;AAC9D;AAEO,SAAS,8BAA+D,aAAkB;AAC/F,MAAI,SAAmB;AACvB,aAAW,cAAc,aAAa;AACpC,QAAI,CAAC,QAAQ;AACX,eAAS;AACT;AAAA,IACF;AACA,UAAM,YAAY,qBAAqB,UAAU;AACjD,UAAM,cAAc,qBAAqB,MAAM;AAC/C,QAAI,YAAY,aAAa;AAC3B,eAAS;AACT;AAAA,IACF;AACA,QAAI,YAAY,YAAa;AAC7B,QAAI,oBAAoB,UAAU,KAAK,oBAAoB,MAAM,EAAG,UAAS;AAAA,EAC/E;AACA,SAAO;AACT;AAEO,SAAS,yBAAiE,YAAe,MAAM,oBAAI,KAAK,GAAG;AAChH,aAAW,WAAW;AACtB,aAAW,YAAY,WAAW,aAAa;AAC/C,aAAW,YAAY;AACvB,SAAO;AACT;AAEA,SAAS,gBAAgB,YAAqB;AAC5C,MAAI,cAAc,KAAM,QAAO;AAC/B,SAAO,KAAK,MAAM,KAAK,UAAU,UAAU,CAAC;AAC9C;AAEO,SAAS,gCACd,IACA,QAMA,OACA,MAAM,oBAAI,KAAK,GACf;AACA,SAAO,GAAG,OAAO,gBAAgB;AAAA,IAC/B,UAAU,OAAO;AAAA,IACjB,KAAK,OAAO;AAAA,IACZ,MAAM,OAAO;AAAA,IACb,YAAY,gBAAgB,OAAO,UAAU,KAAK,CAAC;AAAA,IACnD,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,IAChB,UAAU;AAAA,IACV,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -3,7 +3,8 @@ import {
3
3
  MAX_CUSTOM_FIELD_KEYS_PER_RECORD,
4
4
  TOO_MANY_CUSTOM_FIELDS_ERROR
5
5
  } from "@open-mercato/shared/modules/entities/validation";
6
- import { CustomFieldDef, CustomFieldValue } from "../data/entities.js";
6
+ import { CustomFieldValue } from "../data/entities.js";
7
+ import { loadScopedCustomFieldDefs } from "./scoped-field-defs.js";
7
8
  function columnFromKind(kind) {
8
9
  switch (kind) {
9
10
  case "text":
@@ -44,34 +45,7 @@ async function setRecordCustomFields(em, opts) {
44
45
  const preferDefs = opts.preferDefs !== false;
45
46
  let defsByKey;
46
47
  if (preferDefs) {
47
- const defs = await em.find(CustomFieldDef, {
48
- entityId,
49
- isActive: true,
50
- deletedAt: null,
51
- organizationId: { $in: [organizationId, null] },
52
- tenantId: { $in: [tenantId, null] }
53
- });
54
- const scopeScore = (def) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0);
55
- defsByKey = {};
56
- for (const d of defs) {
57
- const existing = defsByKey[d.key];
58
- if (!existing) {
59
- defsByKey[d.key] = d;
60
- continue;
61
- }
62
- const nextScore = scopeScore(d);
63
- const existingScore = scopeScore(existing);
64
- if (nextScore > existingScore) {
65
- defsByKey[d.key] = d;
66
- continue;
67
- }
68
- if (nextScore < existingScore) continue;
69
- const nextUpdatedAt = d.updatedAt instanceof Date ? d.updatedAt.getTime() : new Date(d.updatedAt).getTime();
70
- const existingUpdatedAt = existing.updatedAt instanceof Date ? existing.updatedAt.getTime() : new Date(existing.updatedAt).getTime();
71
- if (nextUpdatedAt >= existingUpdatedAt) {
72
- defsByKey[d.key] = d;
73
- }
74
- }
48
+ defsByKey = await loadScopedCustomFieldDefs(em, { entityId, organizationId, tenantId });
75
49
  }
76
50
  const toPersist = [];
77
51
  let encryptionService;
@@ -94,7 +68,7 @@ async function setRecordCustomFields(em, opts) {
94
68
  for (const fieldKey of keys) {
95
69
  const raw = values[fieldKey];
96
70
  if (raw === void 0) continue;
97
- const def = defsByKey?.[fieldKey];
71
+ const def = defsByKey?.get(fieldKey);
98
72
  const encrypted = Boolean(def?.configJson && def.configJson?.encrypted);
99
73
  const isArray = Array.isArray(raw);
100
74
  if (isArray) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/lib/helpers.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { encryptCustomFieldValue, resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport {\n MAX_CUSTOM_FIELD_KEYS_PER_RECORD,\n TOO_MANY_CUSTOM_FIELDS_ERROR,\n} from '@open-mercato/shared/modules/entities/validation'\nimport { CustomFieldDef, CustomFieldValue } from '../data/entities'\n\ntype Primitive = string | number | boolean | null | undefined\ntype PrimitiveOrArray = Primitive | Primitive[]\n\nexport type SetRecordCustomFieldsOptions = {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, PrimitiveOrArray>\n // When true (default), try to use field definitions to decide storage column\n preferDefs?: boolean\n // Optional: notify external systems (e.g., indexing) when values changed\n onChanged?: (payload: { entityId: string; recordId: string; organizationId: string | null; tenantId: string | null }) => Promise<void> | void\n // Optional: re-use an existing tenant encryption service instance\n encryptionService?: TenantDataEncryptionService | null\n}\n\nfunction columnFromKind(kind: string): keyof CustomFieldValue {\n switch (kind) {\n case 'text':\n case 'select':\n case 'currency':\n case 'dictionary':\n case 'phone':\n return 'valueText'\n case 'multiline':\n return 'valueMultiline'\n case 'integer':\n return 'valueInt'\n case 'float':\n return 'valueFloat'\n case 'boolean':\n return 'valueBool'\n default:\n return 'valueText'\n }\n}\n\nfunction columnFromJsValue(v: Primitive): keyof CustomFieldValue {\n if (v === null || v === undefined) return 'valueText'\n if (typeof v === 'boolean') return 'valueBool'\n if (typeof v === 'number') return Number.isInteger(v) ? 'valueInt' : 'valueFloat'\n return 'valueText'\n}\n\n// Clears all value columns to avoid leftovers on update\nfunction clearValueColumns(cf: CustomFieldValue) {\n cf.valueText = null\n cf.valueMultiline = null\n cf.valueInt = null\n cf.valueFloat = null\n cf.valueBool = null\n}\n\nexport async function setRecordCustomFields(\n em: EntityManager,\n opts: SetRecordCustomFieldsOptions,\n): Promise<void> {\n const { entityId, recordId, values } = opts\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const preferDefs = opts.preferDefs !== false\n\n let defsByKey: Record<string, CustomFieldDef> | undefined\n if (preferDefs) {\n const defs = await em.find(CustomFieldDef, {\n entityId,\n isActive: true,\n deletedAt: null,\n organizationId: { $in: [organizationId, null] as any },\n tenantId: { $in: [tenantId, null] as any },\n })\n const scopeScore = (def: CustomFieldDef) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0)\n defsByKey = {}\n for (const d of defs) {\n const existing = defsByKey[d.key]\n if (!existing) {\n defsByKey[d.key] = d\n continue\n }\n const nextScore = scopeScore(d)\n const existingScore = scopeScore(existing)\n if (nextScore > existingScore) {\n defsByKey[d.key] = d\n continue\n }\n if (nextScore < existingScore) continue\n\n const nextUpdatedAt = d.updatedAt instanceof Date ? d.updatedAt.getTime() : new Date(d.updatedAt).getTime()\n const existingUpdatedAt = existing.updatedAt instanceof Date\n ? existing.updatedAt.getTime()\n : new Date(existing.updatedAt).getTime()\n if (nextUpdatedAt >= existingUpdatedAt) {\n defsByKey[d.key] = d\n }\n }\n }\n\n const toPersist: CustomFieldValue[] = []\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em as any, opts.encryptionService)\n return encryptionService\n }\n const keys = Object.keys(values)\n const presentKeyCount = keys.filter((key) => values[key] !== undefined).length\n if (preferDefs && presentKeyCount > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {\n throw new Error(TOO_MANY_CUSTOM_FIELDS_ERROR)\n }\n\n // Run the per-key delete+insert work inside ONE database transaction so a\n // multi-value replacement is atomic and isolated. The array branch deletes the\n // existing rows for a key and inserts the replacements; without an enclosing\n // transaction those can land in separate commit boundaries under MikroORM's\n // FlushMode.AUTO (a query elsewhere in the unit auto-flushes part of the work),\n // which intermittently left the field with the delete applied but the inserts\n // missing \u2014 the multi-select EDIT reverted to []. The single commit below makes\n // it all-or-nothing. We only open our own transaction when the caller has not\n // already started one (commands fork the request em and may run setCustomFields\n // outside their own withAtomicFlush tx); join an ambient transaction otherwise.\n const txEm = em as {\n begin?: () => Promise<void>\n commit?: () => Promise<void>\n rollback?: () => Promise<void>\n isInTransaction?: () => boolean\n }\n const txCapable =\n typeof txEm.begin === 'function' &&\n typeof txEm.commit === 'function' &&\n typeof txEm.rollback === 'function' &&\n typeof txEm.isInTransaction === 'function'\n const ownCustomFieldTransaction = txCapable && !txEm.isInTransaction!()\n if (ownCustomFieldTransaction) await txEm.begin!()\n try {\n for (const fieldKey of keys) {\n const raw = values[fieldKey]\n if (raw === undefined) continue\n\n const def = defsByKey?.[fieldKey]\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const isArray = Array.isArray(raw)\n // When array (multi-value): replace all existing rows for the key. Delete\n // first, then create replacements, all inside the transaction opened above.\n // Creating rows before a native delete can auto-flush and delete the new\n // values; mixing em.remove(stale) with new rows for the same EAV scope was\n // observed to commit an empty set under MikroORM v7. The explicit order keeps\n // the replacement atomic without letting old-row cleanup target new rows.\n if (isArray) {\n const arr = raw as Primitive[]\n await em.nativeDelete(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n for (const val of arr) {\n const col: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(val)\n const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n clearValueColumns(cf)\n const stored = encrypted\n ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache)\n : val\n switch (col) {\n case 'valueText': cf.valueText = stored == null ? null : String(stored); break\n case 'valueMultiline': cf.valueMultiline = stored == null ? null : String(stored); break\n case 'valueInt': cf.valueInt = stored == null ? null : Number(stored); break\n case 'valueFloat': cf.valueFloat = stored == null ? null : Number(stored); break\n case 'valueBool': cf.valueBool = stored == null ? null : Boolean(stored); break\n default: cf.valueText = stored == null ? null : String(stored); break\n }\n toPersist.push(cf)\n }\n continue\n }\n\n const column: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(raw as Primitive)\n const storedValue = encrypted\n ? await encryptCustomFieldValue(raw as Primitive, tenantId, getEncryptionService(), encryptionCache)\n : raw\n\n let cf = await em.findOne(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n if (!cf) {\n cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n toPersist.push(cf)\n }\n clearValueColumns(cf)\n switch (column) {\n case 'valueText':\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueMultiline':\n cf.valueMultiline = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueInt':\n cf.valueInt = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueFloat':\n cf.valueFloat = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueBool':\n cf.valueBool = (storedValue as Primitive) == null ? null : Boolean(storedValue as Primitive)\n break\n default:\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n }\n }\n\n if (toPersist.length) em.persist(toPersist)\n await em.flush()\n if (ownCustomFieldTransaction) await txEm.commit!()\n } catch (err) {\n if (ownCustomFieldTransaction) {\n try { await txEm.rollback!() } catch { /* surface the original error, not a rollback failure */ }\n }\n throw err\n }\n // Emit hook for indexing if requested (outside CRUD flows). Runs AFTER the\n // transaction commits so consumers observe the persisted rows.\n try {\n if (typeof opts.onChanged === 'function') {\n await opts.onChanged({ entityId, recordId, organizationId, tenantId })\n }\n } catch {\n // Non-blocking\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { encryptCustomFieldValue, resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport {\n MAX_CUSTOM_FIELD_KEYS_PER_RECORD,\n TOO_MANY_CUSTOM_FIELDS_ERROR,\n} from '@open-mercato/shared/modules/entities/validation'\nimport { CustomFieldDef, CustomFieldValue } from '../data/entities'\nimport { loadScopedCustomFieldDefs } from './scoped-field-defs'\n\ntype Primitive = string | number | boolean | null | undefined\ntype PrimitiveOrArray = Primitive | Primitive[]\n\nexport type SetRecordCustomFieldsOptions = {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, PrimitiveOrArray>\n // When true (default), try to use field definitions to decide storage column\n preferDefs?: boolean\n // Optional: notify external systems (e.g., indexing) when values changed\n onChanged?: (payload: { entityId: string; recordId: string; organizationId: string | null; tenantId: string | null }) => Promise<void> | void\n // Optional: re-use an existing tenant encryption service instance\n encryptionService?: TenantDataEncryptionService | null\n}\n\nfunction columnFromKind(kind: string): keyof CustomFieldValue {\n switch (kind) {\n case 'text':\n case 'select':\n case 'currency':\n case 'dictionary':\n case 'phone':\n return 'valueText'\n case 'multiline':\n return 'valueMultiline'\n case 'integer':\n return 'valueInt'\n case 'float':\n return 'valueFloat'\n case 'boolean':\n return 'valueBool'\n default:\n return 'valueText'\n }\n}\n\nfunction columnFromJsValue(v: Primitive): keyof CustomFieldValue {\n if (v === null || v === undefined) return 'valueText'\n if (typeof v === 'boolean') return 'valueBool'\n if (typeof v === 'number') return Number.isInteger(v) ? 'valueInt' : 'valueFloat'\n return 'valueText'\n}\n\n// Clears all value columns to avoid leftovers on update\nfunction clearValueColumns(cf: CustomFieldValue) {\n cf.valueText = null\n cf.valueMultiline = null\n cf.valueInt = null\n cf.valueFloat = null\n cf.valueBool = null\n}\n\nexport async function setRecordCustomFields(\n em: EntityManager,\n opts: SetRecordCustomFieldsOptions,\n): Promise<void> {\n const { entityId, recordId, values } = opts\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const preferDefs = opts.preferDefs !== false\n\n let defsByKey: Map<string, CustomFieldDef> | undefined\n if (preferDefs) {\n defsByKey = await loadScopedCustomFieldDefs(em, { entityId, organizationId, tenantId })\n }\n\n const toPersist: CustomFieldValue[] = []\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em as any, opts.encryptionService)\n return encryptionService\n }\n const keys = Object.keys(values)\n const presentKeyCount = keys.filter((key) => values[key] !== undefined).length\n if (preferDefs && presentKeyCount > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {\n throw new Error(TOO_MANY_CUSTOM_FIELDS_ERROR)\n }\n\n // Run the per-key delete+insert work inside ONE database transaction so a\n // multi-value replacement is atomic and isolated. The array branch deletes the\n // existing rows for a key and inserts the replacements; without an enclosing\n // transaction those can land in separate commit boundaries under MikroORM's\n // FlushMode.AUTO (a query elsewhere in the unit auto-flushes part of the work),\n // which intermittently left the field with the delete applied but the inserts\n // missing \u2014 the multi-select EDIT reverted to []. The single commit below makes\n // it all-or-nothing. We only open our own transaction when the caller has not\n // already started one (commands fork the request em and may run setCustomFields\n // outside their own withAtomicFlush tx); join an ambient transaction otherwise.\n const txEm = em as {\n begin?: () => Promise<void>\n commit?: () => Promise<void>\n rollback?: () => Promise<void>\n isInTransaction?: () => boolean\n }\n const txCapable =\n typeof txEm.begin === 'function' &&\n typeof txEm.commit === 'function' &&\n typeof txEm.rollback === 'function' &&\n typeof txEm.isInTransaction === 'function'\n const ownCustomFieldTransaction = txCapable && !txEm.isInTransaction!()\n if (ownCustomFieldTransaction) await txEm.begin!()\n try {\n for (const fieldKey of keys) {\n const raw = values[fieldKey]\n if (raw === undefined) continue\n\n const def = defsByKey?.get(fieldKey)\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const isArray = Array.isArray(raw)\n // When array (multi-value): replace all existing rows for the key. Delete\n // first, then create replacements, all inside the transaction opened above.\n // Creating rows before a native delete can auto-flush and delete the new\n // values; mixing em.remove(stale) with new rows for the same EAV scope was\n // observed to commit an empty set under MikroORM v7. The explicit order keeps\n // the replacement atomic without letting old-row cleanup target new rows.\n if (isArray) {\n const arr = raw as Primitive[]\n await em.nativeDelete(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n for (const val of arr) {\n const col: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(val)\n const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n clearValueColumns(cf)\n const stored = encrypted\n ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache)\n : val\n switch (col) {\n case 'valueText': cf.valueText = stored == null ? null : String(stored); break\n case 'valueMultiline': cf.valueMultiline = stored == null ? null : String(stored); break\n case 'valueInt': cf.valueInt = stored == null ? null : Number(stored); break\n case 'valueFloat': cf.valueFloat = stored == null ? null : Number(stored); break\n case 'valueBool': cf.valueBool = stored == null ? null : Boolean(stored); break\n default: cf.valueText = stored == null ? null : String(stored); break\n }\n toPersist.push(cf)\n }\n continue\n }\n\n const column: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(raw as Primitive)\n const storedValue = encrypted\n ? await encryptCustomFieldValue(raw as Primitive, tenantId, getEncryptionService(), encryptionCache)\n : raw\n\n let cf = await em.findOne(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n if (!cf) {\n cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n toPersist.push(cf)\n }\n clearValueColumns(cf)\n switch (column) {\n case 'valueText':\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueMultiline':\n cf.valueMultiline = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueInt':\n cf.valueInt = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueFloat':\n cf.valueFloat = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueBool':\n cf.valueBool = (storedValue as Primitive) == null ? null : Boolean(storedValue as Primitive)\n break\n default:\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n }\n }\n\n if (toPersist.length) em.persist(toPersist)\n await em.flush()\n if (ownCustomFieldTransaction) await txEm.commit!()\n } catch (err) {\n if (ownCustomFieldTransaction) {\n try { await txEm.rollback!() } catch { /* surface the original error, not a rollback failure */ }\n }\n throw err\n }\n // Emit hook for indexing if requested (outside CRUD flows). Runs AFTER the\n // transaction commits so consumers observe the persisted rows.\n try {\n if (typeof opts.onChanged === 'function') {\n await opts.onChanged({ entityId, recordId, organizationId, tenantId })\n }\n } catch {\n // Non-blocking\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["cf"]
7
7
  }
@@ -1,21 +1,16 @@
1
1
  import { CustomFieldDef } from "../data/entities.js";
2
+ import { createVisibleDefinitionScopeClause } from "./definition-scope-where.js";
2
3
  const scopeScore = (def) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0);
3
4
  const definitionTime = (def) => def.updatedAt instanceof Date ? def.updatedAt.getTime() : new Date(def.updatedAt).getTime();
4
5
  async function loadScopedCustomFieldDefs(em, opts) {
5
- const organizationId = opts.organizationId ?? null;
6
- const tenantId = opts.tenantId ?? null;
7
6
  const defs = await em.find(CustomFieldDef, {
8
7
  entityId: opts.entityId,
9
8
  isActive: true,
10
9
  deletedAt: null,
11
- $and: [
12
- {
13
- $or: organizationId === null ? [{ organizationId: null }] : [{ organizationId }, { organizationId: null }]
14
- },
15
- {
16
- $or: tenantId === null ? [{ tenantId: null }] : [{ tenantId }, { tenantId: null }]
17
- }
18
- ]
10
+ ...createVisibleDefinitionScopeClause({
11
+ organizationId: opts.organizationId,
12
+ tenantId: opts.tenantId
13
+ })
19
14
  });
20
15
  const byKey = /* @__PURE__ */ new Map();
21
16
  for (const def of defs) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/lib/scoped-field-defs.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef } from '../data/entities'\n\nexport type ScopedCustomFieldDefsOptions = {\n entityId: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\n// Definitions overlay by scope: an org+tenant row shadows a tenant-global row,\n// which shadows an instance-global one. Duplicate keys within the same scope are\n// resolved by recency so the newest definition wins.\nconst scopeScore = (def: CustomFieldDef) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0)\n\nconst definitionTime = (def: CustomFieldDef) =>\n def.updatedAt instanceof Date ? def.updatedAt.getTime() : new Date(def.updatedAt).getTime()\n\nexport async function loadScopedCustomFieldDefs(\n em: EntityManager,\n opts: ScopedCustomFieldDefsOptions,\n): Promise<Map<string, CustomFieldDef>> {\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const defs = await em.find(CustomFieldDef, {\n entityId: opts.entityId,\n isActive: true,\n deletedAt: null,\n $and: [\n {\n $or: organizationId === null\n ? [{ organizationId: null }]\n : [{ organizationId }, { organizationId: null }],\n },\n {\n $or: tenantId === null\n ? [{ tenantId: null }]\n : [{ tenantId }, { tenantId: null }],\n },\n ],\n } as any)\n\n const byKey = new Map<string, CustomFieldDef>()\n for (const def of defs) {\n const existing = byKey.get(def.key)\n if (!existing) {\n byKey.set(def.key, def)\n continue\n }\n const nextScore = scopeScore(def)\n const existingScore = scopeScore(existing)\n if (nextScore > existingScore) {\n byKey.set(def.key, def)\n continue\n }\n if (nextScore < existingScore) continue\n if (definitionTime(def) >= definitionTime(existing)) byKey.set(def.key, def)\n }\n return byKey\n}\n\n// Kind lookup used by read paths that must not reinterpret a stored value against\n// the wrong type. Failures degrade to an empty map: callers then leave values as\n// stored, which is always safer than coercing them blind.\nexport async function loadCustomFieldKinds(\n em: EntityManager,\n opts: ScopedCustomFieldDefsOptions,\n): Promise<Map<string, string>> {\n try {\n const defs = await loadScopedCustomFieldDefs(em, opts)\n const kinds = new Map<string, string>()\n for (const [key, def] of defs) kinds.set(key, String(def.kind || ''))\n return kinds\n } catch {\n return new Map<string, string>()\n }\n}\n"],
5
- "mappings": "AACA,SAAS,sBAAsB;AAW/B,MAAM,aAAa,CAAC,SAAyB,IAAI,WAAW,IAAI,MAAM,IAAI,iBAAiB,IAAI;AAE/F,MAAM,iBAAiB,CAAC,QACtB,IAAI,qBAAqB,OAAO,IAAI,UAAU,QAAQ,IAAI,IAAI,KAAK,IAAI,SAAS,EAAE,QAAQ;AAE5F,eAAsB,0BACpB,IACA,MACsC;AACtC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,OAAO,MAAM,GAAG,KAAK,gBAAgB;AAAA,IACzC,UAAU,KAAK;AAAA,IACf,UAAU;AAAA,IACV,WAAW;AAAA,IACX,MAAM;AAAA,MACJ;AAAA,QACE,KAAK,mBAAmB,OACpB,CAAC,EAAE,gBAAgB,KAAK,CAAC,IACzB,CAAC,EAAE,eAAe,GAAG,EAAE,gBAAgB,KAAK,CAAC;AAAA,MACnD;AAAA,MACA;AAAA,QACE,KAAK,aAAa,OACd,CAAC,EAAE,UAAU,KAAK,CAAC,IACnB,CAAC,EAAE,SAAS,GAAG,EAAE,UAAU,KAAK,CAAC;AAAA,MACvC;AAAA,IACF;AAAA,EACF,CAAQ;AAER,QAAM,QAAQ,oBAAI,IAA4B;AAC9C,aAAW,OAAO,MAAM;AACtB,UAAM,WAAW,MAAM,IAAI,IAAI,GAAG;AAClC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,IAAI,KAAK,GAAG;AACtB;AAAA,IACF;AACA,UAAM,YAAY,WAAW,GAAG;AAChC,UAAM,gBAAgB,WAAW,QAAQ;AACzC,QAAI,YAAY,eAAe;AAC7B,YAAM,IAAI,IAAI,KAAK,GAAG;AACtB;AAAA,IACF;AACA,QAAI,YAAY,cAAe;AAC/B,QAAI,eAAe,GAAG,KAAK,eAAe,QAAQ,EAAG,OAAM,IAAI,IAAI,KAAK,GAAG;AAAA,EAC7E;AACA,SAAO;AACT;AAKA,eAAsB,qBACpB,IACA,MAC8B;AAC9B,MAAI;AACF,UAAM,OAAO,MAAM,0BAA0B,IAAI,IAAI;AACrD,UAAM,QAAQ,oBAAI,IAAoB;AACtC,eAAW,CAAC,KAAK,GAAG,KAAK,KAAM,OAAM,IAAI,KAAK,OAAO,IAAI,QAAQ,EAAE,CAAC;AACpE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,oBAAI,IAAoB;AAAA,EACjC;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef } from '../data/entities'\nimport { createVisibleDefinitionScopeClause } from './definition-scope-where'\n\nexport type ScopedCustomFieldDefsOptions = {\n entityId: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\n// Definitions overlay by scope: an org+tenant row shadows a tenant-global row,\n// which shadows an instance-global one. Duplicate keys within the same scope are\n// resolved by recency so the newest definition wins.\nconst scopeScore = (def: CustomFieldDef) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0)\n\nconst definitionTime = (def: CustomFieldDef) =>\n def.updatedAt instanceof Date ? def.updatedAt.getTime() : new Date(def.updatedAt).getTime()\n\nexport async function loadScopedCustomFieldDefs(\n em: EntityManager,\n opts: ScopedCustomFieldDefsOptions,\n): Promise<Map<string, CustomFieldDef>> {\n const defs = await em.find(CustomFieldDef, {\n entityId: opts.entityId,\n isActive: true,\n deletedAt: null,\n ...createVisibleDefinitionScopeClause({\n organizationId: opts.organizationId,\n tenantId: opts.tenantId,\n }),\n })\n\n const byKey = new Map<string, CustomFieldDef>()\n for (const def of defs) {\n const existing = byKey.get(def.key)\n if (!existing) {\n byKey.set(def.key, def)\n continue\n }\n const nextScore = scopeScore(def)\n const existingScore = scopeScore(existing)\n if (nextScore > existingScore) {\n byKey.set(def.key, def)\n continue\n }\n if (nextScore < existingScore) continue\n if (definitionTime(def) >= definitionTime(existing)) byKey.set(def.key, def)\n }\n return byKey\n}\n\n// Kind lookup used by read paths that must not reinterpret a stored value against\n// the wrong type. Failures degrade to an empty map: callers then leave values as\n// stored, which is always safer than coercing them blind.\nexport async function loadCustomFieldKinds(\n em: EntityManager,\n opts: ScopedCustomFieldDefsOptions,\n): Promise<Map<string, string>> {\n try {\n const defs = await loadScopedCustomFieldDefs(em, opts)\n const kinds = new Map<string, string>()\n for (const [key, def] of defs) kinds.set(key, String(def.kind || ''))\n return kinds\n } catch {\n return new Map<string, string>()\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,sBAAsB;AAC/B,SAAS,0CAA0C;AAWnD,MAAM,aAAa,CAAC,SAAyB,IAAI,WAAW,IAAI,MAAM,IAAI,iBAAiB,IAAI;AAE/F,MAAM,iBAAiB,CAAC,QACtB,IAAI,qBAAqB,OAAO,IAAI,UAAU,QAAQ,IAAI,IAAI,KAAK,IAAI,SAAS,EAAE,QAAQ;AAE5F,eAAsB,0BACpB,IACA,MACsC;AACtC,QAAM,OAAO,MAAM,GAAG,KAAK,gBAAgB;AAAA,IACzC,UAAU,KAAK;AAAA,IACf,UAAU;AAAA,IACV,WAAW;AAAA,IACX,GAAG,mCAAmC;AAAA,MACpC,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,IACjB,CAAC;AAAA,EACH,CAAC;AAED,QAAM,QAAQ,oBAAI,IAA4B;AAC9C,aAAW,OAAO,MAAM;AACtB,UAAM,WAAW,MAAM,IAAI,IAAI,GAAG;AAClC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,IAAI,KAAK,GAAG;AACtB;AAAA,IACF;AACA,UAAM,YAAY,WAAW,GAAG;AAChC,UAAM,gBAAgB,WAAW,QAAQ;AACzC,QAAI,YAAY,eAAe;AAC7B,YAAM,IAAI,IAAI,KAAK,GAAG;AACtB;AAAA,IACF;AACA,QAAI,YAAY,cAAe;AAC/B,QAAI,eAAe,GAAG,KAAK,eAAe,QAAQ,EAAG,OAAM,IAAI,IAAI,KAAK,GAAG;AAAA,EAC7E;AACA,SAAO;AACT;AAKA,eAAsB,qBACpB,IACA,MAC8B;AAC9B,MAAI;AACF,UAAM,OAAO,MAAM,0BAA0B,IAAI,IAAI;AACrD,UAAM,QAAQ,oBAAI,IAAoB;AACtC,eAAW,CAAC,KAAK,GAAG,KAAK,KAAM,OAAM,IAAI,KAAK,OAAO,IAAI,QAAQ,EAAE,CAAC;AACpE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,oBAAI,IAAoB;AAAA,EACjC;AACF;",
6
6
  "names": []
7
7
  }