@openapi-typescript-infra/service 2.2.0 → 2.4.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/CHANGELOG.md +28 -14
- package/SECURITY.md +2 -2
- package/build/config/schema.d.ts +1 -0
- package/build/development/port-finder.js.map +1 -1
- package/build/error.js.map +1 -1
- package/build/express-app/app.js +15 -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/build/types.d.ts +1 -0
- package/config/config.json +2 -1
- package/package.json +2 -2
- package/src/config/schema.ts +3 -0
- package/src/development/port-finder.ts +1 -1
- package/src/error.ts +1 -5
- package/src/express-app/app.ts +18 -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 +14 -2
package/build/types.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export type ResponseFromApp<ResBody = unknown, RLocals extends RequestLocals = R
|
|
|
36
36
|
export interface Service<SLocals extends ServiceLocals = ServiceLocals, RLocals extends RequestLocals = RequestLocals> {
|
|
37
37
|
name?: string;
|
|
38
38
|
configure?: (startOptions: ServiceStartOptions<SLocals, RLocals>, options: ServiceOptions) => ServiceOptions;
|
|
39
|
+
attach?: (app: ServiceExpress<SLocals>) => void | Promise<void>;
|
|
39
40
|
start(app: ServiceExpress<SLocals>): void | Promise<void>;
|
|
40
41
|
stop?: (app: ServiceExpress<SLocals>) => void | Promise<void>;
|
|
41
42
|
healthy?: (app: ServiceExpress<SLocals>) => boolean | Promise<boolean>;
|
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.4.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
|
}
|
|
@@ -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
|
@@ -152,6 +152,10 @@ export async function startApp<
|
|
|
152
152
|
name,
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
if (serviceImpl.attach) {
|
|
156
|
+
await serviceImpl.attach(app);
|
|
157
|
+
}
|
|
158
|
+
|
|
155
159
|
try {
|
|
156
160
|
await enableMetrics(app, name);
|
|
157
161
|
} catch (error) {
|
|
@@ -304,10 +308,14 @@ export async function shutdownApp(app: ServiceExpress) {
|
|
|
304
308
|
(logger as pino.Logger).flush?.();
|
|
305
309
|
}
|
|
306
310
|
|
|
307
|
-
function
|
|
311
|
+
function httpServer<SLocals extends ServiceLocals = ServiceLocals>(
|
|
308
312
|
app: ServiceExpress<SLocals>,
|
|
309
313
|
config: ConfigurationSchema['server'],
|
|
310
314
|
) {
|
|
315
|
+
if (!config.certificate) {
|
|
316
|
+
return http.createServer(app);
|
|
317
|
+
}
|
|
318
|
+
|
|
311
319
|
return https.createServer(
|
|
312
320
|
{
|
|
313
321
|
key: config.key,
|
|
@@ -317,6 +325,13 @@ function tlsServer<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
317
325
|
);
|
|
318
326
|
}
|
|
319
327
|
|
|
328
|
+
function url(config: ConfigurationSchema['server']) {
|
|
329
|
+
if (config.certificate) {
|
|
330
|
+
return `https://${config.hostname}${config.port === 443 ? '' : `:${config.port}`}`;
|
|
331
|
+
}
|
|
332
|
+
return `http://${config.hostname}${config.port === 80 ? '' : `:${config.port}`}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
320
335
|
export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
|
|
321
336
|
app: ServiceExpress<SLocals>,
|
|
322
337
|
shutdownHandler?: () => Promise<void>,
|
|
@@ -329,7 +344,7 @@ export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
329
344
|
}
|
|
330
345
|
|
|
331
346
|
const { service, logger } = app.locals;
|
|
332
|
-
const server =
|
|
347
|
+
const server = httpServer(app, config);
|
|
333
348
|
let shutdownInProgress = false;
|
|
334
349
|
createTerminus(server, {
|
|
335
350
|
timeout: 15000,
|
|
@@ -384,7 +399,7 @@ export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
384
399
|
const listenPromise = new Promise<void>((accept) => {
|
|
385
400
|
server.listen(port, () => {
|
|
386
401
|
const { locals } = app;
|
|
387
|
-
locals.logger.info({
|
|
402
|
+
locals.logger.info({ url: url(config), service: locals.name }, 'express listening');
|
|
388
403
|
|
|
389
404
|
const serverConfig = locals.config.get('server') as ConfigurationSchema['server'];
|
|
390
405
|
// 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
|
@@ -59,7 +59,13 @@ export interface Service<
|
|
|
59
59
|
options: ServiceOptions,
|
|
60
60
|
) => ServiceOptions;
|
|
61
61
|
|
|
62
|
+
// Run after configuration but before routes are loaded,
|
|
63
|
+
// which is often a good place to add elements to the app locals
|
|
64
|
+
// that are needed during route setup
|
|
65
|
+
attach?: (app: ServiceExpress<SLocals>) => void | Promise<void>;
|
|
66
|
+
|
|
62
67
|
start(app: ServiceExpress<SLocals>): void | Promise<void>;
|
|
68
|
+
|
|
63
69
|
stop?: (app: ServiceExpress<SLocals>) => void | Promise<void>;
|
|
64
70
|
|
|
65
71
|
healthy?: (app: ServiceExpress<SLocals>) => boolean | Promise<boolean>;
|
|
@@ -71,11 +77,17 @@ export interface Service<
|
|
|
71
77
|
onRequest?(req: RequestWithApp<SLocals>, res: Response<unknown, RLocals>): void | Promise<void>;
|
|
72
78
|
|
|
73
79
|
// This runs after body parsing but before routing
|
|
74
|
-
authorize?(
|
|
80
|
+
authorize?(
|
|
81
|
+
req: RequestWithApp<SLocals>,
|
|
82
|
+
res: Response<unknown, RLocals>,
|
|
83
|
+
): boolean | Promise<boolean>;
|
|
75
84
|
|
|
76
85
|
// Add or redact any fields for logging. Note this will be called twice per request,
|
|
77
86
|
// once at the start and once at the end. Modify the values directly.
|
|
78
|
-
getLogFields?(
|
|
87
|
+
getLogFields?(
|
|
88
|
+
req: RequestWithApp<SLocals>,
|
|
89
|
+
values: Record<string, string | string[] | number | undefined>,
|
|
90
|
+
): void;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
export type ServiceFactory<
|