@naturalcycles/backend-lib 9.82.0 → 9.84.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/admin/adminMiddleware.d.ts +2 -2
- package/dist/admin/base.admin.service.d.ts +6 -6
- package/dist/auth/auth.service.d.ts +3 -3
- package/dist/db/httpDB.d.ts +3 -3
- package/dist/express/getDefaultRouter.d.ts +1 -1
- package/dist/server/serverStatsMiddleware.js +3 -2
- package/dist/testing/express.test.service.d.ts +1 -1
- package/dist/testing/express.test.service.js +20 -10
- package/package.json +1 -1
- package/src/admin/adminMiddleware.ts +2 -2
- package/src/admin/base.admin.service.ts +6 -6
- package/src/admin/secureHeaderMiddleware.ts +1 -1
- package/src/auth/auth.service.ts +10 -10
- package/src/db/httpDB.ts +7 -3
- package/src/express/createDefaultApp.ts +4 -1
- package/src/express/getDefaultRouter.ts +3 -1
- package/src/server/compressionMiddleware.ts +1 -1
- package/src/server/serverStatsMiddleware.ts +3 -2
- package/src/testing/express.test.service.ts +20 -10
- package/src/validation/validateRequest.util.ts +1 -1
|
@@ -31,7 +31,7 @@ export interface RequireAdminCfg {
|
|
|
31
31
|
/**
|
|
32
32
|
* @deprecated Use `AuthMiddleware`.
|
|
33
33
|
*/
|
|
34
|
-
export type AdminMiddleware = (reqPermissions?: string[], cfg?: RequireAdminCfg) => BackendRequestHandler;
|
|
34
|
+
export type AdminMiddleware = (reqPermissions?: readonly string[], cfg?: RequireAdminCfg) => BackendRequestHandler;
|
|
35
35
|
/**
|
|
36
36
|
* @deprecated Use `AuthService.getLoginRedirectMiddleware`.
|
|
37
37
|
*/
|
|
@@ -44,7 +44,7 @@ export declare function createAdminMiddleware(adminService: BaseAdminService, cf
|
|
|
44
44
|
*
|
|
45
45
|
* @deprecated Use `AuthService.getLoginRedirectMiddleware`.
|
|
46
46
|
*/
|
|
47
|
-
export declare function requireAdminPermissions(adminService: BaseAdminService, reqPermissions?: string[], cfg?: RequireAdminCfg): BackendRequestHandler;
|
|
47
|
+
export declare function requireAdminPermissions(adminService: BaseAdminService, reqPermissions?: readonly string[], cfg?: RequireAdminCfg): BackendRequestHandler;
|
|
48
48
|
/**
|
|
49
49
|
* @deprecated Use `loginHtmlHandler`.
|
|
50
50
|
*/
|
|
@@ -19,8 +19,8 @@ export declare class BaseAdminService {
|
|
|
19
19
|
* Returns AdminInfo if it has all required permissions.
|
|
20
20
|
* Otherwise returns undefined
|
|
21
21
|
*/
|
|
22
|
-
hasPermissions(req: BackendRequest, reqPermissions?: string[], meta?: AnyObject): Promise<AdminInfo | undefined>;
|
|
23
|
-
requirePermissions(req: BackendRequest, reqPermissions?: string[], meta?: AnyObject, andComparison?: boolean): Promise<AdminInfo>;
|
|
22
|
+
hasPermissions(req: BackendRequest, reqPermissions?: readonly string[], meta?: AnyObject): Promise<AdminInfo | undefined>;
|
|
23
|
+
requirePermissions(req: BackendRequest, reqPermissions?: readonly string[], meta?: AnyObject, andComparison?: boolean): Promise<AdminInfo>;
|
|
24
24
|
/**
|
|
25
25
|
* Doesn't check permissions, nor `authEnabled`. See `checkPermissions`.
|
|
26
26
|
*/
|
|
@@ -28,7 +28,7 @@ export declare class BaseAdminService {
|
|
|
28
28
|
/**
|
|
29
29
|
* Doesn't log or throw. See `requireGranted`.
|
|
30
30
|
*/
|
|
31
|
-
checkPermissions(admin: ResolvedAdmin, reqPermissions?: string[], opt?: CheckPermissionsOptions): CheckPermissionsResult;
|
|
31
|
+
checkPermissions(admin: ResolvedAdmin, reqPermissions?: readonly string[], opt?: CheckPermissionsOptions): CheckPermissionsResult;
|
|
32
32
|
/**
|
|
33
33
|
* Throws 401 if there's no email, 403 if not granted.
|
|
34
34
|
*/
|
|
@@ -51,7 +51,7 @@ export declare class BaseAdminService {
|
|
|
51
51
|
/**
|
|
52
52
|
* To be extended.
|
|
53
53
|
*/
|
|
54
|
-
protected onPermissionCheck(req: BackendRequest, email: string, reqPermissions: string[], required: boolean, granted: boolean, meta?: AnyObject): Promise<void>;
|
|
54
|
+
protected onPermissionCheck(req: BackendRequest, email: string, reqPermissions: readonly string[], required: boolean, granted: boolean, meta?: AnyObject): Promise<void>;
|
|
55
55
|
/**
|
|
56
56
|
* Install it on POST /admin/login url
|
|
57
57
|
*
|
|
@@ -113,7 +113,7 @@ export interface CheckPermissionsResult {
|
|
|
113
113
|
email?: string;
|
|
114
114
|
isAdmin: boolean;
|
|
115
115
|
granted: boolean;
|
|
116
|
-
reqPermissions: string[];
|
|
117
|
-
grantedPermissions: string[];
|
|
116
|
+
reqPermissions: readonly string[];
|
|
117
|
+
grantedPermissions: readonly string[];
|
|
118
118
|
authDisabled: boolean;
|
|
119
119
|
}
|
|
@@ -27,7 +27,7 @@ export declare class AuthService<CTX = AnyObject> {
|
|
|
27
27
|
* Returns the granted permissions (all of the subject's if alwaysAuthorized),
|
|
28
28
|
* or `{ subject: 'authDisabled', permissions: [] }` if auth is disabled.
|
|
29
29
|
*/
|
|
30
|
-
require(resolved: ResolvedAuth | undefined, reqPermissions?: string[], opt?: AuthCheckOptions<CTX>): PermissionInfo;
|
|
30
|
+
require(resolved: ResolvedAuth | undefined, reqPermissions?: readonly string[], opt?: AuthCheckOptions<CTX>): PermissionInfo;
|
|
31
31
|
private checkSubject;
|
|
32
32
|
private assertAuthenticated;
|
|
33
33
|
private authRequiredData;
|
|
@@ -67,9 +67,9 @@ export interface AuthCheck {
|
|
|
67
67
|
subject: string;
|
|
68
68
|
granted: boolean;
|
|
69
69
|
alwaysAuthorized: boolean;
|
|
70
|
-
reqPermissions: string[];
|
|
70
|
+
reqPermissions: readonly string[];
|
|
71
71
|
/** The granted permissions, or reqPermissions if denied */
|
|
72
|
-
checkedPermissions: string[];
|
|
72
|
+
checkedPermissions: readonly string[];
|
|
73
73
|
}
|
|
74
74
|
export interface AuthLoginRedirectMiddlewareCfg<CTX> extends AuthMiddlewareCfg<CTX> {
|
|
75
75
|
loginHtmlPath: string;
|
package/dist/db/httpDB.d.ts
CHANGED
|
@@ -19,10 +19,10 @@ export declare class HttpDB extends BaseCommonDB implements CommonDB {
|
|
|
19
19
|
getTables(): Promise<string[]>;
|
|
20
20
|
getTableSchema<ROW extends ObjectWithId>(table: string): Promise<JsonSchema<ROW>>;
|
|
21
21
|
resetCache(table?: string): Promise<void>;
|
|
22
|
-
getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], opt?: CommonDBOptions): Promise<ROW[]>;
|
|
22
|
+
getByIds<ROW extends ObjectWithId>(table: string, ids: readonly ROW['id'][], opt?: CommonDBOptions): Promise<ROW[]>;
|
|
23
23
|
runQuery<ROW extends ObjectWithId>(query: DBQuery<ROW>, opt?: CommonDBOptions): Promise<RunQueryResult<ROW>>;
|
|
24
24
|
runQueryCount<ROW extends ObjectWithId>(query: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>;
|
|
25
|
-
saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
|
|
26
|
-
deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
|
|
25
|
+
saveBatch<ROW extends ObjectWithId>(table: string, rows: readonly ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
|
|
26
|
+
deleteByIds(table: string, ids: readonly string[], opt?: CommonDBOptions): Promise<number>;
|
|
27
27
|
deleteByQuery<ROW extends ObjectWithId>(query: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>;
|
|
28
28
|
}
|
|
@@ -2,4 +2,4 @@ import type { BackendRequestHandler, BackendRouter } from '../server/server.mode
|
|
|
2
2
|
/**
|
|
3
3
|
* Convenience method.
|
|
4
4
|
*/
|
|
5
|
-
export declare function getDefaultRouter(defaultHandlers?: BackendRequestHandler[]): BackendRouter;
|
|
5
|
+
export declare function getDefaultRouter(defaultHandlers?: readonly BackendRequestHandler[]): BackendRouter;
|
|
@@ -2,7 +2,7 @@ import { _mb } from '@naturalcycles/js-lib';
|
|
|
2
2
|
import { _sum } from '@naturalcycles/js-lib/array/array.util.js';
|
|
3
3
|
import { _sortBy } from '@naturalcycles/js-lib/array/sort.js';
|
|
4
4
|
import { _ms } from '@naturalcycles/js-lib/datetime/time.util.js';
|
|
5
|
-
import {
|
|
5
|
+
import { _percentiles } from '@naturalcycles/js-lib/math/math.util.js';
|
|
6
6
|
import { NumberStack } from '@naturalcycles/js-lib/math/stack.util.js';
|
|
7
7
|
import { _get, _mapValues } from '@naturalcycles/js-lib/object/object.util.js';
|
|
8
8
|
import { htmlEscape } from '@naturalcycles/js-lib/string/escape.js';
|
|
@@ -31,6 +31,7 @@ export const serverStatsHTMLHandler = (req, res) => {
|
|
|
31
31
|
s.pc = _mapValues(s.stack.percentiles(percentiles), (_k, v) => Math.round(v), { mutate: true });
|
|
32
32
|
});
|
|
33
33
|
const allLatencies = _stringMapValues(serverStatsMap).flatMap(s => s.stack.items);
|
|
34
|
+
const allPercentiles = _percentiles(allLatencies, percentiles);
|
|
34
35
|
const all2xx = _sum(_stringMapValues(serverStatsMap).flatMap(s => s['2xx']));
|
|
35
36
|
const all4xx = _sum(_stringMapValues(serverStatsMap).flatMap(s => s['4xx']));
|
|
36
37
|
const all5xx = _sum(_stringMapValues(serverStatsMap).flatMap(s => s['5xx']));
|
|
@@ -55,7 +56,7 @@ export const serverStatsHTMLHandler = (req, res) => {
|
|
|
55
56
|
`<td align="right"><pre>${all2xx}</pre></td>`,
|
|
56
57
|
`<td align="right"><pre>${all4xx}</pre></td>`,
|
|
57
58
|
`<td align="right"><pre>${all5xx}</pre></td>`,
|
|
58
|
-
...percentiles.map(pc => `<td align="right"><pre>${Math.round(
|
|
59
|
+
...percentiles.map(pc => `<td align="right"><pre>${Math.round(allPercentiles[pc])}</pre></td>`),
|
|
59
60
|
`</tr>`,
|
|
60
61
|
..._sortBy(_stringMapEntries(serverStatsMap), ([_, stat]) => _get(stat, sortBy), {
|
|
61
62
|
dir: asc ? 'asc' : 'desc',
|
|
@@ -7,7 +7,7 @@ export interface ExpressApp extends Fetcher, AsyncDisposable {
|
|
|
7
7
|
declare class ExpressTestService {
|
|
8
8
|
createAppFromResource(resource: BackendRequestHandlerCfg, opt?: FetcherOptions, defaultAppCfg?: DefaultAppCfg): Promise<ExpressApp>;
|
|
9
9
|
createAppFromResources(resources: BackendRequestHandlerCfg[], opt?: FetcherOptions): Promise<ExpressApp>;
|
|
10
|
-
createApp(app: BackendApplication, opt?: FetcherOptions): ExpressApp
|
|
10
|
+
createApp(app: BackendApplication, opt?: FetcherOptions): Promise<ExpressApp>;
|
|
11
11
|
/**
|
|
12
12
|
* Creates a "Default Express App" with provided resources.
|
|
13
13
|
* Starts an http server on '127.0.0.1' and random available port.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { getFetcher } from '@naturalcycles/js-lib/http';
|
|
2
2
|
import { pDelay } from '@naturalcycles/js-lib/promise/pDelay.js';
|
|
3
3
|
import { createDefaultApp } from '../express/createDefaultApp.js';
|
|
4
|
+
const TEST_SERVER_HOST = '127.0.0.1';
|
|
4
5
|
const nativeFetchFn = async (url, init) => await globalThis.fetch(url, init);
|
|
5
6
|
// Example:
|
|
6
|
-
// const app = expressTestService.
|
|
7
|
+
// const app = await expressTestService.createAppFromResource(debugResource)
|
|
7
8
|
// afterAll(async () => {
|
|
8
9
|
// await app.close()
|
|
9
10
|
// })
|
|
@@ -19,10 +20,10 @@ class ExpressTestService {
|
|
|
19
20
|
resources,
|
|
20
21
|
}), opt);
|
|
21
22
|
}
|
|
22
|
-
createApp(app, opt) {
|
|
23
|
-
const server = this.createTestServer(app);
|
|
23
|
+
async createApp(app, opt) {
|
|
24
|
+
const server = await this.createTestServer(app);
|
|
24
25
|
const { port } = server.address();
|
|
25
|
-
const baseUrl = `http
|
|
26
|
+
const baseUrl = `http://${TEST_SERVER_HOST}:${port}`;
|
|
26
27
|
const fetcher = getFetcher({
|
|
27
28
|
baseUrl,
|
|
28
29
|
responseType: 'json',
|
|
@@ -63,14 +64,23 @@ class ExpressTestService {
|
|
|
63
64
|
* const { port } = server.address() as AddressInfo
|
|
64
65
|
* const url = `http://127.0.0.1:${port}`
|
|
65
66
|
*/
|
|
66
|
-
createTestServer(app) {
|
|
67
|
+
async createTestServer(app) {
|
|
67
68
|
// Important!
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
69
|
+
// Listening on the same host that the Fetcher connects to.
|
|
70
|
+
// `app.listen(0)` (without a hostname) listens on all interfaces (`::`), and the OS (e.g macOS)
|
|
71
|
+
// may give it a port that some other process (e.g IntelliJ on 63342) already listens on
|
|
72
|
+
// at 127.0.0.1. Requests to 127.0.0.1 would then reach that other process instead.
|
|
73
|
+
// Listening on 127.0.0.1 makes the OS pick a port that is free on 127.0.0.1.
|
|
74
|
+
// The downside is that the port is only known after the async `listening` callback
|
|
75
|
+
// (server.address() returns null until then).
|
|
72
76
|
// enableDestroy(server)
|
|
73
|
-
return
|
|
77
|
+
return await new Promise((resolve, reject) => {
|
|
78
|
+
const server = app.listen(0, TEST_SERVER_HOST, (err) => {
|
|
79
|
+
if (err)
|
|
80
|
+
return reject(err);
|
|
81
|
+
resolve(server);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
74
84
|
}
|
|
75
85
|
}
|
|
76
86
|
export const expressTestService = new ExpressTestService();
|
package/package.json
CHANGED
|
@@ -42,7 +42,7 @@ export interface RequireAdminCfg {
|
|
|
42
42
|
* @deprecated Use `AuthMiddleware`.
|
|
43
43
|
*/
|
|
44
44
|
export type AdminMiddleware = (
|
|
45
|
-
reqPermissions?: string[],
|
|
45
|
+
reqPermissions?: readonly string[],
|
|
46
46
|
cfg?: RequireAdminCfg,
|
|
47
47
|
) => BackendRequestHandler
|
|
48
48
|
|
|
@@ -70,7 +70,7 @@ export function createAdminMiddleware(
|
|
|
70
70
|
*/
|
|
71
71
|
export function requireAdminPermissions(
|
|
72
72
|
adminService: BaseAdminService,
|
|
73
|
-
reqPermissions: string[] = [],
|
|
73
|
+
reqPermissions: readonly string[] = [],
|
|
74
74
|
cfg: RequireAdminCfg = {},
|
|
75
75
|
): BackendRequestHandler {
|
|
76
76
|
const {
|
|
@@ -70,7 +70,7 @@ export class BaseAdminService {
|
|
|
70
70
|
*/
|
|
71
71
|
async hasPermissions(
|
|
72
72
|
req: BackendRequest,
|
|
73
|
-
reqPermissions: string[] = [],
|
|
73
|
+
reqPermissions: readonly string[] = [],
|
|
74
74
|
meta: AnyObject = {},
|
|
75
75
|
): Promise<AdminInfo | undefined> {
|
|
76
76
|
if (!this.cfg.authEnabled) return adminInfoDisabled()
|
|
@@ -93,7 +93,7 @@ export class BaseAdminService {
|
|
|
93
93
|
|
|
94
94
|
async requirePermissions(
|
|
95
95
|
req: BackendRequest,
|
|
96
|
-
reqPermissions: string[] = [],
|
|
96
|
+
reqPermissions: readonly string[] = [],
|
|
97
97
|
meta: AnyObject = {},
|
|
98
98
|
andComparison = true,
|
|
99
99
|
): Promise<AdminInfo> {
|
|
@@ -128,7 +128,7 @@ export class BaseAdminService {
|
|
|
128
128
|
*/
|
|
129
129
|
checkPermissions(
|
|
130
130
|
admin: ResolvedAdmin,
|
|
131
|
-
reqPermissions: string[] = [],
|
|
131
|
+
reqPermissions: readonly string[] = [],
|
|
132
132
|
opt: CheckPermissionsOptions = {},
|
|
133
133
|
): CheckPermissionsResult {
|
|
134
134
|
const { andComparison = true } = opt
|
|
@@ -254,7 +254,7 @@ export class BaseAdminService {
|
|
|
254
254
|
protected async onPermissionCheck(
|
|
255
255
|
req: BackendRequest,
|
|
256
256
|
email: string,
|
|
257
|
-
reqPermissions: string[],
|
|
257
|
+
reqPermissions: readonly string[],
|
|
258
258
|
required: boolean,
|
|
259
259
|
granted: boolean,
|
|
260
260
|
meta: AnyObject = {},
|
|
@@ -366,7 +366,7 @@ export interface CheckPermissionsResult {
|
|
|
366
366
|
email?: string
|
|
367
367
|
isAdmin: boolean
|
|
368
368
|
granted: boolean
|
|
369
|
-
reqPermissions: string[]
|
|
370
|
-
grantedPermissions: string[]
|
|
369
|
+
reqPermissions: readonly string[]
|
|
370
|
+
grantedPermissions: readonly string[]
|
|
371
371
|
authDisabled: boolean
|
|
372
372
|
}
|
|
@@ -34,7 +34,7 @@ export function createSecureHeaderMiddleware(cfg: SecureHeaderMiddlewareCfg): Ad
|
|
|
34
34
|
|
|
35
35
|
function requireSecureHeaderOrAdmin(
|
|
36
36
|
cfg: SecureHeaderMiddlewareCfg,
|
|
37
|
-
reqPermissions?: string[],
|
|
37
|
+
reqPermissions?: readonly string[],
|
|
38
38
|
): BackendRequestHandler {
|
|
39
39
|
const { secureHeaderKey = 'Authorization', secureHeaderValue } = cfg
|
|
40
40
|
|
package/src/auth/auth.service.ts
CHANGED
|
@@ -34,7 +34,7 @@ export class AuthService<CTX = AnyObject> {
|
|
|
34
34
|
|
|
35
35
|
private createHandler(
|
|
36
36
|
cfg: AuthMiddlewareCfg<CTX>,
|
|
37
|
-
reqPermissions: string[] = [],
|
|
37
|
+
reqPermissions: readonly string[] = [],
|
|
38
38
|
loginHref?: string,
|
|
39
39
|
): BackendRequestHandler {
|
|
40
40
|
const {
|
|
@@ -100,7 +100,7 @@ export class AuthService<CTX = AnyObject> {
|
|
|
100
100
|
*/
|
|
101
101
|
require(
|
|
102
102
|
resolved: ResolvedAuth | undefined,
|
|
103
|
-
reqPermissions: string[] = [],
|
|
103
|
+
reqPermissions: readonly string[] = [],
|
|
104
104
|
opt: AuthCheckOptions<CTX> = {},
|
|
105
105
|
): PermissionInfo {
|
|
106
106
|
if (!this.cfg.authEnabled) return { subject: 'authDisabled', permissions: [] }
|
|
@@ -120,9 +120,9 @@ export class AuthService<CTX = AnyObject> {
|
|
|
120
120
|
|
|
121
121
|
private checkSubject(
|
|
122
122
|
resolved: AuthenticatedAuth,
|
|
123
|
-
reqPermissions: string[],
|
|
123
|
+
reqPermissions: readonly string[],
|
|
124
124
|
opt: AuthCheckOptions<CTX>,
|
|
125
|
-
): string[] | undefined {
|
|
125
|
+
): readonly string[] | undefined {
|
|
126
126
|
const { subject, permissions, alwaysAuthorized = false } = resolved
|
|
127
127
|
const { andComparison = true, ctx } = opt
|
|
128
128
|
|
|
@@ -160,8 +160,8 @@ export class AuthService<CTX = AnyObject> {
|
|
|
160
160
|
|
|
161
161
|
private assertGranted(
|
|
162
162
|
subject: string,
|
|
163
|
-
grantedPermissions: string[] | undefined,
|
|
164
|
-
reqPermissions: string[],
|
|
163
|
+
grantedPermissions: readonly string[] | undefined,
|
|
164
|
+
reqPermissions: readonly string[],
|
|
165
165
|
): asserts grantedPermissions is string[] {
|
|
166
166
|
_assert(grantedPermissions, `Permissions required: [${reqPermissions.join(', ')}]`, {
|
|
167
167
|
permissionsRequired: reqPermissions,
|
|
@@ -175,8 +175,8 @@ export class AuthService<CTX = AnyObject> {
|
|
|
175
175
|
* undefined - not granted.
|
|
176
176
|
*/
|
|
177
177
|
private getGrantedPermissions(
|
|
178
|
-
permissions: string[] | undefined,
|
|
179
|
-
reqPermissions: string[],
|
|
178
|
+
permissions: readonly string[] | undefined,
|
|
179
|
+
reqPermissions: readonly string[],
|
|
180
180
|
andComparison: boolean,
|
|
181
181
|
): string[] | undefined {
|
|
182
182
|
if (!permissions) return
|
|
@@ -242,9 +242,9 @@ export interface AuthCheck {
|
|
|
242
242
|
subject: string
|
|
243
243
|
granted: boolean
|
|
244
244
|
alwaysAuthorized: boolean
|
|
245
|
-
reqPermissions: string[]
|
|
245
|
+
reqPermissions: readonly string[]
|
|
246
246
|
/** The granted permissions, or reqPermissions if denied */
|
|
247
|
-
checkedPermissions: string[]
|
|
247
|
+
checkedPermissions: readonly string[]
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
export interface AuthLoginRedirectMiddlewareCfg<CTX> extends AuthMiddlewareCfg<CTX> {
|
package/src/db/httpDB.ts
CHANGED
|
@@ -63,7 +63,7 @@ export class HttpDB extends BaseCommonDB implements CommonDB {
|
|
|
63
63
|
|
|
64
64
|
override async getByIds<ROW extends ObjectWithId>(
|
|
65
65
|
table: string,
|
|
66
|
-
ids: ROW['id'][],
|
|
66
|
+
ids: readonly ROW['id'][],
|
|
67
67
|
opt?: CommonDBOptions,
|
|
68
68
|
): Promise<ROW[]> {
|
|
69
69
|
return await this.fetcher.put(`getByIds`, {
|
|
@@ -101,7 +101,7 @@ export class HttpDB extends BaseCommonDB implements CommonDB {
|
|
|
101
101
|
|
|
102
102
|
override async saveBatch<ROW extends ObjectWithId>(
|
|
103
103
|
table: string,
|
|
104
|
-
rows: ROW[],
|
|
104
|
+
rows: readonly ROW[],
|
|
105
105
|
opt?: CommonDBSaveOptions<ROW>,
|
|
106
106
|
): Promise<void> {
|
|
107
107
|
await this.fetcher.putVoid(`saveBatch`, {
|
|
@@ -113,7 +113,11 @@ export class HttpDB extends BaseCommonDB implements CommonDB {
|
|
|
113
113
|
})
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
override async deleteByIds(
|
|
116
|
+
override async deleteByIds(
|
|
117
|
+
table: string,
|
|
118
|
+
ids: readonly string[],
|
|
119
|
+
opt?: CommonDBOptions,
|
|
120
|
+
): Promise<number> {
|
|
117
121
|
return await this.fetcher.put(`deleteByIds`, {
|
|
118
122
|
json: {
|
|
119
123
|
table,
|
|
@@ -147,7 +147,10 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
|
|
|
147
147
|
return app
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
function useHandlers(
|
|
150
|
+
function useHandlers(
|
|
151
|
+
app: BackendApplication,
|
|
152
|
+
handlers: readonly BackendRequestHandlerCfg[] = [],
|
|
153
|
+
): void {
|
|
151
154
|
handlers
|
|
152
155
|
.map<BackendRequestHandlerWithPath>(cfg => {
|
|
153
156
|
if (typeof cfg === 'function') {
|
|
@@ -4,7 +4,9 @@ import type { BackendRequestHandler, BackendRouter } from '../server/server.mode
|
|
|
4
4
|
/**
|
|
5
5
|
* Convenience method.
|
|
6
6
|
*/
|
|
7
|
-
export function getDefaultRouter(
|
|
7
|
+
export function getDefaultRouter(
|
|
8
|
+
defaultHandlers: readonly BackendRequestHandler[] = [],
|
|
9
|
+
): BackendRouter {
|
|
8
10
|
const router = Router()
|
|
9
11
|
|
|
10
12
|
// Use default handlers
|
|
@@ -413,7 +413,7 @@ function compressSync(method: string, body: Buffer, opts: CodecOptions): Buffer
|
|
|
413
413
|
function addListeners(
|
|
414
414
|
stream: any,
|
|
415
415
|
on: (type: string, listener: (...args: any[]) => void) => void,
|
|
416
|
-
listeners: [string, (...args: any[]) => void][],
|
|
416
|
+
listeners: readonly [string, (...args: any[]) => void][],
|
|
417
417
|
): void {
|
|
418
418
|
for (const [type, listener] of listeners) {
|
|
419
419
|
on.call(stream, type, listener)
|
|
@@ -2,7 +2,7 @@ import { _mb } from '@naturalcycles/js-lib'
|
|
|
2
2
|
import { _sum } from '@naturalcycles/js-lib/array/array.util.js'
|
|
3
3
|
import { _sortBy } from '@naturalcycles/js-lib/array/sort.js'
|
|
4
4
|
import { _ms } from '@naturalcycles/js-lib/datetime/time.util.js'
|
|
5
|
-
import {
|
|
5
|
+
import { _percentiles } from '@naturalcycles/js-lib/math/math.util.js'
|
|
6
6
|
import { NumberStack } from '@naturalcycles/js-lib/math/stack.util.js'
|
|
7
7
|
import { _get, _mapValues } from '@naturalcycles/js-lib/object/object.util.js'
|
|
8
8
|
import { htmlEscape } from '@naturalcycles/js-lib/string/escape.js'
|
|
@@ -51,6 +51,7 @@ export const serverStatsHTMLHandler: BackendRequestHandler = (req, res) => {
|
|
|
51
51
|
s.pc = _mapValues(s.stack.percentiles(percentiles), (_k, v) => Math.round(v), { mutate: true })
|
|
52
52
|
})
|
|
53
53
|
const allLatencies = _stringMapValues(serverStatsMap).flatMap(s => s.stack.items)
|
|
54
|
+
const allPercentiles = _percentiles(allLatencies, percentiles)
|
|
54
55
|
const all2xx = _sum(_stringMapValues(serverStatsMap).flatMap(s => s['2xx']))
|
|
55
56
|
const all4xx = _sum(_stringMapValues(serverStatsMap).flatMap(s => s['4xx']))
|
|
56
57
|
const all5xx = _sum(_stringMapValues(serverStatsMap).flatMap(s => s['5xx']))
|
|
@@ -79,7 +80,7 @@ export const serverStatsHTMLHandler: BackendRequestHandler = (req, res) => {
|
|
|
79
80
|
`<td align="right"><pre>${all4xx}</pre></td>`,
|
|
80
81
|
`<td align="right"><pre>${all5xx}</pre></td>`,
|
|
81
82
|
...percentiles.map(
|
|
82
|
-
pc => `<td align="right"><pre>${Math.round(
|
|
83
|
+
pc => `<td align="right"><pre>${Math.round(allPercentiles[pc]!)}</pre></td>`,
|
|
83
84
|
),
|
|
84
85
|
`</tr>`,
|
|
85
86
|
..._sortBy(_stringMapEntries(serverStatsMap), ([_, stat]) => _get(stat, sortBy) as string, {
|
|
@@ -7,6 +7,8 @@ import { createDefaultApp } from '../express/createDefaultApp.js'
|
|
|
7
7
|
import type { BackendRequestHandlerCfg, DefaultAppCfg } from '../express/createDefaultApp.js'
|
|
8
8
|
import type { BackendApplication } from '../server/server.model.js'
|
|
9
9
|
|
|
10
|
+
const TEST_SERVER_HOST = '127.0.0.1'
|
|
11
|
+
|
|
10
12
|
const nativeFetchFn: FetchFunction = async (url, init) => await globalThis.fetch(url, init)
|
|
11
13
|
|
|
12
14
|
export interface ExpressApp extends Fetcher, AsyncDisposable {
|
|
@@ -14,7 +16,7 @@ export interface ExpressApp extends Fetcher, AsyncDisposable {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
// Example:
|
|
17
|
-
// const app = expressTestService.
|
|
19
|
+
// const app = await expressTestService.createAppFromResource(debugResource)
|
|
18
20
|
// afterAll(async () => {
|
|
19
21
|
// await app.close()
|
|
20
22
|
// })
|
|
@@ -46,10 +48,10 @@ class ExpressTestService {
|
|
|
46
48
|
)
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
createApp(app: BackendApplication, opt?: FetcherOptions): ExpressApp {
|
|
50
|
-
const server = this.createTestServer(app)
|
|
51
|
+
async createApp(app: BackendApplication, opt?: FetcherOptions): Promise<ExpressApp> {
|
|
52
|
+
const server = await this.createTestServer(app)
|
|
51
53
|
const { port } = server.address() as AddressInfo
|
|
52
|
-
const baseUrl = `http
|
|
54
|
+
const baseUrl = `http://${TEST_SERVER_HOST}:${port}`
|
|
53
55
|
|
|
54
56
|
const fetcher = getFetcher({
|
|
55
57
|
baseUrl,
|
|
@@ -95,14 +97,22 @@ class ExpressTestService {
|
|
|
95
97
|
* const { port } = server.address() as AddressInfo
|
|
96
98
|
* const url = `http://127.0.0.1:${port}`
|
|
97
99
|
*/
|
|
98
|
-
private createTestServer(app: BackendApplication): Server {
|
|
100
|
+
private async createTestServer(app: BackendApplication): Promise<Server> {
|
|
99
101
|
// Important!
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
102
|
+
// Listening on the same host that the Fetcher connects to.
|
|
103
|
+
// `app.listen(0)` (without a hostname) listens on all interfaces (`::`), and the OS (e.g macOS)
|
|
104
|
+
// may give it a port that some other process (e.g IntelliJ on 63342) already listens on
|
|
105
|
+
// at 127.0.0.1. Requests to 127.0.0.1 would then reach that other process instead.
|
|
106
|
+
// Listening on 127.0.0.1 makes the OS pick a port that is free on 127.0.0.1.
|
|
107
|
+
// The downside is that the port is only known after the async `listening` callback
|
|
108
|
+
// (server.address() returns null until then).
|
|
104
109
|
// enableDestroy(server)
|
|
105
|
-
return
|
|
110
|
+
return await new Promise<Server>((resolve, reject) => {
|
|
111
|
+
const server = app.listen(0, TEST_SERVER_HOST, (err?: Error) => {
|
|
112
|
+
if (err) return reject(err)
|
|
113
|
+
resolve(server)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
|
|
@@ -31,7 +31,7 @@ const REDACTED = 'REDACTED'
|
|
|
31
31
|
/**
|
|
32
32
|
* Mutates error
|
|
33
33
|
*/
|
|
34
|
-
function redact(redactPaths: string[], obj: any, error: Error): void {
|
|
34
|
+
function redact(redactPaths: readonly string[], obj: any, error: Error): void {
|
|
35
35
|
redactPaths
|
|
36
36
|
.map(path => _get(obj, path) as string)
|
|
37
37
|
.filter(Boolean)
|