@nestia/e2e 0.3.7 → 0.4.1
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/lib/ArrayUtil.js.map +1 -1
- package/lib/DynamicExecutor.js +2 -3
- package/lib/DynamicExecutor.js.map +1 -1
- package/lib/GaffComparator.js.map +1 -1
- package/lib/RandomGenerator.js +3 -5
- package/lib/RandomGenerator.js.map +1 -1
- package/lib/StopWatch.d.ts +3 -3
- package/lib/StopWatch.js +7 -7
- package/lib/StopWatch.js.map +1 -1
- package/lib/TestValidator.d.ts +1 -1
- package/lib/TestValidator.js +2 -5
- package/lib/TestValidator.js.map +1 -1
- package/lib/internal/json_equal_to.js +2 -6
- package/lib/internal/json_equal_to.js.map +1 -1
- package/package.json +5 -7
- package/src/ArrayUtil.ts +79 -80
- package/src/DynamicExecutor.ts +212 -223
- package/src/GaffComparator.ts +50 -50
- package/src/RandomGenerator.ts +132 -136
- package/src/StopWatch.ts +28 -28
- package/src/TestValidator.ts +294 -296
- package/src/internal/json_equal_to.ts +32 -36
package/src/TestValidator.ts
CHANGED
|
@@ -11,331 +11,329 @@ import { json_equal_to } from "./internal/json_equal_to";
|
|
|
11
11
|
* @author Jeongho Nam - https://github.com/samchon
|
|
12
12
|
*/
|
|
13
13
|
export namespace TestValidator {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Test whether condition is satisfied.
|
|
16
|
+
*
|
|
17
|
+
* @param title Title of error message when condition is not satisfied
|
|
18
|
+
* @return Currying function
|
|
19
|
+
*/
|
|
20
|
+
export const predicate =
|
|
21
|
+
(title: string) =>
|
|
22
|
+
<T extends boolean | (() => boolean) | (() => Promise<boolean>)>(
|
|
23
|
+
condition: T,
|
|
24
|
+
): T extends () => Promise<boolean> ? Promise<void> : void => {
|
|
25
|
+
const message = () =>
|
|
26
|
+
`Bug on ${title}: expected condition is not satisfied.`;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// SCALAR
|
|
29
|
+
if (typeof condition === "boolean") {
|
|
30
|
+
if (condition !== true) throw new Error(message());
|
|
31
|
+
return undefined as any;
|
|
32
|
+
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
// CLOSURE
|
|
35
|
+
const output: boolean | Promise<boolean> = condition();
|
|
36
|
+
if (typeof output === "boolean") {
|
|
37
|
+
if (output !== true) throw new Error(message());
|
|
38
|
+
return undefined as any;
|
|
39
|
+
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
// ASYNCHRONOUS
|
|
42
|
+
return new Promise<void>((resolve, reject) => {
|
|
43
|
+
output
|
|
44
|
+
.then((flag) => {
|
|
45
|
+
if (flag === true) resolve();
|
|
46
|
+
else reject(message());
|
|
47
|
+
})
|
|
48
|
+
.catch(reject);
|
|
49
|
+
}) as any;
|
|
50
|
+
};
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
};
|
|
52
|
+
/**
|
|
53
|
+
* Test whether two values are equal.
|
|
54
|
+
*
|
|
55
|
+
* If you want to validate `covers` relationship,
|
|
56
|
+
* call smaller first and then larger.
|
|
57
|
+
*
|
|
58
|
+
* Otherwise you wanna non equals validator, combine with {@link error}.
|
|
59
|
+
*
|
|
60
|
+
* @param title Title of error message when different
|
|
61
|
+
* @param exception Exception filter for ignoring some keys
|
|
62
|
+
* @returns Currying function
|
|
63
|
+
*/
|
|
64
|
+
export const equals =
|
|
65
|
+
(title: string, exception: (key: string) => boolean = () => false) =>
|
|
66
|
+
<T>(x: T) =>
|
|
67
|
+
(y: T) => {
|
|
68
|
+
const diff: string[] = json_equal_to(exception)(x)(y);
|
|
69
|
+
if (diff.length)
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Bug on ${title}: found different values - [${diff.join(", ")}]`,
|
|
72
|
+
);
|
|
73
|
+
};
|
|
76
74
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
};
|
|
75
|
+
/**
|
|
76
|
+
* Test whether error occurs.
|
|
77
|
+
*
|
|
78
|
+
* If error occurs, nothing would be happened.
|
|
79
|
+
*
|
|
80
|
+
* However, no error exists, then exception would be thrown.
|
|
81
|
+
*
|
|
82
|
+
* @param title Title of exception because of no error exists
|
|
83
|
+
*/
|
|
84
|
+
export const error =
|
|
85
|
+
(title: string) =>
|
|
86
|
+
<T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
|
|
87
|
+
const message = () => `Bug on ${title}: exception must be thrown.`;
|
|
88
|
+
try {
|
|
89
|
+
const output: T = task();
|
|
90
|
+
if (is_promise(output))
|
|
91
|
+
return new Promise<void>((resolve, reject) =>
|
|
92
|
+
output.catch(() => resolve()).then(() => reject(message())),
|
|
93
|
+
) as any;
|
|
94
|
+
else throw new Error(message());
|
|
95
|
+
} catch {
|
|
96
|
+
return undefined as any;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
103
99
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
})
|
|
135
|
-
.then(() => reject(new Error(message()))),
|
|
136
|
-
) as any;
|
|
137
|
-
else throw new Error(message());
|
|
138
|
-
} catch (exp) {
|
|
100
|
+
export const httpError =
|
|
101
|
+
(title: string) =>
|
|
102
|
+
(...statuses: number[]) =>
|
|
103
|
+
<T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
|
|
104
|
+
const message = (actual?: number) =>
|
|
105
|
+
typeof actual === "number"
|
|
106
|
+
? `Bug on ${title}: status code must be ${statuses.join(
|
|
107
|
+
" or ",
|
|
108
|
+
)}, but ${actual}.`
|
|
109
|
+
: `Bug on ${title}: status code must be ${statuses.join(
|
|
110
|
+
" or ",
|
|
111
|
+
)}, but succeeded.`;
|
|
112
|
+
const predicate = (exp: any): Error | null =>
|
|
113
|
+
typeof exp === "object" &&
|
|
114
|
+
exp.constructor.name === "HttpError" &&
|
|
115
|
+
statuses.some((val) => val === exp.status)
|
|
116
|
+
? null
|
|
117
|
+
: new Error(
|
|
118
|
+
message(
|
|
119
|
+
typeof exp === "object" && exp.constructor.name === "HttpError"
|
|
120
|
+
? exp.status
|
|
121
|
+
: undefined,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
try {
|
|
125
|
+
const output: T = task();
|
|
126
|
+
if (is_promise(output))
|
|
127
|
+
return new Promise<void>((resolve, reject) =>
|
|
128
|
+
output
|
|
129
|
+
.catch((exp) => {
|
|
139
130
|
const res: Error | null = predicate(exp);
|
|
140
|
-
if (res)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
131
|
+
if (res) reject(res);
|
|
132
|
+
else resolve();
|
|
133
|
+
})
|
|
134
|
+
.then(() => reject(new Error(message()))),
|
|
135
|
+
) as any;
|
|
136
|
+
else throw new Error(message());
|
|
137
|
+
} catch (exp) {
|
|
138
|
+
const res: Error | null = predicate(exp);
|
|
139
|
+
if (res) throw res;
|
|
140
|
+
return undefined!;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
144
143
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
return null;
|
|
144
|
+
export function proceed(task: () => Promise<any>): Promise<Error | null>;
|
|
145
|
+
export function proceed(task: () => any): Error | null;
|
|
146
|
+
export function proceed(
|
|
147
|
+
task: () => any,
|
|
148
|
+
): Promise<Error | null> | (Error | null) {
|
|
149
|
+
try {
|
|
150
|
+
const output: any = task();
|
|
151
|
+
if (is_promise(output))
|
|
152
|
+
return new Promise<Error | null>((resolve) =>
|
|
153
|
+
output
|
|
154
|
+
.catch((exp) => resolve(exp as Error))
|
|
155
|
+
.then(() => resolve(null)),
|
|
156
|
+
);
|
|
157
|
+
} catch (exp) {
|
|
158
|
+
return exp as Error;
|
|
162
159
|
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Validate index API.
|
|
165
|
+
*
|
|
166
|
+
* Test whether two indexed values are equal.
|
|
167
|
+
*
|
|
168
|
+
* If two values are different, then exception would be thrown.
|
|
169
|
+
*
|
|
170
|
+
* @param title Title of error message when different
|
|
171
|
+
* @return Currying function
|
|
172
|
+
*
|
|
173
|
+
* @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_search.ts
|
|
174
|
+
*/
|
|
175
|
+
export const index =
|
|
176
|
+
(title: string) =>
|
|
177
|
+
<Solution extends IEntity<any>>(expected: Solution[]) =>
|
|
178
|
+
<Summary extends IEntity<any>>(
|
|
179
|
+
gotten: Summary[],
|
|
180
|
+
trace: boolean = true,
|
|
181
|
+
): void => {
|
|
182
|
+
const length: number = Math.min(expected.length, gotten.length);
|
|
183
|
+
expected = expected.slice(0, length);
|
|
184
|
+
gotten = gotten.slice(0, length);
|
|
186
185
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
const xIds: string[] = get_ids(expected).slice(0, length);
|
|
187
|
+
const yIds: string[] = get_ids(gotten)
|
|
188
|
+
.filter((id) => id >= xIds[0])
|
|
189
|
+
.slice(0, length);
|
|
191
190
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
191
|
+
const equals: boolean = xIds.every((x, i) => x === yIds[i]);
|
|
192
|
+
if (equals === true) return;
|
|
193
|
+
else if (trace === true)
|
|
194
|
+
console.log({
|
|
195
|
+
expected: xIds,
|
|
196
|
+
gotten: yIds,
|
|
197
|
+
});
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Bug on ${title}: result of the index is different with manual aggregation.`,
|
|
200
|
+
);
|
|
201
|
+
};
|
|
203
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Valiate search options.
|
|
205
|
+
*
|
|
206
|
+
* Test a pagination API supporting search options.
|
|
207
|
+
*
|
|
208
|
+
* @param title Title of error message when searching is invalid
|
|
209
|
+
* @returns Currying function
|
|
210
|
+
*
|
|
211
|
+
* @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_search.ts
|
|
212
|
+
*/
|
|
213
|
+
export const search =
|
|
214
|
+
(title: string) =>
|
|
215
|
+
/**
|
|
216
|
+
* @param getter A pagination API function to be called
|
|
217
|
+
*/
|
|
218
|
+
<Entity extends IEntity<any>, Request>(
|
|
219
|
+
getter: (input: Request) => Promise<Entity[]>,
|
|
220
|
+
) =>
|
|
221
|
+
/**
|
|
222
|
+
* @param total Total entity records for comparison
|
|
223
|
+
* @param sampleCount Sampling count. Default is 1
|
|
224
|
+
*/
|
|
225
|
+
(total: Entity[], sampleCount: number = 1) =>
|
|
204
226
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* Test a pagination API supporting search options.
|
|
208
|
-
*
|
|
209
|
-
* @param title Title of error message when searching is invalid
|
|
210
|
-
* @returns Currying function
|
|
211
|
-
*
|
|
212
|
-
* @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_search.ts
|
|
227
|
+
* @param props Search properties
|
|
213
228
|
*/
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
* @param sampleCount Sampling count. Default is 1
|
|
225
|
-
*/
|
|
226
|
-
(total: Entity[], sampleCount: number = 1) =>
|
|
227
|
-
/**
|
|
228
|
-
* @param props Search properties
|
|
229
|
-
*/
|
|
230
|
-
async <Values extends any[]>(
|
|
231
|
-
props: ISearchProps<Entity, Values, Request>,
|
|
232
|
-
): Promise<void> => {
|
|
233
|
-
const samples: Entity[] =
|
|
234
|
-
RandomGenerator.sample(total)(sampleCount);
|
|
235
|
-
for (const s of samples) {
|
|
236
|
-
const values: Values = props.values(s);
|
|
237
|
-
const filtered: Entity[] = total.filter((entity) =>
|
|
238
|
-
props.filter(entity, values),
|
|
239
|
-
);
|
|
240
|
-
const gotten: Entity[] = await getter(props.request(values));
|
|
229
|
+
async <Values extends any[]>(
|
|
230
|
+
props: ISearchProps<Entity, Values, Request>,
|
|
231
|
+
): Promise<void> => {
|
|
232
|
+
const samples: Entity[] = RandomGenerator.sample(total)(sampleCount);
|
|
233
|
+
for (const s of samples) {
|
|
234
|
+
const values: Values = props.values(s);
|
|
235
|
+
const filtered: Entity[] = total.filter((entity) =>
|
|
236
|
+
props.filter(entity, values),
|
|
237
|
+
);
|
|
238
|
+
const gotten: Entity[] = await getter(props.request(values));
|
|
241
239
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
240
|
+
TestValidator.index(`${title} (${props.fields.join(", ")})`)(filtered)(
|
|
241
|
+
gotten,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
247
245
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
246
|
+
export interface ISearchProps<
|
|
247
|
+
Entity extends IEntity<any>,
|
|
248
|
+
Values extends any[],
|
|
249
|
+
Request,
|
|
250
|
+
> {
|
|
251
|
+
fields: string[];
|
|
252
|
+
values(entity: Entity): Values;
|
|
253
|
+
filter(entity: Entity, values: Values): boolean;
|
|
254
|
+
request(values: Values): Request;
|
|
255
|
+
}
|
|
258
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Validate sorting options.
|
|
259
|
+
*
|
|
260
|
+
* Test a pagination API supporting sorting options.
|
|
261
|
+
*
|
|
262
|
+
* You can validate detailed sorting options both asceding and descending orders
|
|
263
|
+
* with multiple fields. However, as it forms a complicate currying function,
|
|
264
|
+
* I recomend you to see below example code before using.
|
|
265
|
+
*
|
|
266
|
+
* @param title Title of error message when sorting is invalid
|
|
267
|
+
* @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_sort.ts
|
|
268
|
+
*/
|
|
269
|
+
export const sort =
|
|
270
|
+
(title: string) =>
|
|
259
271
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* Test a pagination API supporting sorting options.
|
|
263
|
-
*
|
|
264
|
-
* You can validate detailed sorting options both asceding and descending orders
|
|
265
|
-
* with multiple fields. However, as it forms a complicate currying function,
|
|
266
|
-
* I recomend you to see below example code before using.
|
|
267
|
-
*
|
|
268
|
-
* @param title Title of error message when sorting is invalid
|
|
269
|
-
* @example https://github.com/samchon/nestia-template/blob/master/src/test/features/api/bbs/test_api_bbs_article_index_sort.ts
|
|
272
|
+
* @param getter A pagination API function to be called
|
|
270
273
|
*/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
fields.map(
|
|
298
|
-
(field) => `${direction}${field}` as const,
|
|
299
|
-
) as Sortable,
|
|
300
|
-
);
|
|
301
|
-
if (filter) data = data.filter(filter);
|
|
274
|
+
<
|
|
275
|
+
T extends object,
|
|
276
|
+
Fields extends string,
|
|
277
|
+
Sortable extends Array<`-${Fields}` | `+${Fields}`> = Array<
|
|
278
|
+
`-${Fields}` | `+${Fields}`
|
|
279
|
+
>,
|
|
280
|
+
>(
|
|
281
|
+
getter: (sortable: Sortable) => Promise<T[]>,
|
|
282
|
+
) =>
|
|
283
|
+
/**
|
|
284
|
+
* @param fields List of fields to be sorted
|
|
285
|
+
*/
|
|
286
|
+
(...fields: Fields[]) =>
|
|
287
|
+
/**
|
|
288
|
+
* @param comp Comparator function for validation
|
|
289
|
+
* @param filter Filter function for data if required
|
|
290
|
+
*/
|
|
291
|
+
(comp: (x: T, y: T) => number, filter?: (elem: T) => boolean) =>
|
|
292
|
+
/**
|
|
293
|
+
* @param direction "+" means ascending order, and "-" means descending order
|
|
294
|
+
*/
|
|
295
|
+
async (direction: "+" | "-", trace: boolean = true) => {
|
|
296
|
+
let data: T[] = await getter(
|
|
297
|
+
fields.map((field) => `${direction}${field}` as const) as Sortable,
|
|
298
|
+
);
|
|
299
|
+
if (filter) data = data.filter(filter);
|
|
302
300
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
301
|
+
const reversed: typeof comp =
|
|
302
|
+
direction === "+" ? comp : (x, y) => comp(y, x);
|
|
303
|
+
if (is_sorted(data, (x, y) => reversed(x, y) < 0) === false) {
|
|
304
|
+
if (
|
|
305
|
+
fields.length === 1 &&
|
|
306
|
+
data.length &&
|
|
307
|
+
(data as any)[0][fields[0]] !== undefined &&
|
|
308
|
+
trace
|
|
309
|
+
)
|
|
310
|
+
console.log(data.map((elem) => (elem as any)[fields[0]]));
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Bug on ${title}: wrong sorting on ${direction}(${fields.join(
|
|
313
|
+
", ",
|
|
314
|
+
)}).`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
320
318
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
319
|
+
export type Sortable<Literal extends string> = Array<
|
|
320
|
+
`-${Literal}` | `+${Literal}`
|
|
321
|
+
>;
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
interface IEntity<Type extends string | number | bigint> {
|
|
327
|
-
|
|
325
|
+
id: Type;
|
|
328
326
|
}
|
|
329
327
|
|
|
330
328
|
function get_ids<Entity extends IEntity<any>>(entities: Entity[]): string[] {
|
|
331
|
-
|
|
329
|
+
return entities.map((entity) => entity.id).sort((x, y) => (x < y ? -1 : 1));
|
|
332
330
|
}
|
|
333
331
|
|
|
334
332
|
function is_promise(input: any): input is Promise<any> {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
333
|
+
return (
|
|
334
|
+
typeof input === "object" &&
|
|
335
|
+
input !== null &&
|
|
336
|
+
typeof (input as any).then === "function" &&
|
|
337
|
+
typeof (input as any).catch === "function"
|
|
338
|
+
);
|
|
341
339
|
}
|