@openapi-typescript-infra/service 2.0.2 → 2.2.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.
@@ -1,11 +1,15 @@
1
1
  {
2
+ "$schema": "tsschema://../src/config/schema#ConfigurationSchema",
2
3
  "logging": {
3
4
  "level": "debug"
4
5
  },
5
- "port": 0,
6
+ "server": {
7
+ "port": 0,
8
+ "internalPort": 0
9
+ },
6
10
  "connections": {
7
11
  "default": {
8
12
  "proxy": "http://localhost:9990"
9
13
  }
10
14
  }
11
- }
15
+ }
package/config/test.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
+ "$schema": "tsschema://../src/config/schema#ConfigurationSchema",
2
3
  "logging": {
3
4
  "level": "warn"
4
5
  }
5
- }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openapi-typescript-infra/service",
3
- "version": "2.0.2",
3
+ "version": "2.2.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": {
@@ -67,7 +67,7 @@
67
67
  "@opentelemetry/exporter-trace-otlp-proto": "^0.41.2",
68
68
  "@opentelemetry/instrumentation": "^0.41.2",
69
69
  "@opentelemetry/instrumentation-aws-sdk": "^0.36.0",
70
- "@opentelemetry/instrumentation-dns": "^0.32.1",
70
+ "@opentelemetry/instrumentation-dns": "^0.32.2",
71
71
  "@opentelemetry/instrumentation-express": "^0.33.1",
72
72
  "@opentelemetry/instrumentation-generic-pool": "^0.32.2",
73
73
  "@opentelemetry/instrumentation-graphql": "^0.35.1",
@@ -83,7 +83,7 @@
83
83
  "dotenv": "^16.3.1",
84
84
  "eventsource": "^1.1.2",
85
85
  "express": "next",
86
- "express-openapi-validator": "^5.0.4",
86
+ "express-openapi-validator": "^5.0.6",
87
87
  "glob": "^8.1.0",
88
88
  "lodash": "^4.17.21",
89
89
  "minimist": "^1.2.8",
@@ -97,31 +97,33 @@
97
97
  "devDependencies": {
98
98
  "@commitlint/cli": "^17.7.1",
99
99
  "@commitlint/config-conventional": "^17.7.0",
100
- "@openapi-typescript-infra/coconfig": "^4.0.0",
100
+ "@openapi-typescript-infra/coconfig": "^4.1.0",
101
101
  "@semantic-release/changelog": "^6.0.3",
102
- "@semantic-release/commit-analyzer": "^10.0.1",
102
+ "@semantic-release/commit-analyzer": "^10.0.4",
103
103
  "@semantic-release/exec": "^6.0.3",
104
104
  "@semantic-release/git": "^10.0.1",
105
- "@semantic-release/release-notes-generator": "^11.0.4",
105
+ "@semantic-release/release-notes-generator": "^11.0.7",
106
106
  "@types/cookie-parser": "^1.4.3",
107
107
  "@types/eventsource": "1.1.11",
108
108
  "@types/express": "^4.17.17",
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.5",
112
+ "@types/node": "^18.17.12",
113
113
  "@types/supertest": "^2.0.12",
114
+ "@typescript-eslint/eslint-plugin": "^6.5.0",
115
+ "@typescript-eslint/parser": "^6.5.0",
114
116
  "coconfig": "^0.13.3",
115
- "eslint": "^8.47.0",
116
- "eslint-config-gasbuddy": "^7.2.0",
117
+ "eslint": "^8.48.0",
117
118
  "eslint-config-prettier": "^9.0.0",
119
+ "eslint-plugin-import": "^2.28.1",
118
120
  "pino-pretty": "^10.2.0",
119
121
  "pinst": "^3.0.0",
120
122
  "supertest": "^6.3.3",
121
123
  "ts-node": "^10.9.1",
122
124
  "tsconfig-paths": "^4.2.0",
123
- "typescript": "^5.1.6",
124
- "vitest": "^0.34.2"
125
+ "typescript": "^5.2.2",
126
+ "vitest": "^0.34.3"
125
127
  },
126
128
  "packageManager": "yarn@3.2.3"
127
129
  }
@@ -19,7 +19,7 @@ export interface ConfigurationSchema extends Record<string, unknown> {
19
19
  logHttpRequests?: boolean;
20
20
  logRequestBody?: boolean;
21
21
  logResponseBody?: boolean;
22
- },
22
+ };
23
23
  routing?: {
24
24
  openapi?: boolean;
25
25
  // Relative to the *root directory* of the app
@@ -34,14 +34,14 @@ export interface ConfigurationSchema extends Record<string, unknown> {
34
34
  bodyParsers?: {
35
35
  json?: boolean;
36
36
  form?: boolean;
37
- },
37
+ };
38
38
  // Set static.enabled to true to enable static assets to be served
39
39
  static?: ConfigurationItemEnabled & {
40
40
  // The path relative to the root directory of the app
41
41
  path?: string;
42
42
  // The path on which to mount the static assets (defaults to /)
43
43
  mountPath?: string;
44
- },
44
+ };
45
45
  finalHandlers: {
46
46
  // Whether to create and return errors for unhandled routes
47
47
  notFound?: boolean;
@@ -55,12 +55,18 @@ export interface ConfigurationSchema extends Record<string, unknown> {
55
55
  // information.
56
56
  unnest: boolean;
57
57
  };
58
- },
59
- },
60
- server?: {
61
- internalPort?: number,
62
- port?: number,
63
- metrics: ConfigurationItemEnabled,
64
- },
58
+ };
59
+ };
60
+ server: {
61
+ internalPort?: number;
62
+ port?: number;
63
+ metrics: ConfigurationItemEnabled;
64
+ // To enable HTTPS on the main service, set the key and cert to the
65
+ // actual key material (not the path). Use shortstop file: handler.
66
+ // Note that generally it's better to offload tls termination,
67
+ // but this is useful for dev.
68
+ key?: string;
69
+ certificate?: string;
70
+ };
65
71
  connections: Record<string, ServiceConfiguration>;
66
72
  }
@@ -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
 
@@ -1,5 +1,6 @@
1
1
  import assert from 'assert';
2
2
  import http from 'http';
3
+ import https from 'https';
3
4
  import path from 'path';
4
5
 
5
6
  import express from 'express';
@@ -303,18 +304,32 @@ export async function shutdownApp(app: ServiceExpress) {
303
304
  (logger as pino.Logger).flush?.();
304
305
  }
305
306
 
307
+ function tlsServer<SLocals extends ServiceLocals = ServiceLocals>(
308
+ app: ServiceExpress<SLocals>,
309
+ config: ConfigurationSchema['server'],
310
+ ) {
311
+ return https.createServer(
312
+ {
313
+ key: config.key,
314
+ cert: config.certificate,
315
+ },
316
+ app,
317
+ );
318
+ }
319
+
306
320
  export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
307
321
  app: ServiceExpress<SLocals>,
308
322
  shutdownHandler?: () => Promise<void>,
309
323
  ) {
310
- let port = app.locals.config.get('server:port');
324
+ const config = app.locals.config.get('server') as Required<ConfigurationSchema['server']>;
325
+ let { port } = config;
311
326
 
312
327
  if (port === 0) {
313
- port = await findPort(8001);
328
+ port = (await findPort(8001)) as number;
314
329
  }
315
330
 
316
331
  const { service, logger } = app.locals;
317
- const server = http.createServer(app);
332
+ const server = config.certificate ? tlsServer(app, config) : http.createServer(app);
318
333
  let shutdownInProgress = false;
319
334
  createTerminus(server, {
320
335
  timeout: 15000,
@@ -358,6 +373,10 @@ export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
358
373
  }
359
374
  });
360
375
 
376
+ server.on('error', (error) => {
377
+ logger.error(error, 'Main service listener error');
378
+ });
379
+
361
380
  const metricInfo = (app.locals as AppWithMetrics)[METRICS_KEY] as InternalMetricsInfo;
362
381
  delete (app.locals as AppWithMetrics)[METRICS_KEY];
363
382
 
@@ -2,11 +2,14 @@ import express from 'express';
2
2
  import type { Application } from 'express-serve-static-core';
3
3
 
4
4
  import { InternalLocals, ServiceExpress } from '../types';
5
+ import { findPort } from '../development/port-finder';
5
6
 
6
7
  export async function startInternalApp(mainApp: ServiceExpress, port: number) {
7
8
  const app = express() as unknown as Application<InternalLocals>;
8
9
  app.locals.mainApp = mainApp;
9
10
 
11
+ const finalPort = port === 0 ? await findPort(3001) : port;
12
+
10
13
  app.get('/health', async (req, res) => {
11
14
  if (mainApp.locals.service?.healthy) {
12
15
  try {
@@ -21,9 +24,12 @@ export async function startInternalApp(mainApp: ServiceExpress, port: number) {
21
24
  });
22
25
 
23
26
  const listenPromise = new Promise<void>((accept) => {
24
- app.locals.server = app.listen(port, () => {
27
+ app.locals.server = app.listen(finalPort, () => {
25
28
  accept();
26
29
  });
30
+ app.locals.server.on('error', (error) => {
31
+ mainApp.locals.logger.error(error, 'Internal app server error');
32
+ });
27
33
  });
28
34
 
29
35
  await listenPromise;
package/tsconfig.json CHANGED
@@ -20,7 +20,7 @@
20
20
  "ES2022",
21
21
  "DOM"
22
22
  ],
23
- "module": "CommonJS",
23
+ "module": "NodeNext",
24
24
  "moduleResolution": "NodeNext",
25
25
  "target": "ES2022",
26
26
  "declaration": true,