@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +4 -4
- package/dist/lib/auth/server.js +1 -1
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/dynamicLoader.js +6 -46
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/bootstrap/factory.js +3 -4
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/catalog-visibility/index.js +0 -4
- package/dist/lib/catalog-visibility/index.js.map +2 -2
- package/dist/lib/commands/command-bus.js +13 -45
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/types.js.map +2 -2
- package/dist/lib/crud/exporters.js +1 -1
- package/dist/lib/crud/exporters.js.map +2 -2
- package/dist/lib/crud/ids.js +1 -13
- package/dist/lib/crud/ids.js.map +2 -2
- package/dist/lib/db/pg-errors.js +0 -14
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/localization/index.js +1 -9
- package/dist/lib/localization/index.js.map +2 -2
- package/dist/lib/localization/translatable-fields.js +0 -26
- package/dist/lib/localization/translatable-fields.js.map +2 -2
- package/dist/lib/number.js +0 -6
- package/dist/lib/number.js.map +2 -2
- package/dist/lib/openapi/generator.js +67 -84
- package/dist/lib/openapi/generator.js.map +2 -2
- package/dist/lib/query/engine.js +10 -45
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/join-utils.js +1 -1
- package/dist/lib/query/join-utils.js.map +2 -2
- package/dist/lib/query/types.js.map +2 -2
- package/dist/lib/url.js +0 -24
- package/dist/lib/url.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +3 -7
- package/dist/lib/webhooks/body.js.map +2 -2
- package/dist/lib/webhooks/index.js +0 -2
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/modules/integrations/types.js.map +2 -2
- package/dist/modules/notifications/types.js +0 -4
- package/dist/modules/notifications/types.js.map +3 -3
- package/dist/modules/payment_gateways/types.js +1 -14
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/search.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +3 -14
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/package.json +3 -10
- package/src/lib/__tests__/number.test.ts +1 -24
- package/src/lib/auth/__tests__/server.test.ts +0 -14
- package/src/lib/auth/server.ts +1 -1
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/factory.test.ts +6 -18
- package/src/lib/bootstrap/dynamicLoader.ts +5 -63
- package/src/lib/bootstrap/factory.ts +3 -8
- package/src/lib/bootstrap/types.ts +1 -5
- package/src/lib/catalog-visibility/index.ts +0 -2
- package/src/lib/commands/__tests__/command-bus.test.ts +1 -100
- package/src/lib/commands/command-bus.ts +10 -42
- package/src/lib/commands/command-interceptor.ts +0 -4
- package/src/lib/commands/types.ts +0 -14
- package/src/lib/crud/__tests__/exporters.test.ts +0 -18
- package/src/lib/crud/__tests__/ids.test.ts +1 -34
- package/src/lib/crud/exporters.ts +1 -1
- package/src/lib/crud/ids.ts +0 -24
- package/src/lib/db/__tests__/pg-errors.test.ts +1 -68
- package/src/lib/db/pg-errors.ts +0 -35
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -23
- package/src/lib/localization/__tests__/translatable-fields.test.ts +0 -17
- package/src/lib/localization/index.ts +1 -8
- package/src/lib/localization/translatable-fields.ts +0 -46
- package/src/lib/number.ts +0 -13
- package/src/lib/openapi/generator.ts +77 -106
- package/src/lib/query/engine.ts +8 -58
- package/src/lib/query/join-utils.ts +1 -1
- package/src/lib/query/types.ts +1 -13
- package/src/lib/url.ts +0 -30
- package/src/lib/webhooks/__tests__/body.test.ts +0 -48
- package/src/lib/webhooks/body.ts +4 -11
- package/src/lib/webhooks/index.ts +0 -1
- package/src/modules/integrations/types.ts +0 -6
- package/src/modules/notifications/types.ts +0 -7
- package/src/modules/payment_gateways/__tests__/types.test.ts +0 -44
- package/src/modules/payment_gateways/types.ts +6 -52
- package/src/modules/registry.ts +0 -5
- package/src/modules/search.ts +0 -39
- package/src/modules/widgets/injection-loader.ts +3 -33
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +0 -6
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +0 -7
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js +0 -62
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +0 -7
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +0 -33
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +0 -7
- package/dist/lib/catalog-visibility/hashEffectiveScope.js +0 -9
- package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +0 -7
- package/dist/lib/db/advisoryLock.js +0 -172
- package/dist/lib/db/advisoryLock.js.map +0 -7
- package/dist/lib/query/overlap.js +0 -58
- package/dist/lib/query/overlap.js.map +0 -7
- package/dist/modules/notifications/safeHref.js +0 -20
- package/dist/modules/notifications/safeHref.js.map +0 -7
- package/src/lib/__tests__/url.cross-site.test.ts +0 -140
- package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +0 -141
- package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +0 -174
- package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +0 -3
- package/src/lib/bootstrap/next-subpath-resolve-hook.ts +0 -130
- package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +0 -121
- package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +0 -41
- package/src/lib/catalog-visibility/hashEffectiveScope.ts +0 -8
- package/src/lib/db/__tests__/advisoryLock.integration.test.ts +0 -343
- package/src/lib/db/__tests__/advisoryLock.test.ts +0 -564
- package/src/lib/db/advisoryLock.ts +0 -271
- package/src/lib/openapi/__tests__/optional-path-params.test.ts +0 -102
- package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +0 -89
- package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +0 -79
- package/src/lib/query/__tests__/overlap.test.ts +0 -219
- package/src/lib/query/overlap.ts +0 -106
- package/src/modules/notifications/safeHref.ts +0 -19
- 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) =>
|
|
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) => {
|
package/src/lib/crud/ids.ts
CHANGED
|
@@ -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)
|
package/src/lib/db/pg-errors.ts
CHANGED
|
@@ -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:
|
|
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
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
}
|