@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,11 +1,4 @@
1
- import {
2
- MAX_IDS_PER_REQUEST,
3
- mergeIdFilter,
4
- parseIdsParam,
5
- isIdsParamProvided,
6
- isUuidListParam,
7
- splitIdListParam,
8
- } from '@open-mercato/shared/lib/crud/ids'
1
+ import { MAX_IDS_PER_REQUEST, mergeIdFilter, parseIdsParam, isIdsParamProvided } from '@open-mercato/shared/lib/crud/ids'
9
2
 
10
3
  describe('crud ids helpers', () => {
11
4
  const idA = '550e8400-e29b-41d4-a716-446655440001'
@@ -175,30 +168,4 @@ describe('crud ids helpers', () => {
175
168
  tenantId: 't1',
176
169
  })
177
170
  })
178
-
179
- it('splitIdListParam trims entries and drops empty ones', () => {
180
- expect(splitIdListParam(` ${idA} ,, ${idB} ,`)).toEqual([idA, idB])
181
- expect(splitIdListParam(undefined)).toEqual([])
182
- expect(splitIdListParam(['not', 'a', 'string'])).toEqual([])
183
- })
184
-
185
- it('isUuidListParam accepts absent, empty, and all-UUID lists', () => {
186
- expect(isUuidListParam(undefined)).toBe(true)
187
- expect(isUuidListParam('')).toBe(true)
188
- expect(isUuidListParam(` ${idA}, ${idB.toUpperCase()} `)).toBe(true)
189
- expect(isUuidListParam('00000000-0000-0000-0000-000000000000')).toBe(true)
190
- })
191
-
192
- it('isUuidListParam rejects malformed and mixed lists', () => {
193
- expect(isUuidListParam('not-a-uuid')).toBe(false)
194
- expect(isUuidListParam(`${idA},not-a-uuid`)).toBe(false)
195
- expect(isUuidListParam(`${idA}0`)).toBe(false)
196
- expect(isUuidListParam(`{${idA}}`)).toBe(false)
197
- })
198
-
199
- it('isUuidListParam fails closed on non-string values', () => {
200
- expect(isUuidListParam(['not-a-uuid'])).toBe(false)
201
- expect(isUuidListParam([idA])).toBe(false)
202
- expect(isUuidListParam(null)).toBe(false)
203
- })
204
171
  })
@@ -81,7 +81,7 @@ function escapeXmlValue(value: string): string {
81
81
  }
82
82
 
83
83
  function serializeCsv(prepared: PreparedExport): SerializedExport {
84
- const headers = prepared.columns.map((col) => escapeCsv(neutralizeSpreadsheetFormula(col.header)))
84
+ const headers = prepared.columns.map((col) => col.header)
85
85
  const lines = [headers.join(',')]
86
86
  for (const row of prepared.rows) {
87
87
  const values = prepared.columns.map((col) => {
@@ -51,30 +51,6 @@ export function parseIdsParam(raw: unknown, maxIds: number = MAX_IDS_PER_REQUEST
51
51
  return parsed.slice(0, safeMax)
52
52
  }
53
53
 
54
- 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
55
-
56
- /**
57
- * Splits a comma-separated id list query param into its trimmed, non-empty entries.
58
- */
59
- export function splitIdListParam(raw: unknown): string[] {
60
- if (typeof raw !== 'string') return []
61
- return raw
62
- .split(',')
63
- .map((value) => value.trim())
64
- .filter((value) => value.length > 0)
65
- }
66
-
67
- /**
68
- * Whether every entry of a comma-separated id list query param is UUID-shaped, so it can be
69
- * bound to a uuid column without a database error. Use it to reject a malformed or mixed list
70
- * with a 400 instead of dropping the bad entries and widening the result set (#7265).
71
- */
72
- export function isUuidListParam(raw: unknown): boolean {
73
- if (raw === undefined) return true
74
- if (typeof raw !== 'string') return false
75
- return splitIdListParam(raw).every((value) => UUID_SHAPE_REGEX.test(value))
76
- }
77
-
78
54
  /**
79
55
  * Whether an `?ids=` param was supplied at all (a non-empty string, or repeated
80
56
  * `?ids=` occurrences), regardless of whether any value survived UUID
@@ -1,11 +1,4 @@
1
- import {
2
- getForeignKeyViolationConstraint,
3
- isForeignKeyViolation,
4
- isTransientDbError,
5
- isTransientLockDbError,
6
- isUniqueViolation,
7
- readPgSqlState,
8
- } from '../pg-errors'
1
+ import { getForeignKeyViolationConstraint, isForeignKeyViolation, isTransientDbError, isUniqueViolation, readPgSqlState } from '../pg-errors'
9
2
 
10
3
  describe('isTransientDbError', () => {
11
4
  it('is true for the max_connections SQLSTATE', () => {
@@ -54,66 +47,6 @@ describe('isTransientDbError', () => {
54
47
  })
55
48
  })
56
49
 
57
- describe('isTransientLockDbError', () => {
58
- it('matches lock, statement and idle-in-transaction timeouts', () => {
59
- for (const code of ['55P03', '57014', '25P03']) {
60
- expect(isTransientLockDbError({ code })).toBe(true)
61
- }
62
- })
63
-
64
- it('matches the pg-pool acquire timeout and the node-postgres not-queryable error', () => {
65
- expect(isTransientLockDbError(new Error('timeout exceeded when trying to connect'))).toBe(true)
66
- expect(isTransientLockDbError(new Error('Client has encountered a connection error and is not queryable'))).toBe(true)
67
- })
68
-
69
- it('matches everything isTransientDbError matches', () => {
70
- for (const code of ['53300', '53400', '57P01', '57P02', '57P03', '08000', '08001', '08003', '08006']) {
71
- expect(isTransientLockDbError({ code })).toBe(true)
72
- }
73
- expect(isTransientLockDbError(new Error('sorry, too many clients already'))).toBe(true)
74
- expect(isTransientLockDbError(new Error('Connection terminated unexpectedly'))).toBe(true)
75
- })
76
-
77
- it('looks through cause and previous on every layer of the wrapper chain', () => {
78
- expect(isTransientLockDbError({ message: 'wrapped', cause: { code: '55P03' } })).toBe(true)
79
- expect(isTransientLockDbError({ message: 'wrapped', previous: { code: '57014' } })).toBe(true)
80
- expect(isTransientLockDbError({ message: 'outer', cause: { message: 'inner', previous: { code: '25P03' } } })).toBe(true)
81
- expect(isTransientLockDbError(new Error('commit failed', { cause: new Error('timeout exceeded when trying to connect') }))).toBe(true)
82
- expect(
83
- isTransientLockDbError({ message: 'wrapped', cause: new Error('Client has encountered a connection error and is not queryable') }),
84
- ).toBe(true)
85
- expect(isTransientLockDbError({ message: 'wrapped', previous: { code: '53300' } })).toBe(true)
86
- })
87
-
88
- it('is false for query-level conflicts, socket codes and non-DB errors', () => {
89
- expect(isTransientLockDbError({ code: '23505' })).toBe(false)
90
- expect(isTransientLockDbError({ code: '40P01' })).toBe(false)
91
- expect(isTransientLockDbError({ code: '40001' })).toBe(false)
92
- expect(isTransientLockDbError({ code: 'ECONNREFUSED' })).toBe(false)
93
- expect(isTransientLockDbError(new Error('something unrelated broke'))).toBe(false)
94
- expect(isTransientLockDbError(null)).toBe(false)
95
- expect(isTransientLockDbError(undefined)).toBe(false)
96
- expect(isTransientLockDbError('timeout exceeded when trying to connect')).toBe(false)
97
- })
98
-
99
- it('stops on cyclic or very deep wrapper chains', () => {
100
- const cyclic: Record<string, unknown> = { message: 'loop' }
101
- cyclic.cause = cyclic
102
- expect(isTransientLockDbError(cyclic)).toBe(false)
103
- const deep = { cause: { cause: { cause: { cause: { cause: { code: '55P03' } } } } } }
104
- expect(isTransientLockDbError(deep)).toBe(false)
105
- })
106
-
107
- it('leaves isTransientDbError unchanged for the lock-only signals', () => {
108
- for (const code of ['55P03', '57014', '25P03']) {
109
- expect(isTransientDbError({ code })).toBe(false)
110
- }
111
- expect(isTransientDbError(new Error('timeout exceeded when trying to connect'))).toBe(false)
112
- expect(isTransientDbError(new Error('Client has encountered a connection error and is not queryable'))).toBe(false)
113
- expect(isTransientDbError({ message: 'wrapped', cause: { code: '53300' } })).toBe(false)
114
- })
115
- })
116
-
117
50
  describe('isForeignKeyViolation', () => {
118
51
  it('is true for the foreign_key_violation SQLSTATE', () => {
119
52
  expect(isForeignKeyViolation({ code: '23503' })).toBe(true)
@@ -148,38 +148,3 @@ export function isTransientDbError(err: unknown): boolean {
148
148
  }
149
149
  return false
150
150
  }
151
-
152
- /**
153
- * SQLSTATEs that end a lock transaction early but say nothing about the data it
154
- * would have written, so the work can be retried: lock_not_available (55P03,
155
- * raised by `DB_LOCK_TIMEOUT_MS`), query_canceled (57014, raised by the opt-in
156
- * `DB_STATEMENT_TIMEOUT_MS`) and idle_in_transaction_session_timeout (25P03).
157
- */
158
- const TRANSIENT_LOCK_SQLSTATES = new Set(['55P03', '57014', '25P03'])
159
-
160
- /**
161
- * Driver messages for a connection that never arrived or was lost mid-transaction:
162
- * the pg-pool acquire timeout (`connectionTimeoutMillis`) and node-postgres's
163
- * error for a client whose connection already failed.
164
- */
165
- const TRANSIENT_LOCK_MESSAGE_PATTERNS = [
166
- /timeout exceeded when trying to connect/i,
167
- /client has encountered a connection error and is not queryable/i,
168
- ]
169
-
170
- /**
171
- * @internal Detect a transient failure of a short lock transaction: everything
172
- * `isTransientDbError` matches, plus lock, statement and idle-in-transaction
173
- * timeouts, the pg-pool acquire timeout and node-postgres's "not queryable"
174
- * error. Checks every layer of the driver-error wrapper chain (`cause` /
175
- * `previous`, see `pgErrorCandidates`). `isTransientDbError` keeps its narrower
176
- * scope because its callers answer 503 "service unavailable".
177
- */
178
- export function isTransientLockDbError(err: unknown): boolean {
179
- return pgErrorCandidates(err).some((candidate) => {
180
- if (isTransientDbError(candidate)) return true
181
- if (typeof candidate.code === 'string' && TRANSIENT_LOCK_SQLSTATES.has(candidate.code)) return true
182
- const message = candidate.message
183
- return typeof message === 'string' && TRANSIENT_LOCK_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))
184
- })
185
- }
@@ -883,26 +883,3 @@ describe('TenantDataEncryptionService map read failures (issue #6334)', () => {
883
883
  expect(execute).toHaveBeenCalledTimes(2)
884
884
  })
885
885
  })
886
-
887
- describe('TenantDataEncryptionService transaction-aware map reads', () => {
888
- it('prefers the EntityManager executor over the bare connection', async () => {
889
- const entityId = 'test:transaction_aware_map_read'
890
- const execute = jest.fn(async () => [{
891
- entity_id: entityId,
892
- fields_json: [{ field: 'display_name' }],
893
- }])
894
- const bareExecute = jest.fn(async () => {
895
- throw new Error('bare connection must not be used')
896
- })
897
- const service = new TenantDataEncryptionService({
898
- execute,
899
- getConnection: () => ({ execute: bareExecute }),
900
- } as never)
901
- jest.spyOn(service, 'isEnabled').mockReturnValue(true)
902
-
903
- await expect(service.getEncryptedFieldNames(entityId, 'tenant-transaction', 'org-transaction'))
904
- .resolves.toEqual(['display_name'])
905
- expect(execute).toHaveBeenCalled()
906
- expect(bareExecute).not.toHaveBeenCalled()
907
- })
908
- })
@@ -2,8 +2,6 @@ import {
2
2
  registerTranslatableFields,
3
3
  getTranslatableFields,
4
4
  getTranslatableFieldsRegistry,
5
- registerTranslatableFieldExpander,
6
- getTranslatableFieldExpander,
7
5
  } from '../translatable-fields'
8
6
 
9
7
  // The registry is global mutable state — we snapshot before all tests
@@ -69,18 +67,3 @@ describe('translatable fields registry', () => {
69
67
  expect(getTranslatableFields('b:second')).toEqual(['f2'])
70
68
  })
71
69
  })
72
-
73
- describe('translatable field expander registry', () => {
74
- it('returns undefined for an entity type without an expander', () => {
75
- expect(getTranslatableFieldExpander('expander_test:none')).toBeUndefined()
76
- })
77
-
78
- it('returns the registered expander and keeps earlier registrations', () => {
79
- const first = jest.fn(() => [{ key: 'a.b' }])
80
- const second = jest.fn(() => [])
81
- registerTranslatableFieldExpander('expander_test:first', first)
82
- registerTranslatableFieldExpander('expander_test:second', second)
83
- expect(getTranslatableFieldExpander('expander_test:first')).toBe(first)
84
- expect(getTranslatableFieldExpander('expander_test:second')).toBe(second)
85
- })
86
- })
@@ -1,12 +1,5 @@
1
1
  export type { TranslationRecord, LocaleCode } from './types'
2
2
  export { applyLocalizedContent } from './resolver'
3
- export {
4
- registerTranslatableFields,
5
- getTranslatableFields,
6
- getTranslatableFieldsRegistry,
7
- registerTranslatableFieldExpander,
8
- getTranslatableFieldExpander,
9
- } from './translatable-fields'
10
- export type { TranslatableFieldExpansion, TranslatableFieldExpander } from './translatable-fields'
3
+ export { registerTranslatableFields, getTranslatableFields, getTranslatableFieldsRegistry } from './translatable-fields'
11
4
  export { registerTranslationOverlayPlugin, getTranslationOverlayPlugin } from './overlay-plugin'
12
5
  export type { TranslationOverlayFn, ResolveLocaleFromRequestFn } from './overlay-plugin'
@@ -23,49 +23,3 @@ export function getTranslatableFields(entityType: string): string[] | undefined
23
23
  export function getTranslatableFieldsRegistry(): TranslatableFieldsRegistry {
24
24
  return { ...getGlobal() }
25
25
  }
26
-
27
- export type TranslatableFieldExpansion = {
28
- key: string
29
- label?: string
30
- baseValue?: string
31
- }
32
-
33
- export type TranslatableFieldExpander = (record: Record<string, unknown>) => TranslatableFieldExpansion[]
34
-
35
- type TranslatableFieldExpanderRegistry = Record<string, TranslatableFieldExpander>
36
-
37
- const EXPANDER_GLOBAL_KEY = '__openMercatoTranslatableFieldExpanders__'
38
-
39
- type ExpanderGlobalHost = { [EXPANDER_GLOBAL_KEY]?: TranslatableFieldExpanderRegistry }
40
-
41
- function getExpanderGlobal(): TranslatableFieldExpanderRegistry {
42
- return (globalThis as ExpanderGlobalHost)[EXPANDER_GLOBAL_KEY] ?? {}
43
- }
44
-
45
- export function registerTranslatableFieldExpander(entityType: string, expander: TranslatableFieldExpander): void {
46
- const host = globalThis as ExpanderGlobalHost
47
- host[EXPANDER_GLOBAL_KEY] = { ...getExpanderGlobal(), [entityType]: expander }
48
- }
49
-
50
- export function getTranslatableFieldExpander(entityType: string): TranslatableFieldExpander | undefined {
51
- return getExpanderGlobal()[entityType]
52
- }
53
-
54
- type TranslatableEntityListPathRegistry = Record<string, string>
55
-
56
- const LIST_PATH_GLOBAL_KEY = '__openMercatoTranslatableEntityListPaths__'
57
-
58
- type ListPathGlobalHost = { [LIST_PATH_GLOBAL_KEY]?: TranslatableEntityListPathRegistry }
59
-
60
- function getListPathGlobal(): TranslatableEntityListPathRegistry {
61
- return (globalThis as ListPathGlobalHost)[LIST_PATH_GLOBAL_KEY] ?? {}
62
- }
63
-
64
- export function registerTranslatableEntityListPath(entityType: string, listPath: string): void {
65
- const host = globalThis as ListPathGlobalHost
66
- host[LIST_PATH_GLOBAL_KEY] = { ...getListPathGlobal(), [entityType]: listPath }
67
- }
68
-
69
- export function getTranslatableEntityListPath(entityType: string): string | undefined {
70
- return getListPathGlobal()[entityType]
71
- }
package/src/lib/number.ts CHANGED
@@ -101,19 +101,6 @@ export function parseLocaleNumber(input: string | null | undefined, locale?: str
101
101
  return Number.isFinite(parsed) ? parsed : null
102
102
  }
103
103
 
104
- /**
105
- * Renders a number as the text an input field should show under `locale` — `2,5` under
106
- * `pl-PL`, `2.5` under `en-US`. Unlike `String()`, which always emits a dot, the result uses
107
- * the locale's decimal separator, so a value redisplayed after blur or reload matches what the
108
- * user typed and what the rest of the UI shows. No grouping is applied, and the output always
109
- * round-trips through `parseLocaleNumber`.
110
- */
111
- export function formatLocaleNumberInput(value: number, locale?: string): string {
112
- const serialized = String(value)
113
- const { decimal } = resolveLocaleNumberSeparators(locale)
114
- return decimal === '.' ? serialized : serialized.replace('.', decimal)
115
- }
116
-
117
104
  export function parseNumberWithDefault(
118
105
  raw: string | null | undefined,
119
106
  fallback: number,
@@ -15,12 +15,6 @@ type PathParamInfo = {
15
15
  optional?: boolean
16
16
  }
17
17
 
18
- type PathVariant = {
19
- path: string
20
- params: PathParamInfo[]
21
- omittedParam?: string
22
- }
23
-
24
18
  type ParameterLocation = 'query' | 'path' | 'header'
25
19
 
26
20
  export type JsonSchema = Record<string, unknown>
@@ -125,20 +119,6 @@ function normalizePath(path: string): { path: string; params: PathParamInfo[] }
125
119
  return { path: '/' + normalized, params }
126
120
  }
127
121
 
128
- function expandOptionalPathParams(normalized: { path: string; params: PathParamInfo[] }): PathVariant[] {
129
- const required = normalized.params.filter((param) => !param.optional)
130
- const optionalParam = normalized.params.find((param) => param.optional)
131
- const fullVariant: PathVariant = {
132
- path: normalized.path,
133
- params: normalized.params.map((param) => ({ ...param, optional: false })),
134
- }
135
- if (!optionalParam) return [fullVariant]
136
- const placeholder = `/{${optionalParam.name}}`
137
- if (!normalized.path.endsWith(placeholder)) return [fullVariant]
138
- const basePath = normalized.path.slice(0, -placeholder.length) || '/'
139
- return [fullVariant, { path: basePath, params: required, omittedParam: optionalParam.name }]
140
- }
141
-
142
122
  function unwrap(schema?: ZodTypeAny): {
143
123
  schema: ZodTypeAny | undefined
144
124
  optional: boolean
@@ -701,14 +681,14 @@ function buildParameters(
701
681
 
702
682
  if (location === 'path' && pathParamNames && pathParamNames.length) {
703
683
  const merged = mergePathParamSchemas(schema, pathParamNames)
704
- for (const { name, schema: paramSchema } of merged) {
684
+ for (const { name, schema: paramSchema, optional } of merged) {
705
685
  const jsonSchema = zodToJsonSchema(paramSchema)
706
686
  const example = generateExample(paramSchema)
707
687
  const description = extractZodDescription(paramSchema)
708
688
  params.push({
709
689
  name,
710
690
  in: 'path',
711
- required: true,
691
+ required: !optional,
712
692
  schema: jsonSchema ?? { type: 'string' },
713
693
  example,
714
694
  ...(description ? { description } : {}),
@@ -758,7 +738,7 @@ function buildParameters(
758
738
  }
759
739
 
760
740
  function mergePathParamSchemas(schema: ZodTypeAny | undefined, params: PathParamInfo[]) {
761
- const merged: Array<{ name: string; schema: ZodTypeAny | undefined }> = []
741
+ const merged: Array<{ name: string; schema: ZodTypeAny | undefined; optional: boolean }> = []
762
742
  const map: Record<string, ZodTypeAny> = {}
763
743
  if (schema) {
764
744
  const { schema: unwrapped } = unwrap(schema)
@@ -773,6 +753,7 @@ function mergePathParamSchemas(schema: ZodTypeAny | undefined, params: PathParam
773
753
  merged.push({
774
754
  name: param.name,
775
755
  schema: map[param.name],
756
+ optional: !!param.optional,
776
757
  })
777
758
  }
778
759
  return merged
@@ -1070,7 +1051,6 @@ export function buildOpenApiDocument(modules: Module[], options: OpenApiDocument
1070
1051
 
1071
1052
  const includeAccessControlMetadata = options.includeAccessControlMetadata !== false
1072
1053
  const tags = new Map<string, string | undefined>()
1073
- const derivedOperations = new Set<string>()
1074
1054
 
1075
1055
  for (const moduleEntry of modules) {
1076
1056
  const defaultTag = moduleEntry.info?.title ?? toTitle(moduleEntry.id)
@@ -1080,94 +1060,85 @@ export function buildOpenApiDocument(modules: Module[], options: OpenApiDocument
1080
1060
  for (const api of apis) {
1081
1061
  const routeDoc = collectRouteDoc(api, moduleEntry.id)
1082
1062
  const moduleTag = routeDoc?.tag ?? defaultTag
1063
+ const normalized = normalizePath((api as any).path ?? (api as any).path ?? '')
1064
+ const pathKey = normalized.path
1065
+ if (!doc.paths[pathKey]) doc.paths[pathKey] = {}
1083
1066
  const availableMethods: HttpMethod[] =
1084
1067
  'handlers' in api
1085
1068
  ? HTTP_METHODS.filter((method) => typeof (api as ModuleApiRouteFile).handlers?.[method] === 'function')
1086
1069
  : [api.method as HttpMethod]
1087
1070
 
1088
- for (const normalized of expandOptionalPathParams(normalizePath((api as any).path ?? ''))) {
1089
- const pathKey = normalized.path
1090
- if (!doc.paths[pathKey]) doc.paths[pathKey] = {}
1091
-
1092
- for (const method of availableMethods) {
1093
- const methodLower = method.toLowerCase() as Lowercase<HttpMethod>
1094
- const operationKey = `${methodLower} ${pathKey}`
1095
- const existing = doc.paths[pathKey][methodLower]
1096
- if (existing && (normalized.omittedParam || !derivedOperations.has(operationKey))) continue
1097
- if (normalized.omittedParam) derivedOperations.add(operationKey)
1098
- else derivedOperations.delete(operationKey)
1099
-
1100
- const metadata = 'handlers' in api ? (api as ModuleApiRouteFile).metadata?.[method] : undefined
1101
- const methodDoc = routeDoc?.methods?.[method]
1102
- const summary = methodDoc?.summary ?? routeDoc?.summary ?? `${method} ${pathKey}`
1103
- const baseDescription = methodDoc?.description ?? routeDoc?.description
1104
- const meta = metadata && typeof metadata === 'object' ? (metadata as Record<string, unknown>) : undefined
1105
- const requireFeatures = Array.isArray(meta?.['requireFeatures'])
1106
- ? (meta!['requireFeatures'] as string[])
1107
- : undefined
1108
- const requireRoles = Array.isArray(meta?.['requireRoles'])
1109
- ? (meta!['requireRoles'] as string[])
1110
- : undefined
1111
- const requireAuth = meta?.['requireAuth'] === true
1112
- const descriptionParts: string[] = []
1113
- if (baseDescription) descriptionParts.push(baseDescription)
1114
- if (includeAccessControlMetadata && Array.isArray(requireFeatures) && requireFeatures.length) {
1115
- descriptionParts.push(`Requires features: ${requireFeatures.join(', ')}`)
1116
- }
1117
- if (includeAccessControlMetadata && Array.isArray(requireRoles) && requireRoles.length) {
1118
- descriptionParts.push(`Requires roles: ${requireRoles.join(', ')}`)
1119
- }
1071
+ for (const method of availableMethods) {
1072
+ const methodLower = method.toLowerCase() as Lowercase<HttpMethod>
1073
+ const existing = doc.paths[pathKey][methodLower]
1074
+ if (existing) continue
1075
+
1076
+ const metadata = 'handlers' in api ? (api as ModuleApiRouteFile).metadata?.[method] : undefined
1077
+ const methodDoc = routeDoc?.methods?.[method]
1078
+ const summary = methodDoc?.summary ?? routeDoc?.summary ?? `${method} ${pathKey}`
1079
+ const baseDescription = methodDoc?.description ?? routeDoc?.description
1080
+ const meta = metadata && typeof metadata === 'object' ? (metadata as Record<string, unknown>) : undefined
1081
+ const requireFeatures = Array.isArray(meta?.['requireFeatures'])
1082
+ ? (meta!['requireFeatures'] as string[])
1083
+ : undefined
1084
+ const requireRoles = Array.isArray(meta?.['requireRoles'])
1085
+ ? (meta!['requireRoles'] as string[])
1086
+ : undefined
1087
+ const requireAuth = meta?.['requireAuth'] === true
1088
+ const descriptionParts: string[] = []
1089
+ if (baseDescription) descriptionParts.push(baseDescription)
1090
+ if (includeAccessControlMetadata && Array.isArray(requireFeatures) && requireFeatures.length) {
1091
+ descriptionParts.push(`Requires features: ${requireFeatures.join(', ')}`)
1092
+ }
1093
+ if (includeAccessControlMetadata && Array.isArray(requireRoles) && requireRoles.length) {
1094
+ descriptionParts.push(`Requires roles: ${requireRoles.join(', ')}`)
1095
+ }
1120
1096
 
1121
- const querySchema = methodDoc?.query
1122
- const pathSchema = methodDoc?.pathParams ?? routeDoc?.pathParams
1123
- const headerSchema = methodDoc?.headers
1124
- const requestBody = methodDoc?.requestBody
1125
- const examples = collectExamples(querySchema, requestBody?.schema, pathSchema, headerSchema, metadata)
1126
- const curlSample = buildCurlSample(
1127
- method,
1128
- pathKey,
1129
- normalized.params,
1130
- examples,
1131
- options.baseUrlForExamples ?? 'https://api.open-mercato.local',
1132
- metadata,
1133
- requestBody
1134
- )
1135
-
1136
- doc.paths[pathKey][methodLower] = {
1137
- operationId: methodDoc?.operationId
1138
- ? normalized.omittedParam
1139
- ? `${methodDoc.operationId}_without_${normalizeOperationIdSegment(normalized.omittedParam)}`
1140
- : methodDoc.operationId
1141
- : resolveOperationId(moduleEntry.id, pathKey, method),
1142
- summary,
1143
- description: descriptionParts.length ? descriptionParts.join('\n\n') : undefined,
1144
- tags: methodDoc?.tags ?? (moduleTag ? [moduleTag] : undefined),
1145
- deprecated: methodDoc?.deprecated,
1146
- externalDocs: methodDoc?.externalDocs,
1147
- parameters: [
1148
- ...buildParameters(pathSchema, 'path', normalized.params),
1149
- ...buildParameters(querySchema, 'query'),
1150
- ...buildParameters(headerSchema, 'header'),
1151
- ].filter(Boolean),
1152
- requestBody: buildRequestBody(requestBody),
1153
- responses: buildResponses(method, methodDoc?.responses, methodDoc?.errors, metadata),
1154
- security: buildSecurity(metadata, methodDoc, options.defaultSecurity),
1155
- 'x-codeSamples': methodDoc?.codeSamples ?? [
1156
- {
1157
- lang: 'curl',
1158
- label: 'cURL',
1159
- source: curlSample,
1160
- },
1161
- ],
1162
- ...(includeAccessControlMetadata && Array.isArray(requireFeatures) && requireFeatures.length
1163
- ? { 'x-require-features': requireFeatures }
1164
- : {}),
1165
- ...(includeAccessControlMetadata && Array.isArray(requireRoles) && requireRoles.length
1166
- ? { 'x-require-roles': requireRoles }
1167
- : {}),
1168
- ...(requireAuth ? { 'x-require-auth': true } : {}),
1169
- ...(methodDoc?.extensions ?? {}),
1170
- }
1097
+ const querySchema = methodDoc?.query
1098
+ const pathSchema = methodDoc?.pathParams ?? routeDoc?.pathParams
1099
+ const headerSchema = methodDoc?.headers
1100
+ const requestBody = methodDoc?.requestBody
1101
+ const examples = collectExamples(querySchema, requestBody?.schema, pathSchema, headerSchema, metadata)
1102
+ const curlSample = buildCurlSample(
1103
+ method,
1104
+ pathKey,
1105
+ normalized.params,
1106
+ examples,
1107
+ options.baseUrlForExamples ?? 'https://api.open-mercato.local',
1108
+ metadata,
1109
+ requestBody
1110
+ )
1111
+
1112
+ doc.paths[pathKey][methodLower] = {
1113
+ operationId: methodDoc?.operationId ?? resolveOperationId(moduleEntry.id, pathKey, method),
1114
+ summary,
1115
+ description: descriptionParts.length ? descriptionParts.join('\n\n') : undefined,
1116
+ tags: methodDoc?.tags ?? (moduleTag ? [moduleTag] : undefined),
1117
+ deprecated: methodDoc?.deprecated,
1118
+ externalDocs: methodDoc?.externalDocs,
1119
+ parameters: [
1120
+ ...buildParameters(pathSchema, 'path', normalized.params),
1121
+ ...buildParameters(querySchema, 'query'),
1122
+ ...buildParameters(headerSchema, 'header'),
1123
+ ].filter(Boolean),
1124
+ requestBody: buildRequestBody(requestBody),
1125
+ responses: buildResponses(method, methodDoc?.responses, methodDoc?.errors, metadata),
1126
+ security: buildSecurity(metadata, methodDoc, options.defaultSecurity),
1127
+ 'x-codeSamples': methodDoc?.codeSamples ?? [
1128
+ {
1129
+ lang: 'curl',
1130
+ label: 'cURL',
1131
+ source: curlSample,
1132
+ },
1133
+ ],
1134
+ ...(includeAccessControlMetadata && Array.isArray(requireFeatures) && requireFeatures.length
1135
+ ? { 'x-require-features': requireFeatures }
1136
+ : {}),
1137
+ ...(includeAccessControlMetadata && Array.isArray(requireRoles) && requireRoles.length
1138
+ ? { 'x-require-roles': requireRoles }
1139
+ : {}),
1140
+ ...(requireAuth ? { 'x-require-auth': true } : {}),
1141
+ ...(methodDoc?.extensions ?? {}),
1171
1142
  }
1172
1143
  }
1173
1144
  }