@open-mercato/shared 0.6.7-develop.6660.1.90e1e2eef6 → 0.6.7-develop.6669.1.40b669666b

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.
@@ -1,2 +1,2 @@
1
- [build:shared] found 240 entry points
1
+ [build:shared] found 241 entry points
2
2
  [build:shared] built successfully
@@ -61,7 +61,7 @@ async function compileAndImport(tsPath, allowRecovery = true) {
61
61
  }
62
62
  try {
63
63
  const fileUrl = `${pathToFileURL(jsPath).href}?mtime=${fs.statSync(jsPath).mtimeMs}`;
64
- return import(fileUrl);
64
+ return await import(fileUrl);
65
65
  } catch (error) {
66
66
  if (!allowRecovery) {
67
67
  throw error;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/bootstrap/dynamicLoader.ts"],
4
- "sourcesContent": ["import type { BootstrapData } from './types'\nimport { findAppRoot, type AppRoot } from './appResolver'\nimport { registerEntityIds } from '../encryption/entityIds'\nimport {\n ensureMikroOrmV7GeneratedCacheCompatibility,\n recoverMikroOrmV7GeneratedCacheFromImportError,\n} from './generatedCacheRecovery'\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { pathToFileURL } from 'node:url'\n\n/**\n * Compile a TypeScript file to JavaScript using esbuild bundler.\n * This bundles the file and all its dependencies, handling JSON imports properly.\n * The compiled file is written next to the source file with a .mjs extension.\n */\nasync function compileAndImport(tsPath: string, allowRecovery: boolean = true): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n // Check if we need to recompile (source newer than compiled)\n const tsExists = fs.existsSync(tsPath)\n const jsExists = fs.existsSync(jsPath)\n\n if (!tsExists) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const needsCompile = !jsExists ||\n fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n // Dynamically import esbuild only when needed\n const esbuild = await import('esbuild')\n\n // Plugin to resolve @/ alias to app root (works for @app modules)\n const aliasPlugin: import('esbuild').Plugin = {\n name: 'alias-resolver',\n setup(build) {\n // Resolve @/ alias to app root\n build.onResolve({ filter: /^@\\// }, (args) => {\n const resolved = path.join(appRoot, args.path.slice(2))\n // Try with .ts extension if base path doesn't exist\n if (!fs.existsSync(resolved) && fs.existsSync(resolved + '.ts')) {\n return { path: resolved + '.ts' }\n }\n // Also check for /index.ts if it's a directory\n if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory() && fs.existsSync(path.join(resolved, 'index.ts'))) {\n return { path: path.join(resolved, 'index.ts') }\n }\n return { path: resolved }\n })\n },\n }\n\n // Plugin to mark non-JSON package imports as external\n const externalNonJsonPlugin: import('esbuild').Plugin = {\n name: 'external-non-json',\n setup(build) {\n // Mark all package imports as external EXCEPT JSON files\n // Filter matches paths that don't start with . or / (package imports like @open-mercato/shared)\n build.onResolve({ filter: /^[^./]/ }, (args) => {\n // Skip Windows absolute paths (e.g., C:\\...) - they're local files, not packages\n if (/^[a-zA-Z]:/.test(args.path)) {\n return null // Let esbuild handle it\n }\n // If it's a JSON file, let esbuild bundle it\n if (args.path.endsWith('.json')) {\n return null // Let esbuild handle it\n }\n // Otherwise mark as external\n return { path: args.path, external: true }\n })\n },\n }\n\n // Use esbuild.build with bundling to handle JSON imports\n await esbuild.build({\n entryPoints: [tsPath],\n outfile: jsPath,\n bundle: true,\n format: 'esm',\n platform: 'node',\n target: 'node18',\n plugins: [aliasPlugin, externalNonJsonPlugin],\n // Allow JSON imports\n loader: { '.json': 'json' },\n })\n }\n\n // Import the compiled JavaScript\n try {\n const fileUrl = `${pathToFileURL(jsPath).href}?mtime=${fs.statSync(jsPath).mtimeMs}`\n return import(fileUrl)\n } catch (error) {\n if (!allowRecovery) {\n throw error\n }\n\n const recovered = recoverMikroOrmV7GeneratedCacheFromImportError(appRoot, error)\n if (!recovered.applied) {\n throw error\n }\n\n return compileAndImport(tsPath, false)\n }\n}\n\n\n/**\n * Dynamically load bootstrap data from a resolved app directory.\n *\n * IMPORTANT: This only works in unbundled contexts (CLI, tsx).\n * Do NOT use this in Next.js bundled code - use static imports instead.\n *\n * For CLI context, we skip loading modules.generated.ts which has Next.js dependencies.\n * CLI commands are discovered separately via the CLI module system.\n *\n * @param appRoot - Optional explicit app root path. If not provided, will search from cwd.\n * @returns The loaded bootstrap data\n * @throws Error if app root cannot be found or generated files are missing\n */\nexport async function loadBootstrapData(appRoot?: string): Promise<BootstrapData> {\n const resolved: AppRoot | null = appRoot\n ? {\n generatedDir: path.join(appRoot, '.mercato', 'generated'),\n appDir: appRoot,\n mercatoDir: path.join(appRoot, '.mercato'),\n }\n : findAppRoot()\n\n if (!resolved) {\n throw new Error(\n 'Could not find app root with .mercato/generated directory. ' +\n 'Make sure you run this command from within a Next.js app directory, ' +\n 'or run \"yarn mercato generate\" first to create the generated files.',\n )\n }\n\n const { generatedDir } = resolved\n\n ensureMikroOrmV7GeneratedCacheCompatibility(resolved.appDir)\n\n // IMPORTANT: Load entity IDs FIRST and register them before loading modules.\n // This is because modules (e.g., ce.ts files) use E.xxx.xxx at module scope,\n // and they need entity IDs to be available when they're imported.\n const entityIdsModule = await compileAndImport(path.join(generatedDir, 'entities.ids.generated.ts'))\n registerEntityIds(entityIdsModule.E as BootstrapData['entityIds'])\n\n // Now load the rest of the generated files.\n // modules.cli.generated.ts excludes Next.js-dependent code (routes, APIs, widgets)\n const [\n modulesModule,\n entitiesModule,\n diModule,\n searchModule,\n commandLoadersModule,\n commandInterceptorsModule,\n workflowsModule,\n ] = await Promise.all([\n compileAndImport(path.join(generatedDir, 'modules.cli.generated.ts')),\n compileAndImport(path.join(generatedDir, 'entities.generated.ts')),\n compileAndImport(path.join(generatedDir, 'di.generated.ts')),\n compileAndImport(path.join(generatedDir, 'search.generated.ts')).catch(() => ({ searchModuleConfigs: [] })),\n compileAndImport(path.join(generatedDir, 'command-loaders.generated.ts')).catch(() => ({ commandLoaderEntries: [] })),\n compileAndImport(path.join(generatedDir, 'command-interceptors.generated.ts')).catch(() => ({ commandInterceptorEntries: [] })),\n compileAndImport(path.join(generatedDir, 'workflows.generated.ts')).catch(() => ({ allCodeWorkflows: [] })),\n ])\n\n return {\n modules: modulesModule.modules as BootstrapData['modules'],\n entities: entitiesModule.entities as BootstrapData['entities'],\n diRegistrars: diModule.diRegistrars as BootstrapData['diRegistrars'],\n entityIds: entityIdsModule.E as BootstrapData['entityIds'],\n // Search configs are needed by workers for indexing\n searchModuleConfigs: (searchModule.searchModuleConfigs ?? []) as BootstrapData['searchModuleConfigs'],\n commandLoaderEntries: (commandLoadersModule.commandLoaderEntries ?? []) as BootstrapData['commandLoaderEntries'],\n // Command interceptors must apply in worker/CLI processes too \u2014 the\n // interceptor registry is per-process, so relying on the Next.js runtime's\n // registration silently no-ops every interceptor for queued/CLI commands\n // (#4327).\n commandInterceptorEntries: (commandInterceptorsModule.commandInterceptorEntries ??\n []) as BootstrapData['commandInterceptorEntries'],\n // Code workflow definitions are needed by workers to resume code-defined instances\n codeWorkflows: (workflowsModule.allCodeWorkflows ?? []) as BootstrapData['codeWorkflows'],\n // Empty UI-related data - not needed for CLI\n dashboardWidgetEntries: [],\n injectionWidgetEntries: [],\n injectionTables: [],\n interceptorEntries: [],\n componentOverrideEntries: [],\n }\n}\n\n/**\n * Create and execute bootstrap in CLI context.\n *\n * This is a convenience function that finds the app root, loads the generated\n * data dynamically, and runs bootstrap. Use this in CLI entry points.\n *\n * Returns the loaded bootstrap data so the CLI can register modules directly\n * (avoids module resolution issues when importing @open-mercato/cli/mercato).\n *\n * @param appRoot - Optional explicit app root path\n * @returns The loaded bootstrap data (modules, entities, etc.)\n */\nexport async function bootstrapFromAppRoot(appRoot?: string): Promise<BootstrapData> {\n const { createBootstrap, waitForAsyncRegistration } = await import('./factory.js')\n const data = await loadBootstrapData(appRoot)\n const bootstrap = createBootstrap(data)\n bootstrap()\n // In CLI context, wait for async registrations (UI widgets, search configs, etc.)\n await waitForAsyncRegistration()\n\n return data\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { BootstrapData } from './types'\nimport { findAppRoot, type AppRoot } from './appResolver'\nimport { registerEntityIds } from '../encryption/entityIds'\nimport {\n ensureMikroOrmV7GeneratedCacheCompatibility,\n recoverMikroOrmV7GeneratedCacheFromImportError,\n} from './generatedCacheRecovery'\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { pathToFileURL } from 'node:url'\n\n/**\n * Compile a TypeScript file to JavaScript using esbuild bundler.\n * This bundles the file and all its dependencies, handling JSON imports properly.\n * The compiled file is written next to the source file with a .mjs extension.\n */\nasync function compileAndImport(tsPath: string, allowRecovery: boolean = true): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n // Check if we need to recompile (source newer than compiled)\n const tsExists = fs.existsSync(tsPath)\n const jsExists = fs.existsSync(jsPath)\n\n if (!tsExists) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const needsCompile = !jsExists ||\n fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n // Dynamically import esbuild only when needed\n const esbuild = await import('esbuild')\n\n // Plugin to resolve @/ alias to app root (works for @app modules)\n const aliasPlugin: import('esbuild').Plugin = {\n name: 'alias-resolver',\n setup(build) {\n // Resolve @/ alias to app root\n build.onResolve({ filter: /^@\\// }, (args) => {\n const resolved = path.join(appRoot, args.path.slice(2))\n // Try with .ts extension if base path doesn't exist\n if (!fs.existsSync(resolved) && fs.existsSync(resolved + '.ts')) {\n return { path: resolved + '.ts' }\n }\n // Also check for /index.ts if it's a directory\n if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory() && fs.existsSync(path.join(resolved, 'index.ts'))) {\n return { path: path.join(resolved, 'index.ts') }\n }\n return { path: resolved }\n })\n },\n }\n\n // Plugin to mark non-JSON package imports as external\n const externalNonJsonPlugin: import('esbuild').Plugin = {\n name: 'external-non-json',\n setup(build) {\n // Mark all package imports as external EXCEPT JSON files\n // Filter matches paths that don't start with . or / (package imports like @open-mercato/shared)\n build.onResolve({ filter: /^[^./]/ }, (args) => {\n // Skip Windows absolute paths (e.g., C:\\...) - they're local files, not packages\n if (/^[a-zA-Z]:/.test(args.path)) {\n return null // Let esbuild handle it\n }\n // If it's a JSON file, let esbuild bundle it\n if (args.path.endsWith('.json')) {\n return null // Let esbuild handle it\n }\n // Otherwise mark as external\n return { path: args.path, external: true }\n })\n },\n }\n\n // Use esbuild.build with bundling to handle JSON imports\n await esbuild.build({\n entryPoints: [tsPath],\n outfile: jsPath,\n bundle: true,\n format: 'esm',\n platform: 'node',\n target: 'node18',\n plugins: [aliasPlugin, externalNonJsonPlugin],\n // Allow JSON imports\n loader: { '.json': 'json' },\n })\n }\n\n // Import the compiled JavaScript\n try {\n const fileUrl = `${pathToFileURL(jsPath).href}?mtime=${fs.statSync(jsPath).mtimeMs}`\n return await import(fileUrl)\n } catch (error) {\n if (!allowRecovery) {\n throw error\n }\n\n const recovered = recoverMikroOrmV7GeneratedCacheFromImportError(appRoot, error)\n if (!recovered.applied) {\n throw error\n }\n\n return compileAndImport(tsPath, false)\n }\n}\n\n\n/**\n * Dynamically load bootstrap data from a resolved app directory.\n *\n * IMPORTANT: This only works in unbundled contexts (CLI, tsx).\n * Do NOT use this in Next.js bundled code - use static imports instead.\n *\n * For CLI context, we skip loading modules.generated.ts which has Next.js dependencies.\n * CLI commands are discovered separately via the CLI module system.\n *\n * @param appRoot - Optional explicit app root path. If not provided, will search from cwd.\n * @returns The loaded bootstrap data\n * @throws Error if app root cannot be found or generated files are missing\n */\nexport async function loadBootstrapData(appRoot?: string): Promise<BootstrapData> {\n const resolved: AppRoot | null = appRoot\n ? {\n generatedDir: path.join(appRoot, '.mercato', 'generated'),\n appDir: appRoot,\n mercatoDir: path.join(appRoot, '.mercato'),\n }\n : findAppRoot()\n\n if (!resolved) {\n throw new Error(\n 'Could not find app root with .mercato/generated directory. ' +\n 'Make sure you run this command from within a Next.js app directory, ' +\n 'or run \"yarn mercato generate\" first to create the generated files.',\n )\n }\n\n const { generatedDir } = resolved\n\n ensureMikroOrmV7GeneratedCacheCompatibility(resolved.appDir)\n\n // IMPORTANT: Load entity IDs FIRST and register them before loading modules.\n // This is because modules (e.g., ce.ts files) use E.xxx.xxx at module scope,\n // and they need entity IDs to be available when they're imported.\n const entityIdsModule = await compileAndImport(path.join(generatedDir, 'entities.ids.generated.ts'))\n registerEntityIds(entityIdsModule.E as BootstrapData['entityIds'])\n\n // Now load the rest of the generated files.\n // modules.cli.generated.ts excludes Next.js-dependent code (routes, APIs, widgets)\n const [\n modulesModule,\n entitiesModule,\n diModule,\n searchModule,\n commandLoadersModule,\n commandInterceptorsModule,\n workflowsModule,\n ] = await Promise.all([\n compileAndImport(path.join(generatedDir, 'modules.cli.generated.ts')),\n compileAndImport(path.join(generatedDir, 'entities.generated.ts')),\n compileAndImport(path.join(generatedDir, 'di.generated.ts')),\n compileAndImport(path.join(generatedDir, 'search.generated.ts')).catch(() => ({ searchModuleConfigs: [] })),\n compileAndImport(path.join(generatedDir, 'command-loaders.generated.ts')).catch(() => ({ commandLoaderEntries: [] })),\n compileAndImport(path.join(generatedDir, 'command-interceptors.generated.ts')).catch(() => ({ commandInterceptorEntries: [] })),\n compileAndImport(path.join(generatedDir, 'workflows.generated.ts')).catch(() => ({ allCodeWorkflows: [] })),\n ])\n\n return {\n modules: modulesModule.modules as BootstrapData['modules'],\n entities: entitiesModule.entities as BootstrapData['entities'],\n diRegistrars: diModule.diRegistrars as BootstrapData['diRegistrars'],\n entityIds: entityIdsModule.E as BootstrapData['entityIds'],\n // Search configs are needed by workers for indexing\n searchModuleConfigs: (searchModule.searchModuleConfigs ?? []) as BootstrapData['searchModuleConfigs'],\n commandLoaderEntries: (commandLoadersModule.commandLoaderEntries ?? []) as BootstrapData['commandLoaderEntries'],\n // Command interceptors must apply in worker/CLI processes too \u2014 the\n // interceptor registry is per-process, so relying on the Next.js runtime's\n // registration silently no-ops every interceptor for queued/CLI commands\n // (#4327).\n commandInterceptorEntries: (commandInterceptorsModule.commandInterceptorEntries ??\n []) as BootstrapData['commandInterceptorEntries'],\n // Code workflow definitions are needed by workers to resume code-defined instances\n codeWorkflows: (workflowsModule.allCodeWorkflows ?? []) as BootstrapData['codeWorkflows'],\n // Empty UI-related data - not needed for CLI\n dashboardWidgetEntries: [],\n injectionWidgetEntries: [],\n injectionTables: [],\n interceptorEntries: [],\n componentOverrideEntries: [],\n }\n}\n\n/**\n * Create and execute bootstrap in CLI context.\n *\n * This is a convenience function that finds the app root, loads the generated\n * data dynamically, and runs bootstrap. Use this in CLI entry points.\n *\n * Returns the loaded bootstrap data so the CLI can register modules directly\n * (avoids module resolution issues when importing @open-mercato/cli/mercato).\n *\n * @param appRoot - Optional explicit app root path\n * @returns The loaded bootstrap data (modules, entities, etc.)\n */\nexport async function bootstrapFromAppRoot(appRoot?: string): Promise<BootstrapData> {\n const { createBootstrap, waitForAsyncRegistration } = await import('./factory.js')\n const data = await loadBootstrapData(appRoot)\n const bootstrap = createBootstrap(data)\n bootstrap()\n // In CLI context, wait for async registrations (UI widgets, search configs, etc.)\n await waitForAsyncRegistration()\n\n return data\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,11 @@
1
+ function isUniqueViolation(err) {
2
+ if (!err || typeof err !== "object") return false;
3
+ const code = err.code;
4
+ if (code === "23505") return true;
5
+ const message = err.message;
6
+ return typeof message === "string" && /duplicate key value|unique constraint/i.test(message);
7
+ }
8
+ export {
9
+ isUniqueViolation
10
+ };
11
+ //# sourceMappingURL=pg-errors.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/db/pg-errors.ts"],
4
+ "sourcesContent": ["/**\n * Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of\n * the ORM/driver layer that surfaces it. Shared across modules so duplicate-insert\n * handling stays consistent platform-wide.\n */\nexport function isUniqueViolation(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false\n const code = (err as { code?: string }).code\n if (code === '23505') return true // Postgres unique_violation\n const message = (err as { message?: string }).message\n return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)\n}\n"],
5
+ "mappings": "AAKO,SAAS,kBAAkB,KAAuB;AACvD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,OAAQ,IAA0B;AACxC,MAAI,SAAS,QAAS,QAAO;AAC7B,QAAM,UAAW,IAA6B;AAC9C,SAAO,OAAO,YAAY,YAAY,yCAAyC,KAAK,OAAO;AAC7F;",
6
+ "names": []
7
+ }
@@ -6,6 +6,7 @@ const STRING_TYPED_CUSTOM_FIELD_KINDS = /* @__PURE__ */ new Set([
6
6
  "select",
7
7
  "currency",
8
8
  "dictionary",
9
+ "phone",
9
10
  "email",
10
11
  "url",
11
12
  "string"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/encryption/customFieldValues.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { encryptWithAesGcm, decryptWithAesGcm } from './aes'\nimport { TenantDataEncryptionService } from './tenantDataEncryptionService'\n\n/**\n * Custom field kinds that ALWAYS round-trip as a string. The encrypt path\n * stores raw strings unwrapped, so blindly running `JSON.parse` on the\n * decrypted payload coerces text values like `\"123\"` or `\"true\"` back into\n * numbers/booleans (issue #1734). For these kinds, callers MUST pass the\n * `kind` option so we keep the decrypted value as a string.\n *\n * Numeric (`integer`/`float`) and `boolean` kinds rely on JSON round-trip\n * because the encrypt path JSON-stringifies the typed value before storage.\n * Omitting the kind preserves legacy round-trip behavior for backward\n * compatibility.\n */\nconst STRING_TYPED_CUSTOM_FIELD_KINDS = new Set([\n 'text',\n 'multiline',\n 'select',\n 'currency',\n 'dictionary',\n 'email',\n 'url',\n 'string',\n])\n\nexport type DecryptCustomFieldOptions = {\n /** Field kind, e.g. from `CustomFieldDef.kind`. When string-typed, the helper preserves the decrypted string verbatim. */\n kind?: string | null\n}\n\nfunction shouldPreserveAsString(kind: string | null | undefined): boolean {\n if (!kind) return false\n return STRING_TYPED_CUSTOM_FIELD_KINDS.has(kind)\n}\n\nconst serviceCache = new WeakMap<EntityManager, TenantDataEncryptionService>()\n\nexport function resolveTenantEncryptionService(\n em: EntityManager,\n provided?: TenantDataEncryptionService | null,\n): TenantDataEncryptionService | null {\n if (provided) return provided\n const cached = serviceCache.get(em)\n if (cached) return cached\n const service = new TenantDataEncryptionService(em as any)\n serviceCache.set(em, service)\n return service\n}\n\nasync function resolveDekKey(\n service: TenantDataEncryptionService | null,\n tenantId: string | null | undefined,\n cache?: Map<string | null, string | null>,\n opts?: { createIfMissing?: boolean },\n): Promise<string | null> {\n const scopedTenantId = tenantId ?? null\n if (!service || !service.isEnabled() || !scopedTenantId) return null\n if (cache?.has(scopedTenantId)) return cache.get(scopedTenantId) ?? null\n const dek = await service.getDek(scopedTenantId)\n let key = dek?.key ?? null\n if (!key && opts?.createIfMissing && typeof service.createDek === 'function') {\n const created = await service.createDek(scopedTenantId)\n key = created?.key ?? null\n }\n cache?.set(scopedTenantId, key)\n return key\n}\n\nexport async function encryptCustomFieldValue(\n value: unknown,\n tenantId: string | null | undefined,\n service: TenantDataEncryptionService | null,\n cache?: Map<string | null, string | null>,\n): Promise<unknown> {\n if (value === undefined || value === null) return value\n const key = await resolveDekKey(service, tenantId, cache, { createIfMissing: true })\n if (!key) return value\n const serialized = typeof value === 'string' ? value : JSON.stringify(value)\n return encryptWithAesGcm(serialized, key).value\n}\n\nexport async function decryptCustomFieldValue(\n value: unknown,\n tenantId: string | null | undefined,\n service: TenantDataEncryptionService | null,\n cache?: Map<string | null, string | null>,\n options?: DecryptCustomFieldOptions,\n): Promise<unknown> {\n if (value === undefined || value === null || typeof value !== 'string') return value\n const key = await resolveDekKey(service, tenantId, cache)\n if (!key) return value\n const decrypted = decryptWithAesGcm(value, key)\n if (decrypted === null) return value\n if (shouldPreserveAsString(options?.kind ?? null)) return decrypted\n try {\n return JSON.parse(decrypted)\n } catch {\n return decrypted\n }\n}\n"],
5
- "mappings": "AACA,SAAS,mBAAmB,yBAAyB;AACrD,SAAS,mCAAmC;AAc5C,MAAM,kCAAkC,oBAAI,IAAI;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAOD,SAAS,uBAAuB,MAA0C;AACxE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,gCAAgC,IAAI,IAAI;AACjD;AAEA,MAAM,eAAe,oBAAI,QAAoD;AAEtE,SAAS,+BACd,IACA,UACoC;AACpC,MAAI,SAAU,QAAO;AACrB,QAAM,SAAS,aAAa,IAAI,EAAE;AAClC,MAAI,OAAQ,QAAO;AACnB,QAAM,UAAU,IAAI,4BAA4B,EAAS;AACzD,eAAa,IAAI,IAAI,OAAO;AAC5B,SAAO;AACT;AAEA,eAAe,cACb,SACA,UACA,OACA,MACwB;AACxB,QAAM,iBAAiB,YAAY;AACnC,MAAI,CAAC,WAAW,CAAC,QAAQ,UAAU,KAAK,CAAC,eAAgB,QAAO;AAChE,MAAI,OAAO,IAAI,cAAc,EAAG,QAAO,MAAM,IAAI,cAAc,KAAK;AACpE,QAAM,MAAM,MAAM,QAAQ,OAAO,cAAc;AAC/C,MAAI,MAAM,KAAK,OAAO;AACtB,MAAI,CAAC,OAAO,MAAM,mBAAmB,OAAO,QAAQ,cAAc,YAAY;AAC5E,UAAM,UAAU,MAAM,QAAQ,UAAU,cAAc;AACtD,UAAM,SAAS,OAAO;AAAA,EACxB;AACA,SAAO,IAAI,gBAAgB,GAAG;AAC9B,SAAO;AACT;AAEA,eAAsB,wBACpB,OACA,UACA,SACA,OACkB;AAClB,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,QAAM,MAAM,MAAM,cAAc,SAAS,UAAU,OAAO,EAAE,iBAAiB,KAAK,CAAC;AACnF,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,aAAa,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AAC3E,SAAO,kBAAkB,YAAY,GAAG,EAAE;AAC5C;AAEA,eAAsB,wBACpB,OACA,UACA,SACA,OACA,SACkB;AAClB,MAAI,UAAU,UAAa,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AAC/E,QAAM,MAAM,MAAM,cAAc,SAAS,UAAU,KAAK;AACxD,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,YAAY,kBAAkB,OAAO,GAAG;AAC9C,MAAI,cAAc,KAAM,QAAO;AAC/B,MAAI,uBAAuB,SAAS,QAAQ,IAAI,EAAG,QAAO;AAC1D,MAAI;AACF,WAAO,KAAK,MAAM,SAAS;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { encryptWithAesGcm, decryptWithAesGcm } from './aes'\nimport { TenantDataEncryptionService } from './tenantDataEncryptionService'\n\n/**\n * Custom field kinds that ALWAYS round-trip as a string. The encrypt path\n * stores raw strings unwrapped, so blindly running `JSON.parse` on the\n * decrypted payload coerces text values like `\"123\"` or `\"true\"` back into\n * numbers/booleans (issue #1734). For these kinds, callers MUST pass the\n * `kind` option so we keep the decrypted value as a string.\n *\n * Numeric (`integer`/`float`) and `boolean` kinds rely on JSON round-trip\n * because the encrypt path JSON-stringifies the typed value before storage.\n * Omitting the kind preserves legacy round-trip behavior for backward\n * compatibility.\n */\nconst STRING_TYPED_CUSTOM_FIELD_KINDS = new Set([\n 'text',\n 'multiline',\n 'select',\n 'currency',\n 'dictionary',\n 'phone',\n 'email',\n 'url',\n 'string',\n])\n\nexport type DecryptCustomFieldOptions = {\n /** Field kind, e.g. from `CustomFieldDef.kind`. When string-typed, the helper preserves the decrypted string verbatim. */\n kind?: string | null\n}\n\nfunction shouldPreserveAsString(kind: string | null | undefined): boolean {\n if (!kind) return false\n return STRING_TYPED_CUSTOM_FIELD_KINDS.has(kind)\n}\n\nconst serviceCache = new WeakMap<EntityManager, TenantDataEncryptionService>()\n\nexport function resolveTenantEncryptionService(\n em: EntityManager,\n provided?: TenantDataEncryptionService | null,\n): TenantDataEncryptionService | null {\n if (provided) return provided\n const cached = serviceCache.get(em)\n if (cached) return cached\n const service = new TenantDataEncryptionService(em as any)\n serviceCache.set(em, service)\n return service\n}\n\nasync function resolveDekKey(\n service: TenantDataEncryptionService | null,\n tenantId: string | null | undefined,\n cache?: Map<string | null, string | null>,\n opts?: { createIfMissing?: boolean },\n): Promise<string | null> {\n const scopedTenantId = tenantId ?? null\n if (!service || !service.isEnabled() || !scopedTenantId) return null\n if (cache?.has(scopedTenantId)) return cache.get(scopedTenantId) ?? null\n const dek = await service.getDek(scopedTenantId)\n let key = dek?.key ?? null\n if (!key && opts?.createIfMissing && typeof service.createDek === 'function') {\n const created = await service.createDek(scopedTenantId)\n key = created?.key ?? null\n }\n cache?.set(scopedTenantId, key)\n return key\n}\n\nexport async function encryptCustomFieldValue(\n value: unknown,\n tenantId: string | null | undefined,\n service: TenantDataEncryptionService | null,\n cache?: Map<string | null, string | null>,\n): Promise<unknown> {\n if (value === undefined || value === null) return value\n const key = await resolveDekKey(service, tenantId, cache, { createIfMissing: true })\n if (!key) return value\n const serialized = typeof value === 'string' ? value : JSON.stringify(value)\n return encryptWithAesGcm(serialized, key).value\n}\n\nexport async function decryptCustomFieldValue(\n value: unknown,\n tenantId: string | null | undefined,\n service: TenantDataEncryptionService | null,\n cache?: Map<string | null, string | null>,\n options?: DecryptCustomFieldOptions,\n): Promise<unknown> {\n if (value === undefined || value === null || typeof value !== 'string') return value\n const key = await resolveDekKey(service, tenantId, cache)\n if (!key) return value\n const decrypted = decryptWithAesGcm(value, key)\n if (decrypted === null) return value\n if (shouldPreserveAsString(options?.kind ?? null)) return decrypted\n try {\n return JSON.parse(decrypted)\n } catch {\n return decrypted\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,mBAAmB,yBAAyB;AACrD,SAAS,mCAAmC;AAc5C,MAAM,kCAAkC,oBAAI,IAAI;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAOD,SAAS,uBAAuB,MAA0C;AACxE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,gCAAgC,IAAI,IAAI;AACjD;AAEA,MAAM,eAAe,oBAAI,QAAoD;AAEtE,SAAS,+BACd,IACA,UACoC;AACpC,MAAI,SAAU,QAAO;AACrB,QAAM,SAAS,aAAa,IAAI,EAAE;AAClC,MAAI,OAAQ,QAAO;AACnB,QAAM,UAAU,IAAI,4BAA4B,EAAS;AACzD,eAAa,IAAI,IAAI,OAAO;AAC5B,SAAO;AACT;AAEA,eAAe,cACb,SACA,UACA,OACA,MACwB;AACxB,QAAM,iBAAiB,YAAY;AACnC,MAAI,CAAC,WAAW,CAAC,QAAQ,UAAU,KAAK,CAAC,eAAgB,QAAO;AAChE,MAAI,OAAO,IAAI,cAAc,EAAG,QAAO,MAAM,IAAI,cAAc,KAAK;AACpE,QAAM,MAAM,MAAM,QAAQ,OAAO,cAAc;AAC/C,MAAI,MAAM,KAAK,OAAO;AACtB,MAAI,CAAC,OAAO,MAAM,mBAAmB,OAAO,QAAQ,cAAc,YAAY;AAC5E,UAAM,UAAU,MAAM,QAAQ,UAAU,cAAc;AACtD,UAAM,SAAS,OAAO;AAAA,EACxB;AACA,SAAO,IAAI,gBAAgB,GAAG;AAC9B,SAAO;AACT;AAEA,eAAsB,wBACpB,OACA,UACA,SACA,OACkB;AAClB,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,QAAM,MAAM,MAAM,cAAc,SAAS,UAAU,OAAO,EAAE,iBAAiB,KAAK,CAAC;AACnF,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,aAAa,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AAC3E,SAAO,kBAAkB,YAAY,GAAG,EAAE;AAC5C;AAEA,eAAsB,wBACpB,OACA,UACA,SACA,OACA,SACkB;AAClB,MAAI,UAAU,UAAa,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AAC/E,QAAM,MAAM,MAAM,cAAc,SAAS,UAAU,KAAK;AACxD,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,YAAY,kBAAkB,OAAO,GAAG;AAC9C,MAAI,cAAc,KAAM,QAAO;AAC/B,MAAI,uBAAuB,SAAS,QAAQ,IAAI,EAAG,QAAO;AAC1D,MAAI;AACF,WAAO,KAAK,MAAM,SAAS;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -59,6 +59,15 @@ async function recordIndexerError(deps, input) {
59
59
  const { message, stack } = normalizeError(input.error);
60
60
  const payload = safeJson(input.payload);
61
61
  const now = /* @__PURE__ */ new Date();
62
+ logger.error("Indexer error recorded", {
63
+ source: input.source,
64
+ handler: input.handler,
65
+ entityType: input.entityType ?? null,
66
+ recordId: input.recordId ?? null,
67
+ tenantId: input.tenantId ?? null,
68
+ organizationId: input.organizationId ?? null,
69
+ err: input.error
70
+ });
62
71
  try {
63
72
  await db.insertInto("indexer_error_logs").values({
64
73
  source: input.source,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/indexers/error-log.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'indexers' })\n\nexport type IndexerErrorSource = 'query_index' | 'vector' | 'fulltext'\n\nexport type RecordIndexerErrorInput = {\n source: IndexerErrorSource\n handler: string\n error: unknown\n entityType?: string | null\n recordId?: string | null\n tenantId?: string | null\n organizationId?: string | null\n payload?: unknown\n}\n\ntype RecordIndexerErrorDeps = {\n em?: EntityManager\n db?: Kysely<any>\n}\n\nconst MAX_MESSAGE_LENGTH = 8_192\nconst MAX_STACK_LENGTH = 32_768\n\nfunction truncate(input: string | null | undefined, limit: number): string | null {\n if (!input) return null\n return input.length > limit ? `${input.slice(0, limit - 3)}...` : input\n}\n\nfunction normalizeError(error: unknown): { message: string; stack: string | null } {\n if (error instanceof Error) {\n return {\n message: error.message || error.name || 'Unknown error',\n stack: typeof error.stack === 'string' ? error.stack : null,\n }\n }\n if (typeof error === 'string') {\n return { message: error, stack: null }\n }\n try {\n const json = JSON.stringify(error)\n return { message: json, stack: null }\n } catch {\n return { message: String(error ?? 'Unknown error'), stack: null }\n }\n}\n\nfunction safeJson(value: unknown): unknown {\n if (value === undefined) return null\n try {\n return JSON.parse(JSON.stringify(value))\n } catch {\n if (value == null) return null\n if (typeof value === 'object') {\n return { note: 'unserializable', asString: String(value) }\n }\n return value\n }\n}\n\nfunction pickDb(deps: RecordIndexerErrorDeps): Kysely<any> | null {\n if (deps.db) return deps.db\n if (deps.em) {\n try {\n return deps.em.getKysely<any>()\n } catch {\n return null\n }\n }\n return null\n}\n\nexport async function recordIndexerError(deps: RecordIndexerErrorDeps, input: RecordIndexerErrorInput): Promise<void> {\n const db = pickDb(deps)\n if (!db) {\n logger.error('Unable to record indexer error (missing db connection)', {\n source: input.source,\n handler: input.handler,\n })\n return\n }\n\n const { message, stack } = normalizeError(input.error)\n const payload = safeJson(input.payload)\n const now = new Date()\n\n try {\n await db\n .insertInto('indexer_error_logs' as any)\n .values({\n source: input.source,\n handler: input.handler,\n entity_type: input.entityType ?? null,\n record_id: input.recordId ?? null,\n tenant_id: input.tenantId ?? null,\n organization_id: input.organizationId ?? null,\n payload: payload === null ? null : sql`${JSON.stringify(payload)}::jsonb`,\n message: truncate(message, MAX_MESSAGE_LENGTH),\n stack: truncate(stack, MAX_STACK_LENGTH),\n occurred_at: now,\n } as any)\n .execute()\n } catch (loggingError) {\n logger.error('Failed to persist indexer error', { err: loggingError })\n }\n}\n"],
5
- "mappings": "AACA,SAAsB,WAAW;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,WAAW,CAAC;AAoBrE,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AAEzB,SAAS,SAAS,OAAkC,OAA8B;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,SAAS,QAAQ,GAAG,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,QAAQ;AACpE;AAEA,SAAS,eAAe,OAA2D;AACjF,MAAI,iBAAiB,OAAO;AAC1B,WAAO;AAAA,MACL,SAAS,MAAM,WAAW,MAAM,QAAQ;AAAA,MACxC,OAAO,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;AAAA,IACzD;AAAA,EACF;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,SAAS,OAAO,OAAO,KAAK;AAAA,EACvC;AACA,MAAI;AACF,UAAM,OAAO,KAAK,UAAU,KAAK;AACjC,WAAO,EAAE,SAAS,MAAM,OAAO,KAAK;AAAA,EACtC,QAAQ;AACN,WAAO,EAAE,SAAS,OAAO,SAAS,eAAe,GAAG,OAAO,KAAK;AAAA,EAClE;AACF;AAEA,SAAS,SAAS,OAAyB;AACzC,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI;AACF,WAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAAA,EACzC,QAAQ;AACN,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,EAAE,MAAM,kBAAkB,UAAU,OAAO,KAAK,EAAE;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,MAAkD;AAChE,MAAI,KAAK,GAAI,QAAO,KAAK;AACzB,MAAI,KAAK,IAAI;AACX,QAAI;AACF,aAAO,KAAK,GAAG,UAAe;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,mBAAmB,MAA8B,OAA+C;AACpH,QAAM,KAAK,OAAO,IAAI;AACtB,MAAI,CAAC,IAAI;AACP,WAAO,MAAM,0DAA0D;AAAA,MACrE,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,IACjB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,EAAE,SAAS,MAAM,IAAI,eAAe,MAAM,KAAK;AACrD,QAAM,UAAU,SAAS,MAAM,OAAO;AACtC,QAAM,MAAM,oBAAI,KAAK;AAErB,MAAI;AACF,UAAM,GACH,WAAW,oBAA2B,EACtC,OAAO;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf,aAAa,MAAM,cAAc;AAAA,MACjC,WAAW,MAAM,YAAY;AAAA,MAC7B,WAAW,MAAM,YAAY;AAAA,MAC7B,iBAAiB,MAAM,kBAAkB;AAAA,MACzC,SAAS,YAAY,OAAO,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,MAChE,SAAS,SAAS,SAAS,kBAAkB;AAAA,MAC7C,OAAO,SAAS,OAAO,gBAAgB;AAAA,MACvC,aAAa;AAAA,IACf,CAAQ,EACP,QAAQ;AAAA,EACb,SAAS,cAAc;AACrB,WAAO,MAAM,mCAAmC,EAAE,KAAK,aAAa,CAAC;AAAA,EACvE;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'indexers' })\n\nexport type IndexerErrorSource = 'query_index' | 'vector' | 'fulltext'\n\nexport type RecordIndexerErrorInput = {\n source: IndexerErrorSource\n handler: string\n error: unknown\n entityType?: string | null\n recordId?: string | null\n tenantId?: string | null\n organizationId?: string | null\n payload?: unknown\n}\n\ntype RecordIndexerErrorDeps = {\n em?: EntityManager\n db?: Kysely<any>\n}\n\nconst MAX_MESSAGE_LENGTH = 8_192\nconst MAX_STACK_LENGTH = 32_768\n\nfunction truncate(input: string | null | undefined, limit: number): string | null {\n if (!input) return null\n return input.length > limit ? `${input.slice(0, limit - 3)}...` : input\n}\n\nfunction normalizeError(error: unknown): { message: string; stack: string | null } {\n if (error instanceof Error) {\n return {\n message: error.message || error.name || 'Unknown error',\n stack: typeof error.stack === 'string' ? error.stack : null,\n }\n }\n if (typeof error === 'string') {\n return { message: error, stack: null }\n }\n try {\n const json = JSON.stringify(error)\n return { message: json, stack: null }\n } catch {\n return { message: String(error ?? 'Unknown error'), stack: null }\n }\n}\n\nfunction safeJson(value: unknown): unknown {\n if (value === undefined) return null\n try {\n return JSON.parse(JSON.stringify(value))\n } catch {\n if (value == null) return null\n if (typeof value === 'object') {\n return { note: 'unserializable', asString: String(value) }\n }\n return value\n }\n}\n\nfunction pickDb(deps: RecordIndexerErrorDeps): Kysely<any> | null {\n if (deps.db) return deps.db\n if (deps.em) {\n try {\n return deps.em.getKysely<any>()\n } catch {\n return null\n }\n }\n return null\n}\n\nexport async function recordIndexerError(deps: RecordIndexerErrorDeps, input: RecordIndexerErrorInput): Promise<void> {\n const db = pickDb(deps)\n if (!db) {\n logger.error('Unable to record indexer error (missing db connection)', {\n source: input.source,\n handler: input.handler,\n })\n return\n }\n\n const { message, stack } = normalizeError(input.error)\n const payload = safeJson(input.payload)\n const now = new Date()\n\n // Persisting to indexer_error_logs is not enough on its own: nobody watches that\n // table, and a failing database is exactly when the insert below is least likely\n // to land. Emit through the log facade too so the failure survives the process.\n // `input.payload` is deliberately omitted \u2014 it can carry record documents.\n logger.error('Indexer error recorded', {\n source: input.source,\n handler: input.handler,\n entityType: input.entityType ?? null,\n recordId: input.recordId ?? null,\n tenantId: input.tenantId ?? null,\n organizationId: input.organizationId ?? null,\n err: input.error,\n })\n\n try {\n await db\n .insertInto('indexer_error_logs' as any)\n .values({\n source: input.source,\n handler: input.handler,\n entity_type: input.entityType ?? null,\n record_id: input.recordId ?? null,\n tenant_id: input.tenantId ?? null,\n organization_id: input.organizationId ?? null,\n payload: payload === null ? null : sql`${JSON.stringify(payload)}::jsonb`,\n message: truncate(message, MAX_MESSAGE_LENGTH),\n stack: truncate(stack, MAX_STACK_LENGTH),\n occurred_at: now,\n } as any)\n .execute()\n } catch (loggingError) {\n logger.error('Failed to persist indexer error', { err: loggingError })\n }\n}\n"],
5
+ "mappings": "AACA,SAAsB,WAAW;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,WAAW,CAAC;AAoBrE,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AAEzB,SAAS,SAAS,OAAkC,OAA8B;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,SAAS,QAAQ,GAAG,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,QAAQ;AACpE;AAEA,SAAS,eAAe,OAA2D;AACjF,MAAI,iBAAiB,OAAO;AAC1B,WAAO;AAAA,MACL,SAAS,MAAM,WAAW,MAAM,QAAQ;AAAA,MACxC,OAAO,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;AAAA,IACzD;AAAA,EACF;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,SAAS,OAAO,OAAO,KAAK;AAAA,EACvC;AACA,MAAI;AACF,UAAM,OAAO,KAAK,UAAU,KAAK;AACjC,WAAO,EAAE,SAAS,MAAM,OAAO,KAAK;AAAA,EACtC,QAAQ;AACN,WAAO,EAAE,SAAS,OAAO,SAAS,eAAe,GAAG,OAAO,KAAK;AAAA,EAClE;AACF;AAEA,SAAS,SAAS,OAAyB;AACzC,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI;AACF,WAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAAA,EACzC,QAAQ;AACN,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,EAAE,MAAM,kBAAkB,UAAU,OAAO,KAAK,EAAE;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,MAAkD;AAChE,MAAI,KAAK,GAAI,QAAO,KAAK;AACzB,MAAI,KAAK,IAAI;AACX,QAAI;AACF,aAAO,KAAK,GAAG,UAAe;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,mBAAmB,MAA8B,OAA+C;AACpH,QAAM,KAAK,OAAO,IAAI;AACtB,MAAI,CAAC,IAAI;AACP,WAAO,MAAM,0DAA0D;AAAA,MACrE,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,IACjB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,EAAE,SAAS,MAAM,IAAI,eAAe,MAAM,KAAK;AACrD,QAAM,UAAU,SAAS,MAAM,OAAO;AACtC,QAAM,MAAM,oBAAI,KAAK;AAMrB,SAAO,MAAM,0BAA0B;AAAA,IACrC,QAAQ,MAAM;AAAA,IACd,SAAS,MAAM;AAAA,IACf,YAAY,MAAM,cAAc;AAAA,IAChC,UAAU,MAAM,YAAY;AAAA,IAC5B,UAAU,MAAM,YAAY;AAAA,IAC5B,gBAAgB,MAAM,kBAAkB;AAAA,IACxC,KAAK,MAAM;AAAA,EACb,CAAC;AAED,MAAI;AACF,UAAM,GACH,WAAW,oBAA2B,EACtC,OAAO;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf,aAAa,MAAM,cAAc;AAAA,MACjC,WAAW,MAAM,YAAY;AAAA,MAC7B,WAAW,MAAM,YAAY;AAAA,MAC7B,iBAAiB,MAAM,kBAAkB;AAAA,MACzC,SAAS,YAAY,OAAO,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,MAChE,SAAS,SAAS,SAAS,kBAAkB;AAAA,MAC7C,OAAO,SAAS,OAAO,gBAAgB;AAAA,MACvC,aAAa;AAAA,IACf,CAAQ,EACP,QAAQ;AAAA,EACb,SAAS,cAAc;AACrB,WAAO,MAAM,mCAAmC,EAAE,KAAK,aAAa,CAAC;AAAA,EACvE;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.7-develop.6660.1.90e1e2eef6";
1
+ const APP_VERSION = "0.6.7-develop.6669.1.40b669666b";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6660.1.90e1e2eef6'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6669.1.40b669666b'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -15,6 +15,7 @@ const cf = {
15
15
  boolean: (key, opts = {}) => ({ key, kind: "boolean", ...opts }),
16
16
  select: (key, options, opts = {}) => ({ key, kind: "select", options, ...opts }),
17
17
  currency: (key, opts = {}) => ({ key, kind: "currency", ...opts }),
18
+ phone: (key, opts = {}) => ({ key, kind: "phone", ...opts }),
18
19
  date: (key, opts = {}) => ({ key, kind: "date", ...opts }),
19
20
  datetime: (key, opts = {}) => ({ key, kind: "datetime", ...opts }),
20
21
  dictionary: (key, dictionaryId, opts = {}) => ({ key, kind: "dictionary", dictionaryId, ...opts })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/modules/dsl.ts"],
4
- "sourcesContent": ["import type { CustomFieldDefinition, CustomFieldSet, EntityExtension, EntityId } from '@open-mercato/shared/modules/entities'\n\nexport function entityId(moduleId: string, entity: string): EntityId {\n return `${moduleId}:${entity}`\n}\n\nexport function linkable(moduleId: string, entities: string[]): Record<string, EntityId> {\n return Object.fromEntries(entities.map((e) => [e, entityId(moduleId, e)]))\n}\n\nexport function defineLink(\n base: EntityId,\n extension: EntityId,\n opts: Pick<EntityExtension, 'join' | 'cardinality' | 'required' | 'description'>\n): EntityExtension {\n return { base, extension, ...opts }\n}\n\nexport const cf = {\n text: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'text', ...opts }),\n multiline: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'multiline', ...opts }),\n integer: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'integer', ...opts }),\n float: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'float', ...opts }),\n boolean: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'boolean', ...opts }),\n select: (key: string, options: string[], opts: Omit<CustomFieldDefinition, 'key' | 'kind' | 'options'> = {}): CustomFieldDefinition => ({ key, kind: 'select', options, ...opts }),\n currency: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'currency', ...opts }),\n date: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'date', ...opts }),\n datetime: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'datetime', ...opts }),\n dictionary: (key: string, dictionaryId: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind' | 'dictionaryId'> = {}): CustomFieldDefinition => ({ key, kind: 'dictionary', dictionaryId, ...opts }),\n}\n\nexport function defineFields(entity: EntityId, fields: CustomFieldDefinition[], source?: string): CustomFieldSet {\n return { entity, fields, source }\n}\n"],
5
- "mappings": "AAEO,SAAS,SAAS,UAAkB,QAA0B;AACnE,SAAO,GAAG,QAAQ,IAAI,MAAM;AAC9B;AAEO,SAAS,SAAS,UAAkB,UAA8C;AACvF,SAAO,OAAO,YAAY,SAAS,IAAI,CAAC,MAAM,CAAC,GAAG,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC;AAC3E;AAEO,SAAS,WACd,MACA,WACA,MACiB;AACjB,SAAO,EAAE,MAAM,WAAW,GAAG,KAAK;AACpC;AAEO,MAAM,KAAK;AAAA,EAChB,MAAM,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,QAAQ,GAAG,KAAK;AAAA,EACpI,WAAW,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,aAAa,GAAG,KAAK;AAAA,EAC9I,SAAS,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,WAAW,GAAG,KAAK;AAAA,EAC1I,OAAO,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,SAAS,GAAG,KAAK;AAAA,EACtI,SAAS,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,WAAW,GAAG,KAAK;AAAA,EAC1I,QAAQ,CAAC,KAAa,SAAmB,OAAgE,CAAC,OAA8B,EAAE,KAAK,MAAM,UAAU,SAAS,GAAG,KAAK;AAAA,EAChL,UAAU,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,YAAY,GAAG,KAAK;AAAA,EAC5I,MAAM,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,QAAQ,GAAG,KAAK;AAAA,EACpI,UAAU,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,YAAY,GAAG,KAAK;AAAA,EAC5I,YAAY,CAAC,KAAa,cAAsB,OAAqE,CAAC,OAA8B,EAAE,KAAK,MAAM,cAAc,cAAc,GAAG,KAAK;AACvM;AAEO,SAAS,aAAa,QAAkB,QAAiC,QAAiC;AAC/G,SAAO,EAAE,QAAQ,QAAQ,OAAO;AAClC;",
4
+ "sourcesContent": ["import type { CustomFieldDefinition, CustomFieldSet, EntityExtension, EntityId } from '@open-mercato/shared/modules/entities'\n\nexport function entityId(moduleId: string, entity: string): EntityId {\n return `${moduleId}:${entity}`\n}\n\nexport function linkable(moduleId: string, entities: string[]): Record<string, EntityId> {\n return Object.fromEntries(entities.map((e) => [e, entityId(moduleId, e)]))\n}\n\nexport function defineLink(\n base: EntityId,\n extension: EntityId,\n opts: Pick<EntityExtension, 'join' | 'cardinality' | 'required' | 'description'>\n): EntityExtension {\n return { base, extension, ...opts }\n}\n\nexport const cf = {\n text: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'text', ...opts }),\n multiline: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'multiline', ...opts }),\n integer: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'integer', ...opts }),\n float: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'float', ...opts }),\n boolean: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'boolean', ...opts }),\n select: (key: string, options: string[], opts: Omit<CustomFieldDefinition, 'key' | 'kind' | 'options'> = {}): CustomFieldDefinition => ({ key, kind: 'select', options, ...opts }),\n currency: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'currency', ...opts }),\n phone: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'phone', ...opts }),\n date: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'date', ...opts }),\n datetime: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'datetime', ...opts }),\n dictionary: (key: string, dictionaryId: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind' | 'dictionaryId'> = {}): CustomFieldDefinition => ({ key, kind: 'dictionary', dictionaryId, ...opts }),\n}\n\nexport function defineFields(entity: EntityId, fields: CustomFieldDefinition[], source?: string): CustomFieldSet {\n return { entity, fields, source }\n}\n"],
5
+ "mappings": "AAEO,SAAS,SAAS,UAAkB,QAA0B;AACnE,SAAO,GAAG,QAAQ,IAAI,MAAM;AAC9B;AAEO,SAAS,SAAS,UAAkB,UAA8C;AACvF,SAAO,OAAO,YAAY,SAAS,IAAI,CAAC,MAAM,CAAC,GAAG,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC;AAC3E;AAEO,SAAS,WACd,MACA,WACA,MACiB;AACjB,SAAO,EAAE,MAAM,WAAW,GAAG,KAAK;AACpC;AAEO,MAAM,KAAK;AAAA,EAChB,MAAM,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,QAAQ,GAAG,KAAK;AAAA,EACpI,WAAW,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,aAAa,GAAG,KAAK;AAAA,EAC9I,SAAS,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,WAAW,GAAG,KAAK;AAAA,EAC1I,OAAO,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,SAAS,GAAG,KAAK;AAAA,EACtI,SAAS,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,WAAW,GAAG,KAAK;AAAA,EAC1I,QAAQ,CAAC,KAAa,SAAmB,OAAgE,CAAC,OAA8B,EAAE,KAAK,MAAM,UAAU,SAAS,GAAG,KAAK;AAAA,EAChL,UAAU,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,YAAY,GAAG,KAAK;AAAA,EAC5I,OAAO,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,SAAS,GAAG,KAAK;AAAA,EACtI,MAAM,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,QAAQ,GAAG,KAAK;AAAA,EACpI,UAAU,CAAC,KAAa,OAAoD,CAAC,OAA8B,EAAE,KAAK,MAAM,YAAY,GAAG,KAAK;AAAA,EAC5I,YAAY,CAAC,KAAa,cAAsB,OAAqE,CAAC,OAA8B,EAAE,KAAK,MAAM,cAAc,cAAc,GAAG,KAAK;AACvM;AAEO,SAAS,aAAa,QAAkB,QAAiC,QAAiC;AAC/G,SAAO,EAAE,QAAQ,QAAQ,OAAO;AAClC;",
6
6
  "names": []
7
7
  }
@@ -9,6 +9,7 @@ const CUSTOM_FIELD_KINDS = [
9
9
  "relation",
10
10
  "attachment",
11
11
  "dictionary",
12
+ "phone",
12
13
  "date",
13
14
  "datetime"
14
15
  ];
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/entities/kinds.ts"],
4
- "sourcesContent": ["export const CUSTOM_FIELD_KINDS = [\n 'text',\n 'multiline',\n 'integer',\n 'float',\n 'boolean',\n 'select',\n 'currency',\n 'relation',\n 'attachment',\n 'dictionary',\n 'date',\n 'datetime',\n] as const\n\nexport type CustomFieldKind = typeof CUSTOM_FIELD_KINDS[number]\n\nexport function isCustomFieldKind(x: string): x is CustomFieldKind {\n return (CUSTOM_FIELD_KINDS as readonly string[]).includes(x)\n}\n\nexport const CURRENCY_OPTIONS_URL = '/api/currencies/currencies/options'\n"],
5
- "mappings": "AAAO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,SAAS,kBAAkB,GAAiC;AACjE,SAAQ,mBAAyC,SAAS,CAAC;AAC7D;AAEO,MAAM,uBAAuB;",
4
+ "sourcesContent": ["export const CUSTOM_FIELD_KINDS = [\n 'text',\n 'multiline',\n 'integer',\n 'float',\n 'boolean',\n 'select',\n 'currency',\n 'relation',\n 'attachment',\n 'dictionary',\n 'phone',\n 'date',\n 'datetime',\n] as const\n\nexport type CustomFieldKind = typeof CUSTOM_FIELD_KINDS[number]\n\nexport function isCustomFieldKind(x: string): x is CustomFieldKind {\n return (CUSTOM_FIELD_KINDS as readonly string[]).includes(x)\n}\n\nexport const CURRENCY_OPTIONS_URL = '/api/currencies/currencies/options'\n"],
5
+ "mappings": "AAAO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,SAAS,kBAAkB,GAAiC;AACjE,SAAQ,mBAAyC,SAAS,CAAC;AAC7D;AAEO,MAAM,uBAAuB;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.7-develop.6660.1.90e1e2eef6",
3
+ "version": "0.6.7-develop.6669.1.40b669666b",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -97,7 +97,7 @@
97
97
  "@mikro-orm/core": "^7.1.5",
98
98
  "@mikro-orm/decorators": "^7.1.5",
99
99
  "@mikro-orm/postgresql": "^7.1.5",
100
- "@open-mercato/cache": "0.6.7-develop.6660.1.90e1e2eef6",
100
+ "@open-mercato/cache": "0.6.7-develop.6669.1.40b669666b",
101
101
  "@types/sanitize-html": "^2.16.1",
102
102
  "dotenv": "^17.4.2",
103
103
  "pino": "^10.3.1",
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @jest-environment node
3
+ *
4
+ * Regression guard for #4526: compileAndImport must await its dynamic import so
5
+ * an import-time rejection is caught by the surrounding try/catch and reaches
6
+ * the MikroORM v7 generated-cache recovery. Returning the promise unawaited let
7
+ * it settle after the try block had exited, so the reactive recovery was dead
8
+ * code for exactly the failure it exists to repair (a stale generated cache
9
+ * whose top-level `@mikro-orm/core` decorator import no longer resolves).
10
+ *
11
+ * generatedCacheRecovery is mocked here: it owns cache detection/deletion and is
12
+ * covered by its own tests, while these cases only assert how dynamicLoader
13
+ * reacts to a rejecting import. The recovery double rewrites the compiled
14
+ * sibling the way a real cache wipe plus recompile would, so the retry loads the
15
+ * refreshed module without invoking esbuild.
16
+ */
17
+ import fs from 'node:fs'
18
+ import os from 'node:os'
19
+ import path from 'node:path'
20
+ import { loadBootstrapData } from '../dynamicLoader'
21
+ import {
22
+ ensureMikroOrmV7GeneratedCacheCompatibility,
23
+ recoverMikroOrmV7GeneratedCacheFromImportError,
24
+ type GeneratedCacheRecoveryResult,
25
+ } from '../generatedCacheRecovery'
26
+
27
+ jest.mock('../generatedCacheRecovery', () => ({
28
+ ensureMikroOrmV7GeneratedCacheCompatibility: jest.fn(),
29
+ recoverMikroOrmV7GeneratedCacheFromImportError: jest.fn(),
30
+ }))
31
+
32
+ const ensureCompatibilityMock = ensureMikroOrmV7GeneratedCacheCompatibility as jest.MockedFunction<
33
+ typeof ensureMikroOrmV7GeneratedCacheCompatibility
34
+ >
35
+ const recoverFromImportErrorMock = recoverMikroOrmV7GeneratedCacheFromImportError as jest.MockedFunction<
36
+ typeof recoverMikroOrmV7GeneratedCacheFromImportError
37
+ >
38
+
39
+ const NO_RECOVERY: GeneratedCacheRecoveryResult = { applied: false, deletedFiles: [], markerPath: null }
40
+ const STALE_DECORATOR_IMPORT_ERROR =
41
+ "The requested module '@mikro-orm/core' does not provide an export named 'Entity'"
42
+
43
+ const STALE_ENTITY_IDS_CACHE = `throw new SyntaxError(${JSON.stringify(STALE_DECORATOR_IMPORT_ERROR)})`
44
+ const FRESH_ENTITY_IDS_CACHE = "module.exports = { E: { example: { todo: 'example:todo' } } }"
45
+
46
+ const BASE_GENERATED_MODULES: Record<string, { ts: string; compiled: string }> = {
47
+ 'modules.cli.generated': { ts: 'export const modules = []', compiled: 'module.exports = { modules: [] }' },
48
+ 'entities.generated': { ts: 'export const entities = []', compiled: 'module.exports = { entities: [] }' },
49
+ 'di.generated': { ts: 'export const diRegistrars = []', compiled: 'module.exports = { diRegistrars: [] }' },
50
+ }
51
+
52
+ let compiledCacheGeneration = 0
53
+
54
+ function writeGeneratedModule(generatedDir: string, baseName: string, source: { ts: string; compiled: string }) {
55
+ fs.writeFileSync(path.join(generatedDir, `${baseName}.ts`), source.ts)
56
+ writeCompiledSibling(generatedDir, baseName, source.compiled)
57
+ }
58
+
59
+ /**
60
+ * Write the .mjs sibling with an mtime ahead of its .ts source so
61
+ * compileAndImport takes its cache path and never invokes esbuild. Each write
62
+ * advances the timestamp, which also gives the retry a distinct `?mtime=` import
63
+ * URL instead of the rejected module's cached one.
64
+ */
65
+ function writeCompiledSibling(generatedDir: string, baseName: string, compiled: string) {
66
+ const compiledPath = path.join(generatedDir, `${baseName}.mjs`)
67
+ fs.writeFileSync(compiledPath, compiled)
68
+ compiledCacheGeneration += 1
69
+ const fresh = new Date(Date.now() + compiledCacheGeneration * 60_000)
70
+ fs.utimesSync(compiledPath, fresh, fresh)
71
+ }
72
+
73
+ function createAppRoot(entityIdsCache: string): string {
74
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-bootstrap-4526-'))
75
+ const generatedDir = path.join(appRoot, '.mercato', 'generated')
76
+ fs.mkdirSync(generatedDir, { recursive: true })
77
+ for (const [baseName, source] of Object.entries(BASE_GENERATED_MODULES)) {
78
+ writeGeneratedModule(generatedDir, baseName, source)
79
+ }
80
+ writeGeneratedModule(generatedDir, 'entities.ids.generated', {
81
+ ts: 'export const E = {}',
82
+ compiled: entityIdsCache,
83
+ })
84
+ return appRoot
85
+ }
86
+
87
+ function recoverByRewritingCache(appRoot: string, compiled: string): GeneratedCacheRecoveryResult {
88
+ const generatedDir = path.join(appRoot, '.mercato', 'generated')
89
+ writeCompiledSibling(generatedDir, 'entities.ids.generated', compiled)
90
+ // Node busts its ESM cache through the `?mtime=` query compileAndImport
91
+ // appends; Jest's registry keys on the resolved path alone, so the retry would
92
+ // otherwise replay the rejected evaluation instead of the rewritten file.
93
+ jest.resetModules()
94
+ return {
95
+ applied: true,
96
+ deletedFiles: [path.join(generatedDir, 'entities.ids.generated.mjs')],
97
+ markerPath: path.join(generatedDir, '.mikro-orm-v7-cache-recovery.json'),
98
+ }
99
+ }
100
+
101
+ describe('compileAndImport — a rejecting import reaches the cache recovery (#4526)', () => {
102
+ const appRoots: string[] = []
103
+
104
+ beforeEach(() => {
105
+ ensureCompatibilityMock.mockReset()
106
+ recoverFromImportErrorMock.mockReset()
107
+ ensureCompatibilityMock.mockReturnValue(NO_RECOVERY)
108
+ recoverFromImportErrorMock.mockReturnValue(NO_RECOVERY)
109
+ })
110
+
111
+ afterAll(() => {
112
+ for (const appRoot of appRoots) {
113
+ fs.rmSync(appRoot, { recursive: true, force: true })
114
+ }
115
+ })
116
+
117
+ function createTrackedAppRoot(entityIdsCache: string): string {
118
+ const appRoot = createAppRoot(entityIdsCache)
119
+ appRoots.push(appRoot)
120
+ return appRoot
121
+ }
122
+
123
+ it('recovers the stale cache and loads the refreshed module', async () => {
124
+ const appRoot = createTrackedAppRoot(STALE_ENTITY_IDS_CACHE)
125
+ recoverFromImportErrorMock.mockImplementation(() => recoverByRewritingCache(appRoot, FRESH_ENTITY_IDS_CACHE))
126
+
127
+ const data = await loadBootstrapData(appRoot)
128
+
129
+ expect(data.entityIds).toEqual({ example: { todo: 'example:todo' } })
130
+ expect(recoverFromImportErrorMock).toHaveBeenCalledTimes(1)
131
+ const [recoveryAppRoot, recoveryError] = recoverFromImportErrorMock.mock.calls[0]
132
+ expect(recoveryAppRoot).toBe(appRoot)
133
+ expect(String((recoveryError as Error).message)).toContain(STALE_DECORATOR_IMPORT_ERROR)
134
+ })
135
+
136
+ it('propagates the import error when no recovery applies', async () => {
137
+ const appRoot = createTrackedAppRoot(STALE_ENTITY_IDS_CACHE)
138
+
139
+ await expect(loadBootstrapData(appRoot)).rejects.toThrow(STALE_DECORATOR_IMPORT_ERROR)
140
+ expect(recoverFromImportErrorMock).toHaveBeenCalledTimes(1)
141
+ })
142
+
143
+ it('retries at most once when the refreshed cache still fails to import', async () => {
144
+ const appRoot = createTrackedAppRoot(STALE_ENTITY_IDS_CACHE)
145
+ recoverFromImportErrorMock.mockImplementation(() => recoverByRewritingCache(appRoot, STALE_ENTITY_IDS_CACHE))
146
+
147
+ await expect(loadBootstrapData(appRoot)).rejects.toThrow(STALE_DECORATOR_IMPORT_ERROR)
148
+ expect(recoverFromImportErrorMock).toHaveBeenCalledTimes(1)
149
+ })
150
+ })
@@ -91,7 +91,7 @@ async function compileAndImport(tsPath: string, allowRecovery: boolean = true):
91
91
  // Import the compiled JavaScript
92
92
  try {
93
93
  const fileUrl = `${pathToFileURL(jsPath).href}?mtime=${fs.statSync(jsPath).mtimeMs}`
94
- return import(fileUrl)
94
+ return await import(fileUrl)
95
95
  } catch (error) {
96
96
  if (!allowRecovery) {
97
97
  throw error
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of
3
+ * the ORM/driver layer that surfaces it. Shared across modules so duplicate-insert
4
+ * handling stays consistent platform-wide.
5
+ */
6
+ export function isUniqueViolation(err: unknown): boolean {
7
+ if (!err || typeof err !== 'object') return false
8
+ const code = (err as { code?: string }).code
9
+ if (code === '23505') return true // Postgres unique_violation
10
+ const message = (err as { message?: string }).message
11
+ return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)
12
+ }
@@ -79,6 +79,23 @@ describe('customFieldValues encryption helpers', () => {
79
79
  expect(await decryptCustomFieldValue(booleanText, 'tenant-1', service, cache, { kind: 'currency' })).toBe('true')
80
80
  expect(await decryptCustomFieldValue(booleanText, 'tenant-1', service, cache, { kind: 'dictionary' })).toBe('true')
81
81
  expect(await decryptCustomFieldValue(booleanText, 'tenant-1', service, cache, { kind: 'email' })).toBe('true')
82
+ expect(await decryptCustomFieldValue(booleanText, 'tenant-1', service, cache, { kind: 'phone' })).toBe('true')
83
+ })
84
+
85
+ it('keeps an all-digit phone value a string instead of coercing it to a number (#62)', async () => {
86
+ const service = {
87
+ isEnabled: () => true,
88
+ getDek: async () => ({ key: fixedKey }),
89
+ } as any
90
+ const cache = new Map<string | null, string | null>()
91
+
92
+ const digitsOnly = await encryptCustomFieldValue('15551234567', 'tenant-1', service, cache)
93
+ const decrypted = await decryptCustomFieldValue(digitsOnly, 'tenant-1', service, cache, { kind: 'phone' })
94
+ expect(decrypted).toBe('15551234567')
95
+ expect(typeof decrypted).toBe('string')
96
+
97
+ const formatted = await encryptCustomFieldValue('+1 212 555 1234', 'tenant-1', service, cache)
98
+ expect(await decryptCustomFieldValue(formatted, 'tenant-1', service, cache, { kind: 'phone' })).toBe('+1 212 555 1234')
82
99
  })
83
100
 
84
101
  it('still parses typed kinds (integer/float/boolean) so legacy round-trip stays correct', async () => {
@@ -20,6 +20,7 @@ const STRING_TYPED_CUSTOM_FIELD_KINDS = new Set([
20
20
  'select',
21
21
  'currency',
22
22
  'dictionary',
23
+ 'phone',
23
24
  'email',
24
25
  'url',
25
26
  'string',
@@ -87,6 +87,20 @@ export async function recordIndexerError(deps: RecordIndexerErrorDeps, input: Re
87
87
  const payload = safeJson(input.payload)
88
88
  const now = new Date()
89
89
 
90
+ // Persisting to indexer_error_logs is not enough on its own: nobody watches that
91
+ // table, and a failing database is exactly when the insert below is least likely
92
+ // to land. Emit through the log facade too so the failure survives the process.
93
+ // `input.payload` is deliberately omitted — it can carry record documents.
94
+ logger.error('Indexer error recorded', {
95
+ source: input.source,
96
+ handler: input.handler,
97
+ entityType: input.entityType ?? null,
98
+ recordId: input.recordId ?? null,
99
+ tenantId: input.tenantId ?? null,
100
+ organizationId: input.organizationId ?? null,
101
+ err: input.error,
102
+ })
103
+
90
104
  try {
91
105
  await db
92
106
  .insertInto('indexer_error_logs' as any)
@@ -39,6 +39,18 @@ describe('DSL helpers', () => {
39
39
  expect(cf.datetime('seen_at')).toMatchObject({ key: 'seen_at', kind: 'datetime' })
40
40
  })
41
41
 
42
+ test('cf.phone helper produces a phone kind (#62)', () => {
43
+ expect(cf.phone('work_phone', { label: 'Work phone', formEditable: true })).toMatchObject({
44
+ key: 'work_phone',
45
+ kind: 'phone',
46
+ label: 'Work phone',
47
+ formEditable: true,
48
+ })
49
+ expect(CUSTOM_FIELD_KINDS).toContain('phone')
50
+ const phoneField: CustomFieldDefinition = { key: 'work_phone', kind: 'phone' }
51
+ expect(phoneField.kind).toBe('phone')
52
+ })
53
+
42
54
  test('CustomFieldKind type stays in sync with the runtime kinds list (#3042)', () => {
43
55
  // The CustomFieldKind type is derived from CUSTOM_FIELD_KINDS, so declaring a
44
56
  // field with any runtime kind — including date/datetime — must type-check.
@@ -24,6 +24,7 @@ export const cf = {
24
24
  boolean: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'boolean', ...opts }),
25
25
  select: (key: string, options: string[], opts: Omit<CustomFieldDefinition, 'key' | 'kind' | 'options'> = {}): CustomFieldDefinition => ({ key, kind: 'select', options, ...opts }),
26
26
  currency: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'currency', ...opts }),
27
+ phone: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'phone', ...opts }),
27
28
  date: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'date', ...opts }),
28
29
  datetime: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'datetime', ...opts }),
29
30
  dictionary: (key: string, dictionaryId: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind' | 'dictionaryId'> = {}): CustomFieldDefinition => ({ key, kind: 'dictionary', dictionaryId, ...opts }),
@@ -9,6 +9,7 @@ export const CUSTOM_FIELD_KINDS = [
9
9
  'relation',
10
10
  'attachment',
11
11
  'dictionary',
12
+ 'phone',
12
13
  'date',
13
14
  'datetime',
14
15
  ] as const