@openapi-typescript-infra/service 6.10.1 → 6.10.2
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/package.json +8 -2
- package/.github/workflows/codeql-analysis.yml +0 -77
- package/.github/workflows/nodejs.yml +0 -62
- package/.trunk/configs/.markdownlint.yaml +0 -10
- package/.trunk/configs/.yamllint.yaml +0 -10
- package/.trunk/trunk.yaml +0 -35
- package/.yarn/patches/confit-npm-3.0.0-eade8c7ce1.patch +0 -52
- package/.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs +0 -541
- package/.yarn/releases/yarn-3.2.3.cjs +0 -783
- package/.yarnrc.yml +0 -7
- package/CHANGELOG.md +0 -525
- package/SECURITY.md +0 -12
- package/__tests__/config.test.ts +0 -53
- package/__tests__/fake-serv/api/fake-serv.yaml +0 -48
- package/__tests__/fake-serv/config/config.json +0 -13
- package/__tests__/fake-serv/src/handlers/hello.ts +0 -17
- package/__tests__/fake-serv/src/index.ts +0 -36
- package/__tests__/fake-serv/src/routes/error.ts +0 -16
- package/__tests__/fake-serv/src/routes/index.ts +0 -19
- package/__tests__/fake-serv/src/routes/other/world.ts +0 -7
- package/__tests__/fake-serv.test.ts +0 -119
- package/__tests__/vitest.test-setup.ts +0 -15
- package/src/bin/start-service.ts +0 -32
- package/src/bootstrap.ts +0 -160
- package/src/config/index.ts +0 -124
- package/src/config/schema.ts +0 -70
- package/src/config/shortstops.ts +0 -155
- package/src/config/validation.ts +0 -23
- package/src/development/port-finder.ts +0 -67
- package/src/development/repl.ts +0 -131
- package/src/env.ts +0 -29
- package/src/error.ts +0 -47
- package/src/express-app/app.ts +0 -438
- package/src/express-app/index.ts +0 -3
- package/src/express-app/internal-server.ts +0 -43
- package/src/express-app/modules.ts +0 -10
- package/src/express-app/route-loader.ts +0 -40
- package/src/express-app/types.ts +0 -32
- package/src/hook.ts +0 -36
- package/src/index.ts +0 -9
- package/src/openapi.ts +0 -184
- package/src/telemetry/DummyExporter.ts +0 -17
- package/src/telemetry/hook-modules.ts +0 -8
- package/src/telemetry/index.ts +0 -168
- package/src/telemetry/instrumentations.ts +0 -103
- package/src/telemetry/requestLogger.ts +0 -267
- package/src/tsx.d.ts +0 -1
- package/src/types.ts +0 -223
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { Service, ServiceLocals } from '../../../src/types.js';
|
|
2
|
-
import { useService } from '../../../src/index.js';
|
|
3
|
-
|
|
4
|
-
export interface FakeServLocals extends ServiceLocals {
|
|
5
|
-
services: {
|
|
6
|
-
fakeServ: {
|
|
7
|
-
get_something(): Promise<{ things: string[] } | Error>;
|
|
8
|
-
};
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function service(): Service<FakeServLocals> {
|
|
13
|
-
const base = useService<FakeServLocals>();
|
|
14
|
-
return {
|
|
15
|
-
...base,
|
|
16
|
-
async start(app) {
|
|
17
|
-
await base.start(app);
|
|
18
|
-
app.locals.services = app.locals.services || {};
|
|
19
|
-
app.locals.services.fakeServ = {
|
|
20
|
-
async get_something() {
|
|
21
|
-
throw new Error('Should not be called.');
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
},
|
|
25
|
-
async onRequest(req, res) {
|
|
26
|
-
await base.onRequest?.(req, res);
|
|
27
|
-
res.locals.rawBody = true;
|
|
28
|
-
},
|
|
29
|
-
async healthy(app) {
|
|
30
|
-
await base.healthy?.(app);
|
|
31
|
-
return new Promise((accept) => {
|
|
32
|
-
setTimeout(accept, 1000);
|
|
33
|
-
});
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { ServiceRouter } from '../../../../src/index';
|
|
2
|
-
import { ServiceError } from '../../../../src/error';
|
|
3
|
-
|
|
4
|
-
export function route(router: ServiceRouter) {
|
|
5
|
-
router.get('/sync', (req) => {
|
|
6
|
-
throw new ServiceError(req.app, 'Synchronous error', { code: 'SyncError' });
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
router.get('/async', async (req) => {
|
|
10
|
-
await new Promise((accept) => {
|
|
11
|
-
setTimeout(accept, 100);
|
|
12
|
-
}).then(() => {
|
|
13
|
-
throw new ServiceError(req.app, 'Async error', { code: 'AsyncError' });
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ServiceExpress, ServiceRouter } from '../../../../src/index';
|
|
2
|
-
import type { FakeServLocals } from '../index';
|
|
3
|
-
|
|
4
|
-
export function route(router: ServiceRouter<FakeServLocals>, app: ServiceExpress<FakeServLocals>) {
|
|
5
|
-
const worldRequests = app.locals.meter.createCounter('world_requests', {
|
|
6
|
-
description: 'Metrics about requests to world',
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
router.get('/world', (req, res) => {
|
|
10
|
-
worldRequests.add(1, { method: 'get' });
|
|
11
|
-
res.json({ hello: 'world' });
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
router.post('/world', async (req, res) => {
|
|
15
|
-
await app.locals.services.fakeServ.get_something();
|
|
16
|
-
worldRequests.add(1);
|
|
17
|
-
res.sendStatus(204);
|
|
18
|
-
});
|
|
19
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
import { describe, expect, test } from 'vitest';
|
|
6
|
-
import request from 'supertest';
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
ServiceStartOptions} from '../src/index.js';
|
|
10
|
-
import {
|
|
11
|
-
listen,
|
|
12
|
-
startApp,
|
|
13
|
-
} from '../src/index.js';
|
|
14
|
-
|
|
15
|
-
import { type FakeServLocals, service } from './fake-serv/src/index.js';
|
|
16
|
-
|
|
17
|
-
function httpRequest(options: http.RequestOptions) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
const req = http.request(options, (res) => {
|
|
20
|
-
let responseData = '';
|
|
21
|
-
res.on('data', (chunk) => {
|
|
22
|
-
responseData += chunk;
|
|
23
|
-
});
|
|
24
|
-
res.on('end', () => {
|
|
25
|
-
resolve(responseData);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
req.on('error', (e) => {
|
|
29
|
-
reject(e);
|
|
30
|
-
});
|
|
31
|
-
req.end();
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('fake-serv', () => {
|
|
36
|
-
test('basic service functionality', async () => {
|
|
37
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
38
|
-
const __dirname = path.dirname(__filename);
|
|
39
|
-
|
|
40
|
-
const options: ServiceStartOptions<FakeServLocals> = {
|
|
41
|
-
service,
|
|
42
|
-
name: 'fake-serv',
|
|
43
|
-
rootDirectory: path.resolve(__dirname, './fake-serv'),
|
|
44
|
-
codepath: 'src',
|
|
45
|
-
version: '1.0.0',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const app = await startApp(options).catch((error) => {
|
|
49
|
-
// eslint-disable-next-line no-console
|
|
50
|
-
console.error(error);
|
|
51
|
-
throw error;
|
|
52
|
-
});
|
|
53
|
-
expect(app.locals.config.server.port).not.toEqual(0);
|
|
54
|
-
expect(app).toBeTruthy();
|
|
55
|
-
|
|
56
|
-
let { body } = await request(app).get('/world').timeout(500).expect(200);
|
|
57
|
-
expect(body.hello).toEqual('world');
|
|
58
|
-
|
|
59
|
-
({ body } = await request(app).get('/other/world').timeout(500).expect(200));
|
|
60
|
-
expect(body.hello).toEqual('jupiter');
|
|
61
|
-
|
|
62
|
-
({ body } = await request(app)
|
|
63
|
-
.get('/hello')
|
|
64
|
-
.query({ greeting: 'Hello Pluto!', number: '6', break_things: true })
|
|
65
|
-
.expect(200));
|
|
66
|
-
expect(body.greeting).toEqual('Hello Pluto!');
|
|
67
|
-
|
|
68
|
-
// Can't convert green to a number
|
|
69
|
-
await request(app)
|
|
70
|
-
.get('/hello')
|
|
71
|
-
.query({ greeting: 'Hello Pluto!', number: 'green' })
|
|
72
|
-
.expect(400);
|
|
73
|
-
|
|
74
|
-
// Make sure body paramater conversion works
|
|
75
|
-
await request(app).post('/hello').send({ number: 'green' }).expect(400);
|
|
76
|
-
await request(app).post('/hello').send({ number: '6' }).expect(204);
|
|
77
|
-
await request(app).post('/hello').send({ number: 6 }).expect(204);
|
|
78
|
-
|
|
79
|
-
({ body } = await request(app).get('/error/sync').timeout(1000).expect(500));
|
|
80
|
-
expect(body.code).toEqual('SyncError');
|
|
81
|
-
|
|
82
|
-
({ body } = await request(app).get('/error/async').timeout(1000).expect(500));
|
|
83
|
-
expect(body.code).toEqual('AsyncError');
|
|
84
|
-
|
|
85
|
-
// Mocking
|
|
86
|
-
await request(app).post('/world').expect(500);
|
|
87
|
-
|
|
88
|
-
const server = await listen(app);
|
|
89
|
-
// Exercise the http module
|
|
90
|
-
await httpRequest({
|
|
91
|
-
hostname: 'localhost',
|
|
92
|
-
port: app.locals.config.server.port,
|
|
93
|
-
path: '/hello?greeting=Hello&number=6',
|
|
94
|
-
method: 'GET',
|
|
95
|
-
});
|
|
96
|
-
await request(app.locals.internalApp)
|
|
97
|
-
.get('/metrics')
|
|
98
|
-
.expect(200)
|
|
99
|
-
.expect((res) => {
|
|
100
|
-
expect(res.text).toMatch(/nodejs_version_info{version/);
|
|
101
|
-
expect(res.text).toMatch(/# UNIT http_server_duration ms/);
|
|
102
|
-
expect(res.text).toMatch(/world_requests_total{method="get".*} 1/);
|
|
103
|
-
expect(res.text).toContain(
|
|
104
|
-
'http_request_duration_seconds_bucket{status_code="200",method="GET",path="/world",service="fake-serv"',
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Clean shutdown
|
|
109
|
-
await new Promise<void>((accept, reject) => {
|
|
110
|
-
server.close((e) => {
|
|
111
|
-
if (e) {
|
|
112
|
-
reject(e);
|
|
113
|
-
} else {
|
|
114
|
-
accept();
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { shutdownGlobalTelemetry, startGlobalTelemetry } from '../src/telemetry/index.js';
|
|
4
|
-
|
|
5
|
-
// Even in testing, this needs to run first so that the instrumentation
|
|
6
|
-
// is loaded BEFORE express is loaded.
|
|
7
|
-
const startPromise = startGlobalTelemetry('fake-serv');
|
|
8
|
-
|
|
9
|
-
beforeAll(async () => {
|
|
10
|
-
await startPromise;
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
afterAll(async () => {
|
|
14
|
-
await shutdownGlobalTelemetry().catch(() => undefined);
|
|
15
|
-
});
|
package/src/bin/start-service.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import minimist from 'minimist';
|
|
3
|
-
|
|
4
|
-
import { serviceRepl } from '../development/repl.js';
|
|
5
|
-
import { isDev } from '../env.js';
|
|
6
|
-
import { bootstrap } from '../bootstrap.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* built - forces the use of the build directory. Defaults to true in stage/prod, not in dev
|
|
10
|
-
* repl - launch the REPL (defaults to disabling telemetry)
|
|
11
|
-
* telemetry - whether to use OpenTelemetry. Defaults to false in dev or with repl
|
|
12
|
-
* nobind - do not listen on http port or expose metrics
|
|
13
|
-
*/
|
|
14
|
-
const argv = minimist(process.argv.slice(2), {
|
|
15
|
-
boolean: ['built', 'repl', 'telemetry', 'nobind'],
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
if (argv.telemetry) {
|
|
19
|
-
await import('../telemetry/hook-modules.js');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const noTelemetry = (argv.repl || isDev()) && !argv.telemetry;
|
|
23
|
-
void bootstrap({
|
|
24
|
-
...argv,
|
|
25
|
-
telemetry: !noTelemetry,
|
|
26
|
-
}).then(({ app, codepath, server }) => {
|
|
27
|
-
if (argv.repl) {
|
|
28
|
-
serviceRepl(app, codepath, () => {
|
|
29
|
-
server?.close();
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
});
|
package/src/bootstrap.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
|
|
4
|
-
import { config } from 'dotenv';
|
|
5
|
-
import { readPackageUp } from 'read-package-up';
|
|
6
|
-
import type { NormalizedPackageJson } from 'read-package-up';
|
|
7
|
-
import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
AnyServiceLocals,
|
|
11
|
-
RequestLocals,
|
|
12
|
-
ServiceLocals,
|
|
13
|
-
ServiceStartOptions,
|
|
14
|
-
} from './types.js';
|
|
15
|
-
import { isDev } from './env.js';
|
|
16
|
-
import { startWithTelemetry } from './telemetry/index.js';
|
|
17
|
-
import type { ConfigurationSchema } from './config/schema.js';
|
|
18
|
-
|
|
19
|
-
interface BootstrapArguments {
|
|
20
|
-
// The name of the service, else discovered via read-package-up
|
|
21
|
-
name?: string;
|
|
22
|
-
// The name of the file with the service function, relative to root
|
|
23
|
-
main?: string;
|
|
24
|
-
// Root directory of the app, else discovered via read-package-up
|
|
25
|
-
root?: string;
|
|
26
|
-
// Use built directory. Omitting lets us determine a sensible default
|
|
27
|
-
built?: boolean;
|
|
28
|
-
// The location of the package.json used for discovery (defaults to cwd)
|
|
29
|
-
packageDir?: string;
|
|
30
|
-
// Whether to engage telemetry
|
|
31
|
-
telemetry?: boolean;
|
|
32
|
-
// Don't bind to http port or expose metrics
|
|
33
|
-
nobind?: boolean;
|
|
34
|
-
// The version of the app, else discovered via read-package-up
|
|
35
|
-
version?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function resolveMain(packageJson: NormalizedPackageJson) {
|
|
39
|
-
if (typeof packageJson.main === 'string') {
|
|
40
|
-
return packageJson.main;
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function getServiceDetails(argv: BootstrapArguments = {}) {
|
|
46
|
-
if (argv.name && argv.root) {
|
|
47
|
-
return {
|
|
48
|
-
rootDirectory: argv.root,
|
|
49
|
-
name: argv.name,
|
|
50
|
-
version: argv.version || '0.0.0',
|
|
51
|
-
main: argv.main || (isDev() && !argv.built ? 'src/index.ts' : 'build/index.js'),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
const cwd = argv.packageDir ? path.resolve(argv.packageDir) : process.cwd();
|
|
55
|
-
const pkg = await readPackageUp({ cwd });
|
|
56
|
-
if (!pkg) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`Unable to find package.json in ${cwd} to get main module. Make sure you are running from the package root directory.`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
const main = resolveMain(pkg.packageJson);
|
|
62
|
-
const parts = pkg.packageJson.name.split('/');
|
|
63
|
-
return {
|
|
64
|
-
main,
|
|
65
|
-
rootDirectory: path.dirname(pkg.path),
|
|
66
|
-
name: parts[parts.length - 1],
|
|
67
|
-
version: pkg.packageJson.version,
|
|
68
|
-
customizer: (pkg.packageJson.config?.telemetry as { customizer?: string })?.customizer,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function getBuildDir(main: string): 'build' | 'dist' {
|
|
73
|
-
const dir = /^(?:\.?\/?)(build|dist)\//.exec(main);
|
|
74
|
-
assert(dir, 'Could not determine build directory - should be dist or build');
|
|
75
|
-
return dir[1] as 'build' | 'dist';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Automagically start your app by using common patterns
|
|
79
|
-
// to find your implementation and settings. This is most useful
|
|
80
|
-
// for jobs or other scripts that need service infra but are
|
|
81
|
-
// not simply the service
|
|
82
|
-
export async function bootstrap<
|
|
83
|
-
SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
|
|
84
|
-
RLocals extends RequestLocals = RequestLocals,
|
|
85
|
-
>(argv?: BootstrapArguments) {
|
|
86
|
-
const { main, rootDirectory, name, version, customizer } = await getServiceDetails(argv);
|
|
87
|
-
|
|
88
|
-
let entrypoint: string;
|
|
89
|
-
let codepath: 'build' | 'dist' | 'src' = 'build';
|
|
90
|
-
if (isDev() && argv?.built !== true) {
|
|
91
|
-
const handlesTs = parseInt(process.versions.node.split('.')[0], 10) >= 24;
|
|
92
|
-
if (!handlesTs) {
|
|
93
|
-
await import('tsx/esm');
|
|
94
|
-
}
|
|
95
|
-
if (main) {
|
|
96
|
-
entrypoint = handlesTs
|
|
97
|
-
? main
|
|
98
|
-
: main.replace(/^(\.?\/?)(build|dist)\//, '$1src/').replace(/\.js$/, '.ts');
|
|
99
|
-
} else {
|
|
100
|
-
entrypoint = './src/index.ts';
|
|
101
|
-
}
|
|
102
|
-
codepath = 'src';
|
|
103
|
-
} else if (main) {
|
|
104
|
-
codepath = getBuildDir(main);
|
|
105
|
-
entrypoint = main;
|
|
106
|
-
} else {
|
|
107
|
-
entrypoint = './build/index.js';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
config({ quiet: true });
|
|
111
|
-
|
|
112
|
-
const absoluteEntrypoint = path.resolve(rootDirectory, entrypoint);
|
|
113
|
-
if (argv?.telemetry) {
|
|
114
|
-
let otelCustomizer:
|
|
115
|
-
| ((options: Partial<NodeSDKConfiguration>) => Partial<NodeSDKConfiguration>)
|
|
116
|
-
| undefined = undefined;
|
|
117
|
-
if (customizer) {
|
|
118
|
-
// Customize OTEL with a dynamic import based on the codePath (so put it in src, generally)
|
|
119
|
-
otelCustomizer = (await import(path.resolve(`${codepath}/${customizer}`)))
|
|
120
|
-
.NodeSDKConfiguration;
|
|
121
|
-
if (typeof otelCustomizer === 'object') {
|
|
122
|
-
otelCustomizer = (v) => ({ ...v, ...(otelCustomizer as Partial<NodeSDKConfiguration>) });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return startWithTelemetry<SLocals, RLocals>({
|
|
126
|
-
name,
|
|
127
|
-
rootDirectory,
|
|
128
|
-
service: absoluteEntrypoint,
|
|
129
|
-
codepath,
|
|
130
|
-
version,
|
|
131
|
-
customizer: otelCustomizer,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// This needs to be required for TS on-the-fly to work
|
|
136
|
-
|
|
137
|
-
const impl = await import(absoluteEntrypoint);
|
|
138
|
-
const opts: ServiceStartOptions<SLocals, RLocals> = {
|
|
139
|
-
name,
|
|
140
|
-
version,
|
|
141
|
-
rootDirectory,
|
|
142
|
-
service: impl.default || impl.service,
|
|
143
|
-
codepath,
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const { startApp, listen } = await import('./express-app/app.js');
|
|
147
|
-
const app = await startApp<SLocals, RLocals>(opts);
|
|
148
|
-
const server = argv?.nobind ? undefined : await listen(app);
|
|
149
|
-
return { server, app, codepath };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function bootstrapCli<
|
|
153
|
-
SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
|
|
154
|
-
RLocals extends RequestLocals = RequestLocals,
|
|
155
|
-
>(argv?: BootstrapArguments) {
|
|
156
|
-
return bootstrap<SLocals, RLocals>({
|
|
157
|
-
nobind: true,
|
|
158
|
-
...argv,
|
|
159
|
-
});
|
|
160
|
-
}
|
package/src/config/index.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
import type { BaseConfitSchema, Confit, Factory, ShortstopHandler } from '@sesamecare-oss/confit';
|
|
6
|
-
import { confit } from '@sesamecare-oss/confit';
|
|
7
|
-
|
|
8
|
-
import { getAvailablePort } from '../development/port-finder.js';
|
|
9
|
-
|
|
10
|
-
import type { ConfigurationSchema } from './schema.js';
|
|
11
|
-
|
|
12
|
-
// Order matters here.
|
|
13
|
-
const ENVIRONMENTS = ['production', 'staging', 'test', 'development'] as const;
|
|
14
|
-
|
|
15
|
-
async function pathExists(f: string) {
|
|
16
|
-
return new Promise((accept, reject) => {
|
|
17
|
-
fs.stat(f, (err) => {
|
|
18
|
-
if (!err) {
|
|
19
|
-
accept(true);
|
|
20
|
-
} else if (err.code === 'ENOENT') {
|
|
21
|
-
accept(false);
|
|
22
|
-
} else {
|
|
23
|
-
reject(err);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function addDefaultConfiguration<Config extends ConfigurationSchema = ConfigurationSchema>(
|
|
30
|
-
configFactory: Factory<Config>,
|
|
31
|
-
directory: string,
|
|
32
|
-
envConfit: Confit<BaseConfitSchema>,
|
|
33
|
-
) {
|
|
34
|
-
const addIfEnv = async (e: (typeof ENVIRONMENTS)[number]) => {
|
|
35
|
-
const c = path.join(directory, `${e}.json`);
|
|
36
|
-
if (envConfit.get().env[e] && (await pathExists(c))) {
|
|
37
|
-
configFactory.addDefault(c);
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
return false;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
await ENVIRONMENTS.reduce(
|
|
44
|
-
(runningPromise, environment) => runningPromise.then((prev) => prev || addIfEnv(environment)),
|
|
45
|
-
Promise.resolve(false),
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const baseConfig = path.join(directory, 'config.json');
|
|
49
|
-
if (await pathExists(baseConfig)) {
|
|
50
|
-
configFactory.addDefault(baseConfig);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface ServiceConfigurationSpec {
|
|
55
|
-
// The LAST configuration is the most "specific" - if a configuration value
|
|
56
|
-
// exists in all directories, the last one wins
|
|
57
|
-
configurationDirectories: string[];
|
|
58
|
-
shortstopHandlers: Record<string, ShortstopHandler<string, unknown>>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function loadConfiguration<Config extends ConfigurationSchema>({
|
|
62
|
-
configurationDirectories: dirs,
|
|
63
|
-
shortstopHandlers,
|
|
64
|
-
}: ServiceConfigurationSpec): Promise<Config> {
|
|
65
|
-
const specificConfig = dirs[dirs.length - 1];
|
|
66
|
-
|
|
67
|
-
// This confit version just gets us environment info
|
|
68
|
-
const envConfit = await confit({ basedir: specificConfig }).create();
|
|
69
|
-
const configFactory = confit<Config>({
|
|
70
|
-
basedir: specificConfig,
|
|
71
|
-
protocols: shortstopHandlers,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Note that in confit, when using addDefault,
|
|
76
|
-
* the FIRST addDefault takes precendence over the next (and so on), so
|
|
77
|
-
* if you override this method, you should register your defaults first.
|
|
78
|
-
*/
|
|
79
|
-
const defaultOrder = dirs.slice(0, dirs.length - 1).reverse();
|
|
80
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
81
|
-
const __dirname = path.dirname(__filename);
|
|
82
|
-
|
|
83
|
-
defaultOrder.push(path.join(__dirname, '../..', 'config'));
|
|
84
|
-
await defaultOrder.reduce(
|
|
85
|
-
(promise, dir) => promise.then(() => addDefaultConfiguration(configFactory, dir, envConfit)),
|
|
86
|
-
Promise.resolve(),
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
const loaded = await configFactory.create();
|
|
90
|
-
|
|
91
|
-
// Because other things need to know the port we choose, we pick it here if it's
|
|
92
|
-
// configured to auto-select
|
|
93
|
-
const serverConfig = loaded.get().server;
|
|
94
|
-
if (serverConfig.port === 0) {
|
|
95
|
-
const port = await getAvailablePort(8001);
|
|
96
|
-
const store = loaded.get();
|
|
97
|
-
store.server = store.server || {};
|
|
98
|
-
store.server.port = port;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// TODO init other stuff based on config here, such as key management or
|
|
102
|
-
// other cloud-aware shortstop handlers
|
|
103
|
-
|
|
104
|
-
// Not sure why this is necessary, but it is
|
|
105
|
-
return loaded.get();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function insertConfigurationBefore(
|
|
109
|
-
configDirs: string[] | undefined,
|
|
110
|
-
insert: string,
|
|
111
|
-
before: string,
|
|
112
|
-
) {
|
|
113
|
-
const copy = [...(configDirs || [])];
|
|
114
|
-
const index = copy.indexOf(before);
|
|
115
|
-
if (index === -1) {
|
|
116
|
-
copy.push(insert, before);
|
|
117
|
-
} else {
|
|
118
|
-
copy.splice(index, 0, insert);
|
|
119
|
-
}
|
|
120
|
-
return copy;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export * from './schema.js';
|
|
124
|
-
export * from './validation.js';
|
package/src/config/schema.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { BaseConfitSchema } from '@sesamecare-oss/confit';
|
|
2
|
-
import type { middleware } from 'express-openapi-validator';
|
|
3
|
-
import type { Level } from 'pino';
|
|
4
|
-
import type bodyParser from 'body-parser';
|
|
5
|
-
|
|
6
|
-
export interface ConfigurationItemEnabled {
|
|
7
|
-
enabled?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ConfigurationSchema extends BaseConfitSchema {
|
|
11
|
-
trustProxy?: string[] | boolean;
|
|
12
|
-
logging?: {
|
|
13
|
-
level?: Level;
|
|
14
|
-
logRequestBody?: boolean;
|
|
15
|
-
logResponseBody?: boolean;
|
|
16
|
-
// Whether to log a "pre" message when request processing starts. Most useful in
|
|
17
|
-
// situations where there is some problem causing requests to hang
|
|
18
|
-
preLog?: boolean;
|
|
19
|
-
};
|
|
20
|
-
routing?: {
|
|
21
|
-
openapi?: boolean | Partial<Parameters<typeof middleware>[0]>;
|
|
22
|
-
// Relative to the *root directory* of the app
|
|
23
|
-
routes?: string;
|
|
24
|
-
// Whether to add middleware that "freezes" the query string
|
|
25
|
-
// rather than preserving the new Express@5 behavior of reparsing
|
|
26
|
-
// every time (which causes problems for OpenAPI validation)
|
|
27
|
-
freezeQuery?: boolean;
|
|
28
|
-
// Whether to compute etag headers. http://expressjs.com/en/api.html#etag.options.table
|
|
29
|
-
etag?: boolean;
|
|
30
|
-
cookieParser?: boolean;
|
|
31
|
-
bodyParsers?: {
|
|
32
|
-
json?: boolean | Parameters<typeof bodyParser.json>[0];
|
|
33
|
-
form?: boolean | Parameters<typeof bodyParser.urlencoded>[0];
|
|
34
|
-
};
|
|
35
|
-
// Set static.enabled to true to enable static assets to be served
|
|
36
|
-
static?: ConfigurationItemEnabled & {
|
|
37
|
-
// The path relative to the root directory of the app
|
|
38
|
-
path?: string;
|
|
39
|
-
// The path on which to mount the static assets (defaults to /)
|
|
40
|
-
mountPath?: string;
|
|
41
|
-
};
|
|
42
|
-
finalHandlers: {
|
|
43
|
-
// Whether to create and return errors for unhandled routes
|
|
44
|
-
notFound?: boolean;
|
|
45
|
-
// Whether to handle errors and return them to clients
|
|
46
|
-
// (currently means we will return JSON errors)
|
|
47
|
-
errors?: ConfigurationItemEnabled & {
|
|
48
|
-
render?: boolean;
|
|
49
|
-
// Check to see if we got an error from an upstream
|
|
50
|
-
// service that has code/domain/message, and if so return
|
|
51
|
-
// that as is. Otherwise we will sanitize it to avoid leaking
|
|
52
|
-
// information.
|
|
53
|
-
unnest: boolean;
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
server: {
|
|
58
|
-
internalPort?: number;
|
|
59
|
-
port?: number;
|
|
60
|
-
// To enable HTTPS on the main service, set the key and cert to the
|
|
61
|
-
// actual key material (not the path). Use shortstop file: handler.
|
|
62
|
-
// Note that generally it's better to offload tls termination,
|
|
63
|
-
// but this is useful for dev.
|
|
64
|
-
key?: string | Uint8Array;
|
|
65
|
-
certificate?: string | Uint8Array;
|
|
66
|
-
// If you have an alternate host name (other than localhost) that
|
|
67
|
-
// should be used when referring to this service, set it here.
|
|
68
|
-
hostname?: string;
|
|
69
|
-
};
|
|
70
|
-
}
|