@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.
@@ -220,23 +220,42 @@ export function createAjv(opt) {
220
220
  ajv.addKeyword({
221
221
  keyword: 'email',
222
222
  type: 'string',
223
- modifying: false,
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
- if (!checkTLD)
229
- return true;
230
- const tld = _substringAfterLast(data, '.');
231
- if (validTLDs.has(tld))
232
- return true;
233
- validate.errors = [
234
- {
235
- instancePath: ctx?.instancePath ?? '',
236
- message: `has an invalid TLD`,
237
- },
238
- ];
239
- return false;
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
- // from `ajv-formats`
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
- // from `ajv-formats`
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
- // from `ajv-formats`
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
- const regex = /^[a-z0-9-]+$/;
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
- const regex = /^[0-9]+\.[0-9]+\.[0-9]+$/;
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
- // IETF language tag (https://en.wikipedia.org/wiki/IETF_language_tag)
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
- const regex = /^[A-Z]{2}$/;
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
- const regex = /^[A-Z]{3}$/;
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 { AnyObject, BaseDBEntity, IANATimezone, IsoDate, IsoDateTime, NumberEnum, StringEnum, StringMap, UnixTimestamp, UnixTimestampMillis } from '@naturalcycles/js-lib/types';
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(/^[a-zA-Z0-9_]{6,64}$/);
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 SEM_VER_REGEX = /^[0-9]+\.[0-9]+\.[0-9]+$/;
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(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.50.0",
4
+ "version": "15.51.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -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: false,
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
- if (!checkTLD) return true
252
+ const cleanData = data.trim()
253
253
 
254
- const tld = _substringAfterLast(data, '.')
255
- if (validTLDs.has(tld)) return true
256
- ;(validate as any).errors = [
257
- {
258
- instancePath: ctx?.instancePath ?? '',
259
- message: `has an invalid TLD`,
260
- },
261
- ]
262
- return false
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
- // from `ajv-formats`
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
- // from `ajv-formats`
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
- const regex = /^[a-z0-9-]+$/
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
- const regex = /^[0-9]+\.[0-9]+\.[0-9]+$/
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
- // IETF language tag (https://en.wikipedia.org/wiki/IETF_language_tag)
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
- const regex = /^[A-Z]{2}$/
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
- const regex = /^[A-Z]{3}$/
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 type {
8
- AnyObject,
9
- BaseDBEntity,
10
- IANATimezone,
11
- IsoDate,
12
- IsoDateTime,
13
- NumberEnum,
14
- StringEnum,
15
- StringMap,
16
- UnixTimestamp,
17
- UnixTimestampMillis,
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(/^[a-zA-Z0-9_]{6,64}$/)
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 SEM_VER_REGEX = /^[0-9]+\.[0-9]+\.[0-9]+$/
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(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/)
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-]+$/