@openapi-typescript-infra/service 5.11.4 → 5.13.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/build/types.d.ts CHANGED
@@ -42,6 +42,7 @@ export interface Service<SLocals extends AnyServiceLocals = ServiceLocals<Config
42
42
  configure?: (startOptions: ServiceStartOptions<SLocals, RLocals>, options: ServiceOptions) => ServiceOptions;
43
43
  attach?: (app: ServiceExpress<SLocals>) => void | Promise<void>;
44
44
  attachServer?: (app: ServiceExpress<SLocals>, server: Server) => void | Promise<void>;
45
+ onListening?: (app: ServiceExpress<SLocals>, port: number) => void | Promise<void>;
45
46
  start(app: ServiceExpress<SLocals>): void | Promise<void>;
46
47
  stop?: (app: ServiceExpress<SLocals>) => void | Promise<void>;
47
48
  healthy?: (app: ServiceExpress<SLocals>) => boolean | Promise<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openapi-typescript-infra/service",
3
- "version": "5.11.4",
3
+ "version": "5.13.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
  "exports": {
6
6
  ".": {
@@ -70,7 +70,7 @@
70
70
  "dependencies": {
71
71
  "@godaddy/terminus": "^4.12.1",
72
72
  "@opentelemetry/api": "^1.9.0",
73
- "@opentelemetry/auto-instrumentations-node": "^0.60.0",
73
+ "@opentelemetry/auto-instrumentations-node": "^0.60.1",
74
74
  "@opentelemetry/exporter-prometheus": "^0.202.0",
75
75
  "@opentelemetry/instrumentation-dns": "^0.46.0",
76
76
  "@opentelemetry/instrumentation-express": "^0.51.0",
@@ -78,10 +78,10 @@
78
78
  "@opentelemetry/instrumentation-graphql": "^0.50.0",
79
79
  "@opentelemetry/instrumentation-http": "^0.202.0",
80
80
  "@opentelemetry/instrumentation-ioredis": "^0.50.0",
81
- "@opentelemetry/instrumentation-net": "^0.46.0",
81
+ "@opentelemetry/instrumentation-net": "^0.46.1",
82
82
  "@opentelemetry/instrumentation-pg": "^0.54.0",
83
83
  "@opentelemetry/instrumentation-pino": "^0.49.0",
84
- "@opentelemetry/instrumentation-undici": "^0.13.0",
84
+ "@opentelemetry/instrumentation-undici": "^0.13.1",
85
85
  "@opentelemetry/resource-detector-container": "^0.7.2",
86
86
  "@opentelemetry/resource-detector-gcp": "^0.36.0",
87
87
  "@opentelemetry/sdk-node": "^0.202.0",
@@ -93,9 +93,9 @@
93
93
  "cookie-parser": "^1.4.7",
94
94
  "dotenv": "^16.5.0",
95
95
  "express": "^5.1.0",
96
- "express-openapi-validator": "^5.5.3",
97
- "glob": "^11.0.2",
98
- "import-in-the-middle": "^1.14.0",
96
+ "express-openapi-validator": "^5.5.7",
97
+ "glob": "^11.0.3",
98
+ "import-in-the-middle": "^1.14.2",
99
99
  "minimist": "^1.2.8",
100
100
  "moderndash": "^4.0.0",
101
101
  "opentelemetry-resource-detector-sync-api": "^0.30.0",
@@ -111,10 +111,10 @@
111
111
  "@semantic-release/exec": "^7.1.0",
112
112
  "@semantic-release/github": "^11.0.3",
113
113
  "@semantic-release/release-notes-generator": "^14.0.3",
114
- "@types/cookie-parser": "^1.4.8",
115
- "@types/express": "^5.0.2",
114
+ "@types/cookie-parser": "^1.4.9",
115
+ "@types/express": "^5.0.3",
116
116
  "@types/minimist": "^1.2.5",
117
- "@types/node": "^22.15.29",
117
+ "@types/node": "^22.15.31",
118
118
  "@types/request-ip": "^0.0.41",
119
119
  "@types/supertest": "^6.0.3",
120
120
  "@typescript-eslint/eslint-plugin": "^7.18.0",
@@ -122,15 +122,16 @@
122
122
  "coconfig": "^1.6.2",
123
123
  "eslint": "^8.57.1",
124
124
  "eslint-config-prettier": "^9.1.0",
125
- "eslint-import-resolver-typescript": "^4.4.2",
125
+ "eslint-import-resolver-typescript": "^4.4.3",
126
126
  "eslint-plugin-import": "^2.31.0",
127
127
  "pino-pretty": "^13.0.0",
128
128
  "pinst": "^3.0.0",
129
+ "prettier": "^3.5.3",
129
130
  "supertest": "^7.1.1",
130
131
  "tsconfig-paths": "^4.2.0",
131
- "tsx": "^4.19.4",
132
+ "tsx": "^4.20.3",
132
133
  "typescript": "^5.8.3",
133
- "vitest": "^3.2.1"
134
+ "vitest": "^3.2.3"
134
135
  },
135
136
  "resolutions": {
136
137
  "qs": "^6.11.0"
package/src/bootstrap.ts CHANGED
@@ -5,7 +5,12 @@ import { config } from 'dotenv';
5
5
  import { readPackageUp } from 'read-package-up';
6
6
  import type { NormalizedPackageJson } from 'read-package-up';
7
7
 
8
- import type { AnyServiceLocals, RequestLocals, ServiceLocals, ServiceStartOptions } from './types.js';
8
+ import type {
9
+ AnyServiceLocals,
10
+ RequestLocals,
11
+ ServiceLocals,
12
+ ServiceStartOptions,
13
+ } from './types.js';
9
14
  import { isDev } from './env.js';
10
15
  import { startWithTelemetry } from './telemetry/index.js';
11
16
  import { ConfigurationSchema } from './config/schema.js';
@@ -64,5 +64,5 @@ async function getEphemeralPort(): Promise<number> {
64
64
  }
65
65
 
66
66
  export async function getAvailablePort(basePort: number): Promise<number> {
67
- return (isTest() || process.env.TEST_RUNNER) ? getEphemeralPort() : findPort(basePort);
67
+ return isTest() || process.env.TEST_RUNNER ? getEphemeralPort() : findPort(basePort);
68
68
  }
@@ -19,11 +19,23 @@ export function serviceRepl<SLocals extends AnyServiceLocals = ServiceLocals<Con
19
19
  codepath: string | undefined,
20
20
  onExit: () => void,
21
21
  ) {
22
+ class FakeReq {
23
+ locals: { app: ServiceExpress<SLocals> } = { app };
24
+ headers: Record<string, string | string[] | undefined> = {};
25
+ query = new URLSearchParams();
26
+ body: unknown = {};
27
+
28
+ constructor(public path: string) {
29
+ this.locals.app = app;
30
+ }
31
+ }
32
+
22
33
  const rl = repl.start({
23
34
  prompt: '> ',
24
35
  });
25
36
  Object.assign(rl.context, app.locals, {
26
37
  app,
38
+ req: new FakeReq('/'),
27
39
  dump(o: unknown) {
28
40
  // eslint-disable-next-line no-console
29
41
  console.log(JSON.stringify(o, null, '\t'));
@@ -47,11 +59,9 @@ export function serviceRepl<SLocals extends AnyServiceLocals = ServiceLocals<Con
47
59
  rl.on('exit', onExit);
48
60
  }
49
61
 
50
- async function loadReplFunctions<SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>>(
51
- app: ServiceExpress<SLocals>,
52
- codepath: string | undefined,
53
- rl: REPLServer,
54
- ) {
62
+ async function loadReplFunctions<
63
+ SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
64
+ >(app: ServiceExpress<SLocals>, codepath: string | undefined, rl: REPLServer) {
55
65
  if (!codepath) {
56
66
  return;
57
67
  }
@@ -104,7 +114,7 @@ type ReplAny = any;
104
114
  */
105
115
  export function repl$<
106
116
  S extends ServiceExpress<ReplAny>,
107
- T extends (app: S, ...args: ReplAny[]) => ReplAny
117
+ T extends (app: S, ...args: ReplAny[]) => ReplAny,
108
118
  >(fn: T, name?: string) {
109
119
  const functionName = name || fn.name;
110
120
  if (!functionName) {
@@ -61,7 +61,7 @@ export async function startApp<
61
61
  Object.assign(mergeObject, {
62
62
  trace_id: ctx.traceId,
63
63
  span_id: ctx.spanId,
64
- trace_flags: ctx.traceFlags
64
+ trace_flags: ctx.traceFlags,
65
65
  });
66
66
  }
67
67
  }
@@ -70,28 +70,28 @@ export async function startApp<
70
70
 
71
71
  const logger = shouldPrettyPrint
72
72
  ? pino(
73
- {
74
- transport: {
75
- target: 'pino-pretty',
76
- options: {
77
- colorize: true,
73
+ {
74
+ transport: {
75
+ target: 'pino-pretty',
76
+ options: {
77
+ colorize: true,
78
+ },
78
79
  },
80
+ mixin: poorMansOtlp,
79
81
  },
80
- mixin: poorMansOtlp,
81
- },
82
- destination,
83
- )
82
+ destination,
83
+ )
84
84
  : pino(
85
- {
86
- formatters: {
87
- level(label) {
88
- return { level: label };
85
+ {
86
+ formatters: {
87
+ level(label) {
88
+ return { level: label };
89
+ },
89
90
  },
91
+ mixin: poorMansOtlp,
90
92
  },
91
- mixin: poorMansOtlp,
92
- },
93
- destination,
94
- );
93
+ destination,
94
+ );
95
95
 
96
96
  const serviceImpl = service();
97
97
  assert(serviceImpl?.start, 'Service function did not return a conforming object');
@@ -192,7 +192,11 @@ export async function startApp<
192
192
  );
193
193
  }
194
194
  if (routing?.bodyParsers?.form) {
195
- app.use(express.urlencoded(typeof routing.bodyParsers.form === 'object' ? routing.bodyParsers.form : {}));
195
+ app.use(
196
+ express.urlencoded(
197
+ typeof routing.bodyParsers.form === 'object' ? routing.bodyParsers.form : {},
198
+ ),
199
+ );
196
200
  }
197
201
 
198
202
  if (serviceImpl.authorize) {
@@ -259,7 +263,13 @@ export async function startApp<
259
263
  );
260
264
  }
261
265
  if (routing?.openapi) {
262
- const openApiMiddleware = await openApi(app, rootDirectory, codepath, codePattern, options.openApiOptions);
266
+ const openApiMiddleware = await openApi(
267
+ app,
268
+ rootDirectory,
269
+ codepath,
270
+ codePattern,
271
+ options.openApiOptions,
272
+ );
263
273
  app.use(openApiMiddleware);
264
274
  }
265
275
 
@@ -352,7 +362,9 @@ export async function listen<SLocals extends AnyServiceLocals = ServiceLocals<Co
352
362
  onShutdown() {
353
363
  return Promise.resolve()
354
364
  .then(() => service.stop?.(app))
355
- .then(() => { logger.info('Service stop complete'); })
365
+ .then(() => {
366
+ logger.info('Service stop complete');
367
+ })
356
368
  .then(shutdownHandler || (() => Promise.resolve()))
357
369
  .then(() => logger.info('Graceful shutdown complete'))
358
370
  .catch((error) => logger.error(error, 'Error terminating tracing'))
@@ -417,6 +429,7 @@ export async function listen<SLocals extends AnyServiceLocals = ServiceLocals<Co
417
429
  });
418
430
 
419
431
  await listenPromise;
432
+ await service.onListening?.(app, port);
420
433
  return server;
421
434
  }
422
435
 
package/src/openapi.ts CHANGED
@@ -76,7 +76,7 @@ export async function openApi<
76
76
 
77
77
  try {
78
78
  app.locals.openApiSpecification = await new OpenAPIFramework({ apiDoc: apiSpec })
79
- .initialize({ visitApi() { } })
79
+ .initialize({ visitApi() {} })
80
80
  .then((docs) => docs.apiDoc)
81
81
  .catch((error) => {
82
82
  app.locals.logger.error(error, 'Failed to parse and load OpenAPI spec');
@@ -130,15 +130,15 @@ export async function openApi<
130
130
  // by setting validateResponses to false in the config.
131
131
  ...(getNodeEnv() === 'test'
132
132
  ? {
133
- validateResponses: {
134
- onError(error: Error, body: unknown, req: Request) {
135
- console.log('Response body fails validation: ', error);
136
- console.log('Emitted from:', req.originalUrl);
137
- console.debug(body);
138
- throw error;
133
+ validateResponses: {
134
+ onError(error: Error, body: unknown, req: Request) {
135
+ console.log('Response body fails validation: ', error);
136
+ console.log('Emitted from:', req.originalUrl);
137
+ console.debug(body);
138
+ throw error;
139
+ },
139
140
  },
140
- },
141
- }
141
+ }
142
142
  : {}),
143
143
  ...(typeof routing.openapi === 'object' ? routing.openapi : {}),
144
144
  ...openApiOptions,
@@ -148,6 +148,6 @@ export async function openApi<
148
148
  } finally {
149
149
  if (_window) {
150
150
  (global as { window: unknown }).window = _window;
151
- };
151
+ }
152
152
  }
153
153
  }
@@ -3,16 +3,6 @@ import module from 'node:module';
3
3
  module.register('import-in-the-middle/hook.mjs', import.meta.url, {
4
4
  parentURL: import.meta.url,
5
5
  data: {
6
- include: [
7
- 'express',
8
- 'pino',
9
- 'http',
10
- 'dns',
11
- 'net',
12
- 'pg',
13
- 'ioredis',
14
- 'undici',
15
- 'generic-pool',
16
- ],
6
+ include: ['express', 'pino', 'http', 'dns', 'net', 'pg', 'ioredis', 'undici', 'generic-pool'],
17
7
  },
18
8
  });
@@ -133,7 +133,7 @@ export async function startWithTelemetry<
133
133
  await startGlobalTelemetry(options.name);
134
134
 
135
135
  // eslint-disable-next-line import/no-unresolved, @typescript-eslint/no-var-requires
136
- const { startApp, listen } = await import('../express-app/app.js') as {
136
+ const { startApp, listen } = (await import('../express-app/app.js')) as {
137
137
  startApp: StartAppFn<SLocals, RLocals>;
138
138
  listen: ListenFn<SLocals>;
139
139
  };
@@ -3,7 +3,10 @@ import { DnsInstrumentation } from '@opentelemetry/instrumentation-dns';
3
3
  import { ExpressInstrumentation, SpanNameHook } from '@opentelemetry/instrumentation-express';
4
4
  import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
5
5
  import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-generic-pool';
6
- import { HttpInstrumentation, IgnoreIncomingRequestFunction } from '@opentelemetry/instrumentation-http';
6
+ import {
7
+ HttpInstrumentation,
8
+ IgnoreIncomingRequestFunction,
9
+ } from '@opentelemetry/instrumentation-http';
7
10
  import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
8
11
  import { NetInstrumentation } from '@opentelemetry/instrumentation-net';
9
12
  import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
package/src/types.ts CHANGED
@@ -78,6 +78,8 @@ export interface Service<
78
78
 
79
79
  // Called after a server is created but before the server starts listening
80
80
  attachServer?: (app: ServiceExpress<SLocals>, server: Server) => void | Promise<void>;
81
+ // Called after the server is listening
82
+ onListening?: (app: ServiceExpress<SLocals>, port: number) => void | Promise<void>;
81
83
 
82
84
  start(app: ServiceExpress<SLocals>): void | Promise<void>;
83
85