@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/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>;
@@ -24,8 +24,9 @@
24
24
  "server": {
25
25
  "port": 8000,
26
26
  "internalPort": 3000,
27
+ "hostname": "localhost",
27
28
  "metrics": {
28
29
  "enabled": true
29
30
  }
30
31
  }
31
- }
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openapi-typescript-infra/service",
3
- "version": "2.2.0",
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": "^18.17.12",
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",
@@ -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; }).code === 'EADDRINUSE') {
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);
@@ -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 tlsServer<SLocals extends ServiceLocals = ServiceLocals>(
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 = config.certificate ? tlsServer(app, config) : http.createServer(app);
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({ port, service: locals.name }, 'express listening');
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
- app,
109
- message || (response.body as Error).message || 'Internal Error',
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 = 'Instrumentation for Node 18 fetch via diagnostics_channel';
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) => this.onRequest(args as FetchRequestArguments));
112
- this.subscribeToChannel('undici:request:headers', (args) => this.onHeaders(args as ResponseFetchRequestArguments));
113
- this.subscribeToChannel('undici:request:trailers', (args) => this.onDone(args as FetchRequestArguments));
114
- this.subscribeToChannel('undici:request:error', (args) => this.onError(args as ErrorFetchRequestArguments));
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 { status?: number; }
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?(req: RequestWithApp<SLocals>, res: Response<unknown, RLocals>): boolean | Promise<boolean>;
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?(req: RequestWithApp<SLocals>, values: Record<string, string | string[] | number | undefined>): void;
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<