@nestia/e2e 7.1.1-dev.20250714 → 7.2.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.
@@ -1,678 +1,678 @@
1
- import { RandomGenerator } from "./RandomGenerator";
2
- import { json_equal_to } from "./internal/json_equal_to";
3
-
4
- /**
5
- * A comprehensive collection of E2E validation utilities for testing
6
- * applications.
7
- *
8
- * TestValidator provides type-safe validation functions for common testing
9
- * scenarios including condition checking, equality validation, error testing,
10
- * HTTP error validation, pagination testing, search functionality validation,
11
- * and sorting validation.
12
- *
13
- * All functions follow a currying pattern to enable reusable test
14
- * configurations and provide detailed error messages for debugging failed
15
- * assertions.
16
- *
17
- * @author Jeongho Nam - https://github.com/samchon
18
- * @example
19
- * ```typescript
20
- * // Basic condition testing
21
- * TestValidator.predicate("user should be authenticated")(user.isAuthenticated);
22
- *
23
- * // Equality validation
24
- * TestValidator.equals("API response should match expected")(expected)(actual);
25
- *
26
- * // Error validation
27
- * TestValidator.error("should throw on invalid input")(() => validateInput(""));
28
- * ```;
29
- */
30
- export namespace TestValidator {
31
- /**
32
- * Validates that a given condition evaluates to true.
33
- *
34
- * Supports synchronous boolean values, synchronous functions returning
35
- * boolean, and asynchronous functions returning Promise<boolean>. The return
36
- * type is automatically inferred based on the input type.
37
- *
38
- * @example
39
- * ```typescript
40
- * // Synchronous boolean
41
- * TestValidator.predicate("user should exist")(user !== null);
42
- *
43
- * // Synchronous function
44
- * TestValidator.predicate("array should be empty")(() => arr.length === 0);
45
- *
46
- * // Asynchronous function
47
- * await TestValidator.predicate("database should be connected")(
48
- * async () => await db.ping()
49
- * );
50
- * ```;
51
- *
52
- * @param title - Descriptive title used in error messages when validation
53
- * fails
54
- * @returns A currying function that accepts the condition to validate
55
- * @throws Error with descriptive message when condition is not satisfied
56
- */
57
- export const predicate =
58
- (title: string) =>
59
- <T extends boolean | (() => boolean) | (() => Promise<boolean>)>(
60
- condition: T,
61
- ): T extends () => Promise<boolean> ? Promise<void> : void => {
62
- const message = () =>
63
- `Bug on ${title}: expected condition is not satisfied.`;
64
-
65
- // SCALAR
66
- if (typeof condition === "boolean") {
67
- if (condition !== true) throw new Error(message());
68
- return undefined as any;
69
- }
70
-
71
- // CLOSURE
72
- const output: boolean | Promise<boolean> = condition();
73
- if (typeof output === "boolean") {
74
- if (output !== true) throw new Error(message());
75
- return undefined as any;
76
- }
77
-
78
- // ASYNCHRONOUS
79
- return new Promise<void>((resolve, reject) => {
80
- output
81
- .then((flag) => {
82
- if (flag === true) resolve();
83
- else reject(message());
84
- })
85
- .catch(reject);
86
- }) as any;
87
- };
88
-
89
- /**
90
- * Validates deep equality between two values using JSON comparison.
91
- *
92
- * Performs recursive comparison of objects and arrays. Supports an optional
93
- * exception filter to ignore specific keys during comparison. Useful for
94
- * validating API responses, data transformations, and object state changes.
95
- *
96
- * **Type Safety Notes:**
97
- *
98
- * - The generic type T is inferred from the `actual` parameter (first in the
99
- * currying chain)
100
- * - The `expected` parameter must be assignable to `T | null | undefined`
101
- * - For objects, `expected` must have the same or subset of properties as
102
- * `actual`
103
- * - For union types like `string | null`, ensure proper type compatibility:
104
- *
105
- * ```typescript
106
- * const x: string | null;
107
- * TestValidator.equals("works")(x)(null); // ✅ Works: null is assignable to string | null
108
- * TestValidator.equals("error")(null)(x); // ❌ Error: x might be string, but expected is null
109
- * ```
110
- *
111
- * @example
112
- * ```typescript
113
- * // Basic equality
114
- * TestValidator.equals("response should match expected")(expectedUser)(actualUser);
115
- *
116
- * // Ignore timestamps in comparison
117
- * TestValidator.equals("user data should match", (key) => key === "updatedAt")(
118
- * expectedUser
119
- * )(actualUser);
120
- *
121
- * // Validate API response structure
122
- * const validateResponse = TestValidator.equals("API response structure");
123
- * validateResponse({ id: 1, name: "John" })({ id: 1, name: "John" });
124
- *
125
- * // Type-safe nullable comparisons
126
- * const nullableData: { name: string } | null = getData();
127
- * TestValidator.equals("nullable check")(nullableData)(null); // ✅ Safe
128
- * ```;
129
- *
130
- * @param title - Descriptive title used in error messages when values differ
131
- * @param exception - Optional filter function to exclude specific keys from
132
- * comparison
133
- * @returns A currying function chain: first accepts expected value, then
134
- * actual value
135
- * @throws Error with detailed diff information when values are not equal
136
- */
137
- export const equals =
138
- (title: string, exception: (key: string) => boolean = () => false) =>
139
- <T>(actual: T) =>
140
- (expected: T | null | undefined) => {
141
- const diff: string[] = json_equal_to(exception)(actual)(expected);
142
- if (diff.length)
143
- throw new Error(
144
- [
145
- `Bug on ${title}: found different values - [${diff.join(", ")}]:`,
146
- "\n",
147
- JSON.stringify({ actual, expected }, null, 2),
148
- ].join("\n"),
149
- );
150
- };
151
-
152
- /**
153
- * Validates deep inequality between two values using JSON comparison.
154
- *
155
- * Performs recursive comparison of objects and arrays to ensure they are NOT
156
- * equal. Supports an optional exception filter to ignore specific keys during
157
- * comparison. Useful for validating that data has changed, objects are
158
- * different, or mutations have occurred.
159
- *
160
- * **Type Safety Notes:**
161
- *
162
- * - The generic type T is inferred from the `actual` parameter (first in the
163
- * currying chain)
164
- * - The `expected` parameter must be assignable to `T | null | undefined`
165
- * - For objects, `expected` must have the same or subset of properties as
166
- * `actual`
167
- * - For union types like `string | null`, ensure proper type compatibility:
168
- *
169
- * ```typescript
170
- * const x: string | null;
171
- * TestValidator.notEquals("works")(x)(null); // ✅ Works: null is assignable to string | null
172
- * TestValidator.notEquals("error")(null)(x); // ❌ Error: x might be string, but expected is null
173
- * ```
174
- *
175
- * @example
176
- * ```typescript
177
- * // Basic inequality
178
- * TestValidator.notEquals("user should be different after update")(originalUser)(updatedUser);
179
- *
180
- * // Ignore timestamps in comparison
181
- * TestValidator.notEquals("user data should differ", (key) => key === "updatedAt")(
182
- * originalUser
183
- * )(modifiedUser);
184
- *
185
- * // Validate state changes
186
- * const validateStateChange = TestValidator.notEquals("state should have changed");
187
- * validateStateChange(initialState)(currentState);
188
- *
189
- * // Type-safe nullable comparisons
190
- * const mutableData: { count: number } | null = getMutableData();
191
- * TestValidator.notEquals("should have changed")(mutableData)(null); // ✅ Safe
192
- * ```;
193
- *
194
- * @param title - Descriptive title used in error messages when values are
195
- * equal
196
- * @param exception - Optional filter function to exclude specific keys from
197
- * comparison
198
- * @returns A currying function chain: first accepts expected value, then
199
- * actual value
200
- * @throws Error when values are equal (indicating validation failure)
201
- */
202
- export const notEquals =
203
- (title: string, exception: (key: string) => boolean = () => false) =>
204
- <T>(actual: T) =>
205
- (expected: T | null | undefined) => {
206
- const diff: string[] = json_equal_to(exception)(actual)(expected);
207
- if (diff.length === 0)
208
- throw new Error(
209
- [
210
- `Bug on ${title}: values should be different but are equal:`,
211
- "\n",
212
- JSON.stringify({ actual, expected }, null, 2),
213
- ].join("\n"),
214
- );
215
- };
216
-
217
- /**
218
- * Validates that a function throws an error or rejects when executed.
219
- *
220
- * Expects the provided function to fail. If the function executes
221
- * successfully without throwing an error or rejecting, this validator will
222
- * throw an exception. Supports both synchronous and asynchronous functions.
223
- *
224
- * @example
225
- * ```typescript
226
- * // Synchronous error validation
227
- * TestValidator.error("should reject invalid email")(
228
- * () => validateEmail("invalid-email")
229
- * );
230
- *
231
- * // Asynchronous error validation
232
- * await TestValidator.error("should reject unauthorized access")(
233
- * async () => await api.functional.getSecretData()
234
- * );
235
- *
236
- * // Validate input validation
237
- * TestValidator.error("should throw on empty string")(
238
- * () => processRequiredInput("")
239
- * );
240
- * ```;
241
- *
242
- * @param title - Descriptive title used in error messages when no error
243
- * occurs
244
- * @returns A currying function that accepts the task function to validate
245
- * @throws Error when the task function does not throw an error or reject
246
- */
247
- export const error =
248
- (title: string) =>
249
- <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
250
- const message = () => `Bug on ${title}: exception must be thrown.`;
251
- try {
252
- const output: T = task();
253
- if (is_promise(output))
254
- return new Promise<void>((resolve, reject) =>
255
- output.catch(() => resolve()).then(() => reject(message())),
256
- ) as any;
257
- else throw new Error(message());
258
- } catch {
259
- return undefined as any;
260
- }
261
- };
262
-
263
- /**
264
- * Validates that a function throws an HTTP error with specific status codes.
265
- *
266
- * Specialized error validator for HTTP operations. Validates that the
267
- * function throws an HttpError with one of the specified status codes. Useful
268
- * for testing API endpoints, authentication, and authorization logic.
269
- *
270
- * @example
271
- * ```typescript
272
- * // Validate 401 Unauthorized
273
- * await TestValidator.httpError("should return 401 for invalid token")(401)(
274
- * async () => await api.functional.getProtectedResource("invalid-token")
275
- * );
276
- *
277
- * // Validate multiple possible error codes
278
- * await TestValidator.httpError("should return client error")(400, 404, 422)(
279
- * async () => await api.functional.updateNonexistentResource(data)
280
- * );
281
- *
282
- * // Validate server errors
283
- * TestValidator.httpError("should handle server errors")(500, 502, 503)(
284
- * () => callFaultyEndpoint()
285
- * );
286
- * ```;
287
- *
288
- * @param title - Descriptive title used in error messages
289
- * @returns A currying function that accepts status codes, then the task
290
- * function
291
- * @throws Error when function doesn't throw HttpError or status code doesn't
292
- * match
293
- */
294
- export const httpError =
295
- (title: string) =>
296
- (...statuses: number[]) =>
297
- <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
298
- const message = (actual?: number) =>
299
- typeof actual === "number"
300
- ? `Bug on ${title}: status code must be ${statuses.join(
301
- " or ",
302
- )}, but ${actual}.`
303
- : `Bug on ${title}: status code must be ${statuses.join(
304
- " or ",
305
- )}, but succeeded.`;
306
- const predicate = (exp: any): Error | null =>
307
- typeof exp === "object" &&
308
- exp.constructor.name === "HttpError" &&
309
- statuses.some((val) => val === exp.status)
310
- ? null
311
- : new Error(
312
- message(
313
- typeof exp === "object" && exp.constructor.name === "HttpError"
314
- ? exp.status
315
- : undefined,
316
- ),
317
- );
318
- try {
319
- const output: T = task();
320
- if (is_promise(output))
321
- return new Promise<void>((resolve, reject) =>
322
- output
323
- .catch((exp) => {
324
- const res: Error | null = predicate(exp);
325
- if (res) reject(res);
326
- else resolve();
327
- })
328
- .then(() => reject(new Error(message()))),
329
- ) as any;
330
- else throw new Error(message());
331
- } catch (exp) {
332
- const res: Error | null = predicate(exp);
333
- if (res) throw res;
334
- return undefined!;
335
- }
336
- };
337
-
338
- /**
339
- * Safely executes a function and captures any errors without throwing.
340
- *
341
- * Utility function for error handling in tests. Executes the provided
342
- * function and returns any error that occurs, or null if successful. Supports
343
- * both synchronous and asynchronous functions. Useful for testing error
344
- * conditions without stopping test execution.
345
- *
346
- * @example
347
- * ```typescript
348
- * // Synchronous error capture
349
- * const error = TestValidator.proceed(() => {
350
- * throw new Error("Something went wrong");
351
- * });
352
- * console.log(error?.message); // "Something went wrong"
353
- *
354
- * // Asynchronous error capture
355
- * const asyncError = await TestValidator.proceed(async () => {
356
- * await failingAsyncOperation();
357
- * });
358
- *
359
- * // Success case
360
- * const noError = TestValidator.proceed(() => {
361
- * return "success";
362
- * });
363
- * console.log(noError); // null
364
- * ```;
365
- *
366
- * @param task - Function to execute safely
367
- * @returns Error object if function throws/rejects, null if successful
368
- */
369
- export function proceed(task: () => Promise<any>): Promise<Error | null>;
370
- export function proceed(task: () => any): Error | null;
371
- export function proceed(
372
- task: () => any,
373
- ): Promise<Error | null> | (Error | null) {
374
- try {
375
- const output: any = task();
376
- if (is_promise(output))
377
- return new Promise<Error | null>((resolve) =>
378
- output
379
- .catch((exp) => resolve(exp as Error))
380
- .then(() => resolve(null)),
381
- );
382
- } catch (exp) {
383
- return exp as Error;
384
- }
385
- return null;
386
- }
387
-
388
- /**
389
- * Validates pagination index API results against expected entity order.
390
- *
391
- * Compares the order of entities returned by a pagination API with manually
392
- * sorted expected results. Validates that entity IDs appear in the correct
393
- * sequence. Commonly used for testing database queries, search results, and
394
- * any paginated data APIs.
395
- *
396
- * @example
397
- * ```typescript
398
- * // Test article pagination
399
- * const expectedArticles = await db.articles.findAll({ order: 'created_at DESC' });
400
- * const actualArticles = await api.functional.getArticles({ page: 1, limit: 10 });
401
- *
402
- * TestValidator.index("article pagination order")(expectedArticles)(
403
- * actualArticles,
404
- * true // enable trace logging
405
- * );
406
- *
407
- * // Test user search results
408
- * const manuallyFilteredUsers = allUsers.filter(u => u.name.includes("John"));
409
- * const apiSearchResults = await api.functional.searchUsers({ query: "John" });
410
- *
411
- * TestValidator.index("user search results")(manuallyFilteredUsers)(
412
- * apiSearchResults
413
- * );
414
- * ```;
415
- *
416
- * @param title - Descriptive title used in error messages when order differs
417
- * @returns A currying function chain: expected entities, then actual entities
418
- * @throws Error when entity order differs between expected and actual results
419
- */
420
- export const index =
421
- (title: string) =>
422
- <Solution extends IEntity<any>>(expected: Solution[]) =>
423
- <Summary extends IEntity<any>>(
424
- gotten: Summary[],
425
- trace: boolean = false,
426
- ): void => {
427
- const length: number = Math.min(expected.length, gotten.length);
428
- expected = expected.slice(0, length);
429
- gotten = gotten.slice(0, length);
430
-
431
- const xIds: string[] = get_ids(expected).slice(0, length);
432
- const yIds: string[] = get_ids(gotten)
433
- .filter((id) => id >= xIds[0])
434
- .slice(0, length);
435
-
436
- const equals: boolean = xIds.every((x, i) => x === yIds[i]);
437
- if (equals === true) return;
438
- else if (trace === true)
439
- console.log({
440
- expected: xIds,
441
- gotten: yIds,
442
- });
443
- throw new Error(
444
- `Bug on ${title}: result of the index is different with manual aggregation.`,
445
- );
446
- };
447
-
448
- /**
449
- * Validates search functionality by testing API results against manual
450
- * filtering.
451
- *
452
- * Comprehensive search validation that samples entities from a complete
453
- * dataset, extracts search values, applies manual filtering, calls the search
454
- * API, and compares results. Validates that search APIs return the correct
455
- * subset of data matching the search criteria.
456
- *
457
- * @example
458
- * ```typescript
459
- * // Test article search functionality
460
- * const allArticles = await db.articles.findAll();
461
- * const searchValidator = TestValidator.search("article search API")(
462
- * (req) => api.searchArticles(req)
463
- * )(allArticles, 5); // test with 5 random samples
464
- *
465
- * await searchValidator({
466
- * fields: ["title", "content"],
467
- * values: (article) => [article.title.split(" ")[0]], // first word
468
- * filter: (article, [keyword]) =>
469
- * article.title.includes(keyword) || article.content.includes(keyword),
470
- * request: ([keyword]) => ({ q: keyword })
471
- * });
472
- *
473
- * // Test user search with multiple criteria
474
- * await TestValidator.search("user search with filters")(
475
- * (req) => api.getUsers(req)
476
- * )(allUsers, 3)({
477
- * fields: ["status", "role"],
478
- * values: (user) => [user.status, user.role],
479
- * filter: (user, [status, role]) =>
480
- * user.status === status && user.role === role,
481
- * request: ([status, role]) => ({ status, role })
482
- * });
483
- * ```;
484
- *
485
- * @param title - Descriptive title used in error messages when search fails
486
- * @returns A currying function chain: API getter function, then dataset and
487
- * sample count
488
- * @throws Error when API search results don't match manual filtering results
489
- */
490
- export const search =
491
- (title: string) =>
492
- <Entity extends IEntity<any>, Request>(
493
- getter: (input: Request) => Promise<Entity[]>,
494
- ) =>
495
- (total: Entity[], sampleCount: number = 1) =>
496
- async <Values extends any[]>(
497
- props: ISearchProps<Entity, Values, Request>,
498
- ): Promise<void> => {
499
- const samples: Entity[] = RandomGenerator.sample(total)(sampleCount);
500
- for (const s of samples) {
501
- const values: Values = props.values(s);
502
- const filtered: Entity[] = total.filter((entity) =>
503
- props.filter(entity, values),
504
- );
505
- const gotten: Entity[] = await getter(props.request(values));
506
-
507
- TestValidator.index(`${title} (${props.fields.join(", ")})`)(filtered)(
508
- gotten,
509
- );
510
- }
511
- };
512
-
513
- /**
514
- * Configuration interface for search validation functionality.
515
- *
516
- * Defines the structure needed to validate search operations by specifying
517
- * how to extract search values from entities, filter the dataset manually,
518
- * and construct API requests.
519
- *
520
- * @template Entity - Type of entities being searched, must have an ID field
521
- * @template Values - Tuple type representing the search values extracted from
522
- * entities
523
- * @template Request - Type of the API request object
524
- */
525
- export interface ISearchProps<
526
- Entity extends IEntity<any>,
527
- Values extends any[],
528
- Request,
529
- > {
530
- /** Field names being searched, used in error messages for identification */
531
- fields: string[];
532
-
533
- /**
534
- * Extracts search values from a sample entity
535
- *
536
- * @param entity - The entity to extract search values from
537
- * @returns Tuple of values used for searching
538
- */
539
- values(entity: Entity): Values;
540
-
541
- /**
542
- * Manual filter function to determine if an entity matches search criteria
543
- *
544
- * @param entity - Entity to test against criteria
545
- * @param values - Search values to match against
546
- * @returns True if entity matches the search criteria
547
- */
548
- filter(entity: Entity, values: Values): boolean;
549
-
550
- /**
551
- * Constructs API request object from search values
552
- *
553
- * @param values - Search values to include in request
554
- * @returns Request object for the search API
555
- */
556
- request(values: Values): Request;
557
- }
558
-
559
- /**
560
- * Validates sorting functionality of pagination APIs.
561
- *
562
- * Tests sorting operations by calling the API with sort parameters and
563
- * validating that results are correctly ordered. Supports multiple fields,
564
- * ascending/descending order, and optional filtering. Provides detailed error
565
- * reporting for sorting failures.
566
- *
567
- * @example
568
- * ```typescript
569
- * // Test single field sorting
570
- * const sortValidator = TestValidator.sort("article sorting")(
571
- * (sortable) => api.getArticles({ sort: sortable })
572
- * )("created_at")(
573
- * (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
574
- * );
575
- *
576
- * await sortValidator("+"); // ascending
577
- * await sortValidator("-"); // descending
578
- *
579
- * // Test multi-field sorting with filtering
580
- * const userSortValidator = TestValidator.sort("user sorting")(
581
- * (sortable) => api.getUsers({ sort: sortable })
582
- * )("status", "created_at")(
583
- * (a, b) => {
584
- * if (a.status !== b.status) return a.status.localeCompare(b.status);
585
- * return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
586
- * },
587
- * (user) => user.isActive // only test active users
588
- * );
589
- *
590
- * await userSortValidator("+", true); // ascending with trace logging
591
- * ```;
592
- *
593
- * @param title - Descriptive title used in error messages when sorting fails
594
- * @returns A currying function chain: API getter, field names, comparator,
595
- * then direction
596
- * @throws Error when API results are not properly sorted according to
597
- * specification
598
- */
599
- export const sort =
600
- (title: string) =>
601
- <
602
- T extends object,
603
- Fields extends string,
604
- Sortable extends Array<`-${Fields}` | `+${Fields}`> = Array<
605
- `-${Fields}` | `+${Fields}`
606
- >,
607
- >(
608
- getter: (sortable: Sortable) => Promise<T[]>,
609
- ) =>
610
- (...fields: Fields[]) =>
611
- (comp: (x: T, y: T) => number, filter?: (elem: T) => boolean) =>
612
- async (direction: "+" | "-", trace: boolean = false) => {
613
- let data: T[] = await getter(
614
- fields.map((field) => `${direction}${field}` as const) as Sortable,
615
- );
616
- if (filter) data = data.filter(filter);
617
-
618
- const reversed: typeof comp =
619
- direction === "+" ? comp : (x, y) => comp(y, x);
620
- if (is_sorted(data, reversed) === false) {
621
- if (
622
- fields.length === 1 &&
623
- data.length &&
624
- (data as any)[0][fields[0]] !== undefined &&
625
- trace
626
- )
627
- console.log(data.map((elem) => (elem as any)[fields[0]]));
628
- throw new Error(
629
- `Bug on ${title}: wrong sorting on ${direction}(${fields.join(
630
- ", ",
631
- )}).`,
632
- );
633
- }
634
- };
635
-
636
- /**
637
- * Type alias for sortable field specifications.
638
- *
639
- * Represents an array of sort field specifications where each field can be
640
- * prefixed with '+' for ascending order or '-' for descending order.
641
- *
642
- * @example
643
- * ```typescript
644
- * type UserSortable = TestValidator.Sortable<"name" | "email" | "created_at">;
645
- * // Results in: Array<"-name" | "+name" | "-email" | "+email" | "-created_at" | "+created_at">
646
- *
647
- * const userSort: UserSortable = ["+name", "-created_at"];
648
- * ```;
649
- *
650
- * @template Literal - String literal type representing available field names
651
- */
652
- export type Sortable<Literal extends string> = Array<
653
- `-${Literal}` | `+${Literal}`
654
- >;
655
- }
656
-
657
- interface IEntity<Type extends string | number | bigint> {
658
- id: Type;
659
- }
660
-
661
- function get_ids<Entity extends IEntity<any>>(entities: Entity[]): string[] {
662
- return entities.map((entity) => entity.id).sort((x, y) => (x < y ? -1 : 1));
663
- }
664
-
665
- function is_promise(input: any): input is Promise<any> {
666
- return (
667
- typeof input === "object" &&
668
- input !== null &&
669
- typeof (input as any).then === "function" &&
670
- typeof (input as any).catch === "function"
671
- );
672
- }
673
-
674
- function is_sorted<T>(data: T[], comp: (x: T, y: T) => number): boolean {
675
- for (let i: number = 1; i < data.length; ++i)
676
- if (comp(data[i - 1], data[i]) > 0) return false;
677
- return true;
678
- }
1
+ import { RandomGenerator } from "./RandomGenerator";
2
+ import { json_equal_to } from "./internal/json_equal_to";
3
+
4
+ /**
5
+ * A comprehensive collection of E2E validation utilities for testing
6
+ * applications.
7
+ *
8
+ * TestValidator provides type-safe validation functions for common testing
9
+ * scenarios including condition checking, equality validation, error testing,
10
+ * HTTP error validation, pagination testing, search functionality validation,
11
+ * and sorting validation.
12
+ *
13
+ * All functions follow a currying pattern to enable reusable test
14
+ * configurations and provide detailed error messages for debugging failed
15
+ * assertions.
16
+ *
17
+ * @author Jeongho Nam - https://github.com/samchon
18
+ * @example
19
+ * ```typescript
20
+ * // Basic condition testing
21
+ * TestValidator.predicate("user should be authenticated")(user.isAuthenticated);
22
+ *
23
+ * // Equality validation
24
+ * TestValidator.equals("API response should match expected")(expected)(actual);
25
+ *
26
+ * // Error validation
27
+ * TestValidator.error("should throw on invalid input")(() => validateInput(""));
28
+ * ```;
29
+ */
30
+ export namespace TestValidator {
31
+ /**
32
+ * Validates that a given condition evaluates to true.
33
+ *
34
+ * Supports synchronous boolean values, synchronous functions returning
35
+ * boolean, and asynchronous functions returning Promise<boolean>. The return
36
+ * type is automatically inferred based on the input type.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * // Synchronous boolean
41
+ * TestValidator.predicate("user should exist")(user !== null);
42
+ *
43
+ * // Synchronous function
44
+ * TestValidator.predicate("array should be empty")(() => arr.length === 0);
45
+ *
46
+ * // Asynchronous function
47
+ * await TestValidator.predicate("database should be connected")(
48
+ * async () => await db.ping()
49
+ * );
50
+ * ```;
51
+ *
52
+ * @param title - Descriptive title used in error messages when validation
53
+ * fails
54
+ * @returns A currying function that accepts the condition to validate
55
+ * @throws Error with descriptive message when condition is not satisfied
56
+ */
57
+ export const predicate =
58
+ (title: string) =>
59
+ <T extends boolean | (() => boolean) | (() => Promise<boolean>)>(
60
+ condition: T,
61
+ ): T extends () => Promise<boolean> ? Promise<void> : void => {
62
+ const message = () =>
63
+ `Bug on ${title}: expected condition is not satisfied.`;
64
+
65
+ // SCALAR
66
+ if (typeof condition === "boolean") {
67
+ if (condition !== true) throw new Error(message());
68
+ return undefined as any;
69
+ }
70
+
71
+ // CLOSURE
72
+ const output: boolean | Promise<boolean> = condition();
73
+ if (typeof output === "boolean") {
74
+ if (output !== true) throw new Error(message());
75
+ return undefined as any;
76
+ }
77
+
78
+ // ASYNCHRONOUS
79
+ return new Promise<void>((resolve, reject) => {
80
+ output
81
+ .then((flag) => {
82
+ if (flag === true) resolve();
83
+ else reject(message());
84
+ })
85
+ .catch(reject);
86
+ }) as any;
87
+ };
88
+
89
+ /**
90
+ * Validates deep equality between two values using JSON comparison.
91
+ *
92
+ * Performs recursive comparison of objects and arrays. Supports an optional
93
+ * exception filter to ignore specific keys during comparison. Useful for
94
+ * validating API responses, data transformations, and object state changes.
95
+ *
96
+ * **Type Safety Notes:**
97
+ *
98
+ * - The generic type T is inferred from the `actual` parameter (first in the
99
+ * currying chain)
100
+ * - The `expected` parameter must be assignable to `T | null | undefined`
101
+ * - For objects, `expected` must have the same or subset of properties as
102
+ * `actual`
103
+ * - For union types like `string | null`, ensure proper type compatibility:
104
+ *
105
+ * ```typescript
106
+ * const x: string | null;
107
+ * TestValidator.equals("works")(x)(null); // ✅ Works: null is assignable to string | null
108
+ * TestValidator.equals("error")(null)(x); // ❌ Error: x might be string, but expected is null
109
+ * ```
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * // Basic equality
114
+ * TestValidator.equals("response should match expected")(expectedUser)(actualUser);
115
+ *
116
+ * // Ignore timestamps in comparison
117
+ * TestValidator.equals("user data should match", (key) => key === "updatedAt")(
118
+ * expectedUser
119
+ * )(actualUser);
120
+ *
121
+ * // Validate API response structure
122
+ * const validateResponse = TestValidator.equals("API response structure");
123
+ * validateResponse({ id: 1, name: "John" })({ id: 1, name: "John" });
124
+ *
125
+ * // Type-safe nullable comparisons
126
+ * const nullableData: { name: string } | null = getData();
127
+ * TestValidator.equals("nullable check")(nullableData)(null); // ✅ Safe
128
+ * ```;
129
+ *
130
+ * @param title - Descriptive title used in error messages when values differ
131
+ * @param exception - Optional filter function to exclude specific keys from
132
+ * comparison
133
+ * @returns A currying function chain: first accepts expected value, then
134
+ * actual value
135
+ * @throws Error with detailed diff information when values are not equal
136
+ */
137
+ export const equals =
138
+ (title: string, exception: (key: string) => boolean = () => false) =>
139
+ <T>(actual: T) =>
140
+ (expected: T | null | undefined) => {
141
+ const diff: string[] = json_equal_to(exception)(actual)(expected);
142
+ if (diff.length)
143
+ throw new Error(
144
+ [
145
+ `Bug on ${title}: found different values - [${diff.join(", ")}]:`,
146
+ "\n",
147
+ JSON.stringify({ actual, expected }, null, 2),
148
+ ].join("\n"),
149
+ );
150
+ };
151
+
152
+ /**
153
+ * Validates deep inequality between two values using JSON comparison.
154
+ *
155
+ * Performs recursive comparison of objects and arrays to ensure they are NOT
156
+ * equal. Supports an optional exception filter to ignore specific keys during
157
+ * comparison. Useful for validating that data has changed, objects are
158
+ * different, or mutations have occurred.
159
+ *
160
+ * **Type Safety Notes:**
161
+ *
162
+ * - The generic type T is inferred from the `actual` parameter (first in the
163
+ * currying chain)
164
+ * - The `expected` parameter must be assignable to `T | null | undefined`
165
+ * - For objects, `expected` must have the same or subset of properties as
166
+ * `actual`
167
+ * - For union types like `string | null`, ensure proper type compatibility:
168
+ *
169
+ * ```typescript
170
+ * const x: string | null;
171
+ * TestValidator.notEquals("works")(x)(null); // ✅ Works: null is assignable to string | null
172
+ * TestValidator.notEquals("error")(null)(x); // ❌ Error: x might be string, but expected is null
173
+ * ```
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * // Basic inequality
178
+ * TestValidator.notEquals("user should be different after update")(originalUser)(updatedUser);
179
+ *
180
+ * // Ignore timestamps in comparison
181
+ * TestValidator.notEquals("user data should differ", (key) => key === "updatedAt")(
182
+ * originalUser
183
+ * )(modifiedUser);
184
+ *
185
+ * // Validate state changes
186
+ * const validateStateChange = TestValidator.notEquals("state should have changed");
187
+ * validateStateChange(initialState)(currentState);
188
+ *
189
+ * // Type-safe nullable comparisons
190
+ * const mutableData: { count: number } | null = getMutableData();
191
+ * TestValidator.notEquals("should have changed")(mutableData)(null); // ✅ Safe
192
+ * ```;
193
+ *
194
+ * @param title - Descriptive title used in error messages when values are
195
+ * equal
196
+ * @param exception - Optional filter function to exclude specific keys from
197
+ * comparison
198
+ * @returns A currying function chain: first accepts expected value, then
199
+ * actual value
200
+ * @throws Error when values are equal (indicating validation failure)
201
+ */
202
+ export const notEquals =
203
+ (title: string, exception: (key: string) => boolean = () => false) =>
204
+ <T>(actual: T) =>
205
+ (expected: T | null | undefined) => {
206
+ const diff: string[] = json_equal_to(exception)(actual)(expected);
207
+ if (diff.length === 0)
208
+ throw new Error(
209
+ [
210
+ `Bug on ${title}: values should be different but are equal:`,
211
+ "\n",
212
+ JSON.stringify({ actual, expected }, null, 2),
213
+ ].join("\n"),
214
+ );
215
+ };
216
+
217
+ /**
218
+ * Validates that a function throws an error or rejects when executed.
219
+ *
220
+ * Expects the provided function to fail. If the function executes
221
+ * successfully without throwing an error or rejecting, this validator will
222
+ * throw an exception. Supports both synchronous and asynchronous functions.
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * // Synchronous error validation
227
+ * TestValidator.error("should reject invalid email")(
228
+ * () => validateEmail("invalid-email")
229
+ * );
230
+ *
231
+ * // Asynchronous error validation
232
+ * await TestValidator.error("should reject unauthorized access")(
233
+ * async () => await api.functional.getSecretData()
234
+ * );
235
+ *
236
+ * // Validate input validation
237
+ * TestValidator.error("should throw on empty string")(
238
+ * () => processRequiredInput("")
239
+ * );
240
+ * ```;
241
+ *
242
+ * @param title - Descriptive title used in error messages when no error
243
+ * occurs
244
+ * @returns A currying function that accepts the task function to validate
245
+ * @throws Error when the task function does not throw an error or reject
246
+ */
247
+ export const error =
248
+ (title: string) =>
249
+ <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
250
+ const message = () => `Bug on ${title}: exception must be thrown.`;
251
+ try {
252
+ const output: T = task();
253
+ if (is_promise(output))
254
+ return new Promise<void>((resolve, reject) =>
255
+ output.catch(() => resolve()).then(() => reject(message())),
256
+ ) as any;
257
+ else throw new Error(message());
258
+ } catch {
259
+ return undefined as any;
260
+ }
261
+ };
262
+
263
+ /**
264
+ * Validates that a function throws an HTTP error with specific status codes.
265
+ *
266
+ * Specialized error validator for HTTP operations. Validates that the
267
+ * function throws an HttpError with one of the specified status codes. Useful
268
+ * for testing API endpoints, authentication, and authorization logic.
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * // Validate 401 Unauthorized
273
+ * await TestValidator.httpError("should return 401 for invalid token")(401)(
274
+ * async () => await api.functional.getProtectedResource("invalid-token")
275
+ * );
276
+ *
277
+ * // Validate multiple possible error codes
278
+ * await TestValidator.httpError("should return client error")(400, 404, 422)(
279
+ * async () => await api.functional.updateNonexistentResource(data)
280
+ * );
281
+ *
282
+ * // Validate server errors
283
+ * TestValidator.httpError("should handle server errors")(500, 502, 503)(
284
+ * () => callFaultyEndpoint()
285
+ * );
286
+ * ```;
287
+ *
288
+ * @param title - Descriptive title used in error messages
289
+ * @returns A currying function that accepts status codes, then the task
290
+ * function
291
+ * @throws Error when function doesn't throw HttpError or status code doesn't
292
+ * match
293
+ */
294
+ export const httpError =
295
+ (title: string) =>
296
+ (...statuses: number[]) =>
297
+ <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
298
+ const message = (actual?: number) =>
299
+ typeof actual === "number"
300
+ ? `Bug on ${title}: status code must be ${statuses.join(
301
+ " or ",
302
+ )}, but ${actual}.`
303
+ : `Bug on ${title}: status code must be ${statuses.join(
304
+ " or ",
305
+ )}, but succeeded.`;
306
+ const predicate = (exp: any): Error | null =>
307
+ typeof exp === "object" &&
308
+ exp.constructor.name === "HttpError" &&
309
+ statuses.some((val) => val === exp.status)
310
+ ? null
311
+ : new Error(
312
+ message(
313
+ typeof exp === "object" && exp.constructor.name === "HttpError"
314
+ ? exp.status
315
+ : undefined,
316
+ ),
317
+ );
318
+ try {
319
+ const output: T = task();
320
+ if (is_promise(output))
321
+ return new Promise<void>((resolve, reject) =>
322
+ output
323
+ .catch((exp) => {
324
+ const res: Error | null = predicate(exp);
325
+ if (res) reject(res);
326
+ else resolve();
327
+ })
328
+ .then(() => reject(new Error(message()))),
329
+ ) as any;
330
+ else throw new Error(message());
331
+ } catch (exp) {
332
+ const res: Error | null = predicate(exp);
333
+ if (res) throw res;
334
+ return undefined!;
335
+ }
336
+ };
337
+
338
+ /**
339
+ * Safely executes a function and captures any errors without throwing.
340
+ *
341
+ * Utility function for error handling in tests. Executes the provided
342
+ * function and returns any error that occurs, or null if successful. Supports
343
+ * both synchronous and asynchronous functions. Useful for testing error
344
+ * conditions without stopping test execution.
345
+ *
346
+ * @example
347
+ * ```typescript
348
+ * // Synchronous error capture
349
+ * const error = TestValidator.proceed(() => {
350
+ * throw new Error("Something went wrong");
351
+ * });
352
+ * console.log(error?.message); // "Something went wrong"
353
+ *
354
+ * // Asynchronous error capture
355
+ * const asyncError = await TestValidator.proceed(async () => {
356
+ * await failingAsyncOperation();
357
+ * });
358
+ *
359
+ * // Success case
360
+ * const noError = TestValidator.proceed(() => {
361
+ * return "success";
362
+ * });
363
+ * console.log(noError); // null
364
+ * ```;
365
+ *
366
+ * @param task - Function to execute safely
367
+ * @returns Error object if function throws/rejects, null if successful
368
+ */
369
+ export function proceed(task: () => Promise<any>): Promise<Error | null>;
370
+ export function proceed(task: () => any): Error | null;
371
+ export function proceed(
372
+ task: () => any,
373
+ ): Promise<Error | null> | (Error | null) {
374
+ try {
375
+ const output: any = task();
376
+ if (is_promise(output))
377
+ return new Promise<Error | null>((resolve) =>
378
+ output
379
+ .catch((exp) => resolve(exp as Error))
380
+ .then(() => resolve(null)),
381
+ );
382
+ } catch (exp) {
383
+ return exp as Error;
384
+ }
385
+ return null;
386
+ }
387
+
388
+ /**
389
+ * Validates pagination index API results against expected entity order.
390
+ *
391
+ * Compares the order of entities returned by a pagination API with manually
392
+ * sorted expected results. Validates that entity IDs appear in the correct
393
+ * sequence. Commonly used for testing database queries, search results, and
394
+ * any paginated data APIs.
395
+ *
396
+ * @example
397
+ * ```typescript
398
+ * // Test article pagination
399
+ * const expectedArticles = await db.articles.findAll({ order: 'created_at DESC' });
400
+ * const actualArticles = await api.functional.getArticles({ page: 1, limit: 10 });
401
+ *
402
+ * TestValidator.index("article pagination order")(expectedArticles)(
403
+ * actualArticles,
404
+ * true // enable trace logging
405
+ * );
406
+ *
407
+ * // Test user search results
408
+ * const manuallyFilteredUsers = allUsers.filter(u => u.name.includes("John"));
409
+ * const apiSearchResults = await api.functional.searchUsers({ query: "John" });
410
+ *
411
+ * TestValidator.index("user search results")(manuallyFilteredUsers)(
412
+ * apiSearchResults
413
+ * );
414
+ * ```;
415
+ *
416
+ * @param title - Descriptive title used in error messages when order differs
417
+ * @returns A currying function chain: expected entities, then actual entities
418
+ * @throws Error when entity order differs between expected and actual results
419
+ */
420
+ export const index =
421
+ (title: string) =>
422
+ <Solution extends IEntity<any>>(expected: Solution[]) =>
423
+ <Summary extends IEntity<any>>(
424
+ gotten: Summary[],
425
+ trace: boolean = false,
426
+ ): void => {
427
+ const length: number = Math.min(expected.length, gotten.length);
428
+ expected = expected.slice(0, length);
429
+ gotten = gotten.slice(0, length);
430
+
431
+ const xIds: string[] = get_ids(expected).slice(0, length);
432
+ const yIds: string[] = get_ids(gotten)
433
+ .filter((id) => id >= xIds[0])
434
+ .slice(0, length);
435
+
436
+ const equals: boolean = xIds.every((x, i) => x === yIds[i]);
437
+ if (equals === true) return;
438
+ else if (trace === true)
439
+ console.log({
440
+ expected: xIds,
441
+ gotten: yIds,
442
+ });
443
+ throw new Error(
444
+ `Bug on ${title}: result of the index is different with manual aggregation.`,
445
+ );
446
+ };
447
+
448
+ /**
449
+ * Validates search functionality by testing API results against manual
450
+ * filtering.
451
+ *
452
+ * Comprehensive search validation that samples entities from a complete
453
+ * dataset, extracts search values, applies manual filtering, calls the search
454
+ * API, and compares results. Validates that search APIs return the correct
455
+ * subset of data matching the search criteria.
456
+ *
457
+ * @example
458
+ * ```typescript
459
+ * // Test article search functionality
460
+ * const allArticles = await db.articles.findAll();
461
+ * const searchValidator = TestValidator.search("article search API")(
462
+ * (req) => api.searchArticles(req)
463
+ * )(allArticles, 5); // test with 5 random samples
464
+ *
465
+ * await searchValidator({
466
+ * fields: ["title", "content"],
467
+ * values: (article) => [article.title.split(" ")[0]], // first word
468
+ * filter: (article, [keyword]) =>
469
+ * article.title.includes(keyword) || article.content.includes(keyword),
470
+ * request: ([keyword]) => ({ q: keyword })
471
+ * });
472
+ *
473
+ * // Test user search with multiple criteria
474
+ * await TestValidator.search("user search with filters")(
475
+ * (req) => api.getUsers(req)
476
+ * )(allUsers, 3)({
477
+ * fields: ["status", "role"],
478
+ * values: (user) => [user.status, user.role],
479
+ * filter: (user, [status, role]) =>
480
+ * user.status === status && user.role === role,
481
+ * request: ([status, role]) => ({ status, role })
482
+ * });
483
+ * ```;
484
+ *
485
+ * @param title - Descriptive title used in error messages when search fails
486
+ * @returns A currying function chain: API getter function, then dataset and
487
+ * sample count
488
+ * @throws Error when API search results don't match manual filtering results
489
+ */
490
+ export const search =
491
+ (title: string) =>
492
+ <Entity extends IEntity<any>, Request>(
493
+ getter: (input: Request) => Promise<Entity[]>,
494
+ ) =>
495
+ (total: Entity[], sampleCount: number = 1) =>
496
+ async <Values extends any[]>(
497
+ props: ISearchProps<Entity, Values, Request>,
498
+ ): Promise<void> => {
499
+ const samples: Entity[] = RandomGenerator.sample(total)(sampleCount);
500
+ for (const s of samples) {
501
+ const values: Values = props.values(s);
502
+ const filtered: Entity[] = total.filter((entity) =>
503
+ props.filter(entity, values),
504
+ );
505
+ const gotten: Entity[] = await getter(props.request(values));
506
+
507
+ TestValidator.index(`${title} (${props.fields.join(", ")})`)(filtered)(
508
+ gotten,
509
+ );
510
+ }
511
+ };
512
+
513
+ /**
514
+ * Configuration interface for search validation functionality.
515
+ *
516
+ * Defines the structure needed to validate search operations by specifying
517
+ * how to extract search values from entities, filter the dataset manually,
518
+ * and construct API requests.
519
+ *
520
+ * @template Entity - Type of entities being searched, must have an ID field
521
+ * @template Values - Tuple type representing the search values extracted from
522
+ * entities
523
+ * @template Request - Type of the API request object
524
+ */
525
+ export interface ISearchProps<
526
+ Entity extends IEntity<any>,
527
+ Values extends any[],
528
+ Request,
529
+ > {
530
+ /** Field names being searched, used in error messages for identification */
531
+ fields: string[];
532
+
533
+ /**
534
+ * Extracts search values from a sample entity
535
+ *
536
+ * @param entity - The entity to extract search values from
537
+ * @returns Tuple of values used for searching
538
+ */
539
+ values(entity: Entity): Values;
540
+
541
+ /**
542
+ * Manual filter function to determine if an entity matches search criteria
543
+ *
544
+ * @param entity - Entity to test against criteria
545
+ * @param values - Search values to match against
546
+ * @returns True if entity matches the search criteria
547
+ */
548
+ filter(entity: Entity, values: Values): boolean;
549
+
550
+ /**
551
+ * Constructs API request object from search values
552
+ *
553
+ * @param values - Search values to include in request
554
+ * @returns Request object for the search API
555
+ */
556
+ request(values: Values): Request;
557
+ }
558
+
559
+ /**
560
+ * Validates sorting functionality of pagination APIs.
561
+ *
562
+ * Tests sorting operations by calling the API with sort parameters and
563
+ * validating that results are correctly ordered. Supports multiple fields,
564
+ * ascending/descending order, and optional filtering. Provides detailed error
565
+ * reporting for sorting failures.
566
+ *
567
+ * @example
568
+ * ```typescript
569
+ * // Test single field sorting
570
+ * const sortValidator = TestValidator.sort("article sorting")(
571
+ * (sortable) => api.getArticles({ sort: sortable })
572
+ * )("created_at")(
573
+ * (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
574
+ * );
575
+ *
576
+ * await sortValidator("+"); // ascending
577
+ * await sortValidator("-"); // descending
578
+ *
579
+ * // Test multi-field sorting with filtering
580
+ * const userSortValidator = TestValidator.sort("user sorting")(
581
+ * (sortable) => api.getUsers({ sort: sortable })
582
+ * )("status", "created_at")(
583
+ * (a, b) => {
584
+ * if (a.status !== b.status) return a.status.localeCompare(b.status);
585
+ * return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
586
+ * },
587
+ * (user) => user.isActive // only test active users
588
+ * );
589
+ *
590
+ * await userSortValidator("+", true); // ascending with trace logging
591
+ * ```;
592
+ *
593
+ * @param title - Descriptive title used in error messages when sorting fails
594
+ * @returns A currying function chain: API getter, field names, comparator,
595
+ * then direction
596
+ * @throws Error when API results are not properly sorted according to
597
+ * specification
598
+ */
599
+ export const sort =
600
+ (title: string) =>
601
+ <
602
+ T extends object,
603
+ Fields extends string,
604
+ Sortable extends Array<`-${Fields}` | `+${Fields}`> = Array<
605
+ `-${Fields}` | `+${Fields}`
606
+ >,
607
+ >(
608
+ getter: (sortable: Sortable) => Promise<T[]>,
609
+ ) =>
610
+ (...fields: Fields[]) =>
611
+ (comp: (x: T, y: T) => number, filter?: (elem: T) => boolean) =>
612
+ async (direction: "+" | "-", trace: boolean = false) => {
613
+ let data: T[] = await getter(
614
+ fields.map((field) => `${direction}${field}` as const) as Sortable,
615
+ );
616
+ if (filter) data = data.filter(filter);
617
+
618
+ const reversed: typeof comp =
619
+ direction === "+" ? comp : (x, y) => comp(y, x);
620
+ if (is_sorted(data, reversed) === false) {
621
+ if (
622
+ fields.length === 1 &&
623
+ data.length &&
624
+ (data as any)[0][fields[0]] !== undefined &&
625
+ trace
626
+ )
627
+ console.log(data.map((elem) => (elem as any)[fields[0]]));
628
+ throw new Error(
629
+ `Bug on ${title}: wrong sorting on ${direction}(${fields.join(
630
+ ", ",
631
+ )}).`,
632
+ );
633
+ }
634
+ };
635
+
636
+ /**
637
+ * Type alias for sortable field specifications.
638
+ *
639
+ * Represents an array of sort field specifications where each field can be
640
+ * prefixed with '+' for ascending order or '-' for descending order.
641
+ *
642
+ * @example
643
+ * ```typescript
644
+ * type UserSortable = TestValidator.Sortable<"name" | "email" | "created_at">;
645
+ * // Results in: Array<"-name" | "+name" | "-email" | "+email" | "-created_at" | "+created_at">
646
+ *
647
+ * const userSort: UserSortable = ["+name", "-created_at"];
648
+ * ```;
649
+ *
650
+ * @template Literal - String literal type representing available field names
651
+ */
652
+ export type Sortable<Literal extends string> = Array<
653
+ `-${Literal}` | `+${Literal}`
654
+ >;
655
+ }
656
+
657
+ interface IEntity<Type extends string | number | bigint> {
658
+ id: Type;
659
+ }
660
+
661
+ function get_ids<Entity extends IEntity<any>>(entities: Entity[]): string[] {
662
+ return entities.map((entity) => entity.id).sort((x, y) => (x < y ? -1 : 1));
663
+ }
664
+
665
+ function is_promise(input: any): input is Promise<any> {
666
+ return (
667
+ typeof input === "object" &&
668
+ input !== null &&
669
+ typeof (input as any).then === "function" &&
670
+ typeof (input as any).catch === "function"
671
+ );
672
+ }
673
+
674
+ function is_sorted<T>(data: T[], comp: (x: T, y: T) => number): boolean {
675
+ for (let i: number = 1; i < data.length; ++i)
676
+ if (comp(data[i - 1], data[i]) > 0) return false;
677
+ return true;
678
+ }