@openapi-typescript-infra/service 1.2.2 → 2.0.1
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/.trunk/configs/.markdownlint.yaml +10 -0
- package/.trunk/configs/.yamllint.yaml +10 -0
- package/.trunk/trunk.yaml +35 -0
- package/CHANGELOG.md +19 -0
- package/README.md +10 -10
- package/build/express-app/app.js +4 -3
- package/build/express-app/app.js.map +1 -1
- package/build/express-app/modules.d.ts +2 -0
- package/build/express-app/modules.js +28 -0
- package/build/express-app/modules.js.map +1 -0
- package/build/express-app/route-loader.js +4 -12
- package/build/express-app/route-loader.js.map +1 -1
- package/build/openapi.d.ts +1 -1
- package/build/openapi.js +29 -6
- package/build/openapi.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +49 -41
- package/src/express-app/app.ts +21 -20
- package/src/express-app/modules.ts +27 -0
- package/src/express-app/route-loader.ts +5 -16
- package/src/openapi.ts +47 -7
- package/{jest.config.js → vitest.config.ts} +8 -3
- package/.husky/commit-msg +0 -4
- package/.husky/pre-commit +0 -6
package/package.json
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openapi-typescript-infra/service",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "An opinionated framework for building configuration driven services - web, api, or
|
|
3
|
+
"version": "2.0.1",
|
|
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": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "vitest",
|
|
8
8
|
"lint": "eslint .",
|
|
9
9
|
"build": "tsc -p tsconfig.build.json && yarn dlx glob-chmod 755 build/bin/*",
|
|
10
10
|
"watch": "tsc -p tsconfig.json -w --preserveWatchOutput",
|
|
11
11
|
"clean": "npx rimraf ./build",
|
|
12
12
|
"prepublishOnly": "yarn build",
|
|
13
|
-
"
|
|
14
|
-
"postpack": "pinst --enable",
|
|
15
|
-
"_postinstall": "husky install && coconfig"
|
|
13
|
+
"_postinstall": "coconfig"
|
|
16
14
|
},
|
|
17
15
|
"repository": {
|
|
18
16
|
"type": "git",
|
|
@@ -27,9 +25,6 @@
|
|
|
27
25
|
"engines": {
|
|
28
26
|
"node": ">=18"
|
|
29
27
|
},
|
|
30
|
-
"lint-staged": {
|
|
31
|
-
"*.{js,jsx,ts,tsx}": "yarn eslint --cache --fix"
|
|
32
|
-
},
|
|
33
28
|
"keywords": [
|
|
34
29
|
"service",
|
|
35
30
|
"openapi",
|
|
@@ -37,7 +32,7 @@
|
|
|
37
32
|
"confit",
|
|
38
33
|
"babel",
|
|
39
34
|
"typescript",
|
|
40
|
-
"
|
|
35
|
+
"vitest"
|
|
41
36
|
],
|
|
42
37
|
"author": "developers@pyralis.com>",
|
|
43
38
|
"license": "MIT",
|
|
@@ -47,6 +42,19 @@
|
|
|
47
42
|
"release": {
|
|
48
43
|
"branches": [
|
|
49
44
|
"main"
|
|
45
|
+
],
|
|
46
|
+
"plugins": [
|
|
47
|
+
"@semantic-release/commit-analyzer",
|
|
48
|
+
"@semantic-release/release-notes-generator",
|
|
49
|
+
"@semantic-release/changelog",
|
|
50
|
+
[
|
|
51
|
+
"@semantic-release/exec",
|
|
52
|
+
{
|
|
53
|
+
"publishCmd": "yarn dlx pinst --disable"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"@semantic-release/npm",
|
|
57
|
+
"@semantic-release/git"
|
|
50
58
|
]
|
|
51
59
|
},
|
|
52
60
|
"homepage": "https://github.com/openapi-typescript-infra/service#readme",
|
|
@@ -55,22 +63,22 @@
|
|
|
55
63
|
"@godaddy/terminus": "^4.12.1",
|
|
56
64
|
"@opentelemetry/api": "^1.4.1",
|
|
57
65
|
"@opentelemetry/api-metrics": "^0.33.0",
|
|
58
|
-
"@opentelemetry/exporter-prometheus": "^0.41.
|
|
59
|
-
"@opentelemetry/exporter-trace-otlp-proto": "^0.41.
|
|
60
|
-
"@opentelemetry/instrumentation": "^0.41.
|
|
61
|
-
"@opentelemetry/instrumentation-aws-sdk": "^0.
|
|
62
|
-
"@opentelemetry/instrumentation-dns": "^0.32.
|
|
63
|
-
"@opentelemetry/instrumentation-express": "^0.33.
|
|
64
|
-
"@opentelemetry/instrumentation-generic-pool": "^0.32.
|
|
65
|
-
"@opentelemetry/instrumentation-graphql": "^0.35.
|
|
66
|
-
"@opentelemetry/instrumentation-http": "^0.41.
|
|
67
|
-
"@opentelemetry/instrumentation-ioredis": "^0.35.
|
|
68
|
-
"@opentelemetry/instrumentation-net": "^0.32.
|
|
69
|
-
"@opentelemetry/instrumentation-pg": "^0.36.
|
|
70
|
-
"@opentelemetry/instrumentation-pino": "^0.34.
|
|
71
|
-
"@opentelemetry/sdk-metrics": "^1.15.
|
|
72
|
-
"@opentelemetry/sdk-node": "^0.41.
|
|
73
|
-
"@opentelemetry/semantic-conventions": "^1.15.
|
|
66
|
+
"@opentelemetry/exporter-prometheus": "^0.41.2",
|
|
67
|
+
"@opentelemetry/exporter-trace-otlp-proto": "^0.41.2",
|
|
68
|
+
"@opentelemetry/instrumentation": "^0.41.2",
|
|
69
|
+
"@opentelemetry/instrumentation-aws-sdk": "^0.36.0",
|
|
70
|
+
"@opentelemetry/instrumentation-dns": "^0.32.1",
|
|
71
|
+
"@opentelemetry/instrumentation-express": "^0.33.1",
|
|
72
|
+
"@opentelemetry/instrumentation-generic-pool": "^0.32.2",
|
|
73
|
+
"@opentelemetry/instrumentation-graphql": "^0.35.1",
|
|
74
|
+
"@opentelemetry/instrumentation-http": "^0.41.2",
|
|
75
|
+
"@opentelemetry/instrumentation-ioredis": "^0.35.1",
|
|
76
|
+
"@opentelemetry/instrumentation-net": "^0.32.1",
|
|
77
|
+
"@opentelemetry/instrumentation-pg": "^0.36.1",
|
|
78
|
+
"@opentelemetry/instrumentation-pino": "^0.34.1",
|
|
79
|
+
"@opentelemetry/sdk-metrics": "^1.15.2",
|
|
80
|
+
"@opentelemetry/sdk-node": "^0.41.2",
|
|
81
|
+
"@opentelemetry/semantic-conventions": "^1.15.2",
|
|
74
82
|
"cookie-parser": "^1.4.6",
|
|
75
83
|
"dotenv": "^16.3.1",
|
|
76
84
|
"eventsource": "^1.1.2",
|
|
@@ -79,7 +87,7 @@
|
|
|
79
87
|
"glob": "^8.1.0",
|
|
80
88
|
"lodash": "^4.17.21",
|
|
81
89
|
"minimist": "^1.2.8",
|
|
82
|
-
"pino": "^8.
|
|
90
|
+
"pino": "^8.15.0",
|
|
83
91
|
"read-pkg-up": "^7.0.1",
|
|
84
92
|
"rest-api-support": "^1.16.3",
|
|
85
93
|
"shortstop-dns": "^1.1.0",
|
|
@@ -87,32 +95,32 @@
|
|
|
87
95
|
"shortstop-yaml": "^1.0.0"
|
|
88
96
|
},
|
|
89
97
|
"devDependencies": {
|
|
90
|
-
"@commitlint/cli": "^17.
|
|
91
|
-
"@commitlint/config-conventional": "^17.
|
|
92
|
-
"@openapi-typescript-infra/coconfig": "^
|
|
98
|
+
"@commitlint/cli": "^17.7.1",
|
|
99
|
+
"@commitlint/config-conventional": "^17.7.0",
|
|
100
|
+
"@openapi-typescript-infra/coconfig": "^4.0.0",
|
|
101
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
102
|
+
"@semantic-release/commit-analyzer": "^10.0.1",
|
|
103
|
+
"@semantic-release/exec": "^6.0.3",
|
|
104
|
+
"@semantic-release/git": "^10.0.1",
|
|
105
|
+
"@semantic-release/release-notes-generator": "^11.0.4",
|
|
93
106
|
"@types/cookie-parser": "^1.4.3",
|
|
94
107
|
"@types/eventsource": "1.1.11",
|
|
95
108
|
"@types/express": "^4.17.17",
|
|
96
109
|
"@types/glob": "^8.1.0",
|
|
97
|
-
"@types/
|
|
98
|
-
"@types/lodash": "^4.14.196",
|
|
110
|
+
"@types/lodash": "^4.14.197",
|
|
99
111
|
"@types/minimist": "^1.2.2",
|
|
100
|
-
"@types/node": "^18.17.
|
|
112
|
+
"@types/node": "^18.17.5",
|
|
101
113
|
"@types/supertest": "^2.0.12",
|
|
102
114
|
"coconfig": "^0.13.3",
|
|
103
|
-
"eslint": "^8.
|
|
115
|
+
"eslint": "^8.47.0",
|
|
104
116
|
"eslint-config-gasbuddy": "^7.2.0",
|
|
105
|
-
"eslint-config-prettier": "^
|
|
106
|
-
"eslint-plugin-jest": "^27.2.3",
|
|
107
|
-
"husky": "^8.0.3",
|
|
108
|
-
"jest": "^29.6.2",
|
|
109
|
-
"lint-staged": "^13.2.3",
|
|
117
|
+
"eslint-config-prettier": "^9.0.0",
|
|
110
118
|
"pino-pretty": "^10.2.0",
|
|
111
119
|
"pinst": "^3.0.0",
|
|
112
120
|
"supertest": "^6.3.3",
|
|
113
|
-
"ts-jest": "^29.1.1",
|
|
114
121
|
"ts-node": "^10.9.1",
|
|
115
|
-
"typescript": "^5.1.6"
|
|
122
|
+
"typescript": "^5.1.6",
|
|
123
|
+
"vitest": "^0.34.2"
|
|
116
124
|
},
|
|
117
125
|
"packageManager": "yarn@3.2.3"
|
|
118
126
|
}
|
package/src/express-app/app.ts
CHANGED
|
@@ -40,7 +40,9 @@ interface InternalMetricsInfo {
|
|
|
40
40
|
exporter?: PrometheusExporter;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
interface AppWithMetrics {
|
|
43
|
+
interface AppWithMetrics {
|
|
44
|
+
[METRICS_KEY]?: InternalMetricsInfo;
|
|
45
|
+
}
|
|
44
46
|
|
|
45
47
|
async function enableMetrics<SLocals extends ServiceLocals = ServiceLocals>(
|
|
46
48
|
app: ServiceExpress<SLocals>,
|
|
@@ -89,9 +91,7 @@ export async function startApp<
|
|
|
89
91
|
SLocals extends ServiceLocals = ServiceLocals,
|
|
90
92
|
RLocals extends RequestLocals = RequestLocals,
|
|
91
93
|
>(startOptions: ServiceStartOptions<SLocals, RLocals>): Promise<ServiceExpress<SLocals>> {
|
|
92
|
-
const {
|
|
93
|
-
service, rootDirectory, codepath = 'build', name,
|
|
94
|
-
} = startOptions;
|
|
94
|
+
const { service, rootDirectory, codepath = 'build', name } = startOptions;
|
|
95
95
|
const shouldPrettyPrint = isDev() && !process.env.NO_PRETTY_LOGS;
|
|
96
96
|
const destination = pino.destination({
|
|
97
97
|
dest: process.env.LOG_TO_FILE || process.stdout.fd,
|
|
@@ -99,24 +99,24 @@ export async function startApp<
|
|
|
99
99
|
});
|
|
100
100
|
const logger = shouldPrettyPrint
|
|
101
101
|
? pino({
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
transport: {
|
|
103
|
+
destination,
|
|
104
|
+
target: 'pino-pretty',
|
|
105
|
+
options: {
|
|
106
|
+
colorize: true,
|
|
107
|
+
},
|
|
107
108
|
},
|
|
108
|
-
}
|
|
109
|
-
})
|
|
109
|
+
})
|
|
110
110
|
: pino(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
{
|
|
112
|
+
formatters: {
|
|
113
|
+
level(label) {
|
|
114
|
+
return { level: label };
|
|
115
|
+
},
|
|
115
116
|
},
|
|
116
117
|
},
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
);
|
|
118
|
+
destination,
|
|
119
|
+
);
|
|
120
120
|
|
|
121
121
|
const serviceImpl = service();
|
|
122
122
|
assert(serviceImpl?.start, 'Service function did not return a conforming object');
|
|
@@ -260,15 +260,16 @@ export async function startApp<
|
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
const codePattern = codepath === 'src' ? '**/*.ts' : '**/*.js';
|
|
263
264
|
if (routing?.routes) {
|
|
264
265
|
await loadRoutes(
|
|
265
266
|
app,
|
|
266
267
|
path.resolve(rootDirectory, codepath, config.get<string>('routing:routes') || 'routes'),
|
|
267
|
-
|
|
268
|
+
codePattern,
|
|
268
269
|
);
|
|
269
270
|
}
|
|
270
271
|
if (routing?.openapi) {
|
|
271
|
-
app.use(openApi(app, rootDirectory, codepath, options.openApiOptions));
|
|
272
|
+
app.use(await openApi(app, rootDirectory, codepath, codePattern, options.openApiOptions));
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
// Putting this here allows more flexible middleware insertion
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { glob } from 'glob';
|
|
2
|
+
|
|
3
|
+
export async function loadModule(path: string): Promise<Record<string, unknown>> {
|
|
4
|
+
try {
|
|
5
|
+
return require(path);
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if ((error as Error).message.includes('Cannot use import statement outside a module')) {
|
|
8
|
+
return import(path);
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function getFilesInDir(pattern: string, dir: string) {
|
|
15
|
+
const files: string[] = await new Promise((accept, reject) => {
|
|
16
|
+
glob(
|
|
17
|
+
pattern,
|
|
18
|
+
{
|
|
19
|
+
nodir: true,
|
|
20
|
+
strict: true,
|
|
21
|
+
cwd: dir,
|
|
22
|
+
},
|
|
23
|
+
(error, matches) => (error ? reject(error) : accept(matches)),
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
@@ -1,30 +1,19 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
|
|
3
|
-
import { glob } from 'glob';
|
|
4
3
|
import express from 'express';
|
|
5
4
|
|
|
6
5
|
import type { ServiceExpress } from '../types';
|
|
7
6
|
|
|
7
|
+
import { getFilesInDir, loadModule } from './modules';
|
|
8
|
+
|
|
8
9
|
export async function loadRoutes(app: ServiceExpress, routingDir: string, pattern: string) {
|
|
9
|
-
const files
|
|
10
|
-
glob(
|
|
11
|
-
pattern,
|
|
12
|
-
{
|
|
13
|
-
nodir: true,
|
|
14
|
-
strict: true,
|
|
15
|
-
cwd: routingDir,
|
|
16
|
-
},
|
|
17
|
-
(error, matches) => (error ? reject(error) : accept(matches)),
|
|
18
|
-
);
|
|
19
|
-
});
|
|
10
|
+
const files = await getFilesInDir(pattern, routingDir);
|
|
20
11
|
|
|
21
12
|
await Promise.all(
|
|
22
13
|
files.map(async (filename) => {
|
|
23
14
|
const routeBase = path.dirname(filename);
|
|
24
15
|
const modulePath = path.resolve(routingDir, filename);
|
|
25
|
-
|
|
26
|
-
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
|
27
|
-
const module = require(modulePath);
|
|
16
|
+
const module = await loadModule(modulePath);
|
|
28
17
|
const mounter = module.default || module.route;
|
|
29
18
|
if (typeof mounter === 'function') {
|
|
30
19
|
const childRouter = express.Router();
|
|
@@ -38,7 +27,7 @@ export async function loadRoutes(app: ServiceExpress, routingDir: string, patter
|
|
|
38
27
|
pathParts.push(fn);
|
|
39
28
|
}
|
|
40
29
|
const finalPath = pathParts.join('/') || '/';
|
|
41
|
-
app.locals.logger.debug({ path: finalPath, source: filename }, 'Registering
|
|
30
|
+
app.locals.logger.debug({ path: finalPath, source: filename }, 'Registering routes');
|
|
42
31
|
app.use(finalPath, childRouter);
|
|
43
32
|
} else {
|
|
44
33
|
app.locals.logger.warn({ filename }, 'Route file had no default export');
|
package/src/openapi.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as OpenApiValidator from 'express-openapi-validator';
|
|
|
5
5
|
import type { Handler } from 'express';
|
|
6
6
|
|
|
7
7
|
import type { ServiceExpress } from './types';
|
|
8
|
+
import { getFilesInDir, loadModule } from './express-app/modules';
|
|
8
9
|
|
|
9
10
|
const notImplementedHandler: Handler = (req, res) => {
|
|
10
11
|
res.status(501).json({
|
|
@@ -16,15 +17,50 @@ const notImplementedHandler: Handler = (req, res) => {
|
|
|
16
17
|
|
|
17
18
|
type OAPIOpts = Parameters<typeof OpenApiValidator.middleware>[0];
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
function stripExtension(filename: string) {
|
|
21
|
+
return filename.slice(0, filename.lastIndexOf('.'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function openApi(
|
|
20
25
|
app: ServiceExpress,
|
|
21
26
|
rootDirectory: string,
|
|
22
27
|
codepath: string,
|
|
28
|
+
pattern: string,
|
|
23
29
|
openApiOptions?: Partial<OAPIOpts>,
|
|
24
30
|
) {
|
|
25
31
|
const apiSpec = path.resolve(rootDirectory, `./api/${app.locals.name}.yaml`);
|
|
26
32
|
app.locals.logger.debug({ apiSpec, codepath }, 'Serving OpenAPI');
|
|
27
33
|
|
|
34
|
+
const basePath = path.resolve(rootDirectory, `${codepath}/handlers`);
|
|
35
|
+
// Because of the weirdness of ESM/CJS interop, and the synchronous nature of
|
|
36
|
+
// the OpenAPI resolver, we need to preload all the modules we might need
|
|
37
|
+
const moduleFiles = await getFilesInDir(
|
|
38
|
+
pattern,
|
|
39
|
+
path.resolve(rootDirectory, `${codepath}/handlers`),
|
|
40
|
+
);
|
|
41
|
+
const preloadedModules = await Promise.all(
|
|
42
|
+
moduleFiles.map((file) => {
|
|
43
|
+
const fullPath = path.join(basePath, file);
|
|
44
|
+
return loadModule(fullPath).catch((error) => {
|
|
45
|
+
app.locals.logger.warn(
|
|
46
|
+
{ file: fullPath, message: error.message },
|
|
47
|
+
'Could not load potential API handler',
|
|
48
|
+
);
|
|
49
|
+
return undefined;
|
|
50
|
+
});
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
const modulesByPath = moduleFiles.reduce(
|
|
54
|
+
(acc, file, index) => {
|
|
55
|
+
const m = preloadedModules[index];
|
|
56
|
+
if (m) {
|
|
57
|
+
acc[`/${stripExtension(file)}`] = m;
|
|
58
|
+
}
|
|
59
|
+
return acc;
|
|
60
|
+
},
|
|
61
|
+
{} as Record<string, Record<string, unknown>>,
|
|
62
|
+
);
|
|
63
|
+
|
|
28
64
|
const defaultOptions: OAPIOpts = {
|
|
29
65
|
apiSpec,
|
|
30
66
|
ignoreUndocumented: true,
|
|
@@ -33,16 +69,20 @@ export function openApi(
|
|
|
33
69
|
coerceTypes: 'array',
|
|
34
70
|
},
|
|
35
71
|
operationHandlers: {
|
|
36
|
-
basePath
|
|
37
|
-
resolver(
|
|
72
|
+
basePath,
|
|
73
|
+
resolver(
|
|
74
|
+
basePath: string,
|
|
75
|
+
route: Parameters<typeof OpenApiValidator.resolvers.defaultResolver>[1],
|
|
76
|
+
) {
|
|
38
77
|
const pathKey = route.openApiRoute.substring(route.basePath.length);
|
|
39
78
|
const modulePath = path.join(basePath, pathKey);
|
|
40
79
|
|
|
41
80
|
try {
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
const module = modulesByPath[pathKey];
|
|
82
|
+
const method = module
|
|
83
|
+
? Object.keys(module).find((m) => m.toUpperCase() === route.method)
|
|
84
|
+
: undefined;
|
|
85
|
+
if (!module || !method) {
|
|
46
86
|
throw new Error(
|
|
47
87
|
`Could not find a [${route.method}] function in ${modulePath} when trying to route [${route.method} ${route.expressRoute}].`,
|
|
48
88
|
);
|
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
* See https://github.com/gas-buddy/coconfig for more information.
|
|
6
6
|
* @version coconfig@0.13.3
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
import cjs from '@openapi-typescript-infra/coconfig';
|
|
9
|
+
import * as esmToCjs from '@openapi-typescript-infra/coconfig';
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
const configModule: any = cjs || esmToCjs;
|
|
9
13
|
|
|
10
14
|
const configItem = configModule.default || configModule.config || configModule;
|
|
11
|
-
const { configuration } = configItem && configItem['
|
|
15
|
+
const { configuration } = configItem && configItem['vitest.config.ts'];
|
|
12
16
|
const resolved = typeof configuration === 'function' ? configuration() : configuration;
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
// eslint-disable-next-line import/no-default-export
|
|
19
|
+
export default resolved;
|
package/.husky/commit-msg
DELETED