@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.
@@ -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.1.0",
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": "^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
  }
@@ -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: shortstop.path(basedir),
131
+ path: betterPath(basedir),
103
132
  sourcepath: shortstop.path(sourcedir),
104
- file: shortstop.file(basedir),
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; }).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);
@@ -304,10 +304,14 @@ export async function shutdownApp(app: ServiceExpress) {
304
304
  (logger as pino.Logger).flush?.();
305
305
  }
306
306
 
307
- function tlsServer<SLocals extends ServiceLocals = ServiceLocals>(
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 = config.certificate ? tlsServer(app, config) : http.createServer(app);
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({ port, service: locals.name }, 'express listening');
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
- 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
@@ -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?(req: RequestWithApp<SLocals>, res: Response<unknown, RLocals>): boolean | Promise<boolean>;
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?(req: RequestWithApp<SLocals>, values: Record<string, string | string[] | number | undefined>): void;
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<