@naturalcycles/nodejs-lib 15.82.0 → 15.83.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.
@@ -82,7 +82,7 @@ export class SlackService {
82
82
  text = '```' + text + '```';
83
83
  }
84
84
  if (msg.mentions?.length) {
85
- text += '\n' + msg.mentions.map(s => `<@${s}>`).join(' ');
85
+ text += '\n' + msg.mentions.map(formatSlackMention).join(' ');
86
86
  }
87
87
  const prefix = await messagePrefixHook(msg);
88
88
  if (prefix === null)
@@ -139,3 +139,12 @@ export function slackDefaultMessagePrefixHook(msg) {
139
139
  }
140
140
  return tokens.filter(Boolean);
141
141
  }
142
+ // Formats a Slack mention based on the ID type:
143
+ // - User IDs (U...) and Bot IDs (B...) use: <@ID>
144
+ // - User Group IDs (S...) use: <!subteam^ID>
145
+ function formatSlackMention(id) {
146
+ if (id.startsWith('S')) {
147
+ return `<!subteam^${id}>`;
148
+ }
149
+ return `<@${id}>`;
150
+ }
@@ -42,9 +42,10 @@ export interface SlackMessage<CTX = any> extends SlackMessageProps {
42
42
  */
43
43
  kv?: AnyObject;
44
44
  /**
45
- * Slack Member IDs to mention at the end of the message.
46
- * Use Member IDs (e.g., 'U1234567890'), not usernames.
47
- * To find a Member ID: click on their profile in Slack → "..." → "Copy member ID".
45
+ * Slack IDs to mention at the end of the message.
46
+ * Supports:
47
+ * - User IDs (e.g., 'U1234567890') - click profile → "..." → "Copy member ID"
48
+ * - User Group IDs (e.g., 'S1234567890') - from user group settings
48
49
  */
49
50
  mentions?: string[];
50
51
  /**
@@ -1,5 +1,5 @@
1
1
  import { _isBetween, _lazyValue } from '@naturalcycles/js-lib';
2
- import { _mapObject, Set2 } from '@naturalcycles/js-lib/object';
2
+ import { _deepCopy, _mapObject, Set2 } from '@naturalcycles/js-lib/object';
3
3
  import { _substringAfterLast } from '@naturalcycles/js-lib/string';
4
4
  import { Ajv2020 } from 'ajv/dist/2020.js';
5
5
  import { validTLDs } from '../tlds.js';
@@ -513,6 +513,53 @@ export function createAjv(opt) {
513
513
  return validate;
514
514
  },
515
515
  });
516
+ ajv.addKeyword({
517
+ keyword: 'anyOfThese',
518
+ modifying: true,
519
+ errors: true,
520
+ schemaType: 'array',
521
+ compile(schemas, _parentSchema, _it) {
522
+ const validators = schemas.map(schema => ajv.compile(schema));
523
+ function validate(data, ctx) {
524
+ let correctValidator;
525
+ let result = false;
526
+ let clonedData;
527
+ // Try each validator until we find one that works!
528
+ for (const validator of validators) {
529
+ clonedData = isPrimitive(data) ? _deepCopy(data) : data;
530
+ result = validator(clonedData);
531
+ if (result) {
532
+ correctValidator = validator;
533
+ break;
534
+ }
535
+ }
536
+ if (result && ctx?.parentData && ctx.parentDataProperty) {
537
+ // If we found a validator and the data is valid and we are validating a property inside an object,
538
+ // then we can inject our result and be done with it.
539
+ ctx.parentData[ctx.parentDataProperty] = clonedData;
540
+ }
541
+ else if (result) {
542
+ // If we found a validator but we are not validating a property inside an object,
543
+ // then we must re-run the validation so that the mutations caused by Ajv
544
+ // will be done on the input data, not only on the clone.
545
+ result = correctValidator(data);
546
+ }
547
+ else {
548
+ // If we didn't find a fitting schema,
549
+ // we add our own error.
550
+ ;
551
+ validate.errors = [
552
+ {
553
+ instancePath: ctx?.instancePath ?? '',
554
+ message: `could not find a suitable schema to validate against`,
555
+ },
556
+ ];
557
+ }
558
+ return result;
559
+ }
560
+ return validate;
561
+ },
562
+ });
516
563
  return ajv;
517
564
  }
518
565
  const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
@@ -652,3 +699,6 @@ function isIsoMonthValid(s) {
652
699
  const month = (s.charCodeAt(5) - ZERO_CODE) * 10 + (s.charCodeAt(6) - ZERO_CODE);
653
700
  return _isBetween(year, 1900, 2500, '[]') && _isBetween(month, 1, 12, '[]');
654
701
  }
702
+ function isPrimitive(data) {
703
+ return data !== null && typeof data === 'object';
704
+ }
@@ -68,7 +68,7 @@ export declare const j: {
68
68
  oneOf<B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[], IN = BuilderInUnion<B>, OUT = BuilderOutUnion<B>>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false>;
69
69
  /**
70
70
  * Use only with primitive values, otherwise this function will throw to avoid bugs.
71
- * To validate objects, use `anyOfBy`.
71
+ * To validate objects, use `anyOfBy` or `anyOfThese`.
72
72
  *
73
73
  * Our Ajv is configured to strip unexpected properties from objects,
74
74
  * and since Ajv is mutating the input, this means that it cannot
@@ -78,7 +78,28 @@ export declare const j: {
78
78
  * Use `oneOf` when schemas are mutually exclusive.
79
79
  */
80
80
  anyOf<B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[], IN = BuilderInUnion<B>, OUT = BuilderOutUnion<B>>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false>;
81
+ /**
82
+ * Pick validation schema for an object based on the value of a specific property.
83
+ *
84
+ * ```
85
+ * const schemaMap = {
86
+ * true: successSchema,
87
+ * false: errorSchema
88
+ * }
89
+ *
90
+ * const schema = j.anyOfBy('success', schemaMap)
91
+ * ```
92
+ */
81
93
  anyOfBy<P extends string, D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>, IN = AnyOfByIn<D>, OUT = AnyOfByOut<D>>(propertyName: P, schemaDictionary: D): JsonSchemaAnyOfByBuilder<IN, OUT, P>;
94
+ /**
95
+ * Custom version of `anyOf` which - in contrast to the original function - does not mutate the input.
96
+ * This comes with a performance penalty, so do not use it where performance matters.
97
+ *
98
+ * ```
99
+ * const schema = j.anyOfThese([successSchema, errorSchema])
100
+ * ```
101
+ */
102
+ anyOfThese<B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[], IN = BuilderInUnion<B>, OUT = BuilderOutUnion<B>>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false>;
82
103
  and(): {
83
104
  silentBob: () => never;
84
105
  };
@@ -369,6 +390,10 @@ export declare class JsonSchemaAnyOfByBuilder<IN, OUT, _P extends string = strin
369
390
  in: IN;
370
391
  constructor(propertyName: string, schemaDictionary: Record<PropertyKey, JsonSchemaTerminal<any, any, any>>);
371
392
  }
393
+ export declare class JsonSchemaAnyOfTheseBuilder<IN, OUT, _P extends string = string> extends JsonSchemaAnyBuilder<AnyOfByInput<IN, _P> | IN, OUT, false> {
394
+ in: IN;
395
+ constructor(propertyName: string, schemaDictionary: Record<PropertyKey, JsonSchemaTerminal<any, any, any>>);
396
+ }
372
397
  type EnumBaseType = 'string' | 'number' | 'other';
373
398
  export interface JsonSchema<IN = unknown, OUT = IN> {
374
399
  readonly in?: IN;
@@ -440,6 +465,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
440
465
  propertyName: string;
441
466
  schemaDictionary: Record<string, JsonSchema>;
442
467
  };
468
+ anyOfThese?: JsonSchema[];
443
469
  }
444
470
  declare function object(props: AnyObject): never;
445
471
  declare function object<IN extends AnyObject>(props: {
@@ -133,7 +133,7 @@ export const j = {
133
133
  },
134
134
  /**
135
135
  * Use only with primitive values, otherwise this function will throw to avoid bugs.
136
- * To validate objects, use `anyOfBy`.
136
+ * To validate objects, use `anyOfBy` or `anyOfThese`.
137
137
  *
138
138
  * Our Ajv is configured to strip unexpected properties from objects,
139
139
  * and since Ajv is mutating the input, this means that it cannot
@@ -149,9 +149,34 @@ export const j = {
149
149
  anyOf: schemas,
150
150
  });
151
151
  },
152
+ /**
153
+ * Pick validation schema for an object based on the value of a specific property.
154
+ *
155
+ * ```
156
+ * const schemaMap = {
157
+ * true: successSchema,
158
+ * false: errorSchema
159
+ * }
160
+ *
161
+ * const schema = j.anyOfBy('success', schemaMap)
162
+ * ```
163
+ */
152
164
  anyOfBy(propertyName, schemaDictionary) {
153
165
  return new JsonSchemaAnyOfByBuilder(propertyName, schemaDictionary);
154
166
  },
167
+ /**
168
+ * Custom version of `anyOf` which - in contrast to the original function - does not mutate the input.
169
+ * This comes with a performance penalty, so do not use it where performance matters.
170
+ *
171
+ * ```
172
+ * const schema = j.anyOfThese([successSchema, errorSchema])
173
+ * ```
174
+ */
175
+ anyOfThese(items) {
176
+ return new JsonSchemaAnyBuilder({
177
+ anyOfThese: items.map(b => b.build()),
178
+ });
179
+ },
155
180
  and() {
156
181
  return {
157
182
  silentBob: () => {
@@ -837,6 +862,22 @@ export class JsonSchemaAnyOfByBuilder extends JsonSchemaAnyBuilder {
837
862
  });
838
863
  }
839
864
  }
865
+ export class JsonSchemaAnyOfTheseBuilder extends JsonSchemaAnyBuilder {
866
+ constructor(propertyName, schemaDictionary) {
867
+ const builtSchemaDictionary = {};
868
+ for (const [key, schema] of Object.entries(schemaDictionary)) {
869
+ builtSchemaDictionary[key] = schema.build();
870
+ }
871
+ super({
872
+ type: 'object',
873
+ hasIsOfTypeCheck: true,
874
+ anyOfBy: {
875
+ propertyName,
876
+ schemaDictionary: builtSchemaDictionary,
877
+ },
878
+ });
879
+ }
880
+ }
840
881
  function object(props) {
841
882
  return new JsonSchemaObjectBuilder(props);
842
883
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.82.0",
4
+ "version": "15.83.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -51,9 +51,10 @@ export interface SlackMessage<CTX = any> extends SlackMessageProps {
51
51
  kv?: AnyObject
52
52
 
53
53
  /**
54
- * Slack Member IDs to mention at the end of the message.
55
- * Use Member IDs (e.g., 'U1234567890'), not usernames.
56
- * To find a Member ID: click on their profile in Slack → "..." → "Copy member ID".
54
+ * Slack IDs to mention at the end of the message.
55
+ * Supports:
56
+ * - User IDs (e.g., 'U1234567890') - click profile → "..." → "Copy member ID"
57
+ * - User Group IDs (e.g., 'S1234567890') - from user group settings
57
58
  */
58
59
  mentions?: string[]
59
60
 
@@ -109,7 +109,7 @@ export class SlackService<CTX = any> {
109
109
  }
110
110
 
111
111
  if (msg.mentions?.length) {
112
- text += '\n' + msg.mentions.map(s => `<@${s}>`).join(' ')
112
+ text += '\n' + msg.mentions.map(formatSlackMention).join(' ')
113
113
  }
114
114
 
115
115
  const prefix = await messagePrefixHook(msg)
@@ -191,3 +191,13 @@ export function slackDefaultMessagePrefixHook(msg: SlackMessage): string[] {
191
191
 
192
192
  return tokens.filter(Boolean)
193
193
  }
194
+
195
+ // Formats a Slack mention based on the ID type:
196
+ // - User IDs (U...) and Bot IDs (B...) use: <@ID>
197
+ // - User Group IDs (S...) use: <!subteam^ID>
198
+ function formatSlackMention(id: string): string {
199
+ if (id.startsWith('S')) {
200
+ return `<!subteam^${id}>`
201
+ }
202
+ return `<@${id}>`
203
+ }
@@ -1,5 +1,5 @@
1
1
  import { _isBetween, _lazyValue } from '@naturalcycles/js-lib'
2
- import { _mapObject, Set2 } from '@naturalcycles/js-lib/object'
2
+ import { _deepCopy, _mapObject, Set2 } from '@naturalcycles/js-lib/object'
3
3
  import { _substringAfterLast } from '@naturalcycles/js-lib/string'
4
4
  import type { AnyObject } from '@naturalcycles/js-lib/types'
5
5
  import { Ajv2020, type Options, type ValidateFunction } from 'ajv/dist/2020.js'
@@ -585,6 +585,56 @@ export function createAjv(opt?: Options): Ajv2020 {
585
585
  },
586
586
  })
587
587
 
588
+ ajv.addKeyword({
589
+ keyword: 'anyOfThese',
590
+ modifying: true,
591
+ errors: true,
592
+ schemaType: 'array',
593
+ compile(schemas: JsonSchemaTerminal<any, any, any>[], _parentSchema, _it) {
594
+ const validators = schemas.map(schema => ajv.compile(schema))
595
+
596
+ function validate(data: AnyObject, ctx: any): boolean {
597
+ let correctValidator: ValidateFunction<unknown> | undefined
598
+ let result = false
599
+ let clonedData: any
600
+
601
+ // Try each validator until we find one that works!
602
+ for (const validator of validators) {
603
+ clonedData = isPrimitive(data) ? _deepCopy(data) : data
604
+ result = validator(clonedData)
605
+ if (result) {
606
+ correctValidator = validator
607
+ break
608
+ }
609
+ }
610
+
611
+ if (result && ctx?.parentData && ctx.parentDataProperty) {
612
+ // If we found a validator and the data is valid and we are validating a property inside an object,
613
+ // then we can inject our result and be done with it.
614
+ ctx.parentData[ctx.parentDataProperty] = clonedData
615
+ } else if (result) {
616
+ // If we found a validator but we are not validating a property inside an object,
617
+ // then we must re-run the validation so that the mutations caused by Ajv
618
+ // will be done on the input data, not only on the clone.
619
+ result = correctValidator!(data)
620
+ } else {
621
+ // If we didn't find a fitting schema,
622
+ // we add our own error.
623
+ ;(validate as any).errors = [
624
+ {
625
+ instancePath: ctx?.instancePath ?? '',
626
+ message: `could not find a suitable schema to validate against`,
627
+ },
628
+ ]
629
+ }
630
+
631
+ return result
632
+ }
633
+
634
+ return validate
635
+ },
636
+ })
637
+
588
638
  return ajv
589
639
  }
590
640
 
@@ -728,3 +778,7 @@ function isIsoMonthValid(s: string): boolean {
728
778
 
729
779
  return _isBetween(year, 1900, 2500, '[]') && _isBetween(month, 1, 12, '[]')
730
780
  }
781
+
782
+ function isPrimitive(data: any): boolean {
783
+ return data !== null && typeof data === 'object'
784
+ }
@@ -217,7 +217,7 @@ export const j = {
217
217
 
218
218
  /**
219
219
  * Use only with primitive values, otherwise this function will throw to avoid bugs.
220
- * To validate objects, use `anyOfBy`.
220
+ * To validate objects, use `anyOfBy` or `anyOfThese`.
221
221
  *
222
222
  * Our Ajv is configured to strip unexpected properties from objects,
223
223
  * and since Ajv is mutating the input, this means that it cannot
@@ -242,6 +242,18 @@ export const j = {
242
242
  })
243
243
  },
244
244
 
245
+ /**
246
+ * Pick validation schema for an object based on the value of a specific property.
247
+ *
248
+ * ```
249
+ * const schemaMap = {
250
+ * true: successSchema,
251
+ * false: errorSchema
252
+ * }
253
+ *
254
+ * const schema = j.anyOfBy('success', schemaMap)
255
+ * ```
256
+ */
245
257
  anyOfBy<
246
258
  P extends string,
247
259
  D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>,
@@ -251,6 +263,24 @@ export const j = {
251
263
  return new JsonSchemaAnyOfByBuilder<IN, OUT, P>(propertyName, schemaDictionary)
252
264
  },
253
265
 
266
+ /**
267
+ * Custom version of `anyOf` which - in contrast to the original function - does not mutate the input.
268
+ * This comes with a performance penalty, so do not use it where performance matters.
269
+ *
270
+ * ```
271
+ * const schema = j.anyOfThese([successSchema, errorSchema])
272
+ * ```
273
+ */
274
+ anyOfThese<
275
+ B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[],
276
+ IN = BuilderInUnion<B>,
277
+ OUT = BuilderOutUnion<B>,
278
+ >(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false> {
279
+ return new JsonSchemaAnyBuilder<IN, OUT, false>({
280
+ anyOfThese: items.map(b => b.build()),
281
+ })
282
+ },
283
+
254
284
  and() {
255
285
  return {
256
286
  silentBob: () => {
@@ -1267,6 +1297,34 @@ export class JsonSchemaAnyOfByBuilder<
1267
1297
  }
1268
1298
  }
1269
1299
 
1300
+ export class JsonSchemaAnyOfTheseBuilder<
1301
+ IN,
1302
+ OUT,
1303
+ // eslint-disable-next-line @typescript-eslint/naming-convention
1304
+ _P extends string = string,
1305
+ > extends JsonSchemaAnyBuilder<AnyOfByInput<IN, _P> | IN, OUT, false> {
1306
+ declare in: IN
1307
+
1308
+ constructor(
1309
+ propertyName: string,
1310
+ schemaDictionary: Record<PropertyKey, JsonSchemaTerminal<any, any, any>>,
1311
+ ) {
1312
+ const builtSchemaDictionary: Record<string, JsonSchema> = {}
1313
+ for (const [key, schema] of Object.entries(schemaDictionary)) {
1314
+ builtSchemaDictionary[key] = schema.build()
1315
+ }
1316
+
1317
+ super({
1318
+ type: 'object',
1319
+ hasIsOfTypeCheck: true,
1320
+ anyOfBy: {
1321
+ propertyName,
1322
+ schemaDictionary: builtSchemaDictionary,
1323
+ },
1324
+ })
1325
+ }
1326
+ }
1327
+
1270
1328
  type EnumBaseType = 'string' | 'number' | 'other'
1271
1329
 
1272
1330
  export interface JsonSchema<IN = unknown, OUT = IN> {
@@ -1350,6 +1408,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
1350
1408
  propertyName: string
1351
1409
  schemaDictionary: Record<string, JsonSchema>
1352
1410
  }
1411
+ anyOfThese?: JsonSchema[]
1353
1412
  }
1354
1413
 
1355
1414
  function object(props: AnyObject): never