@lucaapp/service-utils 5.24.5 → 5.26.0
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/dist/lib/api/api.js +1 -0
- package/dist/lib/api/index.d.ts +1 -0
- package/dist/lib/api/index.js +7 -1
- package/dist/lib/api/openapi.d.ts +18 -0
- package/dist/lib/api/openapi.js +133 -4
- package/dist/lib/kafka/events/payment.d.ts +13 -1
- package/dist/lib/negotiator/language.d.ts +38 -2
- package/dist/lib/negotiator/language.js +76 -3
- package/dist/lib/validation.d.ts +2 -1
- package/dist/lib/validation.js +16 -2
- package/package.json +1 -1
package/dist/lib/api/api.js
CHANGED
package/dist/lib/api/index.d.ts
CHANGED
package/dist/lib/api/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.extendZodWithOpenApi = void 0;
|
|
17
|
+
exports.hoistCombinatorRequiredFields = exports.extendZodWithOpenApi = void 0;
|
|
18
18
|
var zod_to_openapi_1 = require("@asteasolutions/zod-to-openapi");
|
|
19
19
|
Object.defineProperty(exports, "extendZodWithOpenApi", { enumerable: true, get: function () { return zod_to_openapi_1.extendZodWithOpenApi; } });
|
|
20
20
|
__exportStar(require("./api"), exports);
|
|
@@ -22,3 +22,9 @@ __exportStar(require("./endpoint"), exports);
|
|
|
22
22
|
__exportStar(require("./middleware"), exports);
|
|
23
23
|
__exportStar(require("./response"), exports);
|
|
24
24
|
__exportStar(require("./send"), exports);
|
|
25
|
+
// Normalizes an OpenAPI spec's anyOf/allOf combinators the same way
|
|
26
|
+
// generateOpenAPISpec() does, so a frozen/historical spec (a POS compat
|
|
27
|
+
// baseline that predates this generator's fix) can be diffed against a
|
|
28
|
+
// current spec on equal terms. See openapi.ts for what "normalize" means.
|
|
29
|
+
var openapi_1 = require("./openapi");
|
|
30
|
+
Object.defineProperty(exports, "hoistCombinatorRequiredFields", { enumerable: true, get: function () { return openapi_1.hoistCombinatorRequiredFields; } });
|
|
@@ -7,5 +7,23 @@ export declare const forcePathParamsRequired: (spec: {
|
|
|
7
7
|
parameters?: Record<string, unknown>;
|
|
8
8
|
};
|
|
9
9
|
}) => void;
|
|
10
|
+
/**
|
|
11
|
+
* zod-to-openapi represents a same-status multi-error response as `anyOf`, and
|
|
12
|
+
* `.extend()` on a registered/ref'd schema as `allOf: [$ref, inline]`. Either
|
|
13
|
+
* shape hides required/property information that used to be flat at the parent
|
|
14
|
+
* level inside the combinator, which oasdiff's breaking-change diff cannot see
|
|
15
|
+
* through — it reports every such change as fields being removed, even when the
|
|
16
|
+
* change was purely additive. This walks the finished spec and restores the
|
|
17
|
+
* safely-hoistable subset of that information to the parent schema (see
|
|
18
|
+
* hoistAnyOf / hoistAllOf for what "safe" means for each combinator — they are
|
|
19
|
+
* NOT the same rule). Ref topology is untouched: refs are only read to compute
|
|
20
|
+
* what's safe to hoist, never rewritten.
|
|
21
|
+
*/
|
|
22
|
+
export declare const hoistCombinatorRequiredFields: (spec: {
|
|
23
|
+
paths?: Record<string, unknown>;
|
|
24
|
+
components?: {
|
|
25
|
+
schemas?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
}) => void;
|
|
10
28
|
export declare const registerEndpoint: (registry: OpenAPIRegistry, method: HttpMethod, prefixPath: string, path: string, summary: string, options: EndpointOptions<any, any, any, any, any, any>, tags: string[], debug: boolean) => void;
|
|
11
29
|
export {};
|
package/dist/lib/api/openapi.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.registerEndpoint = exports.forcePathParamsRequired = void 0;
|
|
3
|
+
exports.registerEndpoint = exports.hoistCombinatorRequiredFields = exports.forcePathParamsRequired = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const http_1 = require("./types/http");
|
|
6
6
|
const mapExpressParamsToOpenAPIParams = (path) => path.replaceAll(/\:([^\:\/]+)/g, '{$1}');
|
|
7
7
|
const buildResponseConfig = (responseOptions) => {
|
|
8
8
|
const responses = {};
|
|
9
9
|
responseOptions.forEach(response => {
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
10
|
+
// Raw responses that declare no real schema (rawResponseEnd()'s default,
|
|
11
|
+
// schema: z.void()) end the request outside the schema layer and carry
|
|
12
|
+
// the placeholder status 0, which is not a valid OpenAPI response key —
|
|
13
|
+
// skip them; the fallback below documents a generic 200 instead. A route
|
|
14
|
+
// that sets isRawResponseEnd but ALSO declares a real schema (bypassing
|
|
15
|
+
// the response() helper for streaming/binary output while still telling
|
|
16
|
+
// callers what the body looks like) must still be documented normally —
|
|
17
|
+
// skipping unconditionally silently drops real, intentional schema
|
|
18
|
+
// information (LUCA-35888).
|
|
19
|
+
if (response.isRawResponseEnd && response.schema instanceof zod_1.z.ZodVoid) {
|
|
13
20
|
return;
|
|
14
21
|
}
|
|
15
22
|
const isAlreadyPresent = !!responses[String(response.status)];
|
|
@@ -81,6 +88,128 @@ const forcePathParamsRequired = (spec) => {
|
|
|
81
88
|
});
|
|
82
89
|
};
|
|
83
90
|
exports.forcePathParamsRequired = forcePathParamsRequired;
|
|
91
|
+
const resolveSchemaRef = (node, schemas) => {
|
|
92
|
+
if (node && typeof node === 'object' && typeof node.$ref === 'string') {
|
|
93
|
+
return schemas[node.$ref.split('/').pop()];
|
|
94
|
+
}
|
|
95
|
+
return node;
|
|
96
|
+
};
|
|
97
|
+
// Two same-named properties across anyOf branches are only safe to assert at the
|
|
98
|
+
// parent level if they agree on primitive type — an enum/const literal that differs
|
|
99
|
+
// per branch (e.g. a discriminating `error` code) must NOT leak into the hoisted
|
|
100
|
+
// type, or the parent schema would wrongly forbid the other branches' literals.
|
|
101
|
+
const widenAnyOfProperty = (fragments) => {
|
|
102
|
+
if (fragments.some(f => f === undefined || typeof f !== 'object')) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const types = new Set(fragments.map(f => f.type));
|
|
106
|
+
if (types.size !== 1 || fragments[0].type === undefined) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const widened = { type: fragments[0].type };
|
|
110
|
+
if (fragments.some(f => f.nullable)) {
|
|
111
|
+
widened.nullable = true;
|
|
112
|
+
}
|
|
113
|
+
return widened;
|
|
114
|
+
};
|
|
115
|
+
// anyOf is OR — an object only ever matches ONE branch, so a field is safe to
|
|
116
|
+
// assert at the parent only if every branch requires it, and its type is widened
|
|
117
|
+
// to what every branch agrees on (see widenAnyOfProperty). A field required in
|
|
118
|
+
// some but not all branches must stay inside the combinator: hoisting it would
|
|
119
|
+
// assert something false for the branches that lack it, and would wrongly
|
|
120
|
+
// suppress a real breaking-change finding on that branch in the future.
|
|
121
|
+
const hoistAnyOf = (node, schemas) => {
|
|
122
|
+
const branches = node.anyOf.map(b => resolveSchemaRef(b, schemas));
|
|
123
|
+
if (branches.length === 0 ||
|
|
124
|
+
branches.some(b => !b || typeof b !== 'object')) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const requiredSets = branches.map(b => new Set(b.required || []));
|
|
128
|
+
const commonRequiredNames = [...requiredSets[0]].filter(name => requiredSets.every(set => set.has(name)));
|
|
129
|
+
const hoistedProperties = {};
|
|
130
|
+
const hoistedRequired = [];
|
|
131
|
+
for (const name of commonRequiredNames) {
|
|
132
|
+
const fragments = branches.map(b => b.properties?.[name]);
|
|
133
|
+
const widened = widenAnyOfProperty(fragments);
|
|
134
|
+
if (widened) {
|
|
135
|
+
hoistedProperties[name] = widened;
|
|
136
|
+
hoistedRequired.push(name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (hoistedRequired.length === 0) {
|
|
140
|
+
return; // nothing intersects safely — leave the combinator untouched, no-op
|
|
141
|
+
}
|
|
142
|
+
node.type = 'object';
|
|
143
|
+
node.properties = { ...(node.properties || {}), ...hoistedProperties };
|
|
144
|
+
node.required = [...new Set([...(node.required || []), ...hoistedRequired])];
|
|
145
|
+
};
|
|
146
|
+
// allOf is AND — an object must match every branch simultaneously, so unioning
|
|
147
|
+
// every branch's properties/required to the parent is always safe: nothing is
|
|
148
|
+
// asserted that isn't already true of any value satisfying the allOf. No type
|
|
149
|
+
// widening needed (unlike anyOf) since branches aren't alternatives.
|
|
150
|
+
const hoistAllOf = (node, schemas) => {
|
|
151
|
+
const branches = node.allOf.map(b => resolveSchemaRef(b, schemas));
|
|
152
|
+
if (branches.some(b => !b || typeof b !== 'object')) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const unionProperties = {};
|
|
156
|
+
const unionRequired = new Set();
|
|
157
|
+
for (const branch of branches) {
|
|
158
|
+
for (const [key, value] of Object.entries(branch.properties || {})) {
|
|
159
|
+
if (!(key in unionProperties)) {
|
|
160
|
+
unionProperties[key] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const name of branch.required || []) {
|
|
164
|
+
unionRequired.add(name);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (Object.keys(unionProperties).length === 0) {
|
|
168
|
+
return; // nothing to hoist — leave the combinator untouched, no-op
|
|
169
|
+
}
|
|
170
|
+
node.type = 'object';
|
|
171
|
+
node.properties = { ...(node.properties || {}), ...unionProperties };
|
|
172
|
+
node.required = [...new Set([...(node.required || []), ...unionRequired])];
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* zod-to-openapi represents a same-status multi-error response as `anyOf`, and
|
|
176
|
+
* `.extend()` on a registered/ref'd schema as `allOf: [$ref, inline]`. Either
|
|
177
|
+
* shape hides required/property information that used to be flat at the parent
|
|
178
|
+
* level inside the combinator, which oasdiff's breaking-change diff cannot see
|
|
179
|
+
* through — it reports every such change as fields being removed, even when the
|
|
180
|
+
* change was purely additive. This walks the finished spec and restores the
|
|
181
|
+
* safely-hoistable subset of that information to the parent schema (see
|
|
182
|
+
* hoistAnyOf / hoistAllOf for what "safe" means for each combinator — they are
|
|
183
|
+
* NOT the same rule). Ref topology is untouched: refs are only read to compute
|
|
184
|
+
* what's safe to hoist, never rewritten.
|
|
185
|
+
*/
|
|
186
|
+
const hoistCombinatorRequiredFields = (spec) => {
|
|
187
|
+
const schemas = (spec.components?.schemas || {});
|
|
188
|
+
const visit = (node) => {
|
|
189
|
+
if (!node || typeof node !== 'object') {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (Array.isArray(node)) {
|
|
193
|
+
node.forEach(visit);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const obj = node;
|
|
197
|
+
if (Array.isArray(obj.anyOf)) {
|
|
198
|
+
obj.anyOf.forEach(visit);
|
|
199
|
+
hoistAnyOf(obj, schemas);
|
|
200
|
+
}
|
|
201
|
+
else if (Array.isArray(obj.allOf)) {
|
|
202
|
+
obj.allOf.forEach(visit);
|
|
203
|
+
hoistAllOf(obj, schemas);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
Object.values(obj).forEach(visit);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
visit(spec.paths || {});
|
|
210
|
+
visit(schemas);
|
|
211
|
+
};
|
|
212
|
+
exports.hoistCombinatorRequiredFields = hoistCombinatorRequiredFields;
|
|
84
213
|
const registerEndpoint = (registry, method, prefixPath, path, summary,
|
|
85
214
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
215
|
options, tags, debug) => {
|
|
@@ -43,6 +43,18 @@ export type Payment = PaymentBase & {
|
|
|
43
43
|
};
|
|
44
44
|
export declare const PAYMENT_EVENT_VERSION = 2;
|
|
45
45
|
export type PaymentEventVersion = typeof PAYMENT_EVENT_VERSION;
|
|
46
|
+
/**
|
|
47
|
+
* Stable acquirer snapshot fields for CARD fiscalization (LUCA-35099).
|
|
48
|
+
* Populated by backend-pay when available on V3Payment at produce time.
|
|
49
|
+
* Fees are intentionally omitted — pay remains source of truth at report/close.
|
|
50
|
+
*/
|
|
51
|
+
export type PaymentEventCardAcquirerFields = {
|
|
52
|
+
cardLast4?: string | null;
|
|
53
|
+
authCode?: string | null;
|
|
54
|
+
cardScheme?: string | null;
|
|
55
|
+
cardBrandLabel?: string | null;
|
|
56
|
+
terminalId?: string | null;
|
|
57
|
+
};
|
|
46
58
|
export type PaymentEvent = {
|
|
47
59
|
version: PaymentEventVersion;
|
|
48
60
|
uuid: string;
|
|
@@ -60,7 +72,7 @@ export type PaymentEvent = {
|
|
|
60
72
|
orderId?: string;
|
|
61
73
|
statusCompleteAt?: Date | null;
|
|
62
74
|
merchantDetails?: PaymentMerchantDetails;
|
|
63
|
-
};
|
|
75
|
+
} & PaymentEventCardAcquirerFields;
|
|
64
76
|
export declare const consumerPayAmount: (event: PaymentEvent) => number;
|
|
65
77
|
export declare const operatorReceivableAmount: (event: PaymentEvent) => number;
|
|
66
78
|
export type PaymentMerchantDetails = {
|
|
@@ -18,9 +18,44 @@ type LanguageOptions = typeof AVAILABLE_LANGUAGES[number];
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export declare const isInternalLanguage: (lang: string) => lang is InternalLanguages;
|
|
21
|
+
/**
|
|
22
|
+
* Canonicalises a language code for storage: `DE-de` -> `de-DE`.
|
|
23
|
+
* Returns null when the value is absent or not a plausible language code.
|
|
24
|
+
*
|
|
25
|
+
* The region is dropped when keeping it would not fit the `STRING(5)` language
|
|
26
|
+
* columns, so `fil-PH` stores as `fil` rather than overflowing or being thrown
|
|
27
|
+
* away — the language is the part we care about.
|
|
28
|
+
*
|
|
29
|
+
* Unlike {@link toInternalLanguage} this does NOT narrow to the languages our
|
|
30
|
+
* templates support — storage keeps what the user actually asked for, and
|
|
31
|
+
* narrowing happens on retrieval.
|
|
32
|
+
*/
|
|
33
|
+
export declare const normalizeStoredLanguage: (lang?: string | null) => string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Reads the caller's preferred language without narrowing it to the languages
|
|
36
|
+
* our templates support. Use for values that get persisted; use
|
|
37
|
+
* {@link getPreferredLanguageFromRequestHeaders} when the result immediately
|
|
38
|
+
* selects a template.
|
|
39
|
+
*
|
|
40
|
+
* Returns null when no usable `Accept-Language` was sent, so callers can leave
|
|
41
|
+
* the column untouched rather than writing a guessed default.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* getStoredLanguageFromRequestHeaders({ 'accept-language': 'fr-FR,fr;q=0.9' }) // 'fr-FR'
|
|
46
|
+
* getStoredLanguageFromRequestHeaders({}) // null
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare const getStoredLanguageFromRequestHeaders: (headers?: Record<string, string>) => string | null;
|
|
50
|
+
export declare const getStoredLanguageFromRequest: (request: Request) => string | null;
|
|
21
51
|
/**
|
|
22
52
|
* Safely converts a language code to an InternalLanguage.
|
|
23
|
-
*
|
|
53
|
+
* The region subtag is dropped before matching, so `de-DE` resolves to `de`
|
|
54
|
+
* rather than falling back. If the language is not supported, falls back to 'en'.
|
|
55
|
+
*
|
|
56
|
+
* This is the retrieval-side counterpart to {@link normalizeStoredLanguage}:
|
|
57
|
+
* columns may hold any language the user asked for, and every read that feeds
|
|
58
|
+
* a template goes through here.
|
|
24
59
|
*
|
|
25
60
|
* @param lang - Language code to convert
|
|
26
61
|
* @returns The language if supported, otherwise 'en'
|
|
@@ -28,11 +63,12 @@ export declare const isInternalLanguage: (lang: string) => lang is InternalLangu
|
|
|
28
63
|
* @example
|
|
29
64
|
* ```typescript
|
|
30
65
|
* toInternalLanguage('de') // returns 'de'
|
|
66
|
+
* toInternalLanguage('de-DE') // returns 'de'
|
|
31
67
|
* toInternalLanguage('en') // returns 'en'
|
|
32
68
|
* toInternalLanguage('fr') // returns 'en' (fallback)
|
|
33
69
|
* ```
|
|
34
70
|
*/
|
|
35
|
-
export declare const toInternalLanguage: (lang
|
|
71
|
+
export declare const toInternalLanguage: (lang?: string | null) => InternalLanguages;
|
|
36
72
|
export declare const parseAcceptLanguageHeader: (header?: string) => LanguageOptions;
|
|
37
73
|
/**
|
|
38
74
|
* @deprecated Use getPreferredLanguageFromRequestHeaders instead.
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getPreferredLanguageFromRequestHeaders = exports.getPreferredLanguageFromRequest = exports.parseAcceptLanguageHeader = exports.toInternalLanguage = exports.isInternalLanguage = exports.AVAILABLE_LANGUAGES = exports.DEFAULT_LANGUAGE_WHEN_HEADER = exports.DEFAULT_LANGUAGE_WHEN_NO_HEADER = void 0;
|
|
6
|
+
exports.getPreferredLanguageFromRequestHeaders = exports.getPreferredLanguageFromRequest = exports.parseAcceptLanguageHeader = exports.toInternalLanguage = exports.getStoredLanguageFromRequest = exports.getStoredLanguageFromRequestHeaders = exports.normalizeStoredLanguage = exports.isInternalLanguage = exports.AVAILABLE_LANGUAGES = exports.DEFAULT_LANGUAGE_WHEN_HEADER = exports.DEFAULT_LANGUAGE_WHEN_NO_HEADER = void 0;
|
|
7
7
|
const negotiator_1 = __importDefault(require("negotiator"));
|
|
8
8
|
exports.DEFAULT_LANGUAGE_WHEN_NO_HEADER = 'de';
|
|
9
9
|
exports.DEFAULT_LANGUAGE_WHEN_HEADER = 'en';
|
|
@@ -25,9 +25,80 @@ const isInternalLanguage = (lang) => {
|
|
|
25
25
|
return exports.AVAILABLE_LANGUAGES.includes(lang);
|
|
26
26
|
};
|
|
27
27
|
exports.isInternalLanguage = isInternalLanguage;
|
|
28
|
+
/**
|
|
29
|
+
* Shape of a language code we are willing to persist: a 2-3 letter primary
|
|
30
|
+
* subtag, optionally followed by a 2-letter region. Deliberately narrower than
|
|
31
|
+
* full BCP 47 so an attacker-controlled `Accept-Language` cannot write junk;
|
|
32
|
+
* script subtags (`zh-Hant`) are rejected rather than truncated.
|
|
33
|
+
*/
|
|
34
|
+
const STORED_LANGUAGE_PATTERN = /^[a-z]{2,3}(-[a-z]{2})?$/i;
|
|
35
|
+
/**
|
|
36
|
+
* Longest value the `language` columns accept — they are `STRING(5)`, which
|
|
37
|
+
* fits `pt-BR` but not a 3-letter primary subtag with a region (`fil-PH`).
|
|
38
|
+
*/
|
|
39
|
+
const STORED_LANGUAGE_MAX_LENGTH = 5;
|
|
40
|
+
/**
|
|
41
|
+
* Canonicalises a language code for storage: `DE-de` -> `de-DE`.
|
|
42
|
+
* Returns null when the value is absent or not a plausible language code.
|
|
43
|
+
*
|
|
44
|
+
* The region is dropped when keeping it would not fit the `STRING(5)` language
|
|
45
|
+
* columns, so `fil-PH` stores as `fil` rather than overflowing or being thrown
|
|
46
|
+
* away — the language is the part we care about.
|
|
47
|
+
*
|
|
48
|
+
* Unlike {@link toInternalLanguage} this does NOT narrow to the languages our
|
|
49
|
+
* templates support — storage keeps what the user actually asked for, and
|
|
50
|
+
* narrowing happens on retrieval.
|
|
51
|
+
*/
|
|
52
|
+
const normalizeStoredLanguage = (lang) => {
|
|
53
|
+
const trimmed = lang?.trim();
|
|
54
|
+
if (!trimmed || !STORED_LANGUAGE_PATTERN.test(trimmed)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const [primary, region] = trimmed.split('-');
|
|
58
|
+
const canonical = region
|
|
59
|
+
? `${primary.toLowerCase()}-${region.toUpperCase()}`
|
|
60
|
+
: primary.toLowerCase();
|
|
61
|
+
return canonical.length > STORED_LANGUAGE_MAX_LENGTH
|
|
62
|
+
? primary.toLowerCase()
|
|
63
|
+
: canonical;
|
|
64
|
+
};
|
|
65
|
+
exports.normalizeStoredLanguage = normalizeStoredLanguage;
|
|
66
|
+
/**
|
|
67
|
+
* Reads the caller's preferred language without narrowing it to the languages
|
|
68
|
+
* our templates support. Use for values that get persisted; use
|
|
69
|
+
* {@link getPreferredLanguageFromRequestHeaders} when the result immediately
|
|
70
|
+
* selects a template.
|
|
71
|
+
*
|
|
72
|
+
* Returns null when no usable `Accept-Language` was sent, so callers can leave
|
|
73
|
+
* the column untouched rather than writing a guessed default.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* getStoredLanguageFromRequestHeaders({ 'accept-language': 'fr-FR,fr;q=0.9' }) // 'fr-FR'
|
|
78
|
+
* getStoredLanguageFromRequestHeaders({}) // null
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
const getStoredLanguageFromRequestHeaders = (headers) => {
|
|
82
|
+
const acceptLanguageHeader = headers?.['accept-language'];
|
|
83
|
+
if (!acceptLanguageHeader) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// Take the highest-priority entry we can actually store, rather than giving
|
|
87
|
+
// up when the first one happens to be unstorable (e.g. `zh-Hant-TW, zh-TW`).
|
|
88
|
+
const negotiator = new negotiator_1.default({ headers });
|
|
89
|
+
return (negotiator.languages().map(exports.normalizeStoredLanguage).find(Boolean) ?? null);
|
|
90
|
+
};
|
|
91
|
+
exports.getStoredLanguageFromRequestHeaders = getStoredLanguageFromRequestHeaders;
|
|
92
|
+
const getStoredLanguageFromRequest = (request) => (0, exports.getStoredLanguageFromRequestHeaders)(request.headers);
|
|
93
|
+
exports.getStoredLanguageFromRequest = getStoredLanguageFromRequest;
|
|
28
94
|
/**
|
|
29
95
|
* Safely converts a language code to an InternalLanguage.
|
|
30
|
-
*
|
|
96
|
+
* The region subtag is dropped before matching, so `de-DE` resolves to `de`
|
|
97
|
+
* rather than falling back. If the language is not supported, falls back to 'en'.
|
|
98
|
+
*
|
|
99
|
+
* This is the retrieval-side counterpart to {@link normalizeStoredLanguage}:
|
|
100
|
+
* columns may hold any language the user asked for, and every read that feeds
|
|
101
|
+
* a template goes through here.
|
|
31
102
|
*
|
|
32
103
|
* @param lang - Language code to convert
|
|
33
104
|
* @returns The language if supported, otherwise 'en'
|
|
@@ -35,12 +106,14 @@ exports.isInternalLanguage = isInternalLanguage;
|
|
|
35
106
|
* @example
|
|
36
107
|
* ```typescript
|
|
37
108
|
* toInternalLanguage('de') // returns 'de'
|
|
109
|
+
* toInternalLanguage('de-DE') // returns 'de'
|
|
38
110
|
* toInternalLanguage('en') // returns 'en'
|
|
39
111
|
* toInternalLanguage('fr') // returns 'en' (fallback)
|
|
40
112
|
* ```
|
|
41
113
|
*/
|
|
42
114
|
const toInternalLanguage = (lang) => {
|
|
43
|
-
|
|
115
|
+
const primary = lang?.trim().split('-')[0]?.toLowerCase();
|
|
116
|
+
return primary && (0, exports.isInternalLanguage)(primary) ? primary : 'en';
|
|
44
117
|
};
|
|
45
118
|
exports.toInternalLanguage = toInternalLanguage;
|
|
46
119
|
const parseAcceptLanguageHeader = (header) => {
|
package/dist/lib/validation.d.ts
CHANGED
|
@@ -209,7 +209,7 @@ declare const z: {
|
|
|
209
209
|
nullablePhoneNumber: () => ZodEffects<ZodEffects<ZodString, string, string>, string | null, string>;
|
|
210
210
|
strongPassword: () => ZodEffects<ZodString, string, string>;
|
|
211
211
|
zipCode: (locale?: validator.PostalCodeLocale) => ZodEffects<ZodString, string>;
|
|
212
|
-
countryCode: () => ZodString
|
|
212
|
+
countryCode: () => ZodEffects<ZodString, string, string>;
|
|
213
213
|
moneyDecimalString: (locale?: string) => ZodEffects<ZodString, string, string>;
|
|
214
214
|
moneyFromFloat: () => ZodEffects<ZodEffects<ZodNumber>, Money>;
|
|
215
215
|
moneyFromInt: () => ZodEffects<ZodNumber, Money>;
|
|
@@ -386,5 +386,6 @@ declare const z: {
|
|
|
386
386
|
Logic?: any;
|
|
387
387
|
}>;
|
|
388
388
|
};
|
|
389
|
+
export declare const isPostalCodeForCountry: (postalCode: string, countryCode: string) => boolean;
|
|
389
390
|
export * from 'zod';
|
|
390
391
|
export { z };
|
package/dist/lib/validation.js
CHANGED
|
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.z = exports.preprocessBooleanFromString = exports.preprocessBooleanFromQueryString = exports.preprocessNumberFromQueryString = exports.preprocessArrayFromQueryString = void 0;
|
|
21
|
+
exports.z = exports.isPostalCodeForCountry = exports.preprocessBooleanFromString = exports.preprocessBooleanFromQueryString = exports.preprocessNumberFromQueryString = exports.preprocessArrayFromQueryString = void 0;
|
|
22
22
|
const url_1 = require("url");
|
|
23
23
|
const zod_1 = require("zod");
|
|
24
24
|
const validator_1 = __importDefault(require("validator"));
|
|
@@ -36,6 +36,7 @@ const ALPHANUMERIC_REGEX = /^[\da-z]+$/i;
|
|
|
36
36
|
const ONBOARDING_BUSINESS_ENTITY_REGEX = /^[\w !&'()*+,./£À-ÿāăąćĉċčđēėęěĝğģĥħĩīįİıĵķĸĺļłńņōőœŗřśŝşšţŦũūŭůűųŵŷźżžơưếệ''-]+$/i;
|
|
37
37
|
const ONBOARDING_NAME_REGEX = /^[\s'.A-Za-z£À-žơưếệ''-]+$/;
|
|
38
38
|
const COUNTRY_CODE_REGEX = /^[A-Z]{2}$/;
|
|
39
|
+
const HAS_LETTER_REGEX = /\p{L}/u;
|
|
39
40
|
const PASSWORD_REQUIREMENTS = {
|
|
40
41
|
minLength: 9,
|
|
41
42
|
minNumbers: 1,
|
|
@@ -108,6 +109,7 @@ const z = {
|
|
|
108
109
|
safeBusinessStringForOnboarding: () => zod_1.z
|
|
109
110
|
.string()
|
|
110
111
|
.regex(ONBOARDING_BUSINESS_ENTITY_REGEX)
|
|
112
|
+
.regex(HAS_LETTER_REGEX)
|
|
111
113
|
.max(255)
|
|
112
114
|
.trim()
|
|
113
115
|
.min(1)
|
|
@@ -119,6 +121,7 @@ const z = {
|
|
|
119
121
|
safeNameForOnboarding: () => zod_1.z
|
|
120
122
|
.string()
|
|
121
123
|
.regex(ONBOARDING_NAME_REGEX)
|
|
124
|
+
.regex(HAS_LETTER_REGEX)
|
|
122
125
|
.max(255)
|
|
123
126
|
.trim()
|
|
124
127
|
.min(1)
|
|
@@ -202,7 +205,10 @@ const z = {
|
|
|
202
205
|
.trim()
|
|
203
206
|
.max(255)
|
|
204
207
|
.refine(value => validator_1.default.isPostalCode(value, locale || 'any')),
|
|
205
|
-
countryCode: () => zod_1.z
|
|
208
|
+
countryCode: () => zod_1.z
|
|
209
|
+
.string()
|
|
210
|
+
.regex(COUNTRY_CODE_REGEX)
|
|
211
|
+
.refine(value => validator_1.default.isISO31661Alpha2(value)),
|
|
206
212
|
// Money validators
|
|
207
213
|
moneyDecimalString: (locale) => zod_1.z
|
|
208
214
|
.string()
|
|
@@ -432,4 +438,12 @@ const z = {
|
|
|
432
438
|
}),
|
|
433
439
|
};
|
|
434
440
|
exports.z = z;
|
|
441
|
+
const isPostalCodeForCountry = (postalCode, countryCode) => {
|
|
442
|
+
const locale = countryCode;
|
|
443
|
+
if (!validator_1.default.isPostalCodeLocales.includes(locale)) {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
return validator_1.default.isPostalCode(postalCode, locale);
|
|
447
|
+
};
|
|
448
|
+
exports.isPostalCodeForCountry = isPostalCodeForCountry;
|
|
435
449
|
__exportStar(require("zod"), exports);
|