@naturalcycles/nodejs-lib 15.50.0 → 15.51.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/validation/ajv/getAjv.js +32 -13
- package/dist/validation/ajv/jsonSchemaBuilder.d.ts +1 -1
- package/dist/validation/ajv/jsonSchemaBuilder.js +14 -24
- package/dist/validation/joi/joi.shared.schemas.d.ts +1 -10
- package/dist/validation/joi/joi.shared.schemas.js +5 -12
- package/dist/validation/regexes.d.ts +19 -0
- package/dist/validation/regexes.js +23 -0
- package/package.json +1 -1
- package/src/validation/ajv/ajvSchema.ts +1 -1
- package/src/validation/ajv/getAjv.ts +35 -11
- package/src/validation/ajv/jsonSchemaBuilder.ts +24 -29
- package/src/validation/joi/joi.shared.schemas.ts +24 -25
- package/src/validation/regexes.ts +24 -0
|
@@ -220,23 +220,42 @@ export function createAjv(opt) {
|
|
|
220
220
|
ajv.addKeyword({
|
|
221
221
|
keyword: 'email',
|
|
222
222
|
type: 'string',
|
|
223
|
-
modifying:
|
|
223
|
+
modifying: true,
|
|
224
224
|
errors: true,
|
|
225
225
|
schemaType: 'object',
|
|
226
226
|
validate: function validate(opt, data, _schema, ctx) {
|
|
227
227
|
const { checkTLD } = opt;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
228
|
+
const cleanData = data.trim();
|
|
229
|
+
// from `ajv-formats`
|
|
230
|
+
const EMAIL_REGEX = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
|
231
|
+
const result = cleanData.match(EMAIL_REGEX);
|
|
232
|
+
if (!result) {
|
|
233
|
+
;
|
|
234
|
+
validate.errors = [
|
|
235
|
+
{
|
|
236
|
+
instancePath: ctx?.instancePath ?? '',
|
|
237
|
+
message: `is not a valid email address`,
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (checkTLD) {
|
|
243
|
+
const tld = _substringAfterLast(cleanData, '.');
|
|
244
|
+
if (!validTLDs.has(tld)) {
|
|
245
|
+
;
|
|
246
|
+
validate.errors = [
|
|
247
|
+
{
|
|
248
|
+
instancePath: ctx?.instancePath ?? '',
|
|
249
|
+
message: `has an invalid TLD`,
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (ctx?.parentData && ctx.parentDataProperty) {
|
|
256
|
+
ctx.parentData[ctx.parentDataProperty] = cleanData;
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
240
259
|
},
|
|
241
260
|
});
|
|
242
261
|
ajv.addKeyword({
|
|
@@ -97,7 +97,6 @@ export declare class JsonSchemaStringBuilder<IN extends string = string, OUT = I
|
|
|
97
97
|
url(): this;
|
|
98
98
|
ipv4(): this;
|
|
99
99
|
ipv6(): this;
|
|
100
|
-
id(): this;
|
|
101
100
|
slug(): this;
|
|
102
101
|
semVer(): this;
|
|
103
102
|
languageTag(): this;
|
|
@@ -110,6 +109,7 @@ export declare class JsonSchemaStringBuilder<IN extends string = string, OUT = I
|
|
|
110
109
|
* because this call effectively starts a new schema chain as an `enum` validation.
|
|
111
110
|
*/
|
|
112
111
|
ianaTimezone(): JsonSchemaEnumBuilder<string | IANATimezone, IANATimezone, false>;
|
|
112
|
+
base64Url(): this;
|
|
113
113
|
}
|
|
114
114
|
export interface JsonSchemaStringEmailOptions {
|
|
115
115
|
checkTLD: boolean;
|
|
@@ -5,6 +5,7 @@ import { _uniq } from '@naturalcycles/js-lib/array';
|
|
|
5
5
|
import { _assert } from '@naturalcycles/js-lib/error';
|
|
6
6
|
import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object';
|
|
7
7
|
import { _objectAssign, JWT_REGEX, } from '@naturalcycles/js-lib/types';
|
|
8
|
+
import { BASE64URL_REGEX, COUNTRY_CODE_REGEX, CURRENCY_REGEX, IPV4_REGEX, IPV6_REGEX, LANGUAGE_TAG_REGEX, SEMVER_REGEX, SLUG_REGEX, } from '../regexes.js';
|
|
8
9
|
import { TIMEZONES } from '../timezones.js';
|
|
9
10
|
import { isEveryItemNumber, isEveryItemString, JSON_SCHEMA_ORDER, mergeJsonSchemaObjects, } from './jsonSchemaBuilder.util.js';
|
|
10
11
|
export const j = {
|
|
@@ -217,9 +218,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
217
218
|
email(opt) {
|
|
218
219
|
const defaultOptions = { checkTLD: true };
|
|
219
220
|
_objectAssign(this.schema, { email: { ...defaultOptions, ...opt } });
|
|
220
|
-
|
|
221
|
-
const regex = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
|
222
|
-
return this.regex(regex, { msg: 'is not a valid email address' }).trim().toLowerCase();
|
|
221
|
+
return this.trim().toLowerCase();
|
|
223
222
|
}
|
|
224
223
|
trim() {
|
|
225
224
|
_objectAssign(this.schema, { transform: { ...this.schema.transform, trim: true } });
|
|
@@ -267,39 +266,25 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
267
266
|
return this.regex(regex, { msg: 'is not a valid URL format' });
|
|
268
267
|
}
|
|
269
268
|
ipv4() {
|
|
270
|
-
|
|
271
|
-
const regex = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
272
|
-
return this.regex(regex, { msg: 'is not a valid IPv4 format' });
|
|
269
|
+
return this.regex(IPV4_REGEX, { msg: 'is not a valid IPv4 format' });
|
|
273
270
|
}
|
|
274
271
|
ipv6() {
|
|
275
|
-
|
|
276
|
-
const regex = /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i;
|
|
277
|
-
return this.regex(regex, { msg: 'is not a valid IPv6 format' });
|
|
278
|
-
}
|
|
279
|
-
id() {
|
|
280
|
-
const regex = /^[a-z0-9_]{6,64}$/;
|
|
281
|
-
return this.regex(regex, { msg: 'is not a valid ID format' });
|
|
272
|
+
return this.regex(IPV6_REGEX, { msg: 'is not a valid IPv6 format' });
|
|
282
273
|
}
|
|
283
274
|
slug() {
|
|
284
|
-
|
|
285
|
-
return this.regex(regex, { msg: 'is not a valid slug format' });
|
|
275
|
+
return this.regex(SLUG_REGEX, { msg: 'is not a valid slug format' });
|
|
286
276
|
}
|
|
287
277
|
semVer() {
|
|
288
|
-
|
|
289
|
-
return this.regex(regex, { msg: 'is not a valid semver format' });
|
|
278
|
+
return this.regex(SEMVER_REGEX, { msg: 'is not a valid semver format' });
|
|
290
279
|
}
|
|
291
280
|
languageTag() {
|
|
292
|
-
|
|
293
|
-
const regex = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
294
|
-
return this.regex(regex, { msg: 'is not a valid language format' });
|
|
281
|
+
return this.regex(LANGUAGE_TAG_REGEX, { msg: 'is not a valid language format' });
|
|
295
282
|
}
|
|
296
283
|
countryCode() {
|
|
297
|
-
|
|
298
|
-
return this.regex(regex, { msg: 'is not a valid country code format' });
|
|
284
|
+
return this.regex(COUNTRY_CODE_REGEX, { msg: 'is not a valid country code format' });
|
|
299
285
|
}
|
|
300
286
|
currency() {
|
|
301
|
-
|
|
302
|
-
return this.regex(regex, { msg: 'is not a valid currency format' });
|
|
287
|
+
return this.regex(CURRENCY_REGEX, { msg: 'is not a valid currency format' });
|
|
303
288
|
}
|
|
304
289
|
/**
|
|
305
290
|
* Validates that the input is a valid IANATimzone value.
|
|
@@ -311,6 +296,11 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
311
296
|
// UTC is added to assist unit-testing, which uses UTC by default (not technically a valid Iana timezone identifier)
|
|
312
297
|
return j.enum(TIMEZONES, { msg: 'is an invalid IANA timezone' }).branded();
|
|
313
298
|
}
|
|
299
|
+
base64Url() {
|
|
300
|
+
return this.regex(BASE64URL_REGEX, {
|
|
301
|
+
msg: 'contains characters not allowed in Base64 URL characterset',
|
|
302
|
+
});
|
|
303
|
+
}
|
|
314
304
|
}
|
|
315
305
|
export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
|
|
316
306
|
constructor() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnyObject, type BaseDBEntity, type IANATimezone, type IsoDate, type IsoDateTime, type NumberEnum, type StringEnum, type StringMap, type UnixTimestamp, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
|
|
2
2
|
import type { AlternativesSchema, AnySchema, ArraySchema, ObjectSchema } from 'joi';
|
|
3
3
|
import type { NumberSchema } from './number.extensions.js';
|
|
4
4
|
import type { StringSchema } from './string.extensions.js';
|
|
@@ -41,13 +41,9 @@ export declare function objectSchema<T extends AnyObject>(schema: {
|
|
|
41
41
|
export declare function stringMapSchema<T>(key: AnySchema, value: AnySchema<T>): ObjectSchema<StringMap<T>>;
|
|
42
42
|
export declare function oneOfSchema<T = any>(...schemas: AnySchema[]): AlternativesSchema<T>;
|
|
43
43
|
export declare const anySchema: AnySchema<any>;
|
|
44
|
-
export declare const BASE62_REGEX: RegExp;
|
|
45
|
-
export declare const BASE64_REGEX: RegExp;
|
|
46
|
-
export declare const BASE64URL_REGEX: RegExp;
|
|
47
44
|
export declare const base62Schema: StringSchema<string>;
|
|
48
45
|
export declare const base64Schema: StringSchema<string>;
|
|
49
46
|
export declare const base64UrlSchema: StringSchema<string>;
|
|
50
|
-
export declare const JWT_REGEX: RegExp;
|
|
51
47
|
export declare const jwtSchema: StringSchema<string>;
|
|
52
48
|
/**
|
|
53
49
|
* [a-zA-Z0-9_]*
|
|
@@ -57,10 +53,6 @@ export declare const idSchema: StringSchema<string>;
|
|
|
57
53
|
export declare const idBase62Schema: StringSchema<string>;
|
|
58
54
|
export declare const idBase64Schema: StringSchema<string>;
|
|
59
55
|
export declare const idBase64UrlSchema: StringSchema<string>;
|
|
60
|
-
/**
|
|
61
|
-
* `_` should NOT be allowed to be able to use slug-ids as part of natural ids with `_` separator.
|
|
62
|
-
*/
|
|
63
|
-
export declare const SLUG_REGEX: RegExp;
|
|
64
56
|
/**
|
|
65
57
|
* "Slug" - a valid URL, filename, etc.
|
|
66
58
|
*/
|
|
@@ -89,7 +81,6 @@ export declare const emailSchema: StringSchema<string>;
|
|
|
89
81
|
/**
|
|
90
82
|
* Pattern is simplified for our use, it's not a canonical SemVer.
|
|
91
83
|
*/
|
|
92
|
-
export declare const SEM_VER_REGEX: RegExp;
|
|
93
84
|
export declare const semVerSchema: StringSchema<string>;
|
|
94
85
|
export declare const userAgentSchema: StringSchema<string>;
|
|
95
86
|
export declare const utcOffsetSchema: NumberSchema<number>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { _numberEnumKeys, _numberEnumValues, _stringEnumKeys, _stringEnumValues, } from '@naturalcycles/js-lib';
|
|
2
|
+
import { JWT_REGEX, } from '@naturalcycles/js-lib/types';
|
|
3
|
+
import { BASE62_REGEX, BASE64_REGEX, BASE64URL_REGEX, ID_REGEX, MAC_ADDRESS_REGEX, SEMVER_REGEX, SLUG_REGEX, } from '../regexes.js';
|
|
2
4
|
import { Joi } from './joi.extensions.js';
|
|
3
5
|
export const booleanSchema = Joi.boolean();
|
|
4
6
|
export const booleanDefaultToFalseSchema = Joi.boolean().default(false);
|
|
@@ -52,27 +54,19 @@ export function oneOfSchema(...schemas) {
|
|
|
52
54
|
return Joi.alternatives(schemas);
|
|
53
55
|
}
|
|
54
56
|
export const anySchema = Joi.any();
|
|
55
|
-
export const BASE62_REGEX = /^[a-zA-Z0-9]+$/;
|
|
56
|
-
export const BASE64_REGEX = /^[a-zA-Z0-9+/]+={0,2}$/;
|
|
57
|
-
export const BASE64URL_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
58
57
|
export const base62Schema = stringSchema.regex(BASE62_REGEX);
|
|
59
58
|
export const base64Schema = stringSchema.regex(BASE64_REGEX);
|
|
60
59
|
export const base64UrlSchema = stringSchema.regex(BASE64URL_REGEX);
|
|
61
|
-
export const JWT_REGEX = /^[\w-]+\.[\w-]+\.[\w-]+$/;
|
|
62
60
|
export const jwtSchema = stringSchema.regex(JWT_REGEX);
|
|
63
61
|
// 1g498efj5sder3324zer
|
|
64
62
|
/**
|
|
65
63
|
* [a-zA-Z0-9_]*
|
|
66
64
|
* 6-64 length
|
|
67
65
|
*/
|
|
68
|
-
export const idSchema = stringSchema.regex(
|
|
66
|
+
export const idSchema = stringSchema.regex(ID_REGEX);
|
|
69
67
|
export const idBase62Schema = base62Schema.min(8).max(64);
|
|
70
68
|
export const idBase64Schema = base64Schema.min(8).max(64);
|
|
71
69
|
export const idBase64UrlSchema = base64UrlSchema.min(8).max(64);
|
|
72
|
-
/**
|
|
73
|
-
* `_` should NOT be allowed to be able to use slug-ids as part of natural ids with `_` separator.
|
|
74
|
-
*/
|
|
75
|
-
export const SLUG_REGEX = /^[a-z0-9-]*$/;
|
|
76
70
|
/**
|
|
77
71
|
* "Slug" - a valid URL, filename, etc.
|
|
78
72
|
*/
|
|
@@ -116,8 +110,7 @@ export const emailSchema = stringSchema.email().lowercase();
|
|
|
116
110
|
/**
|
|
117
111
|
* Pattern is simplified for our use, it's not a canonical SemVer.
|
|
118
112
|
*/
|
|
119
|
-
export const
|
|
120
|
-
export const semVerSchema = stringSchema.regex(SEM_VER_REGEX);
|
|
113
|
+
export const semVerSchema = stringSchema.regex(SEMVER_REGEX);
|
|
121
114
|
// todo: .error(() => 'should be SemVer')
|
|
122
115
|
export const userAgentSchema = stringSchema
|
|
123
116
|
.min(5) // I've seen UA of `Android` (7 characters)
|
|
@@ -138,5 +131,5 @@ export const baseDBEntitySchema = objectSchema({
|
|
|
138
131
|
created: unixTimestamp2000Schema.optional(),
|
|
139
132
|
updated: unixTimestamp2000Schema.optional(),
|
|
140
133
|
});
|
|
141
|
-
export const macAddressSchema = stringSchema.regex(
|
|
134
|
+
export const macAddressSchema = stringSchema.regex(MAC_ADDRESS_REGEX);
|
|
142
135
|
export const uuidSchema = stringSchema.uuid();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const BASE62_REGEX: RegExp;
|
|
2
|
+
export declare const BASE64_REGEX: RegExp;
|
|
3
|
+
export declare const BASE64URL_REGEX: RegExp;
|
|
4
|
+
export declare const COUNTRY_CODE_REGEX: RegExp;
|
|
5
|
+
export declare const CURRENCY_REGEX: RegExp;
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated
|
|
8
|
+
* Avoid using blanket regex for a concept so ambiguous as "ID".
|
|
9
|
+
* We should always define what kind of an ID we talk about: MongoDB ID, Base64 ID etc.
|
|
10
|
+
*
|
|
11
|
+
* We keep this regex here, because JOI shared schemas has been exporting this check.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ID_REGEX: RegExp;
|
|
14
|
+
export declare const IPV4_REGEX: RegExp;
|
|
15
|
+
export declare const IPV6_REGEX: RegExp;
|
|
16
|
+
export declare const LANGUAGE_TAG_REGEX: RegExp;
|
|
17
|
+
export declare const MAC_ADDRESS_REGEX: RegExp;
|
|
18
|
+
export declare const SEMVER_REGEX: RegExp;
|
|
19
|
+
export declare const SLUG_REGEX: RegExp;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const BASE62_REGEX = /^[a-zA-Z0-9]+$/;
|
|
2
|
+
export const BASE64_REGEX = /^[a-zA-Z0-9+/]+={0,2}$/;
|
|
3
|
+
export const BASE64URL_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
4
|
+
export const COUNTRY_CODE_REGEX = /^[A-Z]{2}$/;
|
|
5
|
+
export const CURRENCY_REGEX = /^[A-Z]{3}$/;
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated
|
|
8
|
+
* Avoid using blanket regex for a concept so ambiguous as "ID".
|
|
9
|
+
* We should always define what kind of an ID we talk about: MongoDB ID, Base64 ID etc.
|
|
10
|
+
*
|
|
11
|
+
* We keep this regex here, because JOI shared schemas has been exporting this check.
|
|
12
|
+
*/
|
|
13
|
+
export const ID_REGEX = /^[a-zA-Z0-9_]{6,64}$/;
|
|
14
|
+
export const IPV4_REGEX =
|
|
15
|
+
// from `ajv-formats`
|
|
16
|
+
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
17
|
+
// from `ajv-formats`
|
|
18
|
+
export const IPV6_REGEX = /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i;
|
|
19
|
+
// IETF language tag (https://en.wikipedia.org/wiki/IETF_language_tag)
|
|
20
|
+
export const LANGUAGE_TAG_REGEX = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
21
|
+
export const MAC_ADDRESS_REGEX = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
|
22
|
+
export const SEMVER_REGEX = /^[0-9]+\.[0-9]+\.[0-9]+$/;
|
|
23
|
+
export const SLUG_REGEX = /^[a-z0-9-]+$/;
|
package/package.json
CHANGED
|
@@ -73,7 +73,7 @@ export class AjvSchema<IN = unknown, OUT = IN> {
|
|
|
73
73
|
let jsonSchema: JsonSchema<IN, OUT>
|
|
74
74
|
|
|
75
75
|
if (AjvSchema.isJsonSchemaBuilder(schema)) {
|
|
76
|
-
jsonSchema = schema.build()
|
|
76
|
+
jsonSchema = (schema as JsonSchemaTerminal<IN, OUT, any>).build()
|
|
77
77
|
AjvSchema.requireValidJsonSchema(jsonSchema)
|
|
78
78
|
} else {
|
|
79
79
|
jsonSchema = schema
|
|
@@ -244,22 +244,46 @@ export function createAjv(opt?: Options): Ajv {
|
|
|
244
244
|
ajv.addKeyword({
|
|
245
245
|
keyword: 'email',
|
|
246
246
|
type: 'string',
|
|
247
|
-
modifying:
|
|
247
|
+
modifying: true,
|
|
248
248
|
errors: true,
|
|
249
249
|
schemaType: 'object',
|
|
250
250
|
validate: function validate(opt: JsonSchemaStringEmailOptions, data: string, _schema, ctx) {
|
|
251
251
|
const { checkTLD } = opt
|
|
252
|
-
|
|
252
|
+
const cleanData = data.trim()
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
254
|
+
// from `ajv-formats`
|
|
255
|
+
const EMAIL_REGEX =
|
|
256
|
+
/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i
|
|
257
|
+
const result = cleanData.match(EMAIL_REGEX)
|
|
258
|
+
|
|
259
|
+
if (!result) {
|
|
260
|
+
;(validate as any).errors = [
|
|
261
|
+
{
|
|
262
|
+
instancePath: ctx?.instancePath ?? '',
|
|
263
|
+
message: `is not a valid email address`,
|
|
264
|
+
},
|
|
265
|
+
]
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (checkTLD) {
|
|
270
|
+
const tld = _substringAfterLast(cleanData, '.')
|
|
271
|
+
if (!validTLDs.has(tld)) {
|
|
272
|
+
;(validate as any).errors = [
|
|
273
|
+
{
|
|
274
|
+
instancePath: ctx?.instancePath ?? '',
|
|
275
|
+
message: `has an invalid TLD`,
|
|
276
|
+
},
|
|
277
|
+
]
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (ctx?.parentData && ctx.parentDataProperty) {
|
|
283
|
+
ctx.parentData[ctx.parentDataProperty] = cleanData
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return true
|
|
263
287
|
},
|
|
264
288
|
})
|
|
265
289
|
|
|
@@ -26,6 +26,16 @@ import {
|
|
|
26
26
|
type UnixTimestamp,
|
|
27
27
|
type UnixTimestampMillis,
|
|
28
28
|
} from '@naturalcycles/js-lib/types'
|
|
29
|
+
import {
|
|
30
|
+
BASE64URL_REGEX,
|
|
31
|
+
COUNTRY_CODE_REGEX,
|
|
32
|
+
CURRENCY_REGEX,
|
|
33
|
+
IPV4_REGEX,
|
|
34
|
+
IPV6_REGEX,
|
|
35
|
+
LANGUAGE_TAG_REGEX,
|
|
36
|
+
SEMVER_REGEX,
|
|
37
|
+
SLUG_REGEX,
|
|
38
|
+
} from '../regexes.js'
|
|
29
39
|
import { TIMEZONES } from '../timezones.js'
|
|
30
40
|
import {
|
|
31
41
|
isEveryItemNumber,
|
|
@@ -302,11 +312,7 @@ export class JsonSchemaStringBuilder<
|
|
|
302
312
|
email(opt?: Partial<JsonSchemaStringEmailOptions>): this {
|
|
303
313
|
const defaultOptions: JsonSchemaStringEmailOptions = { checkTLD: true }
|
|
304
314
|
_objectAssign(this.schema, { email: { ...defaultOptions, ...opt } })
|
|
305
|
-
|
|
306
|
-
// from `ajv-formats`
|
|
307
|
-
const regex =
|
|
308
|
-
/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i
|
|
309
|
-
return this.regex(regex, { msg: 'is not a valid email address' }).trim().toLowerCase()
|
|
315
|
+
return this.trim().toLowerCase()
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
trim(): this {
|
|
@@ -365,48 +371,31 @@ export class JsonSchemaStringBuilder<
|
|
|
365
371
|
}
|
|
366
372
|
|
|
367
373
|
ipv4(): this {
|
|
368
|
-
|
|
369
|
-
const regex =
|
|
370
|
-
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/
|
|
371
|
-
return this.regex(regex, { msg: 'is not a valid IPv4 format' })
|
|
374
|
+
return this.regex(IPV4_REGEX, { msg: 'is not a valid IPv4 format' })
|
|
372
375
|
}
|
|
373
376
|
|
|
374
377
|
ipv6(): this {
|
|
375
|
-
|
|
376
|
-
const regex =
|
|
377
|
-
/^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i
|
|
378
|
-
return this.regex(regex, { msg: 'is not a valid IPv6 format' })
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
id(): this {
|
|
382
|
-
const regex = /^[a-z0-9_]{6,64}$/
|
|
383
|
-
return this.regex(regex, { msg: 'is not a valid ID format' })
|
|
378
|
+
return this.regex(IPV6_REGEX, { msg: 'is not a valid IPv6 format' })
|
|
384
379
|
}
|
|
385
380
|
|
|
386
381
|
slug(): this {
|
|
387
|
-
|
|
388
|
-
return this.regex(regex, { msg: 'is not a valid slug format' })
|
|
382
|
+
return this.regex(SLUG_REGEX, { msg: 'is not a valid slug format' })
|
|
389
383
|
}
|
|
390
384
|
|
|
391
385
|
semVer(): this {
|
|
392
|
-
|
|
393
|
-
return this.regex(regex, { msg: 'is not a valid semver format' })
|
|
386
|
+
return this.regex(SEMVER_REGEX, { msg: 'is not a valid semver format' })
|
|
394
387
|
}
|
|
395
388
|
|
|
396
389
|
languageTag(): this {
|
|
397
|
-
|
|
398
|
-
const regex = /^[a-z]{2}(-[A-Z]{2})?$/
|
|
399
|
-
return this.regex(regex, { msg: 'is not a valid language format' })
|
|
390
|
+
return this.regex(LANGUAGE_TAG_REGEX, { msg: 'is not a valid language format' })
|
|
400
391
|
}
|
|
401
392
|
|
|
402
393
|
countryCode(): this {
|
|
403
|
-
|
|
404
|
-
return this.regex(regex, { msg: 'is not a valid country code format' })
|
|
394
|
+
return this.regex(COUNTRY_CODE_REGEX, { msg: 'is not a valid country code format' })
|
|
405
395
|
}
|
|
406
396
|
|
|
407
397
|
currency(): this {
|
|
408
|
-
|
|
409
|
-
return this.regex(regex, { msg: 'is not a valid currency format' })
|
|
398
|
+
return this.regex(CURRENCY_REGEX, { msg: 'is not a valid currency format' })
|
|
410
399
|
}
|
|
411
400
|
|
|
412
401
|
/**
|
|
@@ -419,6 +408,12 @@ export class JsonSchemaStringBuilder<
|
|
|
419
408
|
// UTC is added to assist unit-testing, which uses UTC by default (not technically a valid Iana timezone identifier)
|
|
420
409
|
return j.enum(TIMEZONES, { msg: 'is an invalid IANA timezone' }).branded<IANATimezone>()
|
|
421
410
|
}
|
|
411
|
+
|
|
412
|
+
base64Url(): this {
|
|
413
|
+
return this.regex(BASE64URL_REGEX, {
|
|
414
|
+
msg: 'contains characters not allowed in Base64 URL characterset',
|
|
415
|
+
})
|
|
416
|
+
}
|
|
422
417
|
}
|
|
423
418
|
|
|
424
419
|
export interface JsonSchemaStringEmailOptions {
|
|
@@ -4,19 +4,29 @@ import {
|
|
|
4
4
|
_stringEnumKeys,
|
|
5
5
|
_stringEnumValues,
|
|
6
6
|
} from '@naturalcycles/js-lib'
|
|
7
|
-
import
|
|
8
|
-
AnyObject,
|
|
9
|
-
BaseDBEntity,
|
|
10
|
-
IANATimezone,
|
|
11
|
-
IsoDate,
|
|
12
|
-
IsoDateTime,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
import {
|
|
8
|
+
type AnyObject,
|
|
9
|
+
type BaseDBEntity,
|
|
10
|
+
type IANATimezone,
|
|
11
|
+
type IsoDate,
|
|
12
|
+
type IsoDateTime,
|
|
13
|
+
JWT_REGEX,
|
|
14
|
+
type NumberEnum,
|
|
15
|
+
type StringEnum,
|
|
16
|
+
type StringMap,
|
|
17
|
+
type UnixTimestamp,
|
|
18
|
+
type UnixTimestampMillis,
|
|
18
19
|
} from '@naturalcycles/js-lib/types'
|
|
19
20
|
import type { AlternativesSchema, AnySchema, ArraySchema, ObjectSchema } from 'joi'
|
|
21
|
+
import {
|
|
22
|
+
BASE62_REGEX,
|
|
23
|
+
BASE64_REGEX,
|
|
24
|
+
BASE64URL_REGEX,
|
|
25
|
+
ID_REGEX,
|
|
26
|
+
MAC_ADDRESS_REGEX,
|
|
27
|
+
SEMVER_REGEX,
|
|
28
|
+
SLUG_REGEX,
|
|
29
|
+
} from '../regexes.js'
|
|
20
30
|
import { Joi } from './joi.extensions.js'
|
|
21
31
|
import type { NumberSchema } from './number.extensions.js'
|
|
22
32
|
import type { StringSchema } from './string.extensions.js'
|
|
@@ -100,15 +110,10 @@ export function oneOfSchema<T = any>(...schemas: AnySchema[]): AlternativesSchem
|
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
export const anySchema = Joi.any()
|
|
103
|
-
|
|
104
|
-
export const BASE62_REGEX = /^[a-zA-Z0-9]+$/
|
|
105
|
-
export const BASE64_REGEX = /^[a-zA-Z0-9+/]+={0,2}$/
|
|
106
|
-
export const BASE64URL_REGEX = /^[a-zA-Z0-9_-]+$/
|
|
107
113
|
export const base62Schema = stringSchema.regex(BASE62_REGEX)
|
|
108
114
|
export const base64Schema = stringSchema.regex(BASE64_REGEX)
|
|
109
115
|
export const base64UrlSchema = stringSchema.regex(BASE64URL_REGEX)
|
|
110
116
|
|
|
111
|
-
export const JWT_REGEX = /^[\w-]+\.[\w-]+\.[\w-]+$/
|
|
112
117
|
export const jwtSchema = stringSchema.regex(JWT_REGEX)
|
|
113
118
|
|
|
114
119
|
// 1g498efj5sder3324zer
|
|
@@ -116,17 +121,12 @@ export const jwtSchema = stringSchema.regex(JWT_REGEX)
|
|
|
116
121
|
* [a-zA-Z0-9_]*
|
|
117
122
|
* 6-64 length
|
|
118
123
|
*/
|
|
119
|
-
export const idSchema = stringSchema.regex(
|
|
124
|
+
export const idSchema = stringSchema.regex(ID_REGEX)
|
|
120
125
|
|
|
121
126
|
export const idBase62Schema = base62Schema.min(8).max(64)
|
|
122
127
|
export const idBase64Schema = base64Schema.min(8).max(64)
|
|
123
128
|
export const idBase64UrlSchema = base64UrlSchema.min(8).max(64)
|
|
124
129
|
|
|
125
|
-
/**
|
|
126
|
-
* `_` should NOT be allowed to be able to use slug-ids as part of natural ids with `_` separator.
|
|
127
|
-
*/
|
|
128
|
-
export const SLUG_REGEX = /^[a-z0-9-]*$/
|
|
129
|
-
|
|
130
130
|
/**
|
|
131
131
|
* "Slug" - a valid URL, filename, etc.
|
|
132
132
|
*/
|
|
@@ -175,8 +175,7 @@ export const emailSchema = stringSchema.email().lowercase()
|
|
|
175
175
|
/**
|
|
176
176
|
* Pattern is simplified for our use, it's not a canonical SemVer.
|
|
177
177
|
*/
|
|
178
|
-
export const
|
|
179
|
-
export const semVerSchema = stringSchema.regex(SEM_VER_REGEX)
|
|
178
|
+
export const semVerSchema = stringSchema.regex(SEMVER_REGEX)
|
|
180
179
|
// todo: .error(() => 'should be SemVer')
|
|
181
180
|
|
|
182
181
|
export const userAgentSchema = stringSchema
|
|
@@ -203,6 +202,6 @@ export const baseDBEntitySchema: ObjectSchema<BaseDBEntity> = objectSchema<BaseD
|
|
|
203
202
|
updated: unixTimestamp2000Schema.optional(),
|
|
204
203
|
})
|
|
205
204
|
|
|
206
|
-
export const macAddressSchema = stringSchema.regex(
|
|
205
|
+
export const macAddressSchema = stringSchema.regex(MAC_ADDRESS_REGEX)
|
|
207
206
|
|
|
208
207
|
export const uuidSchema = stringSchema.uuid()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const BASE62_REGEX = /^[a-zA-Z0-9]+$/
|
|
2
|
+
export const BASE64_REGEX = /^[a-zA-Z0-9+/]+={0,2}$/
|
|
3
|
+
export const BASE64URL_REGEX = /^[a-zA-Z0-9_-]+$/
|
|
4
|
+
export const COUNTRY_CODE_REGEX = /^[A-Z]{2}$/
|
|
5
|
+
export const CURRENCY_REGEX = /^[A-Z]{3}$/
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated
|
|
8
|
+
* Avoid using blanket regex for a concept so ambiguous as "ID".
|
|
9
|
+
* We should always define what kind of an ID we talk about: MongoDB ID, Base64 ID etc.
|
|
10
|
+
*
|
|
11
|
+
* We keep this regex here, because JOI shared schemas has been exporting this check.
|
|
12
|
+
*/
|
|
13
|
+
export const ID_REGEX = /^[a-zA-Z0-9_]{6,64}$/
|
|
14
|
+
export const IPV4_REGEX =
|
|
15
|
+
// from `ajv-formats`
|
|
16
|
+
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/
|
|
17
|
+
// from `ajv-formats`
|
|
18
|
+
export const IPV6_REGEX =
|
|
19
|
+
/^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i
|
|
20
|
+
// IETF language tag (https://en.wikipedia.org/wiki/IETF_language_tag)
|
|
21
|
+
export const LANGUAGE_TAG_REGEX = /^[a-z]{2}(-[A-Z]{2})?$/
|
|
22
|
+
export const MAC_ADDRESS_REGEX = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/
|
|
23
|
+
export const SEMVER_REGEX = /^[0-9]+\.[0-9]+\.[0-9]+$/
|
|
24
|
+
export const SLUG_REGEX = /^[a-z0-9-]+$/
|