@openapi-typescript-infra/service 2.1.0 → 2.3.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/.github/workflows/codeql-analysis.yml +37 -34
- package/.github/workflows/nodejs.yml +16 -14
- package/.github/workflows/npmpublish.yml +8 -1
- package/@types/config.d.ts +12 -4
- package/CHANGELOG.md +28 -14
- package/SECURITY.md +2 -2
- package/build/config/schema.d.ts +1 -0
- package/build/config/shortstops.d.ts +3 -3
- package/build/config/shortstops.js +29 -2
- package/build/config/shortstops.js.map +1 -1
- package/build/development/port-finder.js.map +1 -1
- package/build/error.js.map +1 -1
- package/build/express-app/app.js +12 -3
- package/build/express-app/app.js.map +1 -1
- package/build/service-calls/index.js.map +1 -1
- package/build/telemetry/fetchInstrumentation.js.map +1 -1
- package/build/telemetry/instrumentations.d.ts +1 -1
- package/build/telemetry/requestLogger.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/config/config.json +2 -1
- package/package.json +2 -2
- package/src/config/schema.ts +3 -0
- package/src/config/shortstops.ts +36 -7
- package/src/development/port-finder.ts +1 -1
- package/src/error.ts +1 -5
- package/src/express-app/app.ts +14 -3
- package/src/service-calls/index.ts +5 -13
- package/src/telemetry/fetchInstrumentation.ts +17 -14
- package/src/telemetry/instrumentations.ts +1 -1
- package/src/telemetry/requestLogger.ts +9 -11
- package/src/types.ts +8 -2
package/config/config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openapi-typescript-infra/service",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "An opinionated framework for building configuration driven services - web, api, or ob. Uses OpenAPI, pino logging, express, confit, Typescript and vitest.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"@types/glob": "^8.1.0",
|
|
110
110
|
"@types/lodash": "^4.14.197",
|
|
111
111
|
"@types/minimist": "^1.2.2",
|
|
112
|
-
"@types/node": "^
|
|
112
|
+
"@types/node": "^20.5.7",
|
|
113
113
|
"@types/supertest": "^2.0.12",
|
|
114
114
|
"@typescript-eslint/eslint-plugin": "^6.5.0",
|
|
115
115
|
"@typescript-eslint/parser": "^6.5.0",
|
package/src/config/schema.ts
CHANGED
|
@@ -67,6 +67,9 @@ export interface ConfigurationSchema extends Record<string, unknown> {
|
|
|
67
67
|
// but this is useful for dev.
|
|
68
68
|
key?: string;
|
|
69
69
|
certificate?: string;
|
|
70
|
+
// If you have an alternate host name (other than localhost) that
|
|
71
|
+
// should be used when referring to this service, set it here.
|
|
72
|
+
hostname?: string;
|
|
70
73
|
};
|
|
71
74
|
connections: Record<string, ServiceConfiguration>;
|
|
72
75
|
}
|
package/src/config/shortstops.ts
CHANGED
|
@@ -29,6 +29,38 @@ function betterRequire(basepath: string) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Just like path, but resolve ~/ to the home directory
|
|
34
|
+
*/
|
|
35
|
+
function betterPath(basepath: string) {
|
|
36
|
+
const basePath = shortstop.path(basepath);
|
|
37
|
+
return function pathWithHomeDir(v: string) {
|
|
38
|
+
if (v.startsWith('~/')) {
|
|
39
|
+
return basePath(path.join(os.homedir(), v.slice(2)));
|
|
40
|
+
}
|
|
41
|
+
return basePath(v);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Just like file, but resolve ~/ to the home directory
|
|
47
|
+
*/
|
|
48
|
+
function betterFile(basepath: string) {
|
|
49
|
+
const baseFile = shortstop.file(basepath);
|
|
50
|
+
return function fileWithHomeDir(
|
|
51
|
+
v: string,
|
|
52
|
+
callback: ((error: Error | null, result?: Buffer | string | undefined) => void) | undefined,
|
|
53
|
+
) {
|
|
54
|
+
if (!callback) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (v.startsWith('~/')) {
|
|
58
|
+
return baseFile(path.join(os.homedir(), v.slice(2)), callback);
|
|
59
|
+
}
|
|
60
|
+
return baseFile(v, callback);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
function canonicalizeServiceSuffix(suffix?: string) {
|
|
33
65
|
if (!suffix) {
|
|
34
66
|
return 'internal';
|
|
@@ -70,10 +102,7 @@ const osMethods = {
|
|
|
70
102
|
version: os.version,
|
|
71
103
|
};
|
|
72
104
|
|
|
73
|
-
export function shortstops(
|
|
74
|
-
service: { name: string; },
|
|
75
|
-
sourcedir: string,
|
|
76
|
-
) {
|
|
105
|
+
export function shortstops(service: { name: string }, sourcedir: string) {
|
|
77
106
|
/**
|
|
78
107
|
* Since we use transpiled sources a lot,
|
|
79
108
|
* basedir and sourcedir are meaningfully different reference points.
|
|
@@ -99,10 +128,10 @@ export function shortstops(
|
|
|
99
128
|
},
|
|
100
129
|
|
|
101
130
|
// handle source and base directory intelligently
|
|
102
|
-
path:
|
|
131
|
+
path: betterPath(basedir),
|
|
103
132
|
sourcepath: shortstop.path(sourcedir),
|
|
104
|
-
file:
|
|
105
|
-
sourcefile: shortstop.file(sourcedir)
|
|
133
|
+
file: betterFile(basedir),
|
|
134
|
+
sourcefile: shortstop.file(sourcedir) as ProtocolFn<Buffer | string | undefined>,
|
|
106
135
|
require: betterRequire(basedir),
|
|
107
136
|
sourcerequire: betterRequire(sourcedir),
|
|
108
137
|
|
|
@@ -19,7 +19,7 @@ async function isAvailable(port: number) {
|
|
|
19
19
|
server.once('error', (err) => {
|
|
20
20
|
clearTimeout(timeoutRef);
|
|
21
21
|
|
|
22
|
-
if ((err as { code?: string
|
|
22
|
+
if ((err as { code?: string }).code === 'EADDRINUSE') {
|
|
23
23
|
accept(false);
|
|
24
24
|
return;
|
|
25
25
|
}
|
package/src/error.ts
CHANGED
|
@@ -32,11 +32,7 @@ export class ServiceError extends Error {
|
|
|
32
32
|
// take up the valuable mental space of an error log.
|
|
33
33
|
public expected_error?: boolean;
|
|
34
34
|
|
|
35
|
-
constructor(
|
|
36
|
-
app: ServiceLike<ServiceLocals>,
|
|
37
|
-
message: string,
|
|
38
|
-
spec?: ServiceErrorSpec,
|
|
39
|
-
) {
|
|
35
|
+
constructor(app: ServiceLike<ServiceLocals>, message: string, spec?: ServiceErrorSpec) {
|
|
40
36
|
super(message);
|
|
41
37
|
this.domain = app.locals.name;
|
|
42
38
|
Object.assign(this, spec);
|
package/src/express-app/app.ts
CHANGED
|
@@ -304,10 +304,14 @@ export async function shutdownApp(app: ServiceExpress) {
|
|
|
304
304
|
(logger as pino.Logger).flush?.();
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
function
|
|
307
|
+
function httpServer<SLocals extends ServiceLocals = ServiceLocals>(
|
|
308
308
|
app: ServiceExpress<SLocals>,
|
|
309
309
|
config: ConfigurationSchema['server'],
|
|
310
310
|
) {
|
|
311
|
+
if (!config.certificate) {
|
|
312
|
+
return http.createServer(app);
|
|
313
|
+
}
|
|
314
|
+
|
|
311
315
|
return https.createServer(
|
|
312
316
|
{
|
|
313
317
|
key: config.key,
|
|
@@ -317,6 +321,13 @@ function tlsServer<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
317
321
|
);
|
|
318
322
|
}
|
|
319
323
|
|
|
324
|
+
function url(config: ConfigurationSchema['server']) {
|
|
325
|
+
if (config.certificate) {
|
|
326
|
+
return `https://${config.hostname}${config.port === 443 ? '' : `:${config.port}`}`;
|
|
327
|
+
}
|
|
328
|
+
return `http://${config.hostname}${config.port === 80 ? '' : `:${config.port}`}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
320
331
|
export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
|
|
321
332
|
app: ServiceExpress<SLocals>,
|
|
322
333
|
shutdownHandler?: () => Promise<void>,
|
|
@@ -329,7 +340,7 @@ export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
329
340
|
}
|
|
330
341
|
|
|
331
342
|
const { service, logger } = app.locals;
|
|
332
|
-
const server =
|
|
343
|
+
const server = httpServer(app, config);
|
|
333
344
|
let shutdownInProgress = false;
|
|
334
345
|
createTerminus(server, {
|
|
335
346
|
timeout: 15000,
|
|
@@ -384,7 +395,7 @@ export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
384
395
|
const listenPromise = new Promise<void>((accept) => {
|
|
385
396
|
server.listen(port, () => {
|
|
386
397
|
const { locals } = app;
|
|
387
|
-
locals.logger.info({
|
|
398
|
+
locals.logger.info({ url: url(config), service: locals.name }, 'express listening');
|
|
388
399
|
|
|
389
400
|
const serverConfig = locals.config.get('server') as ConfigurationSchema['server'];
|
|
390
401
|
// Ok now start the internal port if we have one.
|
|
@@ -4,11 +4,7 @@ import type { FetchConfig, FetchRequest, RestApiResponse } from 'rest-api-suppor
|
|
|
4
4
|
import EventSource from 'eventsource';
|
|
5
5
|
|
|
6
6
|
import { ServiceError, ServiceErrorSpec } from '../error';
|
|
7
|
-
import type {
|
|
8
|
-
ServiceExpress,
|
|
9
|
-
ServiceLike,
|
|
10
|
-
ServiceLocals,
|
|
11
|
-
} from '../types';
|
|
7
|
+
import type { ServiceExpress, ServiceLike, ServiceLocals } from '../types';
|
|
12
8
|
import type { ServiceConfiguration } from '../config/schema';
|
|
13
9
|
|
|
14
10
|
type UntypedEventSourceHandler = Parameters<EventSource['addEventListener']>[1];
|
|
@@ -104,14 +100,10 @@ function readResponse<
|
|
|
104
100
|
return response as Extract<ResType, { responseType: 'response' }>;
|
|
105
101
|
}
|
|
106
102
|
const { message, ...spec } = errorSpec || {};
|
|
107
|
-
throw new ServiceError(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
status: response.status,
|
|
112
|
-
...spec,
|
|
113
|
-
},
|
|
114
|
-
);
|
|
103
|
+
throw new ServiceError(app, message || (response.body as Error).message || 'Internal Error', {
|
|
104
|
+
status: response.status,
|
|
105
|
+
...spec,
|
|
106
|
+
});
|
|
115
107
|
}
|
|
116
108
|
|
|
117
109
|
export async function throwOrGetResponse<
|
|
@@ -33,7 +33,7 @@ interface FetchRequestArguments {
|
|
|
33
33
|
error?: Error;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
type ErrorFetchRequestArguments = FetchRequestArguments & { error: Error
|
|
36
|
+
type ErrorFetchRequestArguments = FetchRequestArguments & { error: Error };
|
|
37
37
|
type ResponseFetchRequestArguments = FetchRequestArguments & {
|
|
38
38
|
response: {
|
|
39
39
|
statusCode: number;
|
|
@@ -82,7 +82,8 @@ export class FetchInstrumentation implements Instrumentation {
|
|
|
82
82
|
|
|
83
83
|
public readonly instrumentationVersion = '1.0.0';
|
|
84
84
|
|
|
85
|
-
public readonly instrumentationDescription =
|
|
85
|
+
public readonly instrumentationDescription =
|
|
86
|
+
'Instrumentation for Node 18 fetch via diagnostics_channel';
|
|
86
87
|
|
|
87
88
|
private subscribeToChannel(diagnosticChannel: string, onMessage: diagch.ChannelListener) {
|
|
88
89
|
const channel = diagch.channel(diagnosticChannel);
|
|
@@ -108,24 +109,26 @@ export class FetchInstrumentation implements Instrumentation {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
enable(): void {
|
|
111
|
-
this.subscribeToChannel('undici:request:create', (args) =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.subscribeToChannel('undici:request:
|
|
112
|
+
this.subscribeToChannel('undici:request:create', (args) =>
|
|
113
|
+
this.onRequest(args as FetchRequestArguments),
|
|
114
|
+
);
|
|
115
|
+
this.subscribeToChannel('undici:request:headers', (args) =>
|
|
116
|
+
this.onHeaders(args as ResponseFetchRequestArguments),
|
|
117
|
+
);
|
|
118
|
+
this.subscribeToChannel('undici:request:trailers', (args) =>
|
|
119
|
+
this.onDone(args as FetchRequestArguments),
|
|
120
|
+
);
|
|
121
|
+
this.subscribeToChannel('undici:request:error', (args) =>
|
|
122
|
+
this.onError(args as ErrorFetchRequestArguments),
|
|
123
|
+
);
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
setTracerProvider(tracerProvider: TracerProvider): void {
|
|
118
|
-
this.tracer = tracerProvider.getTracer(
|
|
119
|
-
this.instrumentationName,
|
|
120
|
-
this.instrumentationVersion,
|
|
121
|
-
);
|
|
127
|
+
this.tracer = tracerProvider.getTracer(this.instrumentationName, this.instrumentationVersion);
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
public setMeterProvider(meterProvider: MeterProvider): void {
|
|
125
|
-
this.meter = meterProvider.getMeter(
|
|
126
|
-
this.instrumentationName,
|
|
127
|
-
this.instrumentationVersion,
|
|
128
|
-
);
|
|
131
|
+
this.meter = meterProvider.getMeter(this.instrumentationName, this.instrumentationVersion);
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
setConfig(config: InstrumentationConfig): void {
|
|
@@ -29,7 +29,7 @@ const InstrumentationMap = {
|
|
|
29
29
|
// Config types inferred automatically from the first argument of the constructor
|
|
30
30
|
type ConfigArg<T> = T extends new (...args: infer U) => unknown ? U[0] : never;
|
|
31
31
|
export type InstrumentationConfigMap = {
|
|
32
|
-
[Name in keyof typeof InstrumentationMap]?: ConfigArg<typeof InstrumentationMap[Name]>;
|
|
32
|
+
[Name in keyof typeof InstrumentationMap]?: ConfigArg<(typeof InstrumentationMap)[Name]>;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
export function getAutoInstrumentations(
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
RequestHandler, Request, Response, ErrorRequestHandler,
|
|
3
|
-
} from 'express';
|
|
1
|
+
import type { RequestHandler, Request, Response, ErrorRequestHandler } from 'express';
|
|
4
2
|
|
|
5
3
|
import { ServiceError } from '../error';
|
|
6
4
|
import type { RequestWithApp, ServiceExpress, ServiceLocals } from '../types';
|
|
@@ -25,7 +23,9 @@ interface WithIdentifiedSession {
|
|
|
25
23
|
};
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
interface ErrorWithStatus extends Error {
|
|
26
|
+
interface ErrorWithStatus extends Error {
|
|
27
|
+
status?: number;
|
|
28
|
+
}
|
|
29
29
|
|
|
30
30
|
function getBasicInfo(req: Request) {
|
|
31
31
|
const url = req.originalUrl || req.url;
|
|
@@ -113,20 +113,18 @@ export function loggerMiddleware<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
113
113
|
// data is to monkey-patch.
|
|
114
114
|
const oldWrite = res.write;
|
|
115
115
|
const oldEnd = res.end;
|
|
116
|
-
res.write = ((...args: Parameters<typeof res['write']>) => {
|
|
116
|
+
res.write = ((...args: Parameters<(typeof res)['write']>) => {
|
|
117
117
|
if (prefs.chunks) {
|
|
118
118
|
prefs.chunks.push(Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0]));
|
|
119
119
|
}
|
|
120
|
-
return (oldWrite as typeof res['write']).apply(res, args);
|
|
121
|
-
}) as typeof res['write'];
|
|
122
|
-
res.end = ((
|
|
123
|
-
...args: Parameters<typeof res['end']>
|
|
124
|
-
) => {
|
|
120
|
+
return (oldWrite as (typeof res)['write']).apply(res, args);
|
|
121
|
+
}) as (typeof res)['write'];
|
|
122
|
+
res.end = ((...args: Parameters<(typeof res)['end']>) => {
|
|
125
123
|
if (args[0] && prefs.chunks) {
|
|
126
124
|
prefs.chunks.push(Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0]));
|
|
127
125
|
}
|
|
128
126
|
return oldEnd.apply(res, args);
|
|
129
|
-
}) as typeof res['end'];
|
|
127
|
+
}) as (typeof res)['end'];
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
const preLog: Record<string, string | string[] | number | undefined> = {
|
package/src/types.ts
CHANGED
|
@@ -71,11 +71,17 @@ export interface Service<
|
|
|
71
71
|
onRequest?(req: RequestWithApp<SLocals>, res: Response<unknown, RLocals>): void | Promise<void>;
|
|
72
72
|
|
|
73
73
|
// This runs after body parsing but before routing
|
|
74
|
-
authorize?(
|
|
74
|
+
authorize?(
|
|
75
|
+
req: RequestWithApp<SLocals>,
|
|
76
|
+
res: Response<unknown, RLocals>,
|
|
77
|
+
): boolean | Promise<boolean>;
|
|
75
78
|
|
|
76
79
|
// Add or redact any fields for logging. Note this will be called twice per request,
|
|
77
80
|
// once at the start and once at the end. Modify the values directly.
|
|
78
|
-
getLogFields?(
|
|
81
|
+
getLogFields?(
|
|
82
|
+
req: RequestWithApp<SLocals>,
|
|
83
|
+
values: Record<string, string | string[] | number | undefined>,
|
|
84
|
+
): void;
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
export type ServiceFactory<
|