@open-mercato/shared 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +4 -4
  3. package/dist/lib/auth/server.js +1 -1
  4. package/dist/lib/auth/server.js.map +2 -2
  5. package/dist/lib/bootstrap/dynamicLoader.js +6 -46
  6. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  7. package/dist/lib/bootstrap/factory.js +3 -4
  8. package/dist/lib/bootstrap/factory.js.map +2 -2
  9. package/dist/lib/catalog-visibility/index.js +0 -4
  10. package/dist/lib/catalog-visibility/index.js.map +2 -2
  11. package/dist/lib/commands/command-bus.js +13 -45
  12. package/dist/lib/commands/command-bus.js.map +2 -2
  13. package/dist/lib/commands/types.js.map +2 -2
  14. package/dist/lib/crud/exporters.js +1 -1
  15. package/dist/lib/crud/exporters.js.map +2 -2
  16. package/dist/lib/crud/ids.js +1 -13
  17. package/dist/lib/crud/ids.js.map +2 -2
  18. package/dist/lib/db/pg-errors.js +0 -14
  19. package/dist/lib/db/pg-errors.js.map +2 -2
  20. package/dist/lib/localization/index.js +1 -9
  21. package/dist/lib/localization/index.js.map +2 -2
  22. package/dist/lib/localization/translatable-fields.js +0 -26
  23. package/dist/lib/localization/translatable-fields.js.map +2 -2
  24. package/dist/lib/number.js +0 -6
  25. package/dist/lib/number.js.map +2 -2
  26. package/dist/lib/openapi/generator.js +67 -84
  27. package/dist/lib/openapi/generator.js.map +2 -2
  28. package/dist/lib/query/engine.js +10 -45
  29. package/dist/lib/query/engine.js.map +2 -2
  30. package/dist/lib/query/join-utils.js +1 -1
  31. package/dist/lib/query/join-utils.js.map +2 -2
  32. package/dist/lib/query/types.js.map +2 -2
  33. package/dist/lib/url.js +0 -24
  34. package/dist/lib/url.js.map +2 -2
  35. package/dist/lib/version.js +1 -1
  36. package/dist/lib/version.js.map +1 -1
  37. package/dist/lib/webhooks/body.js +3 -7
  38. package/dist/lib/webhooks/body.js.map +2 -2
  39. package/dist/lib/webhooks/index.js +0 -2
  40. package/dist/lib/webhooks/index.js.map +2 -2
  41. package/dist/modules/integrations/types.js.map +2 -2
  42. package/dist/modules/notifications/types.js +0 -4
  43. package/dist/modules/notifications/types.js.map +3 -3
  44. package/dist/modules/payment_gateways/types.js +1 -14
  45. package/dist/modules/payment_gateways/types.js.map +2 -2
  46. package/dist/modules/registry.js.map +2 -2
  47. package/dist/modules/search.js.map +2 -2
  48. package/dist/modules/widgets/injection-loader.js +3 -14
  49. package/dist/modules/widgets/injection-loader.js.map +2 -2
  50. package/package.json +3 -10
  51. package/src/lib/__tests__/number.test.ts +1 -24
  52. package/src/lib/auth/__tests__/server.test.ts +0 -14
  53. package/src/lib/auth/server.ts +1 -1
  54. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
  55. package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
  56. package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
  57. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
  58. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
  59. package/src/lib/bootstrap/__tests__/factory.test.ts +6 -18
  60. package/src/lib/bootstrap/dynamicLoader.ts +5 -63
  61. package/src/lib/bootstrap/factory.ts +3 -8
  62. package/src/lib/bootstrap/types.ts +1 -5
  63. package/src/lib/catalog-visibility/index.ts +0 -2
  64. package/src/lib/commands/__tests__/command-bus.test.ts +1 -100
  65. package/src/lib/commands/command-bus.ts +10 -42
  66. package/src/lib/commands/command-interceptor.ts +0 -4
  67. package/src/lib/commands/types.ts +0 -14
  68. package/src/lib/crud/__tests__/exporters.test.ts +0 -18
  69. package/src/lib/crud/__tests__/ids.test.ts +1 -34
  70. package/src/lib/crud/exporters.ts +1 -1
  71. package/src/lib/crud/ids.ts +0 -24
  72. package/src/lib/db/__tests__/pg-errors.test.ts +1 -68
  73. package/src/lib/db/pg-errors.ts +0 -35
  74. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -23
  75. package/src/lib/localization/__tests__/translatable-fields.test.ts +0 -17
  76. package/src/lib/localization/index.ts +1 -8
  77. package/src/lib/localization/translatable-fields.ts +0 -46
  78. package/src/lib/number.ts +0 -13
  79. package/src/lib/openapi/generator.ts +77 -106
  80. package/src/lib/query/engine.ts +8 -58
  81. package/src/lib/query/join-utils.ts +1 -1
  82. package/src/lib/query/types.ts +1 -13
  83. package/src/lib/url.ts +0 -30
  84. package/src/lib/webhooks/__tests__/body.test.ts +0 -48
  85. package/src/lib/webhooks/body.ts +4 -11
  86. package/src/lib/webhooks/index.ts +0 -1
  87. package/src/modules/integrations/types.ts +0 -6
  88. package/src/modules/notifications/types.ts +0 -7
  89. package/src/modules/payment_gateways/__tests__/types.test.ts +0 -44
  90. package/src/modules/payment_gateways/types.ts +6 -52
  91. package/src/modules/registry.ts +0 -5
  92. package/src/modules/search.ts +0 -39
  93. package/src/modules/widgets/injection-loader.ts +3 -33
  94. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +0 -6
  95. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +0 -7
  96. package/dist/lib/bootstrap/next-subpath-resolve-hook.js +0 -62
  97. package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +0 -7
  98. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +0 -33
  99. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +0 -7
  100. package/dist/lib/catalog-visibility/hashEffectiveScope.js +0 -9
  101. package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +0 -7
  102. package/dist/lib/db/advisoryLock.js +0 -172
  103. package/dist/lib/db/advisoryLock.js.map +0 -7
  104. package/dist/lib/query/overlap.js +0 -58
  105. package/dist/lib/query/overlap.js.map +0 -7
  106. package/dist/modules/notifications/safeHref.js +0 -20
  107. package/dist/modules/notifications/safeHref.js.map +0 -7
  108. package/src/lib/__tests__/url.cross-site.test.ts +0 -140
  109. package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +0 -141
  110. package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +0 -174
  111. package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +0 -3
  112. package/src/lib/bootstrap/next-subpath-resolve-hook.ts +0 -130
  113. package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +0 -121
  114. package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +0 -41
  115. package/src/lib/catalog-visibility/hashEffectiveScope.ts +0 -8
  116. package/src/lib/db/__tests__/advisoryLock.integration.test.ts +0 -343
  117. package/src/lib/db/__tests__/advisoryLock.test.ts +0 -564
  118. package/src/lib/db/advisoryLock.ts +0 -271
  119. package/src/lib/openapi/__tests__/optional-path-params.test.ts +0 -102
  120. package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +0 -89
  121. package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +0 -79
  122. package/src/lib/query/__tests__/overlap.test.ts +0 -219
  123. package/src/lib/query/overlap.ts +0 -106
  124. package/src/modules/notifications/safeHref.ts +0 -19
  125. package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +0 -78
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/commands/types.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ZodTypeAny } from 'zod'\nimport { randomUUID } from 'crypto'\nimport type { AuthContext } from '../auth/server'\nimport type { OrganizationScope } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { TransactionLifetime } from './transaction-lifetime'\n\n/**\n * Bulk-import / backfill deferral flags. When a command runs under a context that\n * carries this, the command bus and data engine suppress the heavy per-record side\n * effects flagged below so a large backfill can defer them to a single batched pass.\n *\n * IMPORTANT \u2014 the caller owns restoring whatever it suppresses. With `skipReindex`\n * the `query_index` projection (and its search tokens) is stale for every record\n * written under this context until the caller runs a batched `query_index rebuild`\n * for the affected entity types at end-of-run.\n *\n * Concurrency: these flags are read from the context and threaded as a local\n * parameter through the side-effect flush \u2014 no shared engine state is mutated \u2014 so\n * two commands running concurrently with different flags never clobber each other.\n */\nexport type BulkImportSuppression = {\n /** Skip the inline `query_index.upsert_one` / `delete_one` reindex (rebuild after the run). */\n skipReindex?: boolean\n /** Skip the per-record `<module>.<entity>.<action>` domain event emission. */\n skipEvents?: boolean\n /** Advisory: handlers that fan out per-record notifications SHOULD honor this and skip them. */\n skipNotifications?: boolean\n /**\n * Skip a command's own full-collection derived-data rebuild (e.g. category tree\n * `parentId`/`treePath`/`depth`/ancestor-descendant rewrite). The caller MUST run that rebuild\n * itself exactly once after the batch, in a `finally` that covers the completed, cancelled and\n * failed paths \u2014 otherwise every record written under this context is left with a stale\n * derived shape.\n */\n skipDerivedRebuild?: boolean\n}\n\nexport type CommandRuntimeContext = {\n container: AwilixContainer\n auth: AuthContext | null\n organizationScope: OrganizationScope | null\n selectedOrganizationId: string | null\n organizationIds: string[] | null\n request?: Request\n syncOrigin?: string | null\n /**\n * See {@link BulkImportSuppression}. Set by bulk backfill callers to defer heavy\n * per-record side effects (reindex, events, notifications). The caller MUST rebuild\n * the `query_index` for the affected entity types after the run when `skipReindex`\n * is set. Unset for normal (interactive) writes \u2014 they get all side effects.\n */\n bulkImport?: BulkImportSuppression\n /**\n * Marks a trusted server-side invocation (CLI seeding, tenant setup) that runs\n * without an authenticated end-user actor. Commands that gate writes behind a\n * privileged actor (e.g. super-admin-only platform tables) may treat this as\n * an explicit system grant. HTTP request paths MUST NOT set this \u2014 they always\n * carry a real `auth` actor, so a present-but-unprivileged actor stays denied.\n */\n systemActor?: boolean\n /**\n * When set, command handlers that support it MUST run their writes within this\n * existing transactional EntityManager (reusing its row locks) instead of\n * opening their own transaction. Lets a caller compose a command with its own\n * surrounding work as a single atomic, single-locked operation.\n */\n transactionalEm?: EntityManager\n /**\n * Optional server-side guard that supported command handlers invoke after\n * opening their write transaction and before reading or mutating records.\n * The guard receives that same transaction-bound EntityManager and, when the\n * handler provides it, the final input after command interceptors. The input\n * stays optional so existing handlers and callers remain source-compatible.\n */\n beforeTransactionalWrite?: (em: EntityManager, input?: unknown) => Promise<void>\n /**\n * Identifies the owner of an already-active {@link transactionalEm} lifetime.\n * Atomic replay accepts an ambient EntityManager only when this is the exact\n * token returned by `getTransactionLifetime(transactionalEm)` from the outer\n * `withAtomicFlush` phase. The owner completes the token after its real commit\n * or rollback, allowing replay leases and post-commit work to follow that\n * boundary. Omit when the command bus opens and owns the transaction itself.\n */\n transactionLifetime?: TransactionLifetime\n /**\n * Optional request-level replay authorization that the command bus binds to\n * its replay EntityManager. Atomic handlers run this guard in the same\n * transaction as the source-log transition and domain mutation; legacy\n * handlers run it in a dedicated read transaction before preserving their\n * existing replay lifecycle.\n */\n replayTransactionGuard?: (args: {\n operation: CommandReplayOperation\n logEntry: CommandUndoLogEntry\n transactionalEm: EntityManager\n }) => Promise<void> | void\n /**\n * On-behalf-of attribution for non-human principals (Agent Identity &\n * On-Behalf-Of, Wave 4 P2). When an agent runs on behalf of a human, the\n * orchestrator's `runAs` wrapper sets this so every `ActionLog` the command\n * path writes records `actorUserId = runAs.actorUserId` (the agent principal's\n * `auth.User` id), `onBehalfOfUserId = runAs.onBehalfOfUserId` (the invoking\n * human, or null for system-invoked agents), and `sourceKey = runAs.source`\n * (`'agent'`). Additive + optional: callers that omit it keep the existing\n * `ctx.auth.sub`-derived attribution unchanged. This threads agent attribution\n * through the SAME audited Command/CRUD path as a human action \u2014 not a parallel\n * audit path.\n */\n runAs?: CommandRunAsContext\n}\n\nexport type CommandRunAsContext = {\n /** The actor stamped on every ActionLog this context produces (agent `auth.User` id). */\n actorUserId: string\n /** The human (or system) principal the actor acts on behalf of; null when system-invoked. */\n onBehalfOfUserId?: string | null\n /** The audit source key for the attributed writes; `'agent'` for agent runs. */\n source: 'agent'\n}\n\nexport type CommandLogMetadata = {\n skipLog?: boolean\n /**\n * Per-execution replay policy. `false` records the audit entry without an undo\n * token or command payload, so neither undo nor redo can replay sensitive input.\n * Omitted preserves the command handler's existing replay behavior.\n */\n replayable?: boolean\n /**\n * Overrides the command input persisted in the audit entry's redo envelope.\n * The default is the raw input, which for credential-bearing commands would put\n * a plaintext secret at rest in `action_logs`. A handler that still wants its\n * operation to be undoable supplies a redacted projection here instead of\n * switching the whole entry to `replayable: false` \u2014 suppressing the undo token\n * removes the operator's ability to revert a write that carries no secret of its\n * own (undoing a user create only deletes a row).\n */\n redoInput?: unknown\n tenantId?: string | null\n organizationId?: string | null\n actorUserId?: string | null\n onBehalfOfUserId?: string | null\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n parentResourceKind?: string | null\n parentResourceId?: string | null\n undoToken?: string | null\n payload?: unknown\n snapshotBefore?: unknown\n snapshotAfter?: unknown\n relatedResourceKind?: string | null\n relatedResourceId?: string | null\n changes?: Record<string, unknown> | null\n context?: Record<string, unknown> | null\n}\n\nexport type CommandExecuteResult<TResult> = {\n result: TResult\n logEntry: any | null\n /**\n * True when an atomic redo finalized its source action log in the same\n * transaction as the domain mutation and the newly persisted log entry, and\n * that transaction has committed. An ambient replay returns this as false\n * while its caller-owned transaction is still active; the returned result\n * object is updated to true by the owner's commit callback. It remains false\n * on rollback. Callers that historically finalized redo themselves can use\n * this additive signal to avoid a redundant post-commit write.\n */\n replaySourceFinalized?: boolean\n}\n\n/**\n * Shape of the persisted action log handed to a command's `undo()` handler.\n *\n * IMPORTANT: there is intentionally **no `payload` field**. `buildLog()` returns\n * a `payload` in its metadata, but the command bus persists that under\n * `commandPayload` (column `command_payload`, wrapped in a redo envelope) \u2014 the\n * stored row never has a top-level `payload`. Reading `logEntry.payload` in an\n * undo handler is therefore always `undefined` and silently no-ops the undo\n * (issue #2504). Always read the undo snapshot through\n * `extractUndoPayload(logEntry)` from `@open-mercato/shared/lib/commands/undo`,\n * which unwraps `commandPayload`/snapshots correctly. Omitting `payload` here\n * makes the footgun a compile-time error instead of a runtime silent failure.\n */\nexport type CommandUndoLogEntry = {\n id?: string\n commandId?: string\n commandPayload?: unknown | null\n snapshotBefore?: unknown | null\n snapshotAfter?: unknown | null\n resourceKind?: string | null\n resourceId?: string | null\n undoToken?: string | null\n actionLabel?: string | null\n tenantId?: string | null\n organizationId?: string | null\n actorUserId?: string | null\n changesJson?: Record<string, unknown> | null\n contextJson?: Record<string, unknown> | null\n createdAt?: Date | string\n updatedAt?: Date | string\n}\n\nexport type CommandLogBuilderArgs<TInput, TResult> = {\n input: TInput\n result: TResult\n ctx: CommandRuntimeContext\n snapshots: {\n before?: unknown\n after?: unknown\n }\n}\n\nexport type CommandReplayOperation = 'undo' | 'redo'\n\nexport type CommandReplayAuthorizationArgs<TInput> = {\n operation: CommandReplayOperation\n input: TInput\n ctx: CommandRuntimeContext\n logEntry: CommandUndoLogEntry\n}\n\nexport interface CommandHandler<TInput = unknown, TResult = unknown> {\n readonly id: string\n readonly isUndoable?: boolean\n /**\n * Opts replay into the command bus's transaction-bound lifecycle. The source\n * action-log transition, handler mutation, and any new replay log share one\n * EntityManager and commit or roll back together. Handlers that enable this\n * MUST reuse `ctx.transactionalEm` for every replay-time database operation.\n * Omitted preserves the legacy non-atomic replay path.\n */\n readonly atomicReplay?: boolean\n /**\n * Acquires every authorization-state lock needed by an atomic replay before\n * request-level replay authorization and feature-gated interceptors run.\n * Implementations MUST discover their complete actor/target/destination set\n * before taking the first lock and MUST reuse `ctx.transactionalEm`.\n * Omitted preserves the existing lifecycle for handlers that do not need\n * domain-specific authorization stabilization.\n */\n stabilizeReplay?(params: CommandReplayAuthorizationArgs<TInput>): Promise<void> | void\n /**\n * Optional Zod schema describing the command's return value. Feeds the\n * workflows context ledger so downstream activities can reason about the\n * shape a command produces; when absent the ledger renders the output as\n * unknown.\n */\n readonly outputSchema?: ZodTypeAny\n prepare?(input: TInput, ctx: CommandRuntimeContext): Promise<{ before?: unknown } | null> | { before?: unknown } | null\n /**\n * Opt in to taking the undo snapshot inside the write transaction. The bus\n * defers `prepare()` into `ctx.beforeTransactionalWrite`, after the handler's\n * transaction guard and before its first read or mutation.\n */\n readonly prepareSnapshotInsideTransaction?: boolean\n execute(input: TInput, ctx: CommandRuntimeContext): Promise<TResult> | TResult\n buildLog?(args: CommandLogBuilderArgs<TInput, TResult>): Promise<CommandLogMetadata | null | undefined> | CommandLogMetadata | null | undefined\n captureAfter?(input: TInput, result: TResult, ctx: CommandRuntimeContext): Promise<unknown> | unknown\n /**\n * Re-authorizes a stored command against the actor and resource state that\n * exist at replay time. Legacy handlers are checked before claiming an undo\n * log or beginning redo processing. Atomic handlers are checked inside their\n * replay transaction before the guarded source transition; their mutation\n * path can repeat the check after taking domain locks. Throwing aborts replay\n * without committed domain side effects.\n */\n authorizeReplay?(params: CommandReplayAuthorizationArgs<TInput>): Promise<void> | void\n undo?(params: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<void> | void\n /**\n * Optional redo handler. When defined, the command bus calls this instead of\n * `execute()` while replaying a previously undone action (the redo route passes\n * `redoLogEntry` in the execution options). It receives the source action log so\n * it can re-materialize the original record **reusing its id** \u2014 for a create\n * command this restores the soft-deleted row (or re-creates it from the\n * `snapshotAfter`) instead of minting a new id, keeping undo/redo snapshots and\n * references stable (issue #2506, invariant I6). Handlers without `redo` keep the\n * legacy behavior of replaying `execute(__redoInput)`.\n */\n redo?(params: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<TResult> | TResult\n}\n\nexport type CommandExecutionOptions<TInput> = {\n input: TInput\n ctx: CommandRuntimeContext\n metadata?: CommandLogMetadata | null\n skipCacheInvalidation?: boolean\n /**\n * When set, marks this execution as a redo of a previously undone action. If the\n * resolved handler defines a `redo` method, the command bus calls\n * `handler.redo({ input, ctx, logEntry })` instead of `handler.execute(...)`. The\n * rest of the pipeline (snapshots, buildLog, undo-token minting, persistence,\n * cache invalidation, side effects) is identical, so the fresh log entry \u2014 and\n * the `x-om-operation` header derived from it \u2014 automatically carry the restored\n * resourceId. Ignored when the handler has no `redo` method (legacy replay path).\n */\n redoLogEntry?: CommandUndoLogEntry | null\n}\n\nexport function defaultUndoToken(): string {\n return randomUUID()\n}\n"],
5
- "mappings": "AAGA,SAAS,kBAAkB;AA2SpB,SAAS,mBAA2B;AACzC,SAAO,WAAW;AACpB;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ZodTypeAny } from 'zod'\nimport { randomUUID } from 'crypto'\nimport type { AuthContext } from '../auth/server'\nimport type { OrganizationScope } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { TransactionLifetime } from './transaction-lifetime'\n\n/**\n * Bulk-import / backfill deferral flags. When a command runs under a context that\n * carries this, the command bus and data engine suppress the heavy per-record side\n * effects flagged below so a large backfill can defer them to a single batched pass.\n *\n * IMPORTANT \u2014 the caller owns restoring whatever it suppresses. With `skipReindex`\n * the `query_index` projection (and its search tokens) is stale for every record\n * written under this context until the caller runs a batched `query_index rebuild`\n * for the affected entity types at end-of-run.\n *\n * Concurrency: these flags are read from the context and threaded as a local\n * parameter through the side-effect flush \u2014 no shared engine state is mutated \u2014 so\n * two commands running concurrently with different flags never clobber each other.\n */\nexport type BulkImportSuppression = {\n /** Skip the inline `query_index.upsert_one` / `delete_one` reindex (rebuild after the run). */\n skipReindex?: boolean\n /** Skip the per-record `<module>.<entity>.<action>` domain event emission. */\n skipEvents?: boolean\n /** Advisory: handlers that fan out per-record notifications SHOULD honor this and skip them. */\n skipNotifications?: boolean\n /**\n * Skip a command's own full-collection derived-data rebuild (e.g. category tree\n * `parentId`/`treePath`/`depth`/ancestor-descendant rewrite). The caller MUST run that rebuild\n * itself exactly once after the batch, in a `finally` that covers the completed, cancelled and\n * failed paths \u2014 otherwise every record written under this context is left with a stale\n * derived shape.\n */\n skipDerivedRebuild?: boolean\n}\n\nexport type CommandRuntimeContext = {\n container: AwilixContainer\n auth: AuthContext | null\n organizationScope: OrganizationScope | null\n selectedOrganizationId: string | null\n organizationIds: string[] | null\n request?: Request\n syncOrigin?: string | null\n /**\n * See {@link BulkImportSuppression}. Set by bulk backfill callers to defer heavy\n * per-record side effects (reindex, events, notifications). The caller MUST rebuild\n * the `query_index` for the affected entity types after the run when `skipReindex`\n * is set. Unset for normal (interactive) writes \u2014 they get all side effects.\n */\n bulkImport?: BulkImportSuppression\n /**\n * Marks a trusted server-side invocation (CLI seeding, tenant setup) that runs\n * without an authenticated end-user actor. Commands that gate writes behind a\n * privileged actor (e.g. super-admin-only platform tables) may treat this as\n * an explicit system grant. HTTP request paths MUST NOT set this \u2014 they always\n * carry a real `auth` actor, so a present-but-unprivileged actor stays denied.\n */\n systemActor?: boolean\n /**\n * When set, command handlers that support it MUST run their writes within this\n * existing transactional EntityManager (reusing its row locks) instead of\n * opening their own transaction. Lets a caller compose a command with its own\n * surrounding work as a single atomic, single-locked operation.\n */\n transactionalEm?: EntityManager\n /**\n * Identifies the owner of an already-active {@link transactionalEm} lifetime.\n * Atomic replay accepts an ambient EntityManager only when this is the exact\n * token returned by `getTransactionLifetime(transactionalEm)` from the outer\n * `withAtomicFlush` phase. The owner completes the token after its real commit\n * or rollback, allowing replay leases and post-commit work to follow that\n * boundary. Omit when the command bus opens and owns the transaction itself.\n */\n transactionLifetime?: TransactionLifetime\n /**\n * Optional request-level replay authorization that the command bus binds to\n * its replay EntityManager. Atomic handlers run this guard in the same\n * transaction as the source-log transition and domain mutation; legacy\n * handlers run it in a dedicated read transaction before preserving their\n * existing replay lifecycle.\n */\n replayTransactionGuard?: (args: {\n operation: CommandReplayOperation\n logEntry: CommandUndoLogEntry\n transactionalEm: EntityManager\n }) => Promise<void> | void\n /**\n * On-behalf-of attribution for non-human principals (Agent Identity &\n * On-Behalf-Of, Wave 4 P2). When an agent runs on behalf of a human, the\n * orchestrator's `runAs` wrapper sets this so every `ActionLog` the command\n * path writes records `actorUserId = runAs.actorUserId` (the agent principal's\n * `auth.User` id), `onBehalfOfUserId = runAs.onBehalfOfUserId` (the invoking\n * human, or null for system-invoked agents), and `sourceKey = runAs.source`\n * (`'agent'`). Additive + optional: callers that omit it keep the existing\n * `ctx.auth.sub`-derived attribution unchanged. This threads agent attribution\n * through the SAME audited Command/CRUD path as a human action \u2014 not a parallel\n * audit path.\n */\n runAs?: CommandRunAsContext\n}\n\nexport type CommandRunAsContext = {\n /** The actor stamped on every ActionLog this context produces (agent `auth.User` id). */\n actorUserId: string\n /** The human (or system) principal the actor acts on behalf of; null when system-invoked. */\n onBehalfOfUserId?: string | null\n /** The audit source key for the attributed writes; `'agent'` for agent runs. */\n source: 'agent'\n}\n\nexport type CommandLogMetadata = {\n skipLog?: boolean\n /**\n * Per-execution replay policy. `false` records the audit entry without an undo\n * token or command payload, so neither undo nor redo can replay sensitive input.\n * Omitted preserves the command handler's existing replay behavior.\n */\n replayable?: boolean\n /**\n * Overrides the command input persisted in the audit entry's redo envelope.\n * The default is the raw input, which for credential-bearing commands would put\n * a plaintext secret at rest in `action_logs`. A handler that still wants its\n * operation to be undoable supplies a redacted projection here instead of\n * switching the whole entry to `replayable: false` \u2014 suppressing the undo token\n * removes the operator's ability to revert a write that carries no secret of its\n * own (undoing a user create only deletes a row).\n */\n redoInput?: unknown\n tenantId?: string | null\n organizationId?: string | null\n actorUserId?: string | null\n onBehalfOfUserId?: string | null\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n parentResourceKind?: string | null\n parentResourceId?: string | null\n undoToken?: string | null\n payload?: unknown\n snapshotBefore?: unknown\n snapshotAfter?: unknown\n relatedResourceKind?: string | null\n relatedResourceId?: string | null\n changes?: Record<string, unknown> | null\n context?: Record<string, unknown> | null\n}\n\nexport type CommandExecuteResult<TResult> = {\n result: TResult\n logEntry: any | null\n /**\n * True when an atomic redo finalized its source action log in the same\n * transaction as the domain mutation and the newly persisted log entry, and\n * that transaction has committed. An ambient replay returns this as false\n * while its caller-owned transaction is still active; the returned result\n * object is updated to true by the owner's commit callback. It remains false\n * on rollback. Callers that historically finalized redo themselves can use\n * this additive signal to avoid a redundant post-commit write.\n */\n replaySourceFinalized?: boolean\n}\n\n/**\n * Shape of the persisted action log handed to a command's `undo()` handler.\n *\n * IMPORTANT: there is intentionally **no `payload` field**. `buildLog()` returns\n * a `payload` in its metadata, but the command bus persists that under\n * `commandPayload` (column `command_payload`, wrapped in a redo envelope) \u2014 the\n * stored row never has a top-level `payload`. Reading `logEntry.payload` in an\n * undo handler is therefore always `undefined` and silently no-ops the undo\n * (issue #2504). Always read the undo snapshot through\n * `extractUndoPayload(logEntry)` from `@open-mercato/shared/lib/commands/undo`,\n * which unwraps `commandPayload`/snapshots correctly. Omitting `payload` here\n * makes the footgun a compile-time error instead of a runtime silent failure.\n */\nexport type CommandUndoLogEntry = {\n id?: string\n commandId?: string\n commandPayload?: unknown | null\n snapshotBefore?: unknown | null\n snapshotAfter?: unknown | null\n resourceKind?: string | null\n resourceId?: string | null\n undoToken?: string | null\n actionLabel?: string | null\n tenantId?: string | null\n organizationId?: string | null\n actorUserId?: string | null\n changesJson?: Record<string, unknown> | null\n contextJson?: Record<string, unknown> | null\n createdAt?: Date | string\n updatedAt?: Date | string\n}\n\nexport type CommandLogBuilderArgs<TInput, TResult> = {\n input: TInput\n result: TResult\n ctx: CommandRuntimeContext\n snapshots: {\n before?: unknown\n after?: unknown\n }\n}\n\nexport type CommandReplayOperation = 'undo' | 'redo'\n\nexport type CommandReplayAuthorizationArgs<TInput> = {\n operation: CommandReplayOperation\n input: TInput\n ctx: CommandRuntimeContext\n logEntry: CommandUndoLogEntry\n}\n\nexport interface CommandHandler<TInput = unknown, TResult = unknown> {\n readonly id: string\n readonly isUndoable?: boolean\n /**\n * Opts replay into the command bus's transaction-bound lifecycle. The source\n * action-log transition, handler mutation, and any new replay log share one\n * EntityManager and commit or roll back together. Handlers that enable this\n * MUST reuse `ctx.transactionalEm` for every replay-time database operation.\n * Omitted preserves the legacy non-atomic replay path.\n */\n readonly atomicReplay?: boolean\n /**\n * Acquires every authorization-state lock needed by an atomic replay before\n * request-level replay authorization and feature-gated interceptors run.\n * Implementations MUST discover their complete actor/target/destination set\n * before taking the first lock and MUST reuse `ctx.transactionalEm`.\n * Omitted preserves the existing lifecycle for handlers that do not need\n * domain-specific authorization stabilization.\n */\n stabilizeReplay?(params: CommandReplayAuthorizationArgs<TInput>): Promise<void> | void\n /**\n * Optional Zod schema describing the command's return value. Feeds the\n * workflows context ledger so downstream activities can reason about the\n * shape a command produces; when absent the ledger renders the output as\n * unknown.\n */\n readonly outputSchema?: ZodTypeAny\n prepare?(input: TInput, ctx: CommandRuntimeContext): Promise<{ before?: unknown } | null> | { before?: unknown } | null\n execute(input: TInput, ctx: CommandRuntimeContext): Promise<TResult> | TResult\n buildLog?(args: CommandLogBuilderArgs<TInput, TResult>): Promise<CommandLogMetadata | null | undefined> | CommandLogMetadata | null | undefined\n captureAfter?(input: TInput, result: TResult, ctx: CommandRuntimeContext): Promise<unknown> | unknown\n /**\n * Re-authorizes a stored command against the actor and resource state that\n * exist at replay time. Legacy handlers are checked before claiming an undo\n * log or beginning redo processing. Atomic handlers are checked inside their\n * replay transaction before the guarded source transition; their mutation\n * path can repeat the check after taking domain locks. Throwing aborts replay\n * without committed domain side effects.\n */\n authorizeReplay?(params: CommandReplayAuthorizationArgs<TInput>): Promise<void> | void\n undo?(params: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<void> | void\n /**\n * Optional redo handler. When defined, the command bus calls this instead of\n * `execute()` while replaying a previously undone action (the redo route passes\n * `redoLogEntry` in the execution options). It receives the source action log so\n * it can re-materialize the original record **reusing its id** \u2014 for a create\n * command this restores the soft-deleted row (or re-creates it from the\n * `snapshotAfter`) instead of minting a new id, keeping undo/redo snapshots and\n * references stable (issue #2506, invariant I6). Handlers without `redo` keep the\n * legacy behavior of replaying `execute(__redoInput)`.\n */\n redo?(params: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<TResult> | TResult\n}\n\nexport type CommandExecutionOptions<TInput> = {\n input: TInput\n ctx: CommandRuntimeContext\n metadata?: CommandLogMetadata | null\n skipCacheInvalidation?: boolean\n /**\n * When set, marks this execution as a redo of a previously undone action. If the\n * resolved handler defines a `redo` method, the command bus calls\n * `handler.redo({ input, ctx, logEntry })` instead of `handler.execute(...)`. The\n * rest of the pipeline (snapshots, buildLog, undo-token minting, persistence,\n * cache invalidation, side effects) is identical, so the fresh log entry \u2014 and\n * the `x-om-operation` header derived from it \u2014 automatically carry the restored\n * resourceId. Ignored when the handler has no `redo` method (legacy replay path).\n */\n redoLogEntry?: CommandUndoLogEntry | null\n}\n\nexport function defaultUndoToken(): string {\n return randomUUID()\n}\n"],
5
+ "mappings": "AAGA,SAAS,kBAAkB;AA6RpB,SAAS,mBAA2B;AACzC,SAAO,WAAW;AACpB;",
6
6
  "names": []
7
7
  }
@@ -47,7 +47,7 @@ function escapeXmlValue(value) {
47
47
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
48
48
  }
49
49
  function serializeCsv(prepared) {
50
- const headers = prepared.columns.map((col) => escapeCsv(neutralizeSpreadsheetFormula(col.header)));
50
+ const headers = prepared.columns.map((col) => col.header);
51
51
  const lines = [headers.join(",")];
52
52
  for (const row of prepared.rows) {
53
53
  const values = prepared.columns.map((col) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/exporters.ts"],
4
- "sourcesContent": ["export type CrudExportFormat = 'csv' | 'json' | 'xml' | 'markdown'\n\nexport type CrudExportColumn = {\n field: string\n header: string\n}\n\nexport type PreparedExport = {\n columns: CrudExportColumn[]\n rows: Array<Record<string, unknown>>\n}\n\nexport type SerializedExport = {\n body: string\n contentType: string\n fileExtension: string\n}\n\nconst CSV_CONTENT_TYPE = 'text/csv; charset=utf-8'\nconst JSON_CONTENT_TYPE = 'application/json; charset=utf-8'\nconst XML_CONTENT_TYPE = 'application/xml; charset=utf-8'\nconst MARKDOWN_CONTENT_TYPE = 'text/markdown; charset=utf-8'\n\nfunction normalizeValue(value: unknown): string {\n if (value == null) return ''\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'bigint') return value.toString()\n if (typeof value === 'object') {\n if (Array.isArray(value)) return value.map((v) => normalizeValue(v)).filter(Boolean).join(', ')\n return JSON.stringify(value)\n }\n return String(value)\n}\n\nfunction escapeCsv(value: string): string {\n if (/[\",\\n\\r]/.test(value)) {\n return `\"${value.replace(/\"/g, '\"\"')}\"`\n }\n return value\n}\n\nfunction neutralizeSpreadsheetFormula(value: string): string {\n if (/^[=+\\-@\\t\\r\\n]/.test(value)) {\n return `'${value}`\n }\n return value\n}\n\nfunction normalizeCsvValue(value: unknown): string {\n const normalized = normalizeValue(value)\n if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') {\n return normalized\n }\n return neutralizeSpreadsheetFormula(normalized)\n}\n\nfunction escapeMarkdown(value: string): string {\n const escaped = value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\\|/g, '\\\\|')\n .replace(/\\r?\\n/g, '<br />')\n return escaped || ' '\n}\n\nfunction escapeXmlTag(tag: string, fallbackIndex: number): string {\n const sanitized = tag.replace(/[^A-Za-z0-9_:-]/g, '_')\n const normalized = sanitized.length > 0 ? sanitized : `field_${fallbackIndex}`\n if (/^[^A-Za-z_]/.test(normalized)) {\n return `f_${normalized}`\n }\n return normalized\n}\n\nfunction escapeXmlValue(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;')\n}\n\nfunction serializeCsv(prepared: PreparedExport): SerializedExport {\n const headers = prepared.columns.map((col) => escapeCsv(neutralizeSpreadsheetFormula(col.header)))\n const lines = [headers.join(',')]\n for (const row of prepared.rows) {\n const values = prepared.columns.map((col) => {\n const raw = normalizeCsvValue(row[col.field])\n return escapeCsv(raw)\n })\n lines.push(values.join(','))\n }\n return {\n body: lines.join('\\n'),\n contentType: CSV_CONTENT_TYPE,\n fileExtension: 'csv',\n }\n}\n\nfunction serializeJson(prepared: PreparedExport): SerializedExport {\n // For JSON we return objects keyed by exported field names with human headers as metadata\n const payload = prepared.rows.map((row) => {\n const obj: Record<string, unknown> = {}\n for (const column of prepared.columns) {\n obj[column.header] = row[column.field]\n }\n return obj\n })\n return {\n body: JSON.stringify(payload, null, 2),\n contentType: JSON_CONTENT_TYPE,\n fileExtension: 'json',\n }\n}\n\nfunction serializeXml(prepared: PreparedExport): SerializedExport {\n const lines: string[] = ['<?xml version=\"1.0\" encoding=\"UTF-8\"?>', '<records>']\n for (const row of prepared.rows) {\n lines.push(' <record>')\n prepared.columns.forEach((column, index) => {\n const tag = escapeXmlTag(column.field || column.header, index)\n const value = escapeXmlValue(normalizeValue(row[column.field]))\n lines.push(` <${tag}>${value}</${tag}>`)\n })\n lines.push(' </record>')\n }\n lines.push('</records>')\n return {\n body: lines.join('\\n'),\n contentType: XML_CONTENT_TYPE,\n fileExtension: 'xml',\n }\n}\n\nfunction serializeMarkdown(prepared: PreparedExport): SerializedExport {\n const headers = prepared.columns.map((col) => escapeMarkdown(col.header))\n const headerLine = `| ${headers.join(' | ')} |`\n const dividerLine = `| ${prepared.columns.map(() => '---').join(' | ')} |`\n const rows = prepared.rows.map((row) => {\n const cells = prepared.columns.map((col) => escapeMarkdown(normalizeValue(row[col.field])))\n return `| ${cells.join(' | ')} |`\n })\n const body = [headerLine, dividerLine, ...rows].join('\\n')\n return {\n body,\n contentType: MARKDOWN_CONTENT_TYPE,\n fileExtension: 'md',\n }\n}\n\nexport function serializeExport(prepared: PreparedExport, format: CrudExportFormat): SerializedExport {\n switch (format) {\n case 'csv':\n return serializeCsv(prepared)\n case 'json':\n return serializeJson(prepared)\n case 'xml':\n return serializeXml(prepared)\n case 'markdown':\n return serializeMarkdown(prepared)\n default:\n return serializeJson(prepared)\n }\n}\n\nexport function normalizeExportFormat(raw: unknown): CrudExportFormat | null {\n if (typeof raw !== 'string' || raw.trim() === '') return null\n const value = raw.toLowerCase()\n if (value === 'csv') return 'csv'\n if (value === 'json' || value === 'application/json') return 'json'\n if (value === 'xml' || value === 'application/xml') return 'xml'\n if (value === 'markdown' || value === 'md' || value === 'text/markdown') return 'markdown'\n return null\n}\n\nexport function defaultExportFilename(base: string | undefined | null, format: CrudExportFormat): string {\n const safeBase = (base && base.trim().length > 0 ? base.trim() : 'export')\n .replace(/[^a-z0-9_\\-]/gi, '_')\n const suffix = format === 'markdown' ? 'md' : format\n return `${safeBase}.${suffix}`\n}\n\nexport function toHeaderLabel(key: string): string {\n const withoutPrefix = key.replace(/^cf[:_\\-\\s]+/i, '')\n const normalized = withoutPrefix.replace(/[_\\-\\s]+/g, ' ').trim()\n if (!normalized) return 'Field'\n return normalized.replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\nexport function ensureColumns(rows: Array<Record<string, unknown>>, hint?: CrudExportColumn[]): CrudExportColumn[] {\n if (hint && hint.length > 0) return hint\n const used = new Map<string, string>()\n rows.forEach((row) => {\n Object.keys(row || {}).forEach((key) => {\n if (!used.has(key)) used.set(key, key)\n })\n })\n if (used.size === 0) return [{ field: 'id', header: 'ID' }]\n return Array.from(used.keys()).map((key) => ({\n field: key,\n header: toHeaderLabel(key),\n }))\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["export type CrudExportFormat = 'csv' | 'json' | 'xml' | 'markdown'\n\nexport type CrudExportColumn = {\n field: string\n header: string\n}\n\nexport type PreparedExport = {\n columns: CrudExportColumn[]\n rows: Array<Record<string, unknown>>\n}\n\nexport type SerializedExport = {\n body: string\n contentType: string\n fileExtension: string\n}\n\nconst CSV_CONTENT_TYPE = 'text/csv; charset=utf-8'\nconst JSON_CONTENT_TYPE = 'application/json; charset=utf-8'\nconst XML_CONTENT_TYPE = 'application/xml; charset=utf-8'\nconst MARKDOWN_CONTENT_TYPE = 'text/markdown; charset=utf-8'\n\nfunction normalizeValue(value: unknown): string {\n if (value == null) return ''\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'bigint') return value.toString()\n if (typeof value === 'object') {\n if (Array.isArray(value)) return value.map((v) => normalizeValue(v)).filter(Boolean).join(', ')\n return JSON.stringify(value)\n }\n return String(value)\n}\n\nfunction escapeCsv(value: string): string {\n if (/[\",\\n\\r]/.test(value)) {\n return `\"${value.replace(/\"/g, '\"\"')}\"`\n }\n return value\n}\n\nfunction neutralizeSpreadsheetFormula(value: string): string {\n if (/^[=+\\-@\\t\\r\\n]/.test(value)) {\n return `'${value}`\n }\n return value\n}\n\nfunction normalizeCsvValue(value: unknown): string {\n const normalized = normalizeValue(value)\n if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') {\n return normalized\n }\n return neutralizeSpreadsheetFormula(normalized)\n}\n\nfunction escapeMarkdown(value: string): string {\n const escaped = value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\\|/g, '\\\\|')\n .replace(/\\r?\\n/g, '<br />')\n return escaped || ' '\n}\n\nfunction escapeXmlTag(tag: string, fallbackIndex: number): string {\n const sanitized = tag.replace(/[^A-Za-z0-9_:-]/g, '_')\n const normalized = sanitized.length > 0 ? sanitized : `field_${fallbackIndex}`\n if (/^[^A-Za-z_]/.test(normalized)) {\n return `f_${normalized}`\n }\n return normalized\n}\n\nfunction escapeXmlValue(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;')\n}\n\nfunction serializeCsv(prepared: PreparedExport): SerializedExport {\n const headers = prepared.columns.map((col) => col.header)\n const lines = [headers.join(',')]\n for (const row of prepared.rows) {\n const values = prepared.columns.map((col) => {\n const raw = normalizeCsvValue(row[col.field])\n return escapeCsv(raw)\n })\n lines.push(values.join(','))\n }\n return {\n body: lines.join('\\n'),\n contentType: CSV_CONTENT_TYPE,\n fileExtension: 'csv',\n }\n}\n\nfunction serializeJson(prepared: PreparedExport): SerializedExport {\n // For JSON we return objects keyed by exported field names with human headers as metadata\n const payload = prepared.rows.map((row) => {\n const obj: Record<string, unknown> = {}\n for (const column of prepared.columns) {\n obj[column.header] = row[column.field]\n }\n return obj\n })\n return {\n body: JSON.stringify(payload, null, 2),\n contentType: JSON_CONTENT_TYPE,\n fileExtension: 'json',\n }\n}\n\nfunction serializeXml(prepared: PreparedExport): SerializedExport {\n const lines: string[] = ['<?xml version=\"1.0\" encoding=\"UTF-8\"?>', '<records>']\n for (const row of prepared.rows) {\n lines.push(' <record>')\n prepared.columns.forEach((column, index) => {\n const tag = escapeXmlTag(column.field || column.header, index)\n const value = escapeXmlValue(normalizeValue(row[column.field]))\n lines.push(` <${tag}>${value}</${tag}>`)\n })\n lines.push(' </record>')\n }\n lines.push('</records>')\n return {\n body: lines.join('\\n'),\n contentType: XML_CONTENT_TYPE,\n fileExtension: 'xml',\n }\n}\n\nfunction serializeMarkdown(prepared: PreparedExport): SerializedExport {\n const headers = prepared.columns.map((col) => escapeMarkdown(col.header))\n const headerLine = `| ${headers.join(' | ')} |`\n const dividerLine = `| ${prepared.columns.map(() => '---').join(' | ')} |`\n const rows = prepared.rows.map((row) => {\n const cells = prepared.columns.map((col) => escapeMarkdown(normalizeValue(row[col.field])))\n return `| ${cells.join(' | ')} |`\n })\n const body = [headerLine, dividerLine, ...rows].join('\\n')\n return {\n body,\n contentType: MARKDOWN_CONTENT_TYPE,\n fileExtension: 'md',\n }\n}\n\nexport function serializeExport(prepared: PreparedExport, format: CrudExportFormat): SerializedExport {\n switch (format) {\n case 'csv':\n return serializeCsv(prepared)\n case 'json':\n return serializeJson(prepared)\n case 'xml':\n return serializeXml(prepared)\n case 'markdown':\n return serializeMarkdown(prepared)\n default:\n return serializeJson(prepared)\n }\n}\n\nexport function normalizeExportFormat(raw: unknown): CrudExportFormat | null {\n if (typeof raw !== 'string' || raw.trim() === '') return null\n const value = raw.toLowerCase()\n if (value === 'csv') return 'csv'\n if (value === 'json' || value === 'application/json') return 'json'\n if (value === 'xml' || value === 'application/xml') return 'xml'\n if (value === 'markdown' || value === 'md' || value === 'text/markdown') return 'markdown'\n return null\n}\n\nexport function defaultExportFilename(base: string | undefined | null, format: CrudExportFormat): string {\n const safeBase = (base && base.trim().length > 0 ? base.trim() : 'export')\n .replace(/[^a-z0-9_\\-]/gi, '_')\n const suffix = format === 'markdown' ? 'md' : format\n return `${safeBase}.${suffix}`\n}\n\nexport function toHeaderLabel(key: string): string {\n const withoutPrefix = key.replace(/^cf[:_\\-\\s]+/i, '')\n const normalized = withoutPrefix.replace(/[_\\-\\s]+/g, ' ').trim()\n if (!normalized) return 'Field'\n return normalized.replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\nexport function ensureColumns(rows: Array<Record<string, unknown>>, hint?: CrudExportColumn[]): CrudExportColumn[] {\n if (hint && hint.length > 0) return hint\n const used = new Map<string, string>()\n rows.forEach((row) => {\n Object.keys(row || {}).forEach((key) => {\n if (!used.has(key)) used.set(key, key)\n })\n })\n if (used.size === 0) return [{ field: 'id', header: 'ID' }]\n return Array.from(used.keys()).map((key) => ({\n field: key,\n header: toHeaderLabel(key),\n }))\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -40,16 +40,6 @@ function parseIdsParam(raw, maxIds = MAX_IDS_PER_REQUEST) {
40
40
  const parsed = normalizeIdList(values);
41
41
  return parsed.slice(0, safeMax);
42
42
  }
43
- const UUID_SHAPE_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
44
- function splitIdListParam(raw) {
45
- if (typeof raw !== "string") return [];
46
- return raw.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
47
- }
48
- function isUuidListParam(raw) {
49
- if (raw === void 0) return true;
50
- if (typeof raw !== "string") return false;
51
- return splitIdListParam(raw).every((value) => UUID_SHAPE_REGEX.test(value));
52
- }
53
43
  function isIdsParamProvided(raw) {
54
44
  const occurrences = Array.isArray(raw) ? raw : [raw];
55
45
  return occurrences.some((value) => typeof value === "string" && value.trim().length > 0);
@@ -79,9 +69,7 @@ function mergeIdFilter(existingFilters, parsedIds, options) {
79
69
  export {
80
70
  MAX_IDS_PER_REQUEST,
81
71
  isIdsParamProvided,
82
- isUuidListParam,
83
72
  mergeIdFilter,
84
- parseIdsParam,
85
- splitIdListParam
73
+ parseIdsParam
86
74
  };
87
75
  //# sourceMappingURL=ids.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/ids.ts"],
4
- "sourcesContent": ["import type { Where } from '@open-mercato/shared/lib/query/types'\nimport { toQueryValueList } from './query-params'\n\nexport const MAX_IDS_PER_REQUEST = 200\n\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n\nfunction isUuid(value: unknown): value is string {\n return typeof value === 'string' && UUID_REGEX.test(value)\n}\n\nfunction normalizeIdList(values: string[]): string[] {\n if (values.length === 0) return values\n const deduped = new Set<string>()\n for (const value of values) {\n const trimmed = value.trim()\n if (!trimmed || !isUuid(trimmed)) continue\n deduped.add(trimmed)\n }\n return Array.from(deduped)\n}\n\nfunction readExistingIds(filter: unknown): string[] | null {\n if (typeof filter === 'string') {\n return isUuid(filter) ? [filter] : null\n }\n if (Array.isArray(filter)) {\n return normalizeIdList(\n filter.filter((value): value is string => typeof value === 'string'),\n )\n }\n if (!filter || typeof filter !== 'object') return null\n\n const operators = filter as Record<string, unknown>\n if (isUuid(operators.$eq)) return [operators.$eq]\n\n if (Array.isArray(operators.$in)) {\n return normalizeIdList(\n operators.$in.filter((value): value is string => typeof value === 'string'),\n )\n }\n\n return null\n}\n\nexport function parseIdsParam(raw: unknown, maxIds: number = MAX_IDS_PER_REQUEST): string[] {\n const values = toQueryValueList(raw)\n if (values.length === 0) return []\n const safeMax = Number.isFinite(maxIds) && maxIds > 0 ? Math.floor(maxIds) : MAX_IDS_PER_REQUEST\n const parsed = normalizeIdList(values)\n return parsed.slice(0, safeMax)\n}\n\nconst UUID_SHAPE_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/**\n * Splits a comma-separated id list query param into its trimmed, non-empty entries.\n */\nexport function splitIdListParam(raw: unknown): string[] {\n if (typeof raw !== 'string') return []\n return raw\n .split(',')\n .map((value) => value.trim())\n .filter((value) => value.length > 0)\n}\n\n/**\n * Whether every entry of a comma-separated id list query param is UUID-shaped, so it can be\n * bound to a uuid column without a database error. Use it to reject a malformed or mixed list\n * with a 400 instead of dropping the bad entries and widening the result set (#7265).\n */\nexport function isUuidListParam(raw: unknown): boolean {\n if (raw === undefined) return true\n if (typeof raw !== 'string') return false\n return splitIdListParam(raw).every((value) => UUID_SHAPE_REGEX.test(value))\n}\n\n/**\n * Whether an `?ids=` param was supplied at all (a non-empty string, or repeated\n * `?ids=` occurrences), regardless of whether any value survived UUID\n * validation. Lets a caller tell \"no ids filter requested\" apart from \"ids\n * filter requested but every value was malformed\" \u2014 the latter must match\n * nothing, not fall back to the full list (#4143 Finding 3).\n */\nexport function isIdsParamProvided(raw: unknown): boolean {\n const occurrences = Array.isArray(raw) ? raw : [raw]\n return occurrences.some((value) => typeof value === 'string' && value.trim().length > 0)\n}\n\nexport function mergeIdFilter<Fields extends Record<string, unknown>>(\n existingFilters: Where<Fields>,\n parsedIds: string[],\n options?: { idsParamProvided?: boolean },\n): Where<Fields> {\n if (parsedIds.length === 0) {\n // The `?ids=` param was supplied but nothing survived UUID validation.\n // Match nothing, mirroring a valid-but-unknown id returning zero rows,\n // rather than silently dropping the filter and returning the full list\n // (record-count side channel \u2014 #4143 Finding 3).\n if (options?.idsParamProvided) {\n return { ...existingFilters, id: { $in: [] } }\n }\n return existingFilters\n }\n\n const existingFilter = (existingFilters as Record<string, unknown>).id\n const existingIds = readExistingIds(existingFilter)\n\n if (!existingIds) {\n // No existing narrowing \u2014 safe to install the user-supplied `$in`.\n if (existingFilter === undefined || existingFilter === null) {\n return { ...existingFilters, id: { $in: parsedIds } }\n }\n // Existing `id` filter is in a shape we do not recognise. Fail closed:\n // preserve the existing filter instead of widening to `parsedIds`. Adding\n // a new recognised shape to `readExistingIds` is preferable to silently\n // dropping a narrowing that another caller put in place.\n return existingFilters\n }\n\n const allowed = new Set(parsedIds)\n const intersection = existingIds.filter((id) => allowed.has(id))\n return {\n ...existingFilters,\n id: { $in: intersection },\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Where } from '@open-mercato/shared/lib/query/types'\nimport { toQueryValueList } from './query-params'\n\nexport const MAX_IDS_PER_REQUEST = 200\n\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n\nfunction isUuid(value: unknown): value is string {\n return typeof value === 'string' && UUID_REGEX.test(value)\n}\n\nfunction normalizeIdList(values: string[]): string[] {\n if (values.length === 0) return values\n const deduped = new Set<string>()\n for (const value of values) {\n const trimmed = value.trim()\n if (!trimmed || !isUuid(trimmed)) continue\n deduped.add(trimmed)\n }\n return Array.from(deduped)\n}\n\nfunction readExistingIds(filter: unknown): string[] | null {\n if (typeof filter === 'string') {\n return isUuid(filter) ? [filter] : null\n }\n if (Array.isArray(filter)) {\n return normalizeIdList(\n filter.filter((value): value is string => typeof value === 'string'),\n )\n }\n if (!filter || typeof filter !== 'object') return null\n\n const operators = filter as Record<string, unknown>\n if (isUuid(operators.$eq)) return [operators.$eq]\n\n if (Array.isArray(operators.$in)) {\n return normalizeIdList(\n operators.$in.filter((value): value is string => typeof value === 'string'),\n )\n }\n\n return null\n}\n\nexport function parseIdsParam(raw: unknown, maxIds: number = MAX_IDS_PER_REQUEST): string[] {\n const values = toQueryValueList(raw)\n if (values.length === 0) return []\n const safeMax = Number.isFinite(maxIds) && maxIds > 0 ? Math.floor(maxIds) : MAX_IDS_PER_REQUEST\n const parsed = normalizeIdList(values)\n return parsed.slice(0, safeMax)\n}\n\n/**\n * Whether an `?ids=` param was supplied at all (a non-empty string, or repeated\n * `?ids=` occurrences), regardless of whether any value survived UUID\n * validation. Lets a caller tell \"no ids filter requested\" apart from \"ids\n * filter requested but every value was malformed\" \u2014 the latter must match\n * nothing, not fall back to the full list (#4143 Finding 3).\n */\nexport function isIdsParamProvided(raw: unknown): boolean {\n const occurrences = Array.isArray(raw) ? raw : [raw]\n return occurrences.some((value) => typeof value === 'string' && value.trim().length > 0)\n}\n\nexport function mergeIdFilter<Fields extends Record<string, unknown>>(\n existingFilters: Where<Fields>,\n parsedIds: string[],\n options?: { idsParamProvided?: boolean },\n): Where<Fields> {\n if (parsedIds.length === 0) {\n // The `?ids=` param was supplied but nothing survived UUID validation.\n // Match nothing, mirroring a valid-but-unknown id returning zero rows,\n // rather than silently dropping the filter and returning the full list\n // (record-count side channel \u2014 #4143 Finding 3).\n if (options?.idsParamProvided) {\n return { ...existingFilters, id: { $in: [] } }\n }\n return existingFilters\n }\n\n const existingFilter = (existingFilters as Record<string, unknown>).id\n const existingIds = readExistingIds(existingFilter)\n\n if (!existingIds) {\n // No existing narrowing \u2014 safe to install the user-supplied `$in`.\n if (existingFilter === undefined || existingFilter === null) {\n return { ...existingFilters, id: { $in: parsedIds } }\n }\n // Existing `id` filter is in a shape we do not recognise. Fail closed:\n // preserve the existing filter instead of widening to `parsedIds`. Adding\n // a new recognised shape to `readExistingIds` is preferable to silently\n // dropping a narrowing that another caller put in place.\n return existingFilters\n }\n\n const allowed = new Set(parsedIds)\n const intersection = existingIds.filter((id) => allowed.has(id))\n return {\n ...existingFilters,\n id: { $in: intersection },\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,wBAAwB;AAE1B,MAAM,sBAAsB;AAEnC,MAAM,aAAa;AAEnB,SAAS,OAAO,OAAiC;AAC/C,SAAO,OAAO,UAAU,YAAY,WAAW,KAAK,KAAK;AAC3D;AAEA,SAAS,gBAAgB,QAA4B;AACnD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,UAAU,oBAAI,IAAY;AAChC,aAAW,SAAS,QAAQ;AAC1B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,WAAW,CAAC,OAAO,OAAO,EAAG;AAClC,YAAQ,IAAI,OAAO;AAAA,EACrB;AACA,SAAO,MAAM,KAAK,OAAO;AAC3B;AAEA,SAAS,gBAAgB,QAAkC;AACzD,MAAI,OAAO,WAAW,UAAU;AAC9B,WAAO,OAAO,MAAM,IAAI,CAAC,MAAM,IAAI;AAAA,EACrC;AACA,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO;AAAA,MACL,OAAO,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAAA,IACrE;AAAA,EACF;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAElD,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,GAAG,EAAG,QAAO,CAAC,UAAU,GAAG;AAEhD,MAAI,MAAM,QAAQ,UAAU,GAAG,GAAG;AAChC,WAAO;AAAA,MACL,UAAU,IAAI,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAAA,IAC5E;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,cAAc,KAAc,SAAiB,qBAA+B;AAC1F,QAAM,SAAS,iBAAiB,GAAG;AACnC,MAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AACjC,QAAM,UAAU,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,KAAK,MAAM,MAAM,IAAI;AAC7E,QAAM,SAAS,gBAAgB,MAAM;AACrC,SAAO,OAAO,MAAM,GAAG,OAAO;AAChC;AASO,SAAS,mBAAmB,KAAuB;AACxD,QAAM,cAAc,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,GAAG;AACnD,SAAO,YAAY,KAAK,CAAC,UAAU,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,CAAC;AACzF;AAEO,SAAS,cACd,iBACA,WACA,SACe;AACf,MAAI,UAAU,WAAW,GAAG;AAK1B,QAAI,SAAS,kBAAkB;AAC7B,aAAO,EAAE,GAAG,iBAAiB,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE;AAAA,IAC/C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,iBAAkB,gBAA4C;AACpE,QAAM,cAAc,gBAAgB,cAAc;AAElD,MAAI,CAAC,aAAa;AAEhB,QAAI,mBAAmB,UAAa,mBAAmB,MAAM;AAC3D,aAAO,EAAE,GAAG,iBAAiB,IAAI,EAAE,KAAK,UAAU,EAAE;AAAA,IACtD;AAKA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,IAAI,IAAI,SAAS;AACjC,QAAM,eAAe,YAAY,OAAO,CAAC,OAAO,QAAQ,IAAI,EAAE,CAAC;AAC/D,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI,EAAE,KAAK,aAAa;AAAA,EAC1B;AACF;",
6
6
  "names": []
7
7
  }
@@ -89,24 +89,10 @@ function isTransientDbError(err) {
89
89
  }
90
90
  return false;
91
91
  }
92
- const TRANSIENT_LOCK_SQLSTATES = /* @__PURE__ */ new Set(["55P03", "57014", "25P03"]);
93
- const TRANSIENT_LOCK_MESSAGE_PATTERNS = [
94
- /timeout exceeded when trying to connect/i,
95
- /client has encountered a connection error and is not queryable/i
96
- ];
97
- function isTransientLockDbError(err) {
98
- return pgErrorCandidates(err).some((candidate) => {
99
- if (isTransientDbError(candidate)) return true;
100
- if (typeof candidate.code === "string" && TRANSIENT_LOCK_SQLSTATES.has(candidate.code)) return true;
101
- const message = candidate.message;
102
- return typeof message === "string" && TRANSIENT_LOCK_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
103
- });
104
- }
105
92
  export {
106
93
  getForeignKeyViolationConstraint,
107
94
  isForeignKeyViolation,
108
95
  isTransientDbError,
109
- isTransientLockDbError,
110
96
  isUniqueViolation,
111
97
  readPgSqlState
112
98
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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\nconst FOREIGN_KEY_VIOLATION_MESSAGE = /violates foreign key constraint(?: \"([^\"]+)\")?/i\n\nconst MAX_ERROR_CHAIN_DEPTH = 4\n\n/**\n * MikroORM wraps driver errors and copies the pg fields onto the wrapper, but\n * the original error may also sit behind `cause` (Node) or `previous`\n * (MikroORM), possibly re-wrapped by a transaction helper. Walk that chain,\n * breadth-first with a small depth cap, so a check works on any layer.\n */\nfunction pgErrorCandidates(err: unknown): Array<Record<string, unknown>> {\n const found: Array<Record<string, unknown>> = []\n const seen = new Set<unknown>()\n let layer: unknown[] = [err]\n for (let depth = 0; depth < MAX_ERROR_CHAIN_DEPTH && layer.length > 0; depth += 1) {\n const next: unknown[] = []\n for (const candidate of layer) {\n if (!candidate || typeof candidate !== 'object' || seen.has(candidate)) continue\n seen.add(candidate)\n const record = candidate as Record<string, unknown>\n found.push(record)\n next.push(record.cause, record.previous)\n }\n layer = next\n }\n return found\n}\n\n/**\n * Detect a Postgres foreign-key violation (SQLSTATE 23503): the row is still\n * referenced by a dependent table, or the payload references a parent that\n * does not exist. Looks through MikroORM's driver-error wrapping.\n */\nexport function isForeignKeyViolation(err: unknown): boolean {\n return pgErrorCandidates(err).some((candidate) => {\n if (candidate.code === '23503') return true // Postgres foreign_key_violation\n return typeof candidate.message === 'string' && FOREIGN_KEY_VIOLATION_MESSAGE.test(candidate.message)\n })\n}\n\n/**\n * Name of the constraint behind a foreign-key violation, read from the pg\n * `constraint` field on any layer of the wrapper chain, or parsed out of the\n * quoted constraint in the driver message when the field is missing.\n */\nexport function getForeignKeyViolationConstraint(err: unknown): string | null {\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.constraint === 'string' && candidate.constraint.length > 0) return candidate.constraint\n }\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.message !== 'string') continue\n const match = FOREIGN_KEY_VIOLATION_MESSAGE.exec(candidate.message)\n if (match?.[1]) return match[1]\n }\n return null\n}\n\n/**\n * A Postgres SQLSTATE is always exactly 5 characters from `[0-9A-Z]`, and every\n * class in the standard SQLSTATE table carries at least one digit. Node system\n * errors (`ECONNREFUSED`), Node internal errors (`ERR_INVALID_ARG_TYPE`), and the\n * five-letter Node errno codes (`EPIPE`, `EPERM`, `EBUSY`) would otherwise be\n * misread as a SQLSTATE, since they also populate a string `code` field; the\n * digit requirement rules those out without excluding any real SQLSTATE.\n */\nconst SQLSTATE_PATTERN = /^(?=.*[0-9])[0-9A-Z]{5}$/\n\n/**\n * The Postgres SQLSTATE behind an error, read from the pg `code` field on any\n * layer of the driver-error wrapper chain (see `pgErrorCandidates`). Returns\n * `null` when no layer carries a `code` matching the SQLSTATE shape, which\n * also covers non-Postgres errors whose `code` is a Node error code.\n */\nexport function readPgSqlState(err: unknown): string | null {\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.code === 'string' && SQLSTATE_PATTERN.test(candidate.code)) return candidate.code\n }\n return null\n}\n\n/**\n * Postgres SQLSTATEs for transient connection / availability failures \u2014 the\n * database (or its connection pool) is temporarily unreachable and the request\n * can succeed on retry. Deliberately scoped to connection/availability codes;\n * query-level conflicts (deadlock 40P01, serialization 40001, lock_not_available\n * 55P03) are NOT included because they do not mean \"service unavailable\".\n */\nconst TRANSIENT_CONNECTION_SQLSTATES = new Set([\n '53300', // too_many_connections\n '53400', // configuration_limit_exceeded\n '57P01', // admin_shutdown\n '57P02', // crash_shutdown\n '57P03', // cannot_connect_now (db starting up)\n '08000', // connection_exception\n '08001', // sqlclient_unable_to_establish_sqlconnection\n '08003', // connection_does_not_exist\n '08006', // connection_failure\n])\n\n/**\n * Postgres-driver / connection-pool messages that describe a transient DB\n * connection failure when the SQLSTATE is dropped by the ORM wrapper. These are\n * intentionally DB-specific phrases only. Bare socket codes (ECONNREFUSED,\n * ETIMEDOUT, \u2026) are deliberately NOT matched: they can originate from any\n * outbound socket (HTTP, cache, queue), so keying off them would falsely\n * attribute unrelated failures to the database.\n */\nconst TRANSIENT_DB_MESSAGE_PATTERNS = [\n /too many clients already/i,\n /unable to acquire a connection/i,\n /timeout acquiring a connection/i,\n /connection terminated/i,\n /the database system is (starting up|shutting down|in recovery)/i,\n]\n\n/**\n * Detect a transient Postgres connection / availability failure (pool exhaustion,\n * `max_connections` reached, DB restarting) via its SQLSTATE or a DB-specific\n * driver message. Gates retryable 503 responses so callers do not report a\n * temporary infrastructure blip as an auth failure (401) or an unexpected server\n * error (500). Scoped to unambiguous DB signals so generic socket errors from\n * non-DB calls are never misclassified.\n */\nexport function isTransientDbError(err: unknown): boolean {\n if (!err || typeof err !== 'object') {\n return false\n }\n const code = (err as { code?: string }).code\n if (typeof code === 'string' && TRANSIENT_CONNECTION_SQLSTATES.has(code)) {\n return true\n }\n const message = (err as { message?: string }).message\n if (typeof message === 'string' && TRANSIENT_DB_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) {\n return true\n }\n return false\n}\n\n/**\n * SQLSTATEs that end a lock transaction early but say nothing about the data it\n * would have written, so the work can be retried: lock_not_available (55P03,\n * raised by `DB_LOCK_TIMEOUT_MS`), query_canceled (57014, raised by the opt-in\n * `DB_STATEMENT_TIMEOUT_MS`) and idle_in_transaction_session_timeout (25P03).\n */\nconst TRANSIENT_LOCK_SQLSTATES = new Set(['55P03', '57014', '25P03'])\n\n/**\n * Driver messages for a connection that never arrived or was lost mid-transaction:\n * the pg-pool acquire timeout (`connectionTimeoutMillis`) and node-postgres's\n * error for a client whose connection already failed.\n */\nconst TRANSIENT_LOCK_MESSAGE_PATTERNS = [\n /timeout exceeded when trying to connect/i,\n /client has encountered a connection error and is not queryable/i,\n]\n\n/**\n * @internal Detect a transient failure of a short lock transaction: everything\n * `isTransientDbError` matches, plus lock, statement and idle-in-transaction\n * timeouts, the pg-pool acquire timeout and node-postgres's \"not queryable\"\n * error. Checks every layer of the driver-error wrapper chain (`cause` /\n * `previous`, see `pgErrorCandidates`). `isTransientDbError` keeps its narrower\n * scope because its callers answer 503 \"service unavailable\".\n */\nexport function isTransientLockDbError(err: unknown): boolean {\n return pgErrorCandidates(err).some((candidate) => {\n if (isTransientDbError(candidate)) return true\n if (typeof candidate.code === 'string' && TRANSIENT_LOCK_SQLSTATES.has(candidate.code)) return true\n const message = candidate.message\n return typeof message === 'string' && TRANSIENT_LOCK_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))\n })\n}\n"],
5
- "mappings": "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",
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\nconst FOREIGN_KEY_VIOLATION_MESSAGE = /violates foreign key constraint(?: \"([^\"]+)\")?/i\n\nconst MAX_ERROR_CHAIN_DEPTH = 4\n\n/**\n * MikroORM wraps driver errors and copies the pg fields onto the wrapper, but\n * the original error may also sit behind `cause` (Node) or `previous`\n * (MikroORM), possibly re-wrapped by a transaction helper. Walk that chain,\n * breadth-first with a small depth cap, so a check works on any layer.\n */\nfunction pgErrorCandidates(err: unknown): Array<Record<string, unknown>> {\n const found: Array<Record<string, unknown>> = []\n const seen = new Set<unknown>()\n let layer: unknown[] = [err]\n for (let depth = 0; depth < MAX_ERROR_CHAIN_DEPTH && layer.length > 0; depth += 1) {\n const next: unknown[] = []\n for (const candidate of layer) {\n if (!candidate || typeof candidate !== 'object' || seen.has(candidate)) continue\n seen.add(candidate)\n const record = candidate as Record<string, unknown>\n found.push(record)\n next.push(record.cause, record.previous)\n }\n layer = next\n }\n return found\n}\n\n/**\n * Detect a Postgres foreign-key violation (SQLSTATE 23503): the row is still\n * referenced by a dependent table, or the payload references a parent that\n * does not exist. Looks through MikroORM's driver-error wrapping.\n */\nexport function isForeignKeyViolation(err: unknown): boolean {\n return pgErrorCandidates(err).some((candidate) => {\n if (candidate.code === '23503') return true // Postgres foreign_key_violation\n return typeof candidate.message === 'string' && FOREIGN_KEY_VIOLATION_MESSAGE.test(candidate.message)\n })\n}\n\n/**\n * Name of the constraint behind a foreign-key violation, read from the pg\n * `constraint` field on any layer of the wrapper chain, or parsed out of the\n * quoted constraint in the driver message when the field is missing.\n */\nexport function getForeignKeyViolationConstraint(err: unknown): string | null {\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.constraint === 'string' && candidate.constraint.length > 0) return candidate.constraint\n }\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.message !== 'string') continue\n const match = FOREIGN_KEY_VIOLATION_MESSAGE.exec(candidate.message)\n if (match?.[1]) return match[1]\n }\n return null\n}\n\n/**\n * A Postgres SQLSTATE is always exactly 5 characters from `[0-9A-Z]`, and every\n * class in the standard SQLSTATE table carries at least one digit. Node system\n * errors (`ECONNREFUSED`), Node internal errors (`ERR_INVALID_ARG_TYPE`), and the\n * five-letter Node errno codes (`EPIPE`, `EPERM`, `EBUSY`) would otherwise be\n * misread as a SQLSTATE, since they also populate a string `code` field; the\n * digit requirement rules those out without excluding any real SQLSTATE.\n */\nconst SQLSTATE_PATTERN = /^(?=.*[0-9])[0-9A-Z]{5}$/\n\n/**\n * The Postgres SQLSTATE behind an error, read from the pg `code` field on any\n * layer of the driver-error wrapper chain (see `pgErrorCandidates`). Returns\n * `null` when no layer carries a `code` matching the SQLSTATE shape, which\n * also covers non-Postgres errors whose `code` is a Node error code.\n */\nexport function readPgSqlState(err: unknown): string | null {\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.code === 'string' && SQLSTATE_PATTERN.test(candidate.code)) return candidate.code\n }\n return null\n}\n\n/**\n * Postgres SQLSTATEs for transient connection / availability failures \u2014 the\n * database (or its connection pool) is temporarily unreachable and the request\n * can succeed on retry. Deliberately scoped to connection/availability codes;\n * query-level conflicts (deadlock 40P01, serialization 40001, lock_not_available\n * 55P03) are NOT included because they do not mean \"service unavailable\".\n */\nconst TRANSIENT_CONNECTION_SQLSTATES = new Set([\n '53300', // too_many_connections\n '53400', // configuration_limit_exceeded\n '57P01', // admin_shutdown\n '57P02', // crash_shutdown\n '57P03', // cannot_connect_now (db starting up)\n '08000', // connection_exception\n '08001', // sqlclient_unable_to_establish_sqlconnection\n '08003', // connection_does_not_exist\n '08006', // connection_failure\n])\n\n/**\n * Postgres-driver / connection-pool messages that describe a transient DB\n * connection failure when the SQLSTATE is dropped by the ORM wrapper. These are\n * intentionally DB-specific phrases only. Bare socket codes (ECONNREFUSED,\n * ETIMEDOUT, \u2026) are deliberately NOT matched: they can originate from any\n * outbound socket (HTTP, cache, queue), so keying off them would falsely\n * attribute unrelated failures to the database.\n */\nconst TRANSIENT_DB_MESSAGE_PATTERNS = [\n /too many clients already/i,\n /unable to acquire a connection/i,\n /timeout acquiring a connection/i,\n /connection terminated/i,\n /the database system is (starting up|shutting down|in recovery)/i,\n]\n\n/**\n * Detect a transient Postgres connection / availability failure (pool exhaustion,\n * `max_connections` reached, DB restarting) via its SQLSTATE or a DB-specific\n * driver message. Gates retryable 503 responses so callers do not report a\n * temporary infrastructure blip as an auth failure (401) or an unexpected server\n * error (500). Scoped to unambiguous DB signals so generic socket errors from\n * non-DB calls are never misclassified.\n */\nexport function isTransientDbError(err: unknown): boolean {\n if (!err || typeof err !== 'object') {\n return false\n }\n const code = (err as { code?: string }).code\n if (typeof code === 'string' && TRANSIENT_CONNECTION_SQLSTATES.has(code)) {\n return true\n }\n const message = (err as { message?: string }).message\n if (typeof message === 'string' && TRANSIENT_DB_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) {\n return true\n }\n return false\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;AAEA,MAAM,gCAAgC;AAEtC,MAAM,wBAAwB;AAQ9B,SAAS,kBAAkB,KAA8C;AACvE,QAAM,QAAwC,CAAC;AAC/C,QAAM,OAAO,oBAAI,IAAa;AAC9B,MAAI,QAAmB,CAAC,GAAG;AAC3B,WAAS,QAAQ,GAAG,QAAQ,yBAAyB,MAAM,SAAS,GAAG,SAAS,GAAG;AACjF,UAAM,OAAkB,CAAC;AACzB,eAAW,aAAa,OAAO;AAC7B,UAAI,CAAC,aAAa,OAAO,cAAc,YAAY,KAAK,IAAI,SAAS,EAAG;AACxE,WAAK,IAAI,SAAS;AAClB,YAAM,SAAS;AACf,YAAM,KAAK,MAAM;AACjB,WAAK,KAAK,OAAO,OAAO,OAAO,QAAQ;AAAA,IACzC;AACA,YAAQ;AAAA,EACV;AACA,SAAO;AACT;AAOO,SAAS,sBAAsB,KAAuB;AAC3D,SAAO,kBAAkB,GAAG,EAAE,KAAK,CAAC,cAAc;AAChD,QAAI,UAAU,SAAS,QAAS,QAAO;AACvC,WAAO,OAAO,UAAU,YAAY,YAAY,8BAA8B,KAAK,UAAU,OAAO;AAAA,EACtG,CAAC;AACH;AAOO,SAAS,iCAAiC,KAA6B;AAC5E,aAAW,aAAa,kBAAkB,GAAG,GAAG;AAC9C,QAAI,OAAO,UAAU,eAAe,YAAY,UAAU,WAAW,SAAS,EAAG,QAAO,UAAU;AAAA,EACpG;AACA,aAAW,aAAa,kBAAkB,GAAG,GAAG;AAC9C,QAAI,OAAO,UAAU,YAAY,SAAU;AAC3C,UAAM,QAAQ,8BAA8B,KAAK,UAAU,OAAO;AAClE,QAAI,QAAQ,CAAC,EAAG,QAAO,MAAM,CAAC;AAAA,EAChC;AACA,SAAO;AACT;AAUA,MAAM,mBAAmB;AAQlB,SAAS,eAAe,KAA6B;AAC1D,aAAW,aAAa,kBAAkB,GAAG,GAAG;AAC9C,QAAI,OAAO,UAAU,SAAS,YAAY,iBAAiB,KAAK,UAAU,IAAI,EAAG,QAAO,UAAU;AAAA,EACpG;AACA,SAAO;AACT;AASA,MAAM,iCAAiC,oBAAI,IAAI;AAAA,EAC7C;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF,CAAC;AAUD,MAAM,gCAAgC;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUO,SAAS,mBAAmB,KAAuB;AACxD,MAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,WAAO;AAAA,EACT;AACA,QAAM,OAAQ,IAA0B;AACxC,MAAI,OAAO,SAAS,YAAY,+BAA+B,IAAI,IAAI,GAAG;AACxE,WAAO;AAAA,EACT;AACA,QAAM,UAAW,IAA6B;AAC9C,MAAI,OAAO,YAAY,YAAY,8BAA8B,KAAK,CAAC,YAAY,QAAQ,KAAK,OAAO,CAAC,GAAG;AACzG,WAAO;AAAA,EACT;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,19 +1,11 @@
1
1
  import { applyLocalizedContent } from "./resolver.js";
2
- import {
3
- registerTranslatableFields,
4
- getTranslatableFields,
5
- getTranslatableFieldsRegistry,
6
- registerTranslatableFieldExpander,
7
- getTranslatableFieldExpander
8
- } from "./translatable-fields.js";
2
+ import { registerTranslatableFields, getTranslatableFields, getTranslatableFieldsRegistry } from "./translatable-fields.js";
9
3
  import { registerTranslationOverlayPlugin, getTranslationOverlayPlugin } from "./overlay-plugin.js";
10
4
  export {
11
5
  applyLocalizedContent,
12
- getTranslatableFieldExpander,
13
6
  getTranslatableFields,
14
7
  getTranslatableFieldsRegistry,
15
8
  getTranslationOverlayPlugin,
16
- registerTranslatableFieldExpander,
17
9
  registerTranslatableFields,
18
10
  registerTranslationOverlayPlugin
19
11
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/localization/index.ts"],
4
- "sourcesContent": ["export type { TranslationRecord, LocaleCode } from './types'\nexport { applyLocalizedContent } from './resolver'\nexport {\n registerTranslatableFields,\n getTranslatableFields,\n getTranslatableFieldsRegistry,\n registerTranslatableFieldExpander,\n getTranslatableFieldExpander,\n} from './translatable-fields'\nexport type { TranslatableFieldExpansion, TranslatableFieldExpander } from './translatable-fields'\nexport { registerTranslationOverlayPlugin, getTranslationOverlayPlugin } from './overlay-plugin'\nexport type { TranslationOverlayFn, ResolveLocaleFromRequestFn } from './overlay-plugin'\n"],
5
- "mappings": "AACA,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,kCAAkC,mCAAmC;",
4
+ "sourcesContent": ["export type { TranslationRecord, LocaleCode } from './types'\nexport { applyLocalizedContent } from './resolver'\nexport { registerTranslatableFields, getTranslatableFields, getTranslatableFieldsRegistry } from './translatable-fields'\nexport { registerTranslationOverlayPlugin, getTranslationOverlayPlugin } from './overlay-plugin'\nexport type { TranslationOverlayFn, ResolveLocaleFromRequestFn } from './overlay-plugin'\n"],
5
+ "mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,4BAA4B,uBAAuB,qCAAqC;AACjG,SAAS,kCAAkC,mCAAmC;",
6
6
  "names": []
7
7
  }
@@ -14,35 +14,9 @@ function getTranslatableFields(entityType) {
14
14
  function getTranslatableFieldsRegistry() {
15
15
  return { ...getGlobal() };
16
16
  }
17
- const EXPANDER_GLOBAL_KEY = "__openMercatoTranslatableFieldExpanders__";
18
- function getExpanderGlobal() {
19
- return globalThis[EXPANDER_GLOBAL_KEY] ?? {};
20
- }
21
- function registerTranslatableFieldExpander(entityType, expander) {
22
- const host = globalThis;
23
- host[EXPANDER_GLOBAL_KEY] = { ...getExpanderGlobal(), [entityType]: expander };
24
- }
25
- function getTranslatableFieldExpander(entityType) {
26
- return getExpanderGlobal()[entityType];
27
- }
28
- const LIST_PATH_GLOBAL_KEY = "__openMercatoTranslatableEntityListPaths__";
29
- function getListPathGlobal() {
30
- return globalThis[LIST_PATH_GLOBAL_KEY] ?? {};
31
- }
32
- function registerTranslatableEntityListPath(entityType, listPath) {
33
- const host = globalThis;
34
- host[LIST_PATH_GLOBAL_KEY] = { ...getListPathGlobal(), [entityType]: listPath };
35
- }
36
- function getTranslatableEntityListPath(entityType) {
37
- return getListPathGlobal()[entityType];
38
- }
39
17
  export {
40
- getTranslatableEntityListPath,
41
- getTranslatableFieldExpander,
42
18
  getTranslatableFields,
43
19
  getTranslatableFieldsRegistry,
44
- registerTranslatableEntityListPath,
45
- registerTranslatableFieldExpander,
46
20
  registerTranslatableFields
47
21
  };
48
22
  //# sourceMappingURL=translatable-fields.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/localization/translatable-fields.ts"],
4
- "sourcesContent": ["type TranslatableFieldsRegistry = Record<string, string[]>\n\n// Use globalThis to survive Turbopack/esbuild module duplication where the same\n// file can be loaded as multiple module instances when mixing dynamic and static imports\nconst GLOBAL_KEY = '__openMercatoTranslatableFields__'\n\nfunction getGlobal(): TranslatableFieldsRegistry {\n return (globalThis as any)[GLOBAL_KEY] ?? {}\n}\n\nfunction setGlobal(registry: TranslatableFieldsRegistry): void {\n (globalThis as any)[GLOBAL_KEY] = registry\n}\n\nexport function registerTranslatableFields(fields: TranslatableFieldsRegistry): void {\n setGlobal({ ...getGlobal(), ...fields })\n}\n\nexport function getTranslatableFields(entityType: string): string[] | undefined {\n return getGlobal()[entityType]\n}\n\nexport function getTranslatableFieldsRegistry(): TranslatableFieldsRegistry {\n return { ...getGlobal() }\n}\n\nexport type TranslatableFieldExpansion = {\n key: string\n label?: string\n baseValue?: string\n}\n\nexport type TranslatableFieldExpander = (record: Record<string, unknown>) => TranslatableFieldExpansion[]\n\ntype TranslatableFieldExpanderRegistry = Record<string, TranslatableFieldExpander>\n\nconst EXPANDER_GLOBAL_KEY = '__openMercatoTranslatableFieldExpanders__'\n\ntype ExpanderGlobalHost = { [EXPANDER_GLOBAL_KEY]?: TranslatableFieldExpanderRegistry }\n\nfunction getExpanderGlobal(): TranslatableFieldExpanderRegistry {\n return (globalThis as ExpanderGlobalHost)[EXPANDER_GLOBAL_KEY] ?? {}\n}\n\nexport function registerTranslatableFieldExpander(entityType: string, expander: TranslatableFieldExpander): void {\n const host = globalThis as ExpanderGlobalHost\n host[EXPANDER_GLOBAL_KEY] = { ...getExpanderGlobal(), [entityType]: expander }\n}\n\nexport function getTranslatableFieldExpander(entityType: string): TranslatableFieldExpander | undefined {\n return getExpanderGlobal()[entityType]\n}\n\ntype TranslatableEntityListPathRegistry = Record<string, string>\n\nconst LIST_PATH_GLOBAL_KEY = '__openMercatoTranslatableEntityListPaths__'\n\ntype ListPathGlobalHost = { [LIST_PATH_GLOBAL_KEY]?: TranslatableEntityListPathRegistry }\n\nfunction getListPathGlobal(): TranslatableEntityListPathRegistry {\n return (globalThis as ListPathGlobalHost)[LIST_PATH_GLOBAL_KEY] ?? {}\n}\n\nexport function registerTranslatableEntityListPath(entityType: string, listPath: string): void {\n const host = globalThis as ListPathGlobalHost\n host[LIST_PATH_GLOBAL_KEY] = { ...getListPathGlobal(), [entityType]: listPath }\n}\n\nexport function getTranslatableEntityListPath(entityType: string): string | undefined {\n return getListPathGlobal()[entityType]\n}\n"],
5
- "mappings": "AAIA,MAAM,aAAa;AAEnB,SAAS,YAAwC;AAC/C,SAAQ,WAAmB,UAAU,KAAK,CAAC;AAC7C;AAEA,SAAS,UAAU,UAA4C;AAC7D,EAAC,WAAmB,UAAU,IAAI;AACpC;AAEO,SAAS,2BAA2B,QAA0C;AACnF,YAAU,EAAE,GAAG,UAAU,GAAG,GAAG,OAAO,CAAC;AACzC;AAEO,SAAS,sBAAsB,YAA0C;AAC9E,SAAO,UAAU,EAAE,UAAU;AAC/B;AAEO,SAAS,gCAA4D;AAC1E,SAAO,EAAE,GAAG,UAAU,EAAE;AAC1B;AAYA,MAAM,sBAAsB;AAI5B,SAAS,oBAAuD;AAC9D,SAAQ,WAAkC,mBAAmB,KAAK,CAAC;AACrE;AAEO,SAAS,kCAAkC,YAAoB,UAA2C;AAC/G,QAAM,OAAO;AACb,OAAK,mBAAmB,IAAI,EAAE,GAAG,kBAAkB,GAAG,CAAC,UAAU,GAAG,SAAS;AAC/E;AAEO,SAAS,6BAA6B,YAA2D;AACtG,SAAO,kBAAkB,EAAE,UAAU;AACvC;AAIA,MAAM,uBAAuB;AAI7B,SAAS,oBAAwD;AAC/D,SAAQ,WAAkC,oBAAoB,KAAK,CAAC;AACtE;AAEO,SAAS,mCAAmC,YAAoB,UAAwB;AAC7F,QAAM,OAAO;AACb,OAAK,oBAAoB,IAAI,EAAE,GAAG,kBAAkB,GAAG,CAAC,UAAU,GAAG,SAAS;AAChF;AAEO,SAAS,8BAA8B,YAAwC;AACpF,SAAO,kBAAkB,EAAE,UAAU;AACvC;",
4
+ "sourcesContent": ["type TranslatableFieldsRegistry = Record<string, string[]>\n\n// Use globalThis to survive Turbopack/esbuild module duplication where the same\n// file can be loaded as multiple module instances when mixing dynamic and static imports\nconst GLOBAL_KEY = '__openMercatoTranslatableFields__'\n\nfunction getGlobal(): TranslatableFieldsRegistry {\n return (globalThis as any)[GLOBAL_KEY] ?? {}\n}\n\nfunction setGlobal(registry: TranslatableFieldsRegistry): void {\n (globalThis as any)[GLOBAL_KEY] = registry\n}\n\nexport function registerTranslatableFields(fields: TranslatableFieldsRegistry): void {\n setGlobal({ ...getGlobal(), ...fields })\n}\n\nexport function getTranslatableFields(entityType: string): string[] | undefined {\n return getGlobal()[entityType]\n}\n\nexport function getTranslatableFieldsRegistry(): TranslatableFieldsRegistry {\n return { ...getGlobal() }\n}\n"],
5
+ "mappings": "AAIA,MAAM,aAAa;AAEnB,SAAS,YAAwC;AAC/C,SAAQ,WAAmB,UAAU,KAAK,CAAC;AAC7C;AAEA,SAAS,UAAU,UAA4C;AAC7D,EAAC,WAAmB,UAAU,IAAI;AACpC;AAEO,SAAS,2BAA2B,QAA0C;AACnF,YAAU,EAAE,GAAG,UAAU,GAAG,GAAG,OAAO,CAAC;AACzC;AAEO,SAAS,sBAAsB,YAA0C;AAC9E,SAAO,UAAU,EAAE,UAAU;AAC/B;AAEO,SAAS,gCAA4D;AAC1E,SAAO,EAAE,GAAG,UAAU,EAAE;AAC1B;",
6
6
  "names": []
7
7
  }
@@ -62,11 +62,6 @@ function parseLocaleNumber(input, locale) {
62
62
  const parsed = Number(normalized);
63
63
  return Number.isFinite(parsed) ? parsed : null;
64
64
  }
65
- function formatLocaleNumberInput(value, locale) {
66
- const serialized = String(value);
67
- const { decimal } = resolveLocaleNumberSeparators(locale);
68
- return decimal === "." ? serialized : serialized.replace(".", decimal);
69
- }
70
65
  function parseNumberWithDefault(raw, fallback, options) {
71
66
  if (raw == null) return fallback;
72
67
  const trimmed = raw.trim();
@@ -78,7 +73,6 @@ function parseNumberWithDefault(raw, fallback, options) {
78
73
  return value;
79
74
  }
80
75
  export {
81
- formatLocaleNumberInput,
82
76
  parseLocaleNumber,
83
77
  parseNumberWithDefault,
84
78
  resolveLocaleNumberSeparators
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/number.ts"],
4
- "sourcesContent": ["type LocaleNumberSeparators = { group: string; decimal: string }\n\nconst DEFAULT_SEPARATORS: LocaleNumberSeparators = { group: ',', decimal: '.' }\nconst separatorCache = new Map<string, LocaleNumberSeparators>()\n\nconst UNICODE_MINUS_SIGNS = /[\u2212\u2012\u2013\u2014]/g\nconst GROUP_LIKE_CHARACTER = /[\\s'\u2019\u02BC]/\nconst GROUP_LIKE_SEPARATOR_IN_POSITION = /(\\d)[\\s'\u2019\u02BC](?=\\d{3}(?!\\d))/g\nconst NORMALIZED_NUMBER = /^[+-]?(\\d+(\\.\\d*)?|\\.\\d+)(e[+-]?\\d+)?$/i\n\n/**\n * Group and decimal separators the given locale uses, derived from `Intl` rather than\n * assumed, so grouping characters such as the narrow no-break space (`fr-FR`) are covered.\n */\nexport function resolveLocaleNumberSeparators(locale?: string): LocaleNumberSeparators {\n const cacheKey = locale ?? ''\n const cached = separatorCache.get(cacheKey)\n if (cached) return cached\n let resolved = DEFAULT_SEPARATORS\n try {\n const parts = new Intl.NumberFormat(locale, {\n useGrouping: true,\n minimumFractionDigits: 1,\n }).formatToParts(12345.6)\n const group = parts.find((part) => part.type === 'group')?.value ?? DEFAULT_SEPARATORS.group\n const decimal = parts.find((part) => part.type === 'decimal')?.value ?? DEFAULT_SEPARATORS.decimal\n resolved = { group, decimal }\n } catch {\n resolved = DEFAULT_SEPARATORS\n }\n separatorCache.set(cacheKey, resolved)\n return resolved\n}\n\nfunction isValidGrouping(integerPart: string, separator: string): boolean {\n const digits = integerPart.replace(/^[+-]/, '')\n const segments = digits.split(separator)\n if (segments.length < 2) return true\n const [first, ...rest] = segments\n if (!/^\\d{1,3}$/.test(first)) return false\n return rest.every((segment) => /^\\d{3}$/.test(segment))\n}\n\n/**\n * Parses a user-typed number written in the conventions of `locale` \u2014 `110,70` under `pl-PL`,\n * `1 234,56` under `fr-FR`, `1,234.56` under `en-US`. Returns `null` when the input is not a\n * number, never a silent `0`, so callers can tell \"unparseable\" apart from \"zero\".\n *\n * Both `,` and `.` are accepted as the decimal separator whichever way the locale runs, because\n * users type the shape their keyboard offers. A SINGLE `,` or `.` is therefore always the decimal\n * point, in every locale: `1.500` is 1.5 under `de-DE` just as it is under `en-US`. Reading a lone\n * separator as grouping instead would turn `1.500` into 1500 with no visible cue \u2014 a silent 1000\u00D7\n * on a money field, and three- and four-decimal unit prices are ordinary here. Grouping is\n * recognized only where it is unambiguous: at least two separators (`1.234.567`), or a whitespace\n * or apostrophe separator standing in a valid group-of-three position (`1 234,56`, `1\u2019234.5`).\n * Whitespace and apostrophes anywhere else are not absorbed \u2014 `1 2` is rejected rather than read\n * as 12 \u2014 so a mistyped or pasted value surfaces as an error instead of a different number.\n *\n * Use it only on strings a user typed. Values arriving from an API or the database are already\n * numbers and MUST NOT go through it.\n */\nexport function parseLocaleNumber(input: string | null | undefined, locale?: string): number | null {\n if (input == null) return null\n const trimmed = input.trim()\n if (!trimmed) return null\n\n const { group } = resolveLocaleNumberSeparators(locale)\n let candidate = trimmed.replace(UNICODE_MINUS_SIGNS, '-')\n if (group && group !== ',' && group !== '.' && !GROUP_LIKE_CHARACTER.test(group)) {\n candidate = candidate.split(group).join(' ')\n }\n candidate = candidate.replace(GROUP_LIKE_SEPARATOR_IN_POSITION, '$1')\n if (!candidate) return null\n\n const hasComma = candidate.includes(',')\n const hasDot = candidate.includes('.')\n let decimalSeparator: string | null = null\n if (hasComma && hasDot) {\n decimalSeparator = candidate.lastIndexOf(',') > candidate.lastIndexOf('.') ? ',' : '.'\n } else if (hasComma || hasDot) {\n const separator = hasComma ? ',' : '.'\n const segments = candidate.split(separator)\n decimalSeparator = segments.length > 2 ? null : separator\n }\n\n const groupSeparator = decimalSeparator\n ? decimalSeparator === ','\n ? '.'\n : ','\n : hasComma\n ? ','\n : '.'\n const [integerPart, ...fractionParts] = decimalSeparator ? candidate.split(decimalSeparator) : [candidate]\n if (fractionParts.length > 1) return null\n if (!isValidGrouping(integerPart, groupSeparator)) return null\n if (fractionParts.length && fractionParts[0].includes(groupSeparator)) return null\n\n const normalized = `${integerPart.split(groupSeparator).join('')}${fractionParts.length ? `.${fractionParts[0]}` : ''}`\n if (!NORMALIZED_NUMBER.test(normalized)) return null\n const parsed = Number(normalized)\n return Number.isFinite(parsed) ? parsed : null\n}\n\n/**\n * Renders a number as the text an input field should show under `locale` \u2014 `2,5` under\n * `pl-PL`, `2.5` under `en-US`. Unlike `String()`, which always emits a dot, the result uses\n * the locale's decimal separator, so a value redisplayed after blur or reload matches what the\n * user typed and what the rest of the UI shows. No grouping is applied, and the output always\n * round-trips through `parseLocaleNumber`.\n */\nexport function formatLocaleNumberInput(value: number, locale?: string): string {\n const serialized = String(value)\n const { decimal } = resolveLocaleNumberSeparators(locale)\n return decimal === '.' ? serialized : serialized.replace('.', decimal)\n}\n\nexport function parseNumberWithDefault(\n raw: string | null | undefined,\n fallback: number,\n options?: { min?: number; integer?: boolean },\n): number {\n if (raw == null) return fallback\n const trimmed = raw.trim()\n if (!trimmed) return fallback\n const value = options?.integer ? Number.parseInt(trimmed, 10) : Number(trimmed)\n if (!Number.isFinite(value)) return fallback\n const min = options?.min ?? -Infinity\n if (value < min) return fallback\n return value\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["type LocaleNumberSeparators = { group: string; decimal: string }\n\nconst DEFAULT_SEPARATORS: LocaleNumberSeparators = { group: ',', decimal: '.' }\nconst separatorCache = new Map<string, LocaleNumberSeparators>()\n\nconst UNICODE_MINUS_SIGNS = /[\u2212\u2012\u2013\u2014]/g\nconst GROUP_LIKE_CHARACTER = /[\\s'\u2019\u02BC]/\nconst GROUP_LIKE_SEPARATOR_IN_POSITION = /(\\d)[\\s'\u2019\u02BC](?=\\d{3}(?!\\d))/g\nconst NORMALIZED_NUMBER = /^[+-]?(\\d+(\\.\\d*)?|\\.\\d+)(e[+-]?\\d+)?$/i\n\n/**\n * Group and decimal separators the given locale uses, derived from `Intl` rather than\n * assumed, so grouping characters such as the narrow no-break space (`fr-FR`) are covered.\n */\nexport function resolveLocaleNumberSeparators(locale?: string): LocaleNumberSeparators {\n const cacheKey = locale ?? ''\n const cached = separatorCache.get(cacheKey)\n if (cached) return cached\n let resolved = DEFAULT_SEPARATORS\n try {\n const parts = new Intl.NumberFormat(locale, {\n useGrouping: true,\n minimumFractionDigits: 1,\n }).formatToParts(12345.6)\n const group = parts.find((part) => part.type === 'group')?.value ?? DEFAULT_SEPARATORS.group\n const decimal = parts.find((part) => part.type === 'decimal')?.value ?? DEFAULT_SEPARATORS.decimal\n resolved = { group, decimal }\n } catch {\n resolved = DEFAULT_SEPARATORS\n }\n separatorCache.set(cacheKey, resolved)\n return resolved\n}\n\nfunction isValidGrouping(integerPart: string, separator: string): boolean {\n const digits = integerPart.replace(/^[+-]/, '')\n const segments = digits.split(separator)\n if (segments.length < 2) return true\n const [first, ...rest] = segments\n if (!/^\\d{1,3}$/.test(first)) return false\n return rest.every((segment) => /^\\d{3}$/.test(segment))\n}\n\n/**\n * Parses a user-typed number written in the conventions of `locale` \u2014 `110,70` under `pl-PL`,\n * `1 234,56` under `fr-FR`, `1,234.56` under `en-US`. Returns `null` when the input is not a\n * number, never a silent `0`, so callers can tell \"unparseable\" apart from \"zero\".\n *\n * Both `,` and `.` are accepted as the decimal separator whichever way the locale runs, because\n * users type the shape their keyboard offers. A SINGLE `,` or `.` is therefore always the decimal\n * point, in every locale: `1.500` is 1.5 under `de-DE` just as it is under `en-US`. Reading a lone\n * separator as grouping instead would turn `1.500` into 1500 with no visible cue \u2014 a silent 1000\u00D7\n * on a money field, and three- and four-decimal unit prices are ordinary here. Grouping is\n * recognized only where it is unambiguous: at least two separators (`1.234.567`), or a whitespace\n * or apostrophe separator standing in a valid group-of-three position (`1 234,56`, `1\u2019234.5`).\n * Whitespace and apostrophes anywhere else are not absorbed \u2014 `1 2` is rejected rather than read\n * as 12 \u2014 so a mistyped or pasted value surfaces as an error instead of a different number.\n *\n * Use it only on strings a user typed. Values arriving from an API or the database are already\n * numbers and MUST NOT go through it.\n */\nexport function parseLocaleNumber(input: string | null | undefined, locale?: string): number | null {\n if (input == null) return null\n const trimmed = input.trim()\n if (!trimmed) return null\n\n const { group } = resolveLocaleNumberSeparators(locale)\n let candidate = trimmed.replace(UNICODE_MINUS_SIGNS, '-')\n if (group && group !== ',' && group !== '.' && !GROUP_LIKE_CHARACTER.test(group)) {\n candidate = candidate.split(group).join(' ')\n }\n candidate = candidate.replace(GROUP_LIKE_SEPARATOR_IN_POSITION, '$1')\n if (!candidate) return null\n\n const hasComma = candidate.includes(',')\n const hasDot = candidate.includes('.')\n let decimalSeparator: string | null = null\n if (hasComma && hasDot) {\n decimalSeparator = candidate.lastIndexOf(',') > candidate.lastIndexOf('.') ? ',' : '.'\n } else if (hasComma || hasDot) {\n const separator = hasComma ? ',' : '.'\n const segments = candidate.split(separator)\n decimalSeparator = segments.length > 2 ? null : separator\n }\n\n const groupSeparator = decimalSeparator\n ? decimalSeparator === ','\n ? '.'\n : ','\n : hasComma\n ? ','\n : '.'\n const [integerPart, ...fractionParts] = decimalSeparator ? candidate.split(decimalSeparator) : [candidate]\n if (fractionParts.length > 1) return null\n if (!isValidGrouping(integerPart, groupSeparator)) return null\n if (fractionParts.length && fractionParts[0].includes(groupSeparator)) return null\n\n const normalized = `${integerPart.split(groupSeparator).join('')}${fractionParts.length ? `.${fractionParts[0]}` : ''}`\n if (!NORMALIZED_NUMBER.test(normalized)) return null\n const parsed = Number(normalized)\n return Number.isFinite(parsed) ? parsed : null\n}\n\nexport function parseNumberWithDefault(\n raw: string | null | undefined,\n fallback: number,\n options?: { min?: number; integer?: boolean },\n): number {\n if (raw == null) return fallback\n const trimmed = raw.trim()\n if (!trimmed) return fallback\n const value = options?.integer ? Number.parseInt(trimmed, 10) : Number(trimmed)\n if (!Number.isFinite(value)) return fallback\n const min = options?.min ?? -Infinity\n if (value < min) return fallback\n return value\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }