@naturalcycles/nodejs-lib 15.126.0 → 15.128.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/fs/fs2.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { RmOptions } from 'node:fs';
1
+ import type { RmOptions, WriteFileOptions } from 'node:fs';
2
2
  import fs from 'node:fs';
3
3
  import fsp from 'node:fs/promises';
4
4
  /**
@@ -22,8 +22,8 @@ declare class FS2 {
22
22
  readBufferAsync(filePath: string): Promise<Buffer>;
23
23
  readJson<T = unknown>(filePath: string): T;
24
24
  readJsonAsync<T = unknown>(filePath: string): Promise<T>;
25
- writeFile(filePath: string, data: string | Buffer): void;
26
- writeFileAsync(filePath: string, data: string | Buffer): Promise<void>;
25
+ writeFile(filePath: string, data: string | Buffer, opt?: WriteFileOptions): void;
26
+ writeFileAsync(filePath: string, data: string | Buffer, opt?: WriteFileOptions): Promise<void>;
27
27
  writeJson(filePath: string, data: any, opt?: JsonOptions): void;
28
28
  writeJsonAsync(filePath: string, data: any, opt?: JsonOptions): Promise<void>;
29
29
  appendFile(filePath: string, data: string | Buffer): void;
package/dist/fs/fs2.js CHANGED
@@ -55,11 +55,11 @@ class FS2 {
55
55
  const str = await fsp.readFile(filePath, 'utf8');
56
56
  return _jsonParse(str);
57
57
  }
58
- writeFile(filePath, data) {
59
- fs.writeFileSync(filePath, data);
58
+ writeFile(filePath, data, opt) {
59
+ fs.writeFileSync(filePath, data, opt);
60
60
  }
61
- async writeFileAsync(filePath, data) {
62
- await fsp.writeFile(filePath, data);
61
+ async writeFileAsync(filePath, data, opt) {
62
+ await fsp.writeFile(filePath, data, opt);
63
63
  }
64
64
  writeJson(filePath, data, opt) {
65
65
  const str = stringify(data, opt);
@@ -4,6 +4,7 @@ import { _deepCopy, _mapObject, Set2 } from '@naturalcycles/js-lib/object';
4
4
  import { _substringAfterLast } from '@naturalcycles/js-lib/string';
5
5
  import { Ajv2020 } from 'ajv/dist/2020.js';
6
6
  import { validTLDs } from '../tlds.js';
7
+ import { jFormats } from './jsonSchemaBuilder.util.js';
7
8
  // oxlint-disable unicorn/prefer-code-point
8
9
  const AJV_OPTIONS = {
9
10
  removeAdditional: true,
@@ -371,6 +372,16 @@ export function createAjv(opt) {
371
372
  return false;
372
373
  },
373
374
  });
375
+ // Annotation only (the values are validated by `enum`), registered so strict mode doesn't reject it
376
+ ajv.addKeyword({
377
+ keyword: 'enumNames',
378
+ schemaType: 'array',
379
+ });
380
+ // Annotation only (validated by the custom keywords, e.g `IsoDate`).
381
+ // `true` makes Ajv skip the format check, while unknown formats would throw in strict mode.
382
+ for (const format of jFormats) {
383
+ ajv.addFormat(format, true);
384
+ }
374
385
  ajv.addKeyword({
375
386
  keyword: 'errorMessages',
376
387
  schemaType: 'object',
@@ -3,4 +3,5 @@ export * from './ajvValidationError.js';
3
3
  export * from './from-data/generateJsonSchemaFromData.js';
4
4
  export * from './getAjv.js';
5
5
  export * from './jSchema.js';
6
+ export { jFormats, type JFormat } from './jsonSchemaBuilder.util.js';
6
7
  export { Ajv };
@@ -3,4 +3,5 @@ export * from './ajvValidationError.js';
3
3
  export * from './from-data/generateJsonSchemaFromData.js';
4
4
  export * from './getAjv.js';
5
5
  export * from './jSchema.js';
6
+ export { jFormats } from './jsonSchemaBuilder.util.js';
6
7
  export { Ajv };
@@ -423,7 +423,7 @@ declare class JSet2Builder<OUT, Opt> extends JBuilder<Set2<OUT>, Opt> {
423
423
  max(maxItems: number): this;
424
424
  }
425
425
  export declare class JEnum<OUT extends string | number | boolean | null, Opt extends boolean = false> extends JBuilder<OUT, Opt> {
426
- constructor(enumValues: readonly OUT[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt);
426
+ constructor(enumValues: readonly OUT[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt, enumNames?: readonly string[]);
427
427
  branded<B extends OUT>(): JEnum<B, Opt>;
428
428
  }
429
429
  export declare class JTuple<ITEMS extends JSchema<any, any>[]> extends JBuilder<TupleOut<ITEMS>, false> {
@@ -595,6 +595,11 @@ export interface JsonSchema<OUT = unknown> {
595
595
  maxItems?: number;
596
596
  uniqueItems?: boolean;
597
597
  enum?: any;
598
+ /**
599
+ * Member names of a NumberEnum, aligned by index with `enum`.
600
+ * Annotation only (not validated), lets consumers render `1` as e.g `PREVENT`.
601
+ */
602
+ enumNames?: readonly string[];
598
603
  hasIsOfTypeCheck?: boolean;
599
604
  email?: JsonSchemaStringEmailOptions;
600
605
  Set2?: JsonSchema;
@@ -113,6 +113,7 @@ export const j = {
113
113
  },
114
114
  enum(input, opt) {
115
115
  let enumValues;
116
+ let enumNames;
116
117
  let baseType = 'other';
117
118
  if (Array.isArray(input)) {
118
119
  enumValues = input;
@@ -126,7 +127,11 @@ export const j = {
126
127
  else if (typeof input === 'object') {
127
128
  const enumType = getEnumType(input);
128
129
  if (enumType === 'NumberEnum') {
129
- enumValues = _numberEnumValues(input);
130
+ // Not via _numberEnumKeys: it misses `_enum()` objects (no reverse mapping)
131
+ // and misaligns with the values when members aren't declared in ascending order
132
+ const numberEntries = Object.entries(input).filter(([, value]) => typeof value === 'number');
133
+ enumValues = numberEntries.map(([, value]) => value);
134
+ enumNames = numberEntries.map(([name]) => name);
130
135
  baseType = 'number';
131
136
  }
132
137
  else if (enumType === 'StringEnum') {
@@ -135,7 +140,7 @@ export const j = {
135
140
  }
136
141
  }
137
142
  _assert(enumValues, 'Unsupported enum input');
138
- return new JEnum(enumValues, baseType, opt);
143
+ return new JEnum(enumValues, baseType, opt, enumNames);
139
144
  },
140
145
  /**
141
146
  * Use only with primitive values, otherwise this function will throw to avoid bugs.
@@ -601,11 +606,15 @@ export class JString extends JBuilder {
601
606
  return new JIsoDate();
602
607
  }
603
608
  isoDateTime() {
604
- return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded();
609
+ return this.cloneAndUpdateSchema({
610
+ format: 'IsoDateTime',
611
+ IsoDateTime: true,
612
+ }).branded();
605
613
  }
606
614
  isoMonth() {
607
615
  return new JBuilder({
608
616
  type: 'string',
617
+ format: 'IsoMonth',
609
618
  IsoMonth: {},
610
619
  });
611
620
  }
@@ -674,6 +683,7 @@ export class JIsoDate extends JBuilder {
674
683
  constructor() {
675
684
  super({
676
685
  type: 'string',
686
+ format: 'IsoDate',
677
687
  IsoDate: {},
678
688
  });
679
689
  }
@@ -771,16 +781,18 @@ export class JNumber extends JBuilder {
771
781
  return this;
772
782
  }
773
783
  unixTimestamp() {
774
- return this.integer().branded();
784
+ return this.integer().cloneAndUpdateSchema({ format: 'UnixTimestamp' }).branded();
775
785
  }
776
786
  unixTimestamp2000() {
777
- return this.integer().min(TS_2000).max(TS_2500).branded();
787
+ return this.unixTimestamp().min(TS_2000).max(TS_2500);
778
788
  }
779
789
  unixTimestampMillis() {
780
- return this.integer().branded();
790
+ return this.integer()
791
+ .cloneAndUpdateSchema({ format: 'UnixTimestampMillis' })
792
+ .branded();
781
793
  }
782
794
  unixTimestamp2000Millis() {
783
- return this.integer().min(TS_2000_MILLIS).max(TS_2500_MILLIS).branded();
795
+ return this.unixTimestampMillis().min(TS_2000_MILLIS).max(TS_2500_MILLIS);
784
796
  }
785
797
  utcOffset() {
786
798
  return this.integer()
@@ -949,8 +961,10 @@ class JSet2Builder extends JBuilder {
949
961
  }
950
962
  }
951
963
  export class JEnum extends JBuilder {
952
- constructor(enumValues, baseType, opt) {
964
+ constructor(enumValues, baseType, opt, enumNames) {
953
965
  const jsonSchema = { enum: enumValues };
966
+ if (enumNames)
967
+ jsonSchema.enumNames = enumNames;
954
968
  // Specifying the base type helps in cases when we ask Ajv to coerce the types.
955
969
  // Having only the `enum` in the schema does not trigger a coercion in Ajv.
956
970
  if (baseType === 'string')
@@ -10,3 +10,12 @@ export declare function mergeJsonSchemaObjects<T1 extends AnyObject, T2 extends
10
10
  export declare function isEveryItemString(arr: any[]): boolean;
11
11
  export declare function isEveryItemNumber(arr: any[]): boolean;
12
12
  export declare function isEveryItemPrimitive(arr: any[]): boolean;
13
+ /**
14
+ * Values of the `format` keyword that `j` builders emit, named after the js-lib types they produce.
15
+ * PascalCase on purpose, to never collide with standard, ajv-formats or OpenAPI formats (all lowercase),
16
+ * whose semantics differ: e.g `date-time` requires an offset, while `IsoDateTime` doesn't.
17
+ *
18
+ * Annotation only: validation is done by the custom keywords (e.g `IsoDate`), not by the format.
19
+ */
20
+ export declare const jFormats: readonly ['IsoDate', 'IsoDateTime', 'IsoMonth', 'UnixTimestamp', 'UnixTimestampMillis'];
21
+ export type JFormat = (typeof jFormats)[number];
@@ -87,3 +87,17 @@ export function isEveryItemPrimitive(arr) {
87
87
  }
88
88
  return true;
89
89
  }
90
+ /**
91
+ * Values of the `format` keyword that `j` builders emit, named after the js-lib types they produce.
92
+ * PascalCase on purpose, to never collide with standard, ajv-formats or OpenAPI formats (all lowercase),
93
+ * whose semantics differ: e.g `date-time` requires an offset, while `IsoDateTime` doesn't.
94
+ *
95
+ * Annotation only: validation is done by the custom keywords (e.g `IsoDate`), not by the format.
96
+ */
97
+ export const jFormats = [
98
+ 'IsoDate',
99
+ 'IsoDateTime',
100
+ 'IsoMonth',
101
+ 'UnixTimestamp',
102
+ 'UnixTimestampMillis',
103
+ ];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.126.0",
4
+ "version": "15.128.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
package/src/fs/fs2.ts CHANGED
@@ -14,7 +14,7 @@ Credit to: fs-extra (https://github.com/jprichardson/node-fs-extra)
14
14
 
15
15
  */
16
16
 
17
- import type { RmOptions, Stats } from 'node:fs'
17
+ import type { RmOptions, Stats, WriteFileOptions } from 'node:fs'
18
18
  import fs from 'node:fs'
19
19
  import fsp from 'node:fs/promises'
20
20
  import path from 'node:path'
@@ -65,12 +65,16 @@ class FS2 {
65
65
  return _jsonParse(str)
66
66
  }
67
67
 
68
- writeFile(filePath: string, data: string | Buffer): void {
69
- fs.writeFileSync(filePath, data)
68
+ writeFile(filePath: string, data: string | Buffer, opt?: WriteFileOptions): void {
69
+ fs.writeFileSync(filePath, data, opt)
70
70
  }
71
71
 
72
- async writeFileAsync(filePath: string, data: string | Buffer): Promise<void> {
73
- await fsp.writeFile(filePath, data)
72
+ async writeFileAsync(
73
+ filePath: string,
74
+ data: string | Buffer,
75
+ opt?: WriteFileOptions,
76
+ ): Promise<void> {
77
+ await fsp.writeFile(filePath, data, opt)
74
78
  }
75
79
 
76
80
  writeJson(filePath: string, data: any, opt?: JsonOptions): void {
@@ -14,6 +14,7 @@ import type {
14
14
  JsonSchemaIsoMonthOptions,
15
15
  JsonSchemaStringEmailOptions,
16
16
  } from './jSchema.js'
17
+ import { jFormats } from './jsonSchemaBuilder.util.js'
17
18
 
18
19
  // oxlint-disable unicorn/prefer-code-point
19
20
 
@@ -426,6 +427,18 @@ export function createAjv(opt?: Options): Ajv2020 {
426
427
  },
427
428
  })
428
429
 
430
+ // Annotation only (the values are validated by `enum`), registered so strict mode doesn't reject it
431
+ ajv.addKeyword({
432
+ keyword: 'enumNames',
433
+ schemaType: 'array',
434
+ })
435
+
436
+ // Annotation only (validated by the custom keywords, e.g `IsoDate`).
437
+ // `true` makes Ajv skip the format check, while unknown formats would throw in strict mode.
438
+ for (const format of jFormats) {
439
+ ajv.addFormat(format, true)
440
+ }
441
+
429
442
  ajv.addKeyword({
430
443
  keyword: 'errorMessages',
431
444
  schemaType: 'object',
@@ -4,5 +4,6 @@ export * from './ajvValidationError.js'
4
4
  export * from './from-data/generateJsonSchemaFromData.js'
5
5
  export * from './getAjv.js'
6
6
  export * from './jSchema.js'
7
+ export { jFormats, type JFormat } from './jsonSchemaBuilder.util.js'
7
8
 
8
9
  export { Ajv }
@@ -182,6 +182,7 @@ export const j = {
182
182
  | { readonly values: readonly (string | number | boolean | null)[] },
183
183
  >(input: T, opt?: JsonBuilderRuleOpt): JEnum<T extends readonly (infer U)[] ? U : Enum<T>> {
184
184
  let enumValues: readonly (string | number | boolean | null)[] | undefined
185
+ let enumNames: string[] | undefined
185
186
  let baseType: EnumBaseType = 'other'
186
187
 
187
188
  if (Array.isArray(input)) {
@@ -194,7 +195,11 @@ export const j = {
194
195
  } else if (typeof input === 'object') {
195
196
  const enumType = getEnumType(input)
196
197
  if (enumType === 'NumberEnum') {
197
- enumValues = _numberEnumValues(input as NumberEnum)
198
+ // Not via _numberEnumKeys: it misses `_enum()` objects (no reverse mapping)
199
+ // and misaligns with the values when members aren't declared in ascending order
200
+ const numberEntries = Object.entries(input).filter(([, value]) => typeof value === 'number')
201
+ enumValues = numberEntries.map(([, value]) => value)
202
+ enumNames = numberEntries.map(([name]) => name)
198
203
  baseType = 'number'
199
204
  } else if (enumType === 'StringEnum') {
200
205
  enumValues = _stringEnumValues(input as StringEnum)
@@ -203,7 +208,7 @@ export const j = {
203
208
  }
204
209
 
205
210
  _assert(enumValues, 'Unsupported enum input')
206
- return new JEnum(enumValues as any, baseType, opt)
211
+ return new JEnum(enumValues as any, baseType, opt, enumNames)
207
212
  },
208
213
 
209
214
  /**
@@ -801,12 +806,16 @@ export class JString<
801
806
  }
802
807
 
803
808
  isoDateTime(): JString<IsoDateTime, Opt> {
804
- return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded<IsoDateTime>()
809
+ return this.cloneAndUpdateSchema({
810
+ format: 'IsoDateTime',
811
+ IsoDateTime: true,
812
+ }).branded<IsoDateTime>()
805
813
  }
806
814
 
807
815
  isoMonth(): JBuilder<IsoMonth, false> {
808
816
  return new JBuilder<IsoMonth, false>({
809
817
  type: 'string',
818
+ format: 'IsoMonth',
810
819
  IsoMonth: {},
811
820
  })
812
821
  }
@@ -893,6 +902,7 @@ export class JIsoDate<Opt extends boolean = false> extends JBuilder<IsoDate, Opt
893
902
  constructor() {
894
903
  super({
895
904
  type: 'string',
905
+ format: 'IsoDate',
896
906
  IsoDate: {},
897
907
  })
898
908
  }
@@ -1027,19 +1037,21 @@ export class JNumber<
1027
1037
  }
1028
1038
 
1029
1039
  unixTimestamp(): JNumber<UnixTimestamp, Opt> {
1030
- return this.integer().branded<UnixTimestamp>()
1040
+ return this.integer().cloneAndUpdateSchema({ format: 'UnixTimestamp' }).branded<UnixTimestamp>()
1031
1041
  }
1032
1042
 
1033
1043
  unixTimestamp2000(): JNumber<UnixTimestamp, Opt> {
1034
- return this.integer().min(TS_2000).max(TS_2500).branded<UnixTimestamp>()
1044
+ return this.unixTimestamp().min(TS_2000).max(TS_2500)
1035
1045
  }
1036
1046
 
1037
1047
  unixTimestampMillis(): JNumber<UnixTimestampMillis, Opt> {
1038
- return this.integer().branded<UnixTimestampMillis>()
1048
+ return this.integer()
1049
+ .cloneAndUpdateSchema({ format: 'UnixTimestampMillis' })
1050
+ .branded<UnixTimestampMillis>()
1039
1051
  }
1040
1052
 
1041
1053
  unixTimestamp2000Millis(): JNumber<UnixTimestampMillis, Opt> {
1042
- return this.integer().min(TS_2000_MILLIS).max(TS_2500_MILLIS).branded<UnixTimestampMillis>()
1054
+ return this.unixTimestampMillis().min(TS_2000_MILLIS).max(TS_2500_MILLIS)
1043
1055
  }
1044
1056
 
1045
1057
  utcOffset(): this {
@@ -1331,8 +1343,14 @@ export class JEnum<
1331
1343
  OUT extends string | number | boolean | null,
1332
1344
  Opt extends boolean = false,
1333
1345
  > extends JBuilder<OUT, Opt> {
1334
- constructor(enumValues: readonly OUT[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt) {
1346
+ constructor(
1347
+ enumValues: readonly OUT[],
1348
+ baseType: EnumBaseType,
1349
+ opt?: JsonBuilderRuleOpt,
1350
+ enumNames?: readonly string[],
1351
+ ) {
1335
1352
  const jsonSchema: JsonSchema = { enum: enumValues }
1353
+ if (enumNames) jsonSchema.enumNames = enumNames
1336
1354
  // Specifying the base type helps in cases when we ask Ajv to coerce the types.
1337
1355
  // Having only the `enum` in the schema does not trigger a coercion in Ajv.
1338
1356
  if (baseType === 'string') jsonSchema.type = 'string'
@@ -2086,6 +2104,11 @@ export interface JsonSchema<OUT = unknown> {
2086
2104
  uniqueItems?: boolean
2087
2105
 
2088
2106
  enum?: any
2107
+ /**
2108
+ * Member names of a NumberEnum, aligned by index with `enum`.
2109
+ * Annotation only (not validated), lets consumers render `1` as e.g `PREVENT`.
2110
+ */
2111
+ enumNames?: readonly string[]
2089
2112
 
2090
2113
  hasIsOfTypeCheck?: boolean
2091
2114
 
@@ -101,3 +101,19 @@ export function isEveryItemPrimitive(arr: any[]): boolean {
101
101
  }
102
102
  return true
103
103
  }
104
+
105
+ /**
106
+ * Values of the `format` keyword that `j` builders emit, named after the js-lib types they produce.
107
+ * PascalCase on purpose, to never collide with standard, ajv-formats or OpenAPI formats (all lowercase),
108
+ * whose semantics differ: e.g `date-time` requires an offset, while `IsoDateTime` doesn't.
109
+ *
110
+ * Annotation only: validation is done by the custom keywords (e.g `IsoDate`), not by the format.
111
+ */
112
+ export const jFormats = [
113
+ 'IsoDate',
114
+ 'IsoDateTime',
115
+ 'IsoMonth',
116
+ 'UnixTimestamp',
117
+ 'UnixTimestampMillis',
118
+ ] as const
119
+ export type JFormat = (typeof jFormats)[number]