@open-mercato/core 0.6.6-develop.6543.1.49fadb4c31 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/api_keys/api/keys/route.js +9 -23
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/api_keys/cli.js +1 -2
- package/dist/modules/api_keys/cli.js.map +2 -2
- package/dist/modules/attachments/api/file/[id]/route.js +7 -9
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +17 -21
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +2 -6
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/cli.js +2 -3
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +0 -22
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +2 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -7
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +7 -11
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +3 -22
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +3 -22
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/cli.js +3 -4
- package/dist/modules/dashboards/cli.js.map +2 -2
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js +1 -9
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
- package/dist/modules/dictionaries/commands/entries.js +14 -7
- package/dist/modules/dictionaries/commands/entries.js.map +2 -2
- package/dist/modules/dictionaries/commands/entry-operations.js +16 -7
- package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
- package/dist/modules/dictionaries/commands/factory.js +3 -9
- package/dist/modules/dictionaries/commands/factory.js.map +2 -2
- package/dist/modules/dictionaries/fields/dictionary.js +41 -56
- package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +3 -10
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/records.js +4 -5
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js +0 -1
- package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +49 -49
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js +0 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js.map +2 -2
- package/dist/modules/entities/cli.js +1 -2
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/query_index/api/status.js +8 -2
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +0 -44
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +27 -2
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +2 -6
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +0 -1
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +0 -9
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +0 -44
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +38 -59
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/lib/shipments/snapshots.js +1 -27
- package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +8 -1
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +1 -1
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/find-definition.js +18 -2
- package/dist/modules/workflows/lib/find-definition.js.map +2 -2
- package/dist/modules/workflows/lib/signal-handler.js +3 -2
- package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +4 -8
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-handler.js +3 -5
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/timer-handler.js +2 -1
- package/dist/modules/workflows/lib/timer-handler.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +5 -13
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +11 -9
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260715120000.js +68 -0
- package/dist/modules/workflows/migrations/Migration20260715120000.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260716120000.js +24 -0
- package/dist/modules/workflows/migrations/Migration20260716120000.js.map +7 -0
- package/dist/modules/workflows/workflows.js +2 -0
- package/dist/modules/workflows/workflows.js.map +2 -2
- package/package.json +8 -9
- package/src/modules/api_keys/api/keys/route.ts +9 -27
- package/src/modules/api_keys/cli.ts +3 -2
- package/src/modules/attachments/api/file/[id]/route.ts +7 -11
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -11
- package/src/modules/attachments/api/route.ts +21 -26
- package/src/modules/auth/api/users/route.ts +2 -11
- package/src/modules/auth/cli.ts +2 -3
- package/src/modules/configs/i18n/de.json +1 -5
- package/src/modules/configs/i18n/en.json +1 -5
- package/src/modules/configs/i18n/es.json +1 -5
- package/src/modules/configs/i18n/pl.json +1 -5
- package/src/modules/configs/lib/upgrade-actions.ts +0 -24
- package/src/modules/customer_accounts/AGENTS.md +6 -8
- package/src/modules/customer_accounts/widgets/injection-table.ts +2 -0
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -11
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +7 -13
- package/src/modules/dashboards/api/roles/widgets/route.ts +3 -22
- package/src/modules/dashboards/api/users/widgets/route.ts +3 -22
- package/src/modules/dashboards/cli.ts +9 -4
- package/src/modules/dashboards/lib/widgetAssignmentScope.ts +0 -18
- package/src/modules/dictionaries/commands/entries.ts +16 -7
- package/src/modules/dictionaries/commands/entry-operations.ts +18 -8
- package/src/modules/dictionaries/commands/factory.ts +2 -8
- package/src/modules/dictionaries/fields/dictionary.tsx +32 -55
- package/src/modules/dictionaries/i18n/de.json +0 -2
- package/src/modules/dictionaries/i18n/en.json +0 -2
- package/src/modules/dictionaries/i18n/es.json +0 -2
- package/src/modules/dictionaries/i18n/pl.json +0 -2
- package/src/modules/directory/utils/organizationScope.ts +2 -36
- package/src/modules/entities/api/records.ts +7 -5
- package/src/modules/entities/backend/entities/system/[entityId]/page.meta.ts +0 -1
- package/src/modules/entities/backend/entities/user/[entityId]/page.meta.ts +0 -1
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +49 -51
- package/src/modules/entities/cli.ts +1 -2
- package/src/modules/entities/i18n/de.json +0 -34
- package/src/modules/entities/i18n/en.json +0 -34
- package/src/modules/entities/i18n/es.json +0 -34
- package/src/modules/entities/i18n/pl.json +0 -34
- package/src/modules/query_index/api/status.ts +8 -2
- package/src/modules/sales/commands/documents.ts +0 -65
- package/src/modules/sales/commands/returns.ts +32 -2
- package/src/modules/sales/components/documents/AddressesSection.tsx +2 -6
- package/src/modules/sales/components/documents/ItemsSection.tsx +0 -5
- package/src/modules/sales/components/documents/LineItemDialog.tsx +0 -10
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +0 -46
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +40 -65
- package/src/modules/sales/i18n/de.json +0 -4
- package/src/modules/sales/i18n/en.json +0 -4
- package/src/modules/sales/i18n/es.json +0 -4
- package/src/modules/sales/i18n/pl.json +0 -4
- package/src/modules/sales/lib/shipments/snapshots.ts +0 -30
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +12 -1
- package/src/modules/workflows/lib/activity-executor.ts +22 -2
- package/src/modules/workflows/lib/find-definition.ts +53 -1
- package/src/modules/workflows/lib/signal-handler.ts +5 -4
- package/src/modules/workflows/lib/step-handler.ts +4 -8
- package/src/modules/workflows/lib/task-handler.ts +2 -4
- package/src/modules/workflows/lib/timer-handler.ts +3 -2
- package/src/modules/workflows/lib/transition-handler.ts +5 -13
- package/src/modules/workflows/lib/workflow-executor.ts +11 -8
- package/src/modules/workflows/migrations/Migration20260715120000.ts +114 -0
- package/src/modules/workflows/migrations/Migration20260716120000.ts +57 -0
- package/src/modules/workflows/workflows.ts +2 -0
- package/dist/modules/attachments/lib/reconcileOrganization.js +0 -110
- package/dist/modules/attachments/lib/reconcileOrganization.js.map +0 -7
- package/dist/modules/attachments/lib/requestScope.js +0 -10
- package/dist/modules/attachments/lib/requestScope.js.map +0 -7
- package/dist/modules/customers/components/detail/useDealsAccess.js +0 -16
- package/dist/modules/customers/components/detail/useDealsAccess.js.map +0 -7
- package/src/modules/attachments/lib/reconcileOrganization.ts +0 -188
- package/src/modules/attachments/lib/requestScope.ts +0 -27
- package/src/modules/customers/components/detail/useDealsAccess.ts +0 -19
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/dictionaries/commands/factory.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n normalizeDictionaryValue,\n sanitizeDictionaryColor,\n sanitizeDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/lib/utils'\nimport {\n registerCommand,\n type CommandHandler,\n type CommandRuntimeContext,\n} from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'\nimport { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { z } from 'zod'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\ntype DictionaryScope = {\n tenantId: string\n organizationId: string\n}\n\nexport function ensureDictionaryEntryScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {\n ensureTenantScope(ctx, scope.tenantId)\n ensureOrganizationScope(ctx, scope.organizationId)\n}\n\nexport type DictionaryEntrySnapshot = {\n id: string\n dictionaryId: string\n dictionaryKey: string\n organizationId: string\n tenantId: string\n value: string\n label: string\n color: string | null\n icon: string | null\n createdAt: string\n updatedAt: string\n position?: number\n isDefault?: boolean\n}\n\nexport type DictionaryEntryUndoPayload = {\n before?: DictionaryEntrySnapshot | null\n after?: DictionaryEntrySnapshot | null\n}\n\nconst ENTRY_CHANGE_KEYS = ['value', 'label', 'color', 'icon', 'position', 'isDefault'] as const\n\ntype ResolveDictionaryForCreate<TCreate> = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n parsed: TCreate\n}) => Promise<{ dictionary: Dictionary; scope: DictionaryScope }>\n\ntype ResolveEntry<TInput> = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n id: string\n parsed?: TInput\n}) => Promise<{ entry: DictionaryEntry; dictionary: Dictionary; scope: DictionaryScope }>\n\ntype EnsureDictionaryForUndo = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n snapshot: DictionaryEntrySnapshot\n}) => Promise<Dictionary | null>\n\ntype EnsureScopeFn = (ctx: CommandRuntimeContext, scope: DictionaryScope) => void\n\ntype DictionaryEntryCommandLabels = {\n singular?: string\n create?: string\n update?: string\n delete?: string\n}\n\nexport type DictionaryEntryCommandConfig<TCreate, TUpdate> = {\n commandPrefix: string\n resourceKind: string\n translationKeyPrefix?: string\n createSchema: z.ZodType<TCreate>\n updateSchema: z.ZodType<TUpdate>\n resolveDictionaryForCreate: ResolveDictionaryForCreate<TCreate>\n resolveEntry: ResolveEntry<TUpdate>\n ensureScope?: EnsureScopeFn\n ensureDictionaryForUndo?: EnsureDictionaryForUndo\n duplicateError?: string\n labels?: DictionaryEntryCommandLabels\n}\n\nexport function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {\n return typeof fn === 'function' ? fn : ensureDictionaryEntryScope\n}\n\nasync function loadSnapshot(em: EntityManager, id: string): Promise<DictionaryEntrySnapshot | null> {\n const entry = await findOneWithDecryption(em, DictionaryEntry, id, { populate: ['dictionary'] })\n if (!entry) return null\n return {\n id: entry.id,\n dictionaryId: entry.dictionary.id,\n dictionaryKey: entry.dictionary.key,\n organizationId: entry.organizationId,\n tenantId: entry.tenantId,\n value: entry.value,\n label: entry.label,\n color: entry.color ?? null,\n icon: entry.icon ?? null,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt:\n entry.createdAt instanceof Date\n ? entry.createdAt.toISOString()\n : new Date(entry.createdAt).toISOString(),\n updatedAt:\n entry.updatedAt instanceof Date\n ? entry.updatedAt.toISOString()\n : new Date(entry.updatedAt).toISOString(),\n }\n}\n\nfunction applySnapshot(entry: DictionaryEntry, snapshot: DictionaryEntrySnapshot): void {\n entry.value = snapshot.value\n entry.normalizedValue = normalizeDictionaryValue(snapshot.value)\n entry.label = snapshot.label\n entry.color = snapshot.color ?? null\n entry.icon = snapshot.icon ?? null\n if (snapshot.position !== undefined) entry.position = snapshot.position\n if (snapshot.isDefault !== undefined) entry.isDefault = snapshot.isDefault\n entry.organizationId = snapshot.organizationId\n entry.tenantId = snapshot.tenantId\n entry.createdAt = new Date(snapshot.createdAt)\n entry.updatedAt = new Date(snapshot.updatedAt)\n}\n\nfunction ensureDictionaryForUndoFactory(\n fallback?: EnsureDictionaryForUndo\n): EnsureDictionaryForUndo {\n if (fallback) return fallback\n return async ({ em, snapshot }) => {\n const dictionary = await em.findOne(Dictionary, snapshot.dictionaryId)\n return dictionary ?? null\n }\n}\n\nfunction getDuplicateError(config: DictionaryEntryCommandConfig<any, any>): string {\n return config.duplicateError ?? 'Value already exists in this dictionary.'\n}\n\nfunction sanitizeCreatePayload(input: any): {\n value: string\n normalized: string\n label: string\n color: string | null\n icon: string | null\n position: number | undefined\n} {\n const rawValue = typeof input?.value === 'string' ? input.value.trim() : ''\n if (!rawValue) {\n throw new CrudHttpError(400, { error: 'Dictionary entry value is required.' })\n }\n const normalized = normalizeDictionaryValue(rawValue)\n const rawLabel = typeof input?.label === 'string' ? input.label.trim() : ''\n const label = rawLabel || rawValue\n const color = sanitizeDictionaryColor(input?.color ?? null)\n const icon = sanitizeDictionaryIcon(input?.icon ?? null)\n const position = typeof input?.position === 'number' ? input.position : undefined\n return { value: rawValue, normalized, label, color, icon, position }\n}\n\nfunction sanitizeUpdatePayload(input: any): {\n value?: string\n normalized?: string\n label?: string\n color?: string | null\n icon?: string | null\n position?: number\n isDefault?: boolean\n} {\n const payload: {\n value?: string\n normalized?: string\n label?: string\n color?: string | null\n icon?: string | null\n position?: number\n isDefault?: boolean\n } = {}\n if (Object.prototype.hasOwnProperty.call(input, 'value')) {\n const rawValue = typeof input?.value === 'string' ? input.value.trim() : ''\n if (!rawValue) {\n throw new CrudHttpError(400, { error: 'Dictionary entry value is required.' })\n }\n payload.value = rawValue\n payload.normalized = normalizeDictionaryValue(rawValue)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'label')) {\n const rawLabel = typeof input?.label === 'string' ? input.label.trim() : ''\n payload.label = rawLabel\n }\n if (Object.prototype.hasOwnProperty.call(input, 'color')) {\n payload.color = sanitizeDictionaryColor(input?.color ?? null)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'icon')) {\n payload.icon = sanitizeDictionaryIcon(input?.icon ?? null)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'position')) {\n const rawPosition = typeof input?.position === 'number' ? input.position : undefined\n if (rawPosition !== undefined) {\n payload.position = rawPosition\n }\n }\n if (Object.prototype.hasOwnProperty.call(input, 'isDefault')) {\n payload.isDefault = typeof input?.isDefault === 'boolean' ? input.isDefault : undefined\n }\n return payload\n}\n\nexport function registerDictionaryEntryCommands<TCreate, TUpdate>(\n config: DictionaryEntryCommandConfig<TCreate, TUpdate>\n): void {\n const scopeEnsurer = toScopeEnsurer(config.ensureScope)\n const ensureDictionaryForUndo = ensureDictionaryForUndoFactory(config.ensureDictionaryForUndo)\n const translationKeyPrefix = config.translationKeyPrefix ?? config.commandPrefix\n const fallbackSingular = config.labels?.singular ?? 'Dictionary entry'\n const fallbackCreate = config.labels?.create ?? `Create ${fallbackSingular}`\n const fallbackUpdate = config.labels?.update ?? `Update ${fallbackSingular}`\n const fallbackDelete = config.labels?.delete ?? `Delete ${fallbackSingular}`\n const duplicateMessage = getDuplicateError(config)\n\n const createCommand: CommandHandler<\n ReturnType<typeof config.createSchema['parse']>,\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.create`,\n async execute(rawInput, ctx) {\n const parsed = config.createSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const { dictionary, scope } = await config.resolveDictionaryForCreate({ em, ctx, parsed })\n scopeEnsurer(ctx, scope)\n\n const { value, normalized, label, color, icon, position } = sanitizeCreatePayload(parsed)\n const duplicate = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue: normalized,\n })\n if (duplicate) {\n throw new CrudHttpError(409, { error: duplicateMessage })\n }\n\n const entry = em.create(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n value,\n normalizedValue: normalized,\n label,\n color,\n icon,\n ...(position !== undefined ? { position } : {}),\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n await em.flush()\n return { entryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadSnapshot(em, result.entryId)\n },\n buildLog: async ({ result, snapshots }) => {\n const after = snapshots.after as DictionaryEntrySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.create`, fallbackCreate),\n resourceKind: config.resourceKind,\n resourceId: result.entryId,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotAfter: after,\n payload: {\n undo: { after } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const after =\n undo?.after ??\n (logEntry?.snapshotAfter as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: after.tenantId, organizationId: after.organizationId })\n const entry = await em.findOne(DictionaryEntry, after.id)\n if (entry) {\n await em.remove(entry).flush()\n return\n }\n await em.nativeDelete(DictionaryEntry, { id: after.id })\n },\n}\n\n const updateCommand: CommandHandler<\n ReturnType<typeof config.updateSchema['parse']>,\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.update`,\n async prepare(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadSnapshot(em, id)\n if (snapshot) {\n scopeEnsurer(ctx, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId })\n }\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n const parsed = config.updateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const id = requireId(parsed, 'Dictionary entry id is required')\n const { entry, dictionary, scope } = await config.resolveEntry({ em, ctx, id, parsed })\n scopeEnsurer(ctx, scope)\n\n const updates = sanitizeUpdatePayload(parsed)\n if (updates.value !== undefined && updates.normalized !== undefined) {\n if (updates.normalized !== entry.normalizedValue) {\n const duplicate = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue: updates.normalized,\n id: { $ne: entry.id },\n } as any)\n if (duplicate) {\n throw new CrudHttpError(409, { error: duplicateMessage })\n }\n entry.value = updates.value\n entry.normalizedValue = updates.normalized\n if (updates.label === undefined) {\n entry.label = entry.value\n }\n }\n }\n if (updates.label !== undefined) {\n entry.label = updates.label || entry.value\n }\n if (Object.prototype.hasOwnProperty.call(updates, 'color')) {\n entry.color = updates.color ?? null\n }\n if (Object.prototype.hasOwnProperty.call(updates, 'icon')) {\n entry.icon = updates.icon ?? null\n }\n if (updates.position !== undefined) {\n entry.position = updates.position\n }\n if (updates.isDefault !== undefined) {\n entry.isDefault = updates.isDefault\n }\n await em.flush()\n return { entryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadSnapshot(em, result.entryId)\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as DictionaryEntrySnapshot | undefined\n const after = snapshots.after as DictionaryEntrySnapshot | undefined\n if (!before || !after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.update`, fallbackUpdate),\n resourceKind: config.resourceKind,\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes: buildChanges(\n before as Record<string, unknown>,\n after as Record<string, unknown>,\n ENTRY_CHANGE_KEYS,\n ),\n payload: {\n undo: { before, after } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const before =\n undo?.before ??\n (logEntry?.snapshotBefore as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: before.tenantId, organizationId: before.organizationId })\n const dictionary = await ensureDictionaryForUndo({ em, ctx, snapshot: before })\n if (!dictionary) return\n let entry = await em.findOne(DictionaryEntry, before.id)\n if (!entry) {\n entry = em.create(DictionaryEntry, {\n id: before.id,\n dictionary,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n value: before.value,\n normalizedValue: normalizeDictionaryValue(before.value),\n label: before.label,\n color: before.color,\n icon: before.icon,\n createdAt: new Date(before.createdAt),\n updatedAt: new Date(before.updatedAt),\n })\n em.persist(entry)\n } else {\n entry.dictionary = dictionary\n applySnapshot(entry, before)\n }\n await em.flush()\n },\n }\n\n const deleteCommand: CommandHandler<\n { body?: Record<string, unknown>; query?: Record<string, unknown> },\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.delete`,\n async prepare(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadSnapshot(em, id)\n if (snapshot) {\n scopeEnsurer(ctx, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId })\n }\n return snapshot ? { before: snapshot } : {}\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const { entry, scope } = await config.resolveEntry({ em, ctx, id })\n scopeEnsurer(ctx, scope)\n em.remove(entry)\n await em.flush()\n return { entryId: id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as DictionaryEntrySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.delete`, fallbackDelete),\n resourceKind: config.resourceKind,\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: { before } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const before =\n undo?.before ??\n (logEntry?.snapshotBefore as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: before.tenantId, organizationId: before.organizationId })\n const dictionary = await ensureDictionaryForUndo({ em, ctx, snapshot: before })\n if (!dictionary) return\n let entry = await em.findOne(DictionaryEntry, before.id)\n if (!entry) {\n entry = em.create(DictionaryEntry, {\n id: before.id,\n dictionary,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n value: before.value,\n normalizedValue: normalizeDictionaryValue(before.value),\n label: before.label,\n color: before.color,\n icon: before.icon,\n createdAt: new Date(before.createdAt),\n updatedAt: new Date(before.updatedAt),\n })\n em.persist(entry)\n } else {\n entry.dictionary = dictionary\n applySnapshot(entry, before)\n }\n await em.flush()\n },\n }\n\n registerCommand(createCommand)\n registerCommand(updateCommand)\n registerCommand(deleteCommand)\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY,uBAAuB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAGK;AACP,SAAS,0BAA0B;AACnC,SAAS,cAAc,iBAAiB;AACxC,SAAS,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n normalizeDictionaryValue,\n sanitizeDictionaryColor,\n sanitizeDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/lib/utils'\nimport {\n registerCommand,\n type CommandHandler,\n type CommandRuntimeContext,\n} from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { z } from 'zod'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\ntype DictionaryScope = {\n tenantId: string\n organizationId: string\n}\n\nexport type DictionaryEntrySnapshot = {\n id: string\n dictionaryId: string\n dictionaryKey: string\n organizationId: string\n tenantId: string\n value: string\n label: string\n color: string | null\n icon: string | null\n createdAt: string\n updatedAt: string\n position?: number\n isDefault?: boolean\n}\n\nexport type DictionaryEntryUndoPayload = {\n before?: DictionaryEntrySnapshot | null\n after?: DictionaryEntrySnapshot | null\n}\n\nconst ENTRY_CHANGE_KEYS = ['value', 'label', 'color', 'icon', 'position', 'isDefault'] as const\n\ntype ResolveDictionaryForCreate<TCreate> = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n parsed: TCreate\n}) => Promise<{ dictionary: Dictionary; scope: DictionaryScope }>\n\ntype ResolveEntry<TInput> = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n id: string\n parsed?: TInput\n}) => Promise<{ entry: DictionaryEntry; dictionary: Dictionary; scope: DictionaryScope }>\n\ntype EnsureDictionaryForUndo = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n snapshot: DictionaryEntrySnapshot\n}) => Promise<Dictionary | null>\n\ntype EnsureScopeFn = (ctx: CommandRuntimeContext, scope: DictionaryScope) => void\n\ntype DictionaryEntryCommandLabels = {\n singular?: string\n create?: string\n update?: string\n delete?: string\n}\n\nexport type DictionaryEntryCommandConfig<TCreate, TUpdate> = {\n commandPrefix: string\n resourceKind: string\n translationKeyPrefix?: string\n createSchema: z.ZodType<TCreate>\n updateSchema: z.ZodType<TUpdate>\n resolveDictionaryForCreate: ResolveDictionaryForCreate<TCreate>\n resolveEntry: ResolveEntry<TUpdate>\n ensureScope?: EnsureScopeFn\n ensureDictionaryForUndo?: EnsureDictionaryForUndo\n duplicateError?: string\n labels?: DictionaryEntryCommandLabels\n}\n\nfunction toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {\n return typeof fn === 'function' ? fn : () => {}\n}\n\nasync function loadSnapshot(em: EntityManager, id: string): Promise<DictionaryEntrySnapshot | null> {\n const entry = await findOneWithDecryption(em, DictionaryEntry, id, { populate: ['dictionary'] })\n if (!entry) return null\n return {\n id: entry.id,\n dictionaryId: entry.dictionary.id,\n dictionaryKey: entry.dictionary.key,\n organizationId: entry.organizationId,\n tenantId: entry.tenantId,\n value: entry.value,\n label: entry.label,\n color: entry.color ?? null,\n icon: entry.icon ?? null,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt:\n entry.createdAt instanceof Date\n ? entry.createdAt.toISOString()\n : new Date(entry.createdAt).toISOString(),\n updatedAt:\n entry.updatedAt instanceof Date\n ? entry.updatedAt.toISOString()\n : new Date(entry.updatedAt).toISOString(),\n }\n}\n\nfunction applySnapshot(entry: DictionaryEntry, snapshot: DictionaryEntrySnapshot): void {\n entry.value = snapshot.value\n entry.normalizedValue = normalizeDictionaryValue(snapshot.value)\n entry.label = snapshot.label\n entry.color = snapshot.color ?? null\n entry.icon = snapshot.icon ?? null\n if (snapshot.position !== undefined) entry.position = snapshot.position\n if (snapshot.isDefault !== undefined) entry.isDefault = snapshot.isDefault\n entry.organizationId = snapshot.organizationId\n entry.tenantId = snapshot.tenantId\n entry.createdAt = new Date(snapshot.createdAt)\n entry.updatedAt = new Date(snapshot.updatedAt)\n}\n\nfunction ensureDictionaryForUndoFactory(\n fallback?: EnsureDictionaryForUndo\n): EnsureDictionaryForUndo {\n if (fallback) return fallback\n return async ({ em, snapshot }) => {\n const dictionary = await em.findOne(Dictionary, snapshot.dictionaryId)\n return dictionary ?? null\n }\n}\n\nfunction getDuplicateError(config: DictionaryEntryCommandConfig<any, any>): string {\n return config.duplicateError ?? 'Value already exists in this dictionary.'\n}\n\nfunction sanitizeCreatePayload(input: any): {\n value: string\n normalized: string\n label: string\n color: string | null\n icon: string | null\n position: number | undefined\n} {\n const rawValue = typeof input?.value === 'string' ? input.value.trim() : ''\n if (!rawValue) {\n throw new CrudHttpError(400, { error: 'Dictionary entry value is required.' })\n }\n const normalized = normalizeDictionaryValue(rawValue)\n const rawLabel = typeof input?.label === 'string' ? input.label.trim() : ''\n const label = rawLabel || rawValue\n const color = sanitizeDictionaryColor(input?.color ?? null)\n const icon = sanitizeDictionaryIcon(input?.icon ?? null)\n const position = typeof input?.position === 'number' ? input.position : undefined\n return { value: rawValue, normalized, label, color, icon, position }\n}\n\nfunction sanitizeUpdatePayload(input: any): {\n value?: string\n normalized?: string\n label?: string\n color?: string | null\n icon?: string | null\n position?: number\n isDefault?: boolean\n} {\n const payload: {\n value?: string\n normalized?: string\n label?: string\n color?: string | null\n icon?: string | null\n position?: number\n isDefault?: boolean\n } = {}\n if (Object.prototype.hasOwnProperty.call(input, 'value')) {\n const rawValue = typeof input?.value === 'string' ? input.value.trim() : ''\n if (!rawValue) {\n throw new CrudHttpError(400, { error: 'Dictionary entry value is required.' })\n }\n payload.value = rawValue\n payload.normalized = normalizeDictionaryValue(rawValue)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'label')) {\n const rawLabel = typeof input?.label === 'string' ? input.label.trim() : ''\n payload.label = rawLabel\n }\n if (Object.prototype.hasOwnProperty.call(input, 'color')) {\n payload.color = sanitizeDictionaryColor(input?.color ?? null)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'icon')) {\n payload.icon = sanitizeDictionaryIcon(input?.icon ?? null)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'position')) {\n const rawPosition = typeof input?.position === 'number' ? input.position : undefined\n if (rawPosition !== undefined) {\n payload.position = rawPosition\n }\n }\n if (Object.prototype.hasOwnProperty.call(input, 'isDefault')) {\n payload.isDefault = typeof input?.isDefault === 'boolean' ? input.isDefault : undefined\n }\n return payload\n}\n\nexport function registerDictionaryEntryCommands<TCreate, TUpdate>(\n config: DictionaryEntryCommandConfig<TCreate, TUpdate>\n): void {\n const scopeEnsurer = toScopeEnsurer(config.ensureScope)\n const ensureDictionaryForUndo = ensureDictionaryForUndoFactory(config.ensureDictionaryForUndo)\n const translationKeyPrefix = config.translationKeyPrefix ?? config.commandPrefix\n const fallbackSingular = config.labels?.singular ?? 'Dictionary entry'\n const fallbackCreate = config.labels?.create ?? `Create ${fallbackSingular}`\n const fallbackUpdate = config.labels?.update ?? `Update ${fallbackSingular}`\n const fallbackDelete = config.labels?.delete ?? `Delete ${fallbackSingular}`\n const duplicateMessage = getDuplicateError(config)\n\n const createCommand: CommandHandler<\n ReturnType<typeof config.createSchema['parse']>,\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.create`,\n async execute(rawInput, ctx) {\n const parsed = config.createSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const { dictionary, scope } = await config.resolveDictionaryForCreate({ em, ctx, parsed })\n scopeEnsurer(ctx, scope)\n\n const { value, normalized, label, color, icon, position } = sanitizeCreatePayload(parsed)\n const duplicate = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue: normalized,\n })\n if (duplicate) {\n throw new CrudHttpError(409, { error: duplicateMessage })\n }\n\n const entry = em.create(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n value,\n normalizedValue: normalized,\n label,\n color,\n icon,\n ...(position !== undefined ? { position } : {}),\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n await em.flush()\n return { entryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadSnapshot(em, result.entryId)\n },\n buildLog: async ({ result, snapshots }) => {\n const after = snapshots.after as DictionaryEntrySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.create`, fallbackCreate),\n resourceKind: config.resourceKind,\n resourceId: result.entryId,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotAfter: after,\n payload: {\n undo: { after } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const after =\n undo?.after ??\n (logEntry?.snapshotAfter as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: after.tenantId, organizationId: after.organizationId })\n const entry = await em.findOne(DictionaryEntry, after.id)\n if (entry) {\n await em.remove(entry).flush()\n return\n }\n await em.nativeDelete(DictionaryEntry, { id: after.id })\n },\n}\n\n const updateCommand: CommandHandler<\n ReturnType<typeof config.updateSchema['parse']>,\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.update`,\n async prepare(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadSnapshot(em, id)\n if (snapshot) {\n scopeEnsurer(ctx, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId })\n }\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n const parsed = config.updateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const id = requireId(parsed, 'Dictionary entry id is required')\n const { entry, dictionary, scope } = await config.resolveEntry({ em, ctx, id, parsed })\n scopeEnsurer(ctx, scope)\n\n const updates = sanitizeUpdatePayload(parsed)\n if (updates.value !== undefined && updates.normalized !== undefined) {\n if (updates.normalized !== entry.normalizedValue) {\n const duplicate = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue: updates.normalized,\n id: { $ne: entry.id },\n } as any)\n if (duplicate) {\n throw new CrudHttpError(409, { error: duplicateMessage })\n }\n entry.value = updates.value\n entry.normalizedValue = updates.normalized\n if (updates.label === undefined) {\n entry.label = entry.value\n }\n }\n }\n if (updates.label !== undefined) {\n entry.label = updates.label || entry.value\n }\n if (Object.prototype.hasOwnProperty.call(updates, 'color')) {\n entry.color = updates.color ?? null\n }\n if (Object.prototype.hasOwnProperty.call(updates, 'icon')) {\n entry.icon = updates.icon ?? null\n }\n if (updates.position !== undefined) {\n entry.position = updates.position\n }\n if (updates.isDefault !== undefined) {\n entry.isDefault = updates.isDefault\n }\n await em.flush()\n return { entryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadSnapshot(em, result.entryId)\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as DictionaryEntrySnapshot | undefined\n const after = snapshots.after as DictionaryEntrySnapshot | undefined\n if (!before || !after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.update`, fallbackUpdate),\n resourceKind: config.resourceKind,\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes: buildChanges(\n before as Record<string, unknown>,\n after as Record<string, unknown>,\n ENTRY_CHANGE_KEYS,\n ),\n payload: {\n undo: { before, after } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const before =\n undo?.before ??\n (logEntry?.snapshotBefore as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: before.tenantId, organizationId: before.organizationId })\n const dictionary = await ensureDictionaryForUndo({ em, ctx, snapshot: before })\n if (!dictionary) return\n let entry = await em.findOne(DictionaryEntry, before.id)\n if (!entry) {\n entry = em.create(DictionaryEntry, {\n id: before.id,\n dictionary,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n value: before.value,\n normalizedValue: normalizeDictionaryValue(before.value),\n label: before.label,\n color: before.color,\n icon: before.icon,\n createdAt: new Date(before.createdAt),\n updatedAt: new Date(before.updatedAt),\n })\n em.persist(entry)\n } else {\n entry.dictionary = dictionary\n applySnapshot(entry, before)\n }\n await em.flush()\n },\n }\n\n const deleteCommand: CommandHandler<\n { body?: Record<string, unknown>; query?: Record<string, unknown> },\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.delete`,\n async prepare(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadSnapshot(em, id)\n if (snapshot) {\n scopeEnsurer(ctx, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId })\n }\n return snapshot ? { before: snapshot } : {}\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const { entry, scope } = await config.resolveEntry({ em, ctx, id })\n scopeEnsurer(ctx, scope)\n em.remove(entry)\n await em.flush()\n return { entryId: id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as DictionaryEntrySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.delete`, fallbackDelete),\n resourceKind: config.resourceKind,\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: { before } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const before =\n undo?.before ??\n (logEntry?.snapshotBefore as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: before.tenantId, organizationId: before.organizationId })\n const dictionary = await ensureDictionaryForUndo({ em, ctx, snapshot: before })\n if (!dictionary) return\n let entry = await em.findOne(DictionaryEntry, before.id)\n if (!entry) {\n entry = em.create(DictionaryEntry, {\n id: before.id,\n dictionary,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n value: before.value,\n normalizedValue: normalizeDictionaryValue(before.value),\n label: before.label,\n color: before.color,\n icon: before.icon,\n createdAt: new Date(before.createdAt),\n updatedAt: new Date(before.updatedAt),\n })\n em.persist(entry)\n } else {\n entry.dictionary = dictionary\n applySnapshot(entry, before)\n }\n await em.flush()\n },\n }\n\n registerCommand(createCommand)\n registerCommand(updateCommand)\n registerCommand(deleteCommand)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -49,12 +49,9 @@ function DictionaryFieldDefEditor({ def, onChange }) {
|
|
|
49
49
|
const [error, setError] = React.useState(null);
|
|
50
50
|
const multiId = React.useId();
|
|
51
51
|
const inlineCreateId = React.useId();
|
|
52
|
-
const inlineCreateHintId = React.useId();
|
|
53
|
-
const defaultValueHintId = React.useId();
|
|
54
52
|
const selectedId = typeof def?.configJson?.dictionaryId === "string" ? def?.configJson?.dictionaryId : "";
|
|
55
53
|
const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false;
|
|
56
54
|
const isMulti = def?.configJson?.multi === true;
|
|
57
|
-
const inlineCreateDisabled = !selectedId || isMulti;
|
|
58
55
|
const errorLoadLabel = t("dictionaries.customFields.errorLoad", "Failed to load dictionaries.");
|
|
59
56
|
React.useEffect(() => {
|
|
60
57
|
let cancelled = false;
|
|
@@ -123,56 +120,47 @@ function DictionaryFieldDefEditor({ def, onChange }) {
|
|
|
123
120
|
/* @__PURE__ */ jsx("span", { children: t("dictionaries.customFields.selectedHint", "Entries from this dictionary populate the field.") }),
|
|
124
121
|
/* @__PURE__ */ jsx("a", { href: manageHref, className: "font-medium text-primary hover:underline", target: "_blank", rel: "noreferrer", children: t("dictionaries.customFields.manageLink", "Manage dictionaries") })
|
|
125
122
|
] }) : null,
|
|
126
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
127
|
-
/* @__PURE__ */
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
)
|
|
149
|
-
] }),
|
|
150
|
-
/* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 text-xs", children: [
|
|
151
|
-
/* @__PURE__ */ jsx(
|
|
152
|
-
Checkbox,
|
|
153
|
-
{
|
|
154
|
-
id: inlineCreateId,
|
|
155
|
-
checked: !isMulti && inlineCreate,
|
|
156
|
-
onCheckedChange: (checked) => onChange({ dictionaryInlineCreate: checked === true }),
|
|
157
|
-
disabled: inlineCreateDisabled,
|
|
158
|
-
"aria-describedby": isMulti ? inlineCreateHintId : void 0
|
|
159
|
-
}
|
|
160
|
-
),
|
|
161
|
-
/* @__PURE__ */ jsx(
|
|
162
|
-
"label",
|
|
163
|
-
{
|
|
164
|
-
htmlFor: inlineCreateId,
|
|
165
|
-
className: !inlineCreateDisabled ? "cursor-pointer select-none" : "cursor-not-allowed opacity-60",
|
|
166
|
-
children: t("dictionaries.customFields.allowInlineCreate", "Allow inline creation inside forms")
|
|
167
|
-
}
|
|
168
|
-
)
|
|
169
|
-
] })
|
|
170
|
-
] }),
|
|
171
|
-
isMulti ? /* @__PURE__ */ jsx("p", { id: inlineCreateHintId, className: "text-xs text-muted-foreground", children: t(
|
|
172
|
-
"dictionaries.customFields.inlineCreateSingleOnly",
|
|
173
|
-
"Inline creation is available for single-entry dictionary fields only."
|
|
174
|
-
) }) : null
|
|
123
|
+
/* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 text-xs", children: [
|
|
124
|
+
/* @__PURE__ */ jsx(
|
|
125
|
+
Checkbox,
|
|
126
|
+
{
|
|
127
|
+
id: multiId,
|
|
128
|
+
checked: isMulti,
|
|
129
|
+
onCheckedChange: (checked) => {
|
|
130
|
+
const enabled = checked === true;
|
|
131
|
+
onChange({ multi: enabled, defaultValue: enabled ? void 0 : def?.configJson?.defaultValue });
|
|
132
|
+
},
|
|
133
|
+
disabled: !selectedId
|
|
134
|
+
}
|
|
135
|
+
),
|
|
136
|
+
/* @__PURE__ */ jsx(
|
|
137
|
+
"label",
|
|
138
|
+
{
|
|
139
|
+
htmlFor: multiId,
|
|
140
|
+
className: selectedId ? "cursor-pointer select-none" : "cursor-not-allowed opacity-60",
|
|
141
|
+
children: t("dictionaries.customFields.allowMultiple", "Allow selecting multiple entries")
|
|
142
|
+
}
|
|
143
|
+
)
|
|
175
144
|
] }),
|
|
145
|
+
!isMulti ? /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 text-xs", children: [
|
|
146
|
+
/* @__PURE__ */ jsx(
|
|
147
|
+
Checkbox,
|
|
148
|
+
{
|
|
149
|
+
id: inlineCreateId,
|
|
150
|
+
checked: inlineCreate,
|
|
151
|
+
onCheckedChange: (checked) => onChange({ dictionaryInlineCreate: checked === true }),
|
|
152
|
+
disabled: !selectedId
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
/* @__PURE__ */ jsx(
|
|
156
|
+
"label",
|
|
157
|
+
{
|
|
158
|
+
htmlFor: inlineCreateId,
|
|
159
|
+
className: selectedId ? "cursor-pointer select-none" : "cursor-not-allowed opacity-60",
|
|
160
|
+
children: t("dictionaries.customFields.allowInlineCreate", "Allow inline creation inside forms")
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
] }) : null,
|
|
176
164
|
selectedId && !isMulti ? /* @__PURE__ */ jsx(
|
|
177
165
|
DictionaryDefaultSelector,
|
|
178
166
|
{
|
|
@@ -180,10 +168,7 @@ function DictionaryFieldDefEditor({ def, onChange }) {
|
|
|
180
168
|
defaultValue: typeof def?.configJson?.defaultValue === "string" ? def.configJson.defaultValue : "",
|
|
181
169
|
onChange: (value) => onChange({ defaultValue: value || void 0 })
|
|
182
170
|
}
|
|
183
|
-
) :
|
|
184
|
-
"dictionaries.customFields.defaultValueMultiUnavailable",
|
|
185
|
-
"Default values are not available for multi-select dictionary fields."
|
|
186
|
-
) }) }) : null
|
|
171
|
+
) : null
|
|
187
172
|
] });
|
|
188
173
|
}
|
|
189
174
|
function DictionaryFieldInput({ value, setValue, disabled, def }) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/dictionaries/fields/dictionary.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\nimport { FieldRegistry } from '@open-mercato/ui/backend/fields/registry'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { DictionarySelectControl } from '../components/DictionarySelectControl'\nimport { useDictionaryEntries } from '../components/hooks/useDictionaryEntries'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'dictionary-field' })\n\ntype DictionaryFieldDefinition = {\n dictionaryId?: string\n dictionaryInlineCreate?: boolean\n multi?: boolean\n defaultValue?: string\n}\n\ntype Props = CrudCustomFieldRenderProps & { def?: DictionaryFieldDefinition }\n\ntype DictionarySummary = {\n id: string\n name: string\n key: string\n isActive: boolean\n}\n\nfunction DictionaryDefaultSelector({\n dictionaryId,\n defaultValue,\n onChange,\n}: {\n dictionaryId: string\n defaultValue: string\n onChange: (value: string) => void\n}) {\n const t = useT()\n const { data, isLoading } = useDictionaryEntries(dictionaryId)\n const entries = data?.entries ?? []\n const isStale = defaultValue && entries.length > 0 && !entries.some((e) => e.value === defaultValue)\n\n return (\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium text-muted-foreground\">\n {t('dictionaries.customFields.defaultValue', 'Default value')}\n </label>\n <Select\n value={defaultValue ?? ''}\n onValueChange={(next) => onChange(next ?? '')}\n >\n <SelectTrigger size=\"sm\">\n <SelectValue placeholder={t('dictionaries.customFields.defaultValueNone', 'No default')} />\n </SelectTrigger>\n <SelectContent>\n {entries.map((entry) => (\n <SelectItem key={entry.value} value={entry.value}>\n {entry.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {isLoading ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.loading', 'Loading dictionaries\u2026')}\n </p>\n ) : null}\n {isStale ? (\n <p className=\"text-xs text-status-warning-text\">\n {t('dictionaries.customFields.defaultValueStale', 'Default entry not found \u2014 it may have been deleted or renamed.')}\n </p>\n ) : null}\n </div>\n )\n}\n\nfunction DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: DictionaryFieldDefinition } | undefined; onChange: (patch: Partial<DictionaryFieldDefinition>) => void }) {\n const t = useT()\n const [items, setItems] = React.useState<DictionarySummary[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const multiId = React.useId()\n const inlineCreateId = React.useId()\n const inlineCreateHintId = React.useId()\n const defaultValueHintId = React.useId()\n const selectedId = typeof def?.configJson?.dictionaryId === 'string' ? def?.configJson?.dictionaryId : ''\n const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false\n const isMulti = def?.configJson?.multi === true\n const inlineCreateDisabled = !selectedId || isMulti\n const errorLoadLabel = t('dictionaries.customFields.errorLoad', 'Failed to load dictionaries.')\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const call = await apiCall<{ items?: unknown[]; error?: string }>(\n '/api/dictionaries?includeInactive=true',\n )\n if (!call.ok) {\n const message =\n typeof call.result?.error === 'string' ? call.result.error : 'Failed to load dictionaries'\n throw new Error(message)\n }\n const entries = Array.isArray(call.result?.items) ? call.result!.items : []\n if (!cancelled) {\n setItems(\n entries.map((entry: any) => ({\n id: String(entry.id),\n name: typeof entry.name === 'string' && entry.name.trim().length ? entry.name : String(entry.key ?? entry.id),\n key: typeof entry.key === 'string' ? entry.key : '',\n isActive: entry.isActive !== false,\n })),\n )\n }\n } catch (err) {\n if (!cancelled) {\n logger.error('Failed to load dictionaries list', { err })\n setError(errorLoadLabel)\n }\n } finally {\n if (!cancelled) {\n setLoading(false)\n }\n }\n }\n load().catch(() => {})\n return () => {\n cancelled = true\n }\n }, [errorLoadLabel])\n\n const manageHref = '/backend/config/dictionaries'\n\n return (\n <div className=\"mt-3 space-y-3 rounded border border-dashed border-muted-foreground/40 bg-muted/30 p-3\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium text-muted-foreground\">\n {t('dictionaries.customFields.dictionaryLabel', 'Dictionary source')}\n </label>\n <Select\n value={selectedId ?? ''}\n onValueChange={(next) => onChange({ dictionaryId: next || undefined })}\n >\n <SelectTrigger size=\"sm\">\n <SelectValue placeholder={t('dictionaries.customFields.dictionaryPlaceholder', 'Select a dictionary')} />\n </SelectTrigger>\n <SelectContent>\n {items.map((item) => (\n <SelectItem key={item.id} value={item.id}>\n {item.name}\n {item.isActive ? '' : ` (${t('dictionaries.customFields.inactive', 'inactive')})`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {loading ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.loading', 'Loading dictionaries\u2026')}\n </p>\n ) : null}\n {error ? <p className=\"text-xs text-status-error-text\">{error}</p> : null}\n {!loading && !error && items.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.empty', 'No dictionaries available yet. Create one first.')}\n </p>\n ) : null}\n </div>\n {selectedId ? (\n <div className=\"flex flex-wrap items-center justify-between gap-2 rounded bg-background/80 px-2 py-1 text-xs text-muted-foreground\">\n <span>{t('dictionaries.customFields.selectedHint', 'Entries from this dictionary populate the field.')}</span>\n <a href={manageHref} className=\"font-medium text-primary hover:underline\" target=\"_blank\" rel=\"noreferrer\">\n {t('dictionaries.customFields.manageLink', 'Manage dictionaries')}\n </a>\n </div>\n ) : null}\n <div className=\"space-y-2\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-x-4\">\n <div className=\"inline-flex items-center gap-2 text-xs\">\n <Checkbox\n id={multiId}\n checked={isMulti}\n onCheckedChange={(checked) => {\n const enabled = checked === true\n onChange({ multi: enabled, defaultValue: enabled ? undefined : def?.configJson?.defaultValue })\n }}\n disabled={!selectedId}\n />\n <label\n htmlFor={multiId}\n className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}\n >\n {t('dictionaries.customFields.allowMultiple', 'Allow selecting multiple entries')}\n </label>\n </div>\n <div className=\"inline-flex items-center gap-2 text-xs\">\n <Checkbox\n id={inlineCreateId}\n checked={!isMulti && inlineCreate}\n onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}\n disabled={inlineCreateDisabled}\n aria-describedby={isMulti ? inlineCreateHintId : undefined}\n />\n <label\n htmlFor={inlineCreateId}\n className={!inlineCreateDisabled ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}\n >\n {t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}\n </label>\n </div>\n </div>\n {isMulti ? (\n <p id={inlineCreateHintId} className=\"text-xs text-muted-foreground\">\n {t(\n 'dictionaries.customFields.inlineCreateSingleOnly',\n 'Inline creation is available for single-entry dictionary fields only.',\n )}\n </p>\n ) : null}\n </div>\n {selectedId && !isMulti ? (\n <DictionaryDefaultSelector\n dictionaryId={selectedId}\n defaultValue={typeof def?.configJson?.defaultValue === 'string' ? def.configJson.defaultValue : ''}\n onChange={(value) => onChange({ defaultValue: value || undefined })}\n />\n ) : selectedId && isMulti ? (\n <div className=\"inline-flex items-center gap-2 text-xs\">\n <p id={defaultValueHintId} className=\"text-xs text-muted-foreground\">\n {t(\n 'dictionaries.customFields.defaultValueMultiUnavailable',\n 'Default values are not available for multi-select dictionary fields.',\n )}\n </p>\n </div>\n ) : null}\n </div>\n )\n}\n\nfunction DictionaryFieldInput({ value, setValue, disabled, def }: Props) {\n const t = useT()\n const dictionaryId = def?.dictionaryId\n if (!dictionaryId) {\n return (\n <div className=\"rounded border border-dashed p-3 text-sm text-muted-foreground\">\n {t('dictionaries.config.entries.error.load', 'Failed to load dictionary entries.')}\n </div>\n )\n }\n const normalizedValue = typeof value === 'string' ? value : Array.isArray(value) ? String(value[0] ?? '') : undefined\n return (\n <DictionarySelectControl\n dictionaryId={dictionaryId}\n value={normalizedValue ?? ''}\n onChange={(next) => setValue(next ?? undefined)}\n allowInlineCreate={def?.dictionaryInlineCreate !== false}\n disabled={disabled}\n />\n )\n}\n\nFieldRegistry.register('dictionary', {\n input: DictionaryFieldInput,\n defEditor: (props) => <DictionaryFieldDefEditor {...props} />,\n})\n"],
|
|
5
|
-
"mappings": ";AAqDM,cAGA,YAHA;AAnDN,YAAY,WAAW;AAEvB,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,+BAA+B;AACxC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,cAAc,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAkBnF,SAAS,0BAA0B;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,MAAM,UAAU,IAAI,qBAAqB,YAAY;AAC7D,QAAM,UAAU,MAAM,WAAW,CAAC;AAClC,QAAM,UAAU,gBAAgB,QAAQ,SAAS,KAAK,CAAC,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,YAAY;AAEnG,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,WAAM,WAAU,6CACd,YAAE,0CAA0C,eAAe,GAC9D;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,gBAAgB;AAAA,QACvB,eAAe,CAAC,SAAS,SAAS,QAAQ,EAAE;AAAA,QAE5C;AAAA,8BAAC,iBAAc,MAAK,MAClB,8BAAC,eAAY,aAAa,EAAE,8CAA8C,YAAY,GAAG,GAC3F;AAAA,UACA,oBAAC,iBACE,kBAAQ,IAAI,CAAC,UACZ,oBAAC,cAA6B,OAAO,MAAM,OACxC,gBAAM,SADQ,MAAM,KAEvB,CACD,GACH;AAAA;AAAA;AAAA,IACF;AAAA,IACC,YACC,oBAAC,OAAE,WAAU,iCACV,YAAE,qCAAqC,4BAAuB,GACjE,IACE;AAAA,IACH,UACC,oBAAC,OAAE,WAAU,oCACV,YAAE,+CAA+C,qEAAgE,GACpH,IACE;AAAA,KACN;AAEJ;AAEA,SAAS,yBAAyB,EAAE,KAAK,SAAS,GAAmI;AACnL,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAA8B,CAAC,CAAC;AAChE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,UAAU,MAAM,MAAM;AAC5B,QAAM,iBAAiB,MAAM,MAAM;AACnC,QAAM,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\nimport { FieldRegistry } from '@open-mercato/ui/backend/fields/registry'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { DictionarySelectControl } from '../components/DictionarySelectControl'\nimport { useDictionaryEntries } from '../components/hooks/useDictionaryEntries'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'dictionary-field' })\n\ntype DictionaryFieldDefinition = {\n dictionaryId?: string\n dictionaryInlineCreate?: boolean\n multi?: boolean\n defaultValue?: string\n}\n\ntype Props = CrudCustomFieldRenderProps & { def?: DictionaryFieldDefinition }\n\ntype DictionarySummary = {\n id: string\n name: string\n key: string\n isActive: boolean\n}\n\nfunction DictionaryDefaultSelector({\n dictionaryId,\n defaultValue,\n onChange,\n}: {\n dictionaryId: string\n defaultValue: string\n onChange: (value: string) => void\n}) {\n const t = useT()\n const { data, isLoading } = useDictionaryEntries(dictionaryId)\n const entries = data?.entries ?? []\n const isStale = defaultValue && entries.length > 0 && !entries.some((e) => e.value === defaultValue)\n\n return (\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium text-muted-foreground\">\n {t('dictionaries.customFields.defaultValue', 'Default value')}\n </label>\n <Select\n value={defaultValue ?? ''}\n onValueChange={(next) => onChange(next ?? '')}\n >\n <SelectTrigger size=\"sm\">\n <SelectValue placeholder={t('dictionaries.customFields.defaultValueNone', 'No default')} />\n </SelectTrigger>\n <SelectContent>\n {entries.map((entry) => (\n <SelectItem key={entry.value} value={entry.value}>\n {entry.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {isLoading ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.loading', 'Loading dictionaries\u2026')}\n </p>\n ) : null}\n {isStale ? (\n <p className=\"text-xs text-status-warning-text\">\n {t('dictionaries.customFields.defaultValueStale', 'Default entry not found \u2014 it may have been deleted or renamed.')}\n </p>\n ) : null}\n </div>\n )\n}\n\nfunction DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: DictionaryFieldDefinition } | undefined; onChange: (patch: Partial<DictionaryFieldDefinition>) => void }) {\n const t = useT()\n const [items, setItems] = React.useState<DictionarySummary[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const multiId = React.useId()\n const inlineCreateId = React.useId()\n const selectedId = typeof def?.configJson?.dictionaryId === 'string' ? def?.configJson?.dictionaryId : ''\n const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false\n const isMulti = def?.configJson?.multi === true\n const errorLoadLabel = t('dictionaries.customFields.errorLoad', 'Failed to load dictionaries.')\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const call = await apiCall<{ items?: unknown[]; error?: string }>(\n '/api/dictionaries?includeInactive=true',\n )\n if (!call.ok) {\n const message =\n typeof call.result?.error === 'string' ? call.result.error : 'Failed to load dictionaries'\n throw new Error(message)\n }\n const entries = Array.isArray(call.result?.items) ? call.result!.items : []\n if (!cancelled) {\n setItems(\n entries.map((entry: any) => ({\n id: String(entry.id),\n name: typeof entry.name === 'string' && entry.name.trim().length ? entry.name : String(entry.key ?? entry.id),\n key: typeof entry.key === 'string' ? entry.key : '',\n isActive: entry.isActive !== false,\n })),\n )\n }\n } catch (err) {\n if (!cancelled) {\n logger.error('Failed to load dictionaries list', { err })\n setError(errorLoadLabel)\n }\n } finally {\n if (!cancelled) {\n setLoading(false)\n }\n }\n }\n load().catch(() => {})\n return () => {\n cancelled = true\n }\n }, [errorLoadLabel])\n\n const manageHref = '/backend/config/dictionaries'\n\n return (\n <div className=\"mt-3 space-y-3 rounded border border-dashed border-muted-foreground/40 bg-muted/30 p-3\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium text-muted-foreground\">\n {t('dictionaries.customFields.dictionaryLabel', 'Dictionary source')}\n </label>\n <Select\n value={selectedId ?? ''}\n onValueChange={(next) => onChange({ dictionaryId: next || undefined })}\n >\n <SelectTrigger size=\"sm\">\n <SelectValue placeholder={t('dictionaries.customFields.dictionaryPlaceholder', 'Select a dictionary')} />\n </SelectTrigger>\n <SelectContent>\n {items.map((item) => (\n <SelectItem key={item.id} value={item.id}>\n {item.name}\n {item.isActive ? '' : ` (${t('dictionaries.customFields.inactive', 'inactive')})`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {loading ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.loading', 'Loading dictionaries\u2026')}\n </p>\n ) : null}\n {error ? <p className=\"text-xs text-status-error-text\">{error}</p> : null}\n {!loading && !error && items.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.empty', 'No dictionaries available yet. Create one first.')}\n </p>\n ) : null}\n </div>\n {selectedId ? (\n <div className=\"flex flex-wrap items-center justify-between gap-2 rounded bg-background/80 px-2 py-1 text-xs text-muted-foreground\">\n <span>{t('dictionaries.customFields.selectedHint', 'Entries from this dictionary populate the field.')}</span>\n <a href={manageHref} className=\"font-medium text-primary hover:underline\" target=\"_blank\" rel=\"noreferrer\">\n {t('dictionaries.customFields.manageLink', 'Manage dictionaries')}\n </a>\n </div>\n ) : null}\n <div className=\"inline-flex items-center gap-2 text-xs\">\n <Checkbox\n id={multiId}\n checked={isMulti}\n onCheckedChange={(checked) => {\n const enabled = checked === true\n onChange({ multi: enabled, defaultValue: enabled ? undefined : def?.configJson?.defaultValue })\n }}\n disabled={!selectedId}\n />\n <label\n htmlFor={multiId}\n className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}\n >\n {t('dictionaries.customFields.allowMultiple', 'Allow selecting multiple entries')}\n </label>\n </div>\n {!isMulti ? (\n <div className=\"inline-flex items-center gap-2 text-xs\">\n <Checkbox\n id={inlineCreateId}\n checked={inlineCreate}\n onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}\n disabled={!selectedId}\n />\n <label\n htmlFor={inlineCreateId}\n className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}\n >\n {t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}\n </label>\n </div>\n ) : null}\n {selectedId && !isMulti ? (\n <DictionaryDefaultSelector\n dictionaryId={selectedId}\n defaultValue={typeof def?.configJson?.defaultValue === 'string' ? def.configJson.defaultValue : ''}\n onChange={(value) => onChange({ defaultValue: value || undefined })}\n />\n ) : null}\n </div>\n )\n}\n\nfunction DictionaryFieldInput({ value, setValue, disabled, def }: Props) {\n const t = useT()\n const dictionaryId = def?.dictionaryId\n if (!dictionaryId) {\n return (\n <div className=\"rounded border border-dashed p-3 text-sm text-muted-foreground\">\n {t('dictionaries.config.entries.error.load', 'Failed to load dictionary entries.')}\n </div>\n )\n }\n const normalizedValue = typeof value === 'string' ? value : Array.isArray(value) ? String(value[0] ?? '') : undefined\n return (\n <DictionarySelectControl\n dictionaryId={dictionaryId}\n value={normalizedValue ?? ''}\n onChange={(next) => setValue(next ?? undefined)}\n allowInlineCreate={def?.dictionaryInlineCreate !== false}\n disabled={disabled}\n />\n )\n}\n\nFieldRegistry.register('dictionary', {\n input: DictionaryFieldInput,\n defEditor: (props) => <DictionaryFieldDefEditor {...props} />,\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -34,8 +34,7 @@ function isValidCachedScope(value) {
|
|
|
34
34
|
const record = value;
|
|
35
35
|
const idOk = (v) => v === null || typeof v === "string";
|
|
36
36
|
const arrOk = (v) => v === null || Array.isArray(v) && v.every((entry) => typeof entry === "string");
|
|
37
|
-
|
|
38
|
-
return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds) && flagOk;
|
|
37
|
+
return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds);
|
|
39
38
|
}
|
|
40
39
|
function resolveCacheFromContainer(container) {
|
|
41
40
|
if (!container) return null;
|
|
@@ -198,10 +197,9 @@ async function resolveOrganizationScope({
|
|
|
198
197
|
const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice;
|
|
199
198
|
const widenToAllOrgs = explicitAllOrgsChoice && hasUnrestrictedAccess || effectiveSuperAdmin && noOrgSelection;
|
|
200
199
|
const initialSelected = normalizedSelectedId ?? (widenToAllOrgs ? null : accountOrgId ?? null);
|
|
201
|
-
const selectionResolvesToOrg = (id) => orgDescendants.has(id);
|
|
202
200
|
let effectiveSelected = null;
|
|
203
201
|
if (initialSelected) {
|
|
204
|
-
if (
|
|
202
|
+
if (allowedSet === null || allowedSet.has(initialSelected)) {
|
|
205
203
|
effectiveSelected = initialSelected;
|
|
206
204
|
}
|
|
207
205
|
}
|
|
@@ -214,9 +212,6 @@ async function resolveOrganizationScope({
|
|
|
214
212
|
filterSet = null;
|
|
215
213
|
} else if (auth.orgId) {
|
|
216
214
|
filterSet = loadFallbackSet();
|
|
217
|
-
if (!effectiveSelected && fallbackOrgId && filterSet && filterSet.size > 0) {
|
|
218
|
-
effectiveSelected = fallbackOrgId;
|
|
219
|
-
}
|
|
220
215
|
}
|
|
221
216
|
if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {
|
|
222
217
|
const computed = loadFallbackSet();
|
|
@@ -227,13 +222,11 @@ async function resolveOrganizationScope({
|
|
|
227
222
|
}
|
|
228
223
|
}
|
|
229
224
|
}
|
|
230
|
-
const selectionRejected = normalizedSelectedId !== null && effectiveSelected !== normalizedSelectedId;
|
|
231
225
|
return {
|
|
232
226
|
selectedId: effectiveSelected,
|
|
233
227
|
filterIds: filterSet ? Array.from(filterSet) : null,
|
|
234
228
|
allowedIds: allowedSet ? Array.from(allowedSet) : null,
|
|
235
|
-
tenantId
|
|
236
|
-
...selectionRejected ? { selectionRejected: true } : {}
|
|
229
|
+
tenantId
|
|
237
230
|
};
|
|
238
231
|
}
|
|
239
232
|
async function resolveOrganizationScopeForRequest({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/directory/utils/organizationScope.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('directory').child({ component: 'org-scope-cache' })\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type OrganizationScope = {\n selectedId: string | null\n filterIds: string[] | null\n allowedIds: string[] | null\n tenantId: string | null\n // True when the caller explicitly selected a concrete organization (cookie /\n // selectedId param) that could not be honored \u2014 it does not exist for the\n // tenant or is not accessible. Reads degrade gracefully (filterIds/selectedId\n // fall back to the caller's accessible orgs), but writes MUST fail loudly on\n // this so a record never lands under an org the caller did not intend. Absent\n // (undefined) means the selection \u2014 if any \u2014 was honored.\n selectionRejected?: boolean\n}\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded as a backstop. Tag-based invalidation also fires\n// eagerly: per-user entries are dropped by RbacService.invalidateUserCache\n// (every ACL/role grant change goes through it \u2014 see buildOrgScopeUserCacheTag)\n// and per-tenant entries by the directory.organization.* subscriber plus\n// RbacService.invalidateTenantCache (role-ACL changes).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\n// Tag builders are exported so the modules that own the \"this user's scope\n// changed\" / \"this tenant's org tree changed\" signals (auth RBAC invalidation,\n// the directory.organization.* subscriber) can drop the matching cross-request\n// cache entries without re-deriving the tag format. Keeping the format in one\n// place is what lets the TTL be enabled safely (issue #2259).\nexport function buildOrgScopeUserCacheTag(userId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`\n}\n\nexport function buildOrgScopeTenantCacheTag(tenantId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n buildOrgScopeUserCacheTag(parts.userId),\n buildOrgScopeTenantCacheTag(parts.effectiveTenantId),\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n const flagOk = record.selectionRejected === undefined || typeof record.selectionRejected === 'boolean'\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds) && flagOk\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([buildOrgScopeUserCacheTag(userId)])\n } catch (err) {\n logger.warn('Cache invalidate user failed', { err })\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)])\n } catch (err) {\n logger.warn('Cache invalidate tenant failed', { err })\n }\n}\n\n// Issue #2259 \u2014 per-request memoization. resolveOrganizationScopeForRequest\n// runs at least twice per CRUD request: once for the route-level feature check\n// (resolveFeatureCheckContext) and once inside the shared factory's withCtx.\n// Those two call sites use different request-scoped DI containers but are handed\n// the SAME Request instance, so memoizing the resolved scope on a WeakMap keyed\n// by that request collapses the duplicate work \u2014 and the duplicate\n// `organizations` SELECT \u2014 into a single resolution. The inner map is keyed by\n// the same identity tuple as the cross-request cache key, so distinct explicit\n// selectedId/tenant overrides on one request stay independent. There is no\n// staleness risk: the memo lives only for the lifetime of one request and is\n// dropped with the request object by the GC.\nconst orgScopeRequestMemo = new WeakMap<object, Map<string, Promise<OrganizationScope>>>()\n\nfunction getRequestScopeMemo(request: unknown): Map<string, Promise<OrganizationScope>> | null {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return null\n const key = request as object\n let memo = orgScopeRequestMemo.get(key)\n if (!memo) {\n memo = new Map<string, Promise<OrganizationScope>>()\n orgScopeRequestMemo.set(key, memo)\n }\n return memo\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nfunction normalizeOrganizationIds(ids: string[]): string[] {\n return Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n}\n\n// Map each organization id to itself plus its persisted descendant ids. Only\n// orgs that exist for the tenant and are not soft-deleted are included, so an\n// unknown/inaccessible id simply has no entry (matching the per-id query that\n// returned an empty set for it).\ntype OrgDescendantMap = Map<string, string[]>\n\n// Issue #2228 \u2014 single round-trip for org-scope resolution. Instead of issuing\n// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4\n// sequential queries per request: accessible set, fallback set, selected set),\n// gather every candidate id up front and fetch their descendant expansions in\n// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.\nasync function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {\n const unique = normalizeOrganizationIds(ids)\n if (!unique.length) return new Map()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const map: OrgDescendantMap = new Map()\n for (const org of orgs) {\n const id = String(org.id)\n const expansion = [id]\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) expansion.push(String(desc))\n }\n map.set(id, expansion)\n }\n return map\n}\n\nfunction expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {\n const set = new Set<string>()\n for (const value of ids) {\n const id = normalizeOrganizationId(value)\n if (!id || isAllOrganizationsSelection(id)) continue\n const expansion = map.get(id)\n if (!expansion) continue\n for (const entry of expansion) set.add(entry)\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: RbacService\n auth: AuthContext\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n\n // Every id that could be expanded below \u2014 accessible set, fallback (account)\n // org, and the requested selection \u2014 is known up front, so fetch them all in\n // a single `organizations` query and expand from the in-memory map.\n const candidateIds = [\n ...(accessibleList ?? []),\n ...(fallbackOrgId ? [fallbackOrgId] : []),\n ...(normalizedSelectedId ? [normalizedSelectedId] : []),\n ]\n const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)\n const loadFallbackSet = (): Set<string> | null =>\n fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = expandWithDescendants(orgDescendants, accessibleList)\n }\n\n if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n allowedSet = computed\n }\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n // A selection is only honored when it resolves to a real, non-deleted org for\n // this tenant. `orgDescendants` holds exactly the existing orgs among the\n // candidate ids, so a stale/dead id (e.g. a selected-org cookie or JWT org\n // that no longer resolves after a DB reset) has no entry and is dropped here.\n // Without the existence guard an unrestricted (all-orgs) principal \u2014 whose\n // `allowedSet` is null \u2014 would accept the dead id as `effectiveSelected`, and\n // writes derived from `selectedId` would land under an org the read scope\n // (`filterIds`) never filters to, silently orphaning the record.\n const selectionResolvesToOrg = (id: string): boolean => orgDescendants.has(id)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if ((allowedSet === null || allowedSet.has(initialSelected)) && selectionResolvesToOrg(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = loadFallbackSet()\n // Keep the write target (`selectedId`) aligned with the read scope\n // (`filterIds`): when an unrestricted principal's requested selection was\n // dropped above, fall the selection back to the account org too so a record\n // created here is always readable back by the same caller.\n if (!effectiveSelected && fallbackOrgId && filterSet && filterSet.size > 0) {\n effectiveSelected = fallbackOrgId\n }\n }\n\n if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n // A concrete organization was explicitly requested (`normalizedSelectedId` is\n // null for both \"no selection\" and the \"all organizations\" token) but the\n // resolver could not honor it \u2014 it was dropped as non-existent/inaccessible\n // and the effective selection fell back to something else. Surface this so the\n // write layer can reject the request instead of silently creating the record\n // under the fallback org. Only set the field when true to keep the scope shape\n // unchanged for the common (honored) case.\n const selectionRejected = normalizedSelectedId !== null && effectiveSelected !== normalizedSelectedId\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n ...(selectionRejected ? { selectionRejected: true } : {}),\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n const requestMemo = getRequestScopeMemo(request)\n if (requestMemo && cacheKey) {\n const memoized = requestMemo.get(cacheKey)\n if (memoized) return memoized\n }\n\n const resolveScope = async (): Promise<OrganizationScope> => {\n if (cache && cacheKey && typeof cache.get === 'function') {\n try {\n const cached = await cache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n logger.warn('Cache read failed', { err })\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (cache && cacheKey && userId && typeof cache.set === 'function') {\n try {\n await cache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n logger.warn('Cache write failed', { err })\n }\n }\n\n return baseScope\n }\n\n if (requestMemo && cacheKey) {\n const pending = resolveScope()\n requestMemo.set(cacheKey, pending)\n return pending\n }\n\n return resolveScope()\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<FeatureCheckContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n const organizationId =\n scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n\n return { organizationId, scope, allowedOrganizationIds }\n}\n"],
|
|
5
|
-
"mappings": "AAGA,SAAS,oBAAoB;AAC7B,SAAS,mCAAmC;AAI5C,SAAS,iCAAiC,iCAAiC;AAC3E,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,kBAAkB,CAAC;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('directory').child({ component: 'org-scope-cache' })\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type OrganizationScope = {\n selectedId: string | null\n filterIds: string[] | null\n allowedIds: string[] | null\n tenantId: string | null\n}\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded as a backstop. Tag-based invalidation also fires\n// eagerly: per-user entries are dropped by RbacService.invalidateUserCache\n// (every ACL/role grant change goes through it \u2014 see buildOrgScopeUserCacheTag)\n// and per-tenant entries by the directory.organization.* subscriber plus\n// RbacService.invalidateTenantCache (role-ACL changes).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\n// Tag builders are exported so the modules that own the \"this user's scope\n// changed\" / \"this tenant's org tree changed\" signals (auth RBAC invalidation,\n// the directory.organization.* subscriber) can drop the matching cross-request\n// cache entries without re-deriving the tag format. Keeping the format in one\n// place is what lets the TTL be enabled safely (issue #2259).\nexport function buildOrgScopeUserCacheTag(userId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`\n}\n\nexport function buildOrgScopeTenantCacheTag(tenantId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n buildOrgScopeUserCacheTag(parts.userId),\n buildOrgScopeTenantCacheTag(parts.effectiveTenantId),\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds)\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([buildOrgScopeUserCacheTag(userId)])\n } catch (err) {\n logger.warn('Cache invalidate user failed', { err })\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)])\n } catch (err) {\n logger.warn('Cache invalidate tenant failed', { err })\n }\n}\n\n// Issue #2259 \u2014 per-request memoization. resolveOrganizationScopeForRequest\n// runs at least twice per CRUD request: once for the route-level feature check\n// (resolveFeatureCheckContext) and once inside the shared factory's withCtx.\n// Those two call sites use different request-scoped DI containers but are handed\n// the SAME Request instance, so memoizing the resolved scope on a WeakMap keyed\n// by that request collapses the duplicate work \u2014 and the duplicate\n// `organizations` SELECT \u2014 into a single resolution. The inner map is keyed by\n// the same identity tuple as the cross-request cache key, so distinct explicit\n// selectedId/tenant overrides on one request stay independent. There is no\n// staleness risk: the memo lives only for the lifetime of one request and is\n// dropped with the request object by the GC.\nconst orgScopeRequestMemo = new WeakMap<object, Map<string, Promise<OrganizationScope>>>()\n\nfunction getRequestScopeMemo(request: unknown): Map<string, Promise<OrganizationScope>> | null {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return null\n const key = request as object\n let memo = orgScopeRequestMemo.get(key)\n if (!memo) {\n memo = new Map<string, Promise<OrganizationScope>>()\n orgScopeRequestMemo.set(key, memo)\n }\n return memo\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nfunction normalizeOrganizationIds(ids: string[]): string[] {\n return Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n}\n\n// Map each organization id to itself plus its persisted descendant ids. Only\n// orgs that exist for the tenant and are not soft-deleted are included, so an\n// unknown/inaccessible id simply has no entry (matching the per-id query that\n// returned an empty set for it).\ntype OrgDescendantMap = Map<string, string[]>\n\n// Issue #2228 \u2014 single round-trip for org-scope resolution. Instead of issuing\n// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4\n// sequential queries per request: accessible set, fallback set, selected set),\n// gather every candidate id up front and fetch their descendant expansions in\n// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.\nasync function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {\n const unique = normalizeOrganizationIds(ids)\n if (!unique.length) return new Map()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const map: OrgDescendantMap = new Map()\n for (const org of orgs) {\n const id = String(org.id)\n const expansion = [id]\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) expansion.push(String(desc))\n }\n map.set(id, expansion)\n }\n return map\n}\n\nfunction expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {\n const set = new Set<string>()\n for (const value of ids) {\n const id = normalizeOrganizationId(value)\n if (!id || isAllOrganizationsSelection(id)) continue\n const expansion = map.get(id)\n if (!expansion) continue\n for (const entry of expansion) set.add(entry)\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: RbacService\n auth: AuthContext\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n\n // Every id that could be expanded below \u2014 accessible set, fallback (account)\n // org, and the requested selection \u2014 is known up front, so fetch them all in\n // a single `organizations` query and expand from the in-memory map.\n const candidateIds = [\n ...(accessibleList ?? []),\n ...(fallbackOrgId ? [fallbackOrgId] : []),\n ...(normalizedSelectedId ? [normalizedSelectedId] : []),\n ]\n const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)\n const loadFallbackSet = (): Set<string> | null =>\n fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = expandWithDescendants(orgDescendants, accessibleList)\n }\n\n if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n allowedSet = computed\n }\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if (allowedSet === null || allowedSet.has(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = loadFallbackSet()\n }\n\n if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n const requestMemo = getRequestScopeMemo(request)\n if (requestMemo && cacheKey) {\n const memoized = requestMemo.get(cacheKey)\n if (memoized) return memoized\n }\n\n const resolveScope = async (): Promise<OrganizationScope> => {\n if (cache && cacheKey && typeof cache.get === 'function') {\n try {\n const cached = await cache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n logger.warn('Cache read failed', { err })\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (cache && cacheKey && userId && typeof cache.set === 'function') {\n try {\n await cache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n logger.warn('Cache write failed', { err })\n }\n }\n\n return baseScope\n }\n\n if (requestMemo && cacheKey) {\n const pending = resolveScope()\n requestMemo.set(cacheKey, pending)\n return pending\n }\n\n return resolveScope()\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<FeatureCheckContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n const organizationId =\n scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n\n return { organizationId, scope, allowedOrganizationIds }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,7 +6,6 @@ import { normalizeExportFormat, serializeExport, defaultExportFilename, ensureCo
|
|
|
6
6
|
import { resolveOrganizationScope, getSelectedOrganizationFromRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
7
7
|
import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from "@open-mercato/shared/lib/data/engine";
|
|
8
8
|
import { parseBooleanToken, parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
9
|
-
import { parseCommaSeparatedList } from "@open-mercato/shared/lib/string";
|
|
10
9
|
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
11
10
|
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
12
11
|
import { getModules } from "@open-mercato/shared/lib/i18n/server";
|
|
@@ -112,7 +111,7 @@ async function GET(req) {
|
|
|
112
111
|
const sortDir = (url.searchParams.get("sortDir") || "asc").toLowerCase() === "desc" ? "desc" : "asc";
|
|
113
112
|
const withDeleted = parseBooleanWithDefault(url.searchParams.get("withDeleted"), false);
|
|
114
113
|
const searchTerm = (url.searchParams.get("search") || "").trim();
|
|
115
|
-
const searchFields =
|
|
114
|
+
const searchFields = (url.searchParams.get("searchFields") || "").split(",").map((field) => field.trim()).filter(Boolean);
|
|
116
115
|
const qpEntries = [];
|
|
117
116
|
for (const [key, val] of url.searchParams.entries()) {
|
|
118
117
|
if (["entityId", "page", "pageSize", "sortField", "sortDir", "withDeleted", "format", "exportScope", "export_scope", "all", "full", "search", "searchFields"].includes(key)) continue;
|
|
@@ -162,11 +161,11 @@ async function GET(req) {
|
|
|
162
161
|
if (key.startsWith("cf_")) {
|
|
163
162
|
if (key.endsWith("In")) {
|
|
164
163
|
const base = key.slice(0, -2);
|
|
165
|
-
const values =
|
|
164
|
+
const values = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
166
165
|
filtersObj[base] = { $in: values };
|
|
167
166
|
} else {
|
|
168
167
|
if (val.includes(",")) {
|
|
169
|
-
const values =
|
|
168
|
+
const values = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
170
169
|
filtersObj[key] = { $in: values };
|
|
171
170
|
} else {
|
|
172
171
|
const parsed = parseBooleanToken(val);
|
|
@@ -175,7 +174,7 @@ async function GET(req) {
|
|
|
175
174
|
}
|
|
176
175
|
} else if (allowAnyKey) {
|
|
177
176
|
if (val.includes(",")) {
|
|
178
|
-
const values =
|
|
177
|
+
const values = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
179
178
|
filtersObj[key] = { $in: values };
|
|
180
179
|
} else {
|
|
181
180
|
const parsed = parseBooleanToken(val);
|