@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.
Files changed (48) hide show
  1. package/package.json +8 -2
  2. package/.github/workflows/codeql-analysis.yml +0 -77
  3. package/.github/workflows/nodejs.yml +0 -62
  4. package/.trunk/configs/.markdownlint.yaml +0 -10
  5. package/.trunk/configs/.yamllint.yaml +0 -10
  6. package/.trunk/trunk.yaml +0 -35
  7. package/.yarn/patches/confit-npm-3.0.0-eade8c7ce1.patch +0 -52
  8. package/.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs +0 -541
  9. package/.yarn/releases/yarn-3.2.3.cjs +0 -783
  10. package/.yarnrc.yml +0 -7
  11. package/CHANGELOG.md +0 -525
  12. package/SECURITY.md +0 -12
  13. package/__tests__/config.test.ts +0 -53
  14. package/__tests__/fake-serv/api/fake-serv.yaml +0 -48
  15. package/__tests__/fake-serv/config/config.json +0 -13
  16. package/__tests__/fake-serv/src/handlers/hello.ts +0 -17
  17. package/__tests__/fake-serv/src/index.ts +0 -36
  18. package/__tests__/fake-serv/src/routes/error.ts +0 -16
  19. package/__tests__/fake-serv/src/routes/index.ts +0 -19
  20. package/__tests__/fake-serv/src/routes/other/world.ts +0 -7
  21. package/__tests__/fake-serv.test.ts +0 -119
  22. package/__tests__/vitest.test-setup.ts +0 -15
  23. package/src/bin/start-service.ts +0 -32
  24. package/src/bootstrap.ts +0 -160
  25. package/src/config/index.ts +0 -124
  26. package/src/config/schema.ts +0 -70
  27. package/src/config/shortstops.ts +0 -155
  28. package/src/config/validation.ts +0 -23
  29. package/src/development/port-finder.ts +0 -67
  30. package/src/development/repl.ts +0 -131
  31. package/src/env.ts +0 -29
  32. package/src/error.ts +0 -47
  33. package/src/express-app/app.ts +0 -438
  34. package/src/express-app/index.ts +0 -3
  35. package/src/express-app/internal-server.ts +0 -43
  36. package/src/express-app/modules.ts +0 -10
  37. package/src/express-app/route-loader.ts +0 -40
  38. package/src/express-app/types.ts +0 -32
  39. package/src/hook.ts +0 -36
  40. package/src/index.ts +0 -9
  41. package/src/openapi.ts +0 -184
  42. package/src/telemetry/DummyExporter.ts +0 -17
  43. package/src/telemetry/hook-modules.ts +0 -8
  44. package/src/telemetry/index.ts +0 -168
  45. package/src/telemetry/instrumentations.ts +0 -103
  46. package/src/telemetry/requestLogger.ts +0 -267
  47. package/src/tsx.d.ts +0 -1
  48. package/src/types.ts +0 -223
package/src/openapi.ts DELETED
@@ -1,184 +0,0 @@
1
- import path from 'path';
2
-
3
- import { merge } from 'moderndash';
4
- import * as OpenApiValidator from 'express-openapi-validator';
5
- import { OpenAPIFramework } from 'express-openapi-validator/dist/framework/index.js';
6
- import type { Handler, Request, RequestHandler } from 'express';
7
-
8
- import type { AnyServiceLocals, ServiceExpress, ServiceLocals } from './types.js';
9
- import { getNodeEnv, isProd, isStaging } from './env.js';
10
- import { getFilesInDir } from './express-app/modules.js';
11
- import type { ConfigurationSchema } from './config/schema.js';
12
-
13
- const notImplementedHandler: Handler = (req, res) => {
14
- res.status(501).json({
15
- code: 'NotImplemented',
16
- domain: 'http',
17
- message: 'This method is not yet implemented',
18
- });
19
- };
20
-
21
- type OAPIOpts = Parameters<typeof OpenApiValidator.middleware>[0];
22
-
23
- function stripExtension(filename: string) {
24
- return filename.slice(0, filename.lastIndexOf('.'));
25
- }
26
-
27
- export async function openApi<
28
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
29
- >(
30
- app: ServiceExpress<SLocals>,
31
- rootDirectory: string,
32
- codepath: string,
33
- pattern: string,
34
- openApiOptions?: Partial<OAPIOpts>,
35
- ) {
36
- const apiSpec = openApiOptions?.apiSpec
37
- ? undefined
38
- : path.resolve(rootDirectory, `./api/${app.locals.name}.yaml`);
39
- if (apiSpec) {
40
- app.locals.logger.debug({ apiSpec, codepath }, 'Serving OpenAPI');
41
- } else if (openApiOptions?.apiSpec) {
42
- app.locals.logger.debug({ codepath }, 'Serving OpenAPI');
43
- }
44
-
45
- const basePath = path.resolve(rootDirectory, `${codepath}/handlers`);
46
- // Because of the weirdness of ESM/CJS interop, and the synchronous nature of
47
- // the OpenAPI resolver, we need to preload all the modules we might need
48
- const moduleFiles = await getFilesInDir(
49
- pattern,
50
- path.resolve(rootDirectory, `${codepath}/handlers`),
51
- );
52
- const preloadedModules = await Promise.all(
53
- moduleFiles.map((file) => {
54
- const fullPath = path.join(basePath, file);
55
- return import(fullPath).catch((error) => {
56
- if (isStaging() || isProd()) {
57
- app.locals.logger.fatal(
58
- { file: fullPath, message: error.message },
59
- 'Could not load potential API handler',
60
- );
61
- process.exit(1);
62
- }
63
- app.locals.logger.warn(
64
- { file: fullPath, message: error.message },
65
- 'Could not load potential API handler',
66
- );
67
- return undefined;
68
- });
69
- }),
70
- );
71
- const modulesByPath = moduleFiles.reduce<Record<string, Record<string, unknown>>>(
72
- (acc, file, index) => {
73
- const m = preloadedModules[index];
74
- if (m) {
75
- acc[`/${stripExtension(file)}`] = m;
76
- }
77
- return acc;
78
- },
79
- {},
80
- );
81
-
82
- // This is nuts, but there are testing frameworks or some other things
83
- // that seem to set window in Node. The OpenAPI infra will fail under that
84
- // circumstance.
85
- const _window = global.window;
86
- if (_window) {
87
- delete (global as { window: unknown }).window;
88
- }
89
-
90
- try {
91
- app.locals.openApiSpecification = await new OpenAPIFramework({
92
- apiDoc: (openApiOptions?.apiSpec ?? apiSpec) as string,
93
- })
94
- .initialize({ visitApi() {} })
95
- .then((docs) => docs.apiDoc)
96
- .catch((error) => {
97
- app.locals.logger.error(error, 'Failed to parse and load OpenAPI spec');
98
- });
99
-
100
- const defaultOptions: OAPIOpts = {
101
- apiSpec: app.locals.openApiSpecification,
102
- // We force full dereferencing of the OpenAPI spec so that all `$ref` schemas are
103
- // inlined before express-openapi-validator builds its request coercion rules.
104
- // Without this, the coercer sees `schema: { $ref: ... }` for query parameters and
105
- // cannot resolve the underlying type, so it falls back to treating the parameter
106
- // as a potentially repeated value (i.e., an array). That causes AJV to reject
107
- // valid requests with “should be array” errors. Dereferencing ensures the
108
- // coercer sees the real primitive type/enum and stops misclassifying params.
109
- //
110
- // The one downside is that circular references are not supported, so if you
111
- // need that, you need to be very careful with enums, and override this option.
112
- //
113
- // See https://github.com/cdimascio/express-openapi-validator/issues/1119
114
- $refParser: {
115
- mode: 'dereference',
116
- },
117
- ignoreUndocumented: true,
118
- validateRequests: {
119
- allowUnknownQueryParameters: true,
120
- coerceTypes: 'array',
121
- },
122
- operationHandlers: {
123
- basePath,
124
- resolver(
125
- basePath: string,
126
- route: Parameters<typeof OpenApiValidator.resolvers.defaultResolver>[1],
127
- ) {
128
- const pathKey = route.openApiRoute.substring(route.basePath.length);
129
- const modulePath = path.join(basePath, pathKey);
130
-
131
- try {
132
- const module = modulesByPath[pathKey];
133
- const method = module
134
- ? Object.keys(module).find((m) => m.toUpperCase() === route.method)
135
- : undefined;
136
- if (!module || !method) {
137
- throw new Error(
138
- `Could not find a [${route.method}] function in ${modulePath} when trying to route [${route.method} ${route.expressRoute}].`,
139
- );
140
- }
141
- return module[method] as RequestHandler;
142
- } catch (error) {
143
- app.locals.logger.error(
144
- {
145
- error: (error as Error).message,
146
- pathKey,
147
- modulePath: path.relative(rootDirectory, modulePath),
148
- },
149
- 'Failed to load API method handler',
150
- );
151
- return notImplementedHandler;
152
- }
153
- },
154
- },
155
- };
156
-
157
- const { routing } = app.locals.config;
158
- const combinedOptions = {
159
- // In test mode, validate returned swagger responses. This can easily be disabled
160
- // by setting validateResponses to false in the config.
161
- ...(getNodeEnv() === 'test'
162
- ? {
163
- validateResponses: {
164
- onError(error: Error, body: unknown, req: Request) {
165
- console.log('Response body fails validation: ', error);
166
- console.log('Emitted from:', req.originalUrl);
167
- // eslint-disable-next-line no-console
168
- console.debug(body);
169
- throw error;
170
- },
171
- },
172
- }
173
- : {}),
174
- ...(typeof routing.openapi === 'object' ? routing.openapi : {}),
175
- ...openApiOptions,
176
- };
177
-
178
- return OpenApiValidator.middleware(merge(defaultOptions, combinedOptions));
179
- } finally {
180
- if (_window) {
181
- (global as { window: unknown }).window = _window;
182
- }
183
- }
184
- }
@@ -1,17 +0,0 @@
1
- import type { ExportResult } from '@opentelemetry/core';
2
- import { ExportResultCode } from '@opentelemetry/core';
3
- import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
4
-
5
- export class DummySpanExporter implements SpanExporter {
6
- export(spans: ReadableSpan[], resultCallback: (r: ExportResult) => void) {
7
- setImmediate(() =>
8
- resultCallback({
9
- code: ExportResultCode.SUCCESS,
10
- }),
11
- );
12
- }
13
-
14
- async shutdown() {
15
- // Nothing to do
16
- }
17
- }
@@ -1,8 +0,0 @@
1
- import module from 'node:module';
2
-
3
- module.register('import-in-the-middle/hook.mjs', import.meta.url, {
4
- parentURL: import.meta.url,
5
- data: {
6
- include: ['express', 'pino', 'http', 'dns', 'net', 'pg', 'redis', 'undici', 'generic-pool'],
7
- },
8
- });
@@ -1,168 +0,0 @@
1
- import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';
2
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
3
- import {
4
- detectResources,
5
- envDetector,
6
- hostDetector,
7
- osDetector,
8
- processDetector,
9
- } from '@opentelemetry/resources';
10
- import { containerDetector } from '@opentelemetry/resource-detector-container';
11
- import { gcpDetector } from '@opentelemetry/resource-detector-gcp';
12
- import * as opentelemetry from '@opentelemetry/sdk-node';
13
- import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
14
-
15
- import type {
16
- AnyServiceLocals,
17
- DelayLoadServiceStartOptions,
18
- RequestLocals,
19
- ServiceLocals,
20
- ServiceStartOptions,
21
- } from '../types.js';
22
- import type { ListenFn, StartAppFn } from '../express-app/index.js';
23
- import type { ConfigurationSchema } from '../config/schema.js';
24
-
25
- import { getAutoInstrumentations } from './instrumentations.js';
26
- import { DummySpanExporter } from './DummyExporter.js';
27
-
28
- // OTLP seems to only support http, and this is a default on the local network so I'm keeping it.
29
- // NOSONAR
30
- const baseDefaultOtlpUrl = new URL('http://otlp-exporter:4318/v1').toString();
31
-
32
- function getSpanExporter() {
33
- if (
34
- !process.env.DISABLE_OLTP_EXPORTER &&
35
- (['production', 'staging'].includes(process.env.APP_ENV || process.env.NODE_ENV || '') ||
36
- process.env.OTLP_EXPORTER)
37
- ) {
38
- return new OTLPTraceExporter({
39
- url: process.env.OTLP_EXPORTER || `${baseDefaultOtlpUrl}/traces`,
40
- });
41
- }
42
- if (process.env.ENABLE_CONSOLE_OLTP_EXPORTER) {
43
- return new opentelemetry.tracing.ConsoleSpanExporter();
44
- }
45
- return new DummySpanExporter();
46
- }
47
-
48
- function getLogExporter() {
49
- if (
50
- !process.env.DISABLE_OLTP_EXPORTER &&
51
- (['production', 'staging'].includes(process.env.APP_ENV || process.env.NODE_ENV || '') ||
52
- process.env.OTLP_EXPORTER)
53
- ) {
54
- return new OTLPLogExporter({
55
- url: process.env.OTLP_EXPORTER || `${baseDefaultOtlpUrl}/logs`,
56
- });
57
- }
58
- if (process.env.ENABLE_CONSOLE_OLTP_EXPORTER) {
59
- return new opentelemetry.logs.ConsoleLogRecordExporter();
60
- }
61
- return undefined;
62
- }
63
-
64
- let prometheusExporter: PrometheusExporter | undefined;
65
- let telemetrySdk: opentelemetry.NodeSDK | undefined;
66
-
67
- /**
68
- * OpenTelemetry is not friendly to the idea of stopping
69
- * and starting itself, it seems. So we can only keep a global
70
- * instance of the infrastructure no matter how many times
71
- * you start/stop your service (this is mostly only relevant for testing).
72
- * In addition, since we have to load it right away before configuration
73
- * is available, we can't use configuration to decide anything.
74
- */
75
- export async function startGlobalTelemetry(
76
- serviceName: string,
77
- customizer?:
78
- | ((
79
- options: Partial<opentelemetry.NodeSDKConfiguration>,
80
- ) => Partial<opentelemetry.NodeSDKConfiguration>)
81
- | undefined,
82
- ) {
83
- if (!prometheusExporter) {
84
- const { metrics, logs, NodeSDK } = opentelemetry;
85
-
86
- const resource = detectResources({
87
- detectors: [
88
- envDetector,
89
- hostDetector,
90
- osDetector,
91
- processDetector,
92
- containerDetector,
93
- gcpDetector,
94
- ],
95
- });
96
-
97
- prometheusExporter = new PrometheusExporter({ preventServerStart: true });
98
- const instrumentations = getAutoInstrumentations();
99
- const logExporter = getLogExporter();
100
- const options: Partial<opentelemetry.NodeSDKConfiguration> = {
101
- serviceName,
102
- autoDetectResources: false,
103
- resource,
104
- traceExporter: getSpanExporter(),
105
- metricReader: prometheusExporter,
106
- instrumentations,
107
- logRecordProcessors: logExporter ? [new logs.BatchLogRecordProcessor(logExporter)] : [],
108
- views: [
109
- {
110
- instrumentName: 'http_request_duration_seconds',
111
- instrumentType: metrics.InstrumentType.HISTOGRAM,
112
- aggregation: {
113
- type: metrics.AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
114
- options: {
115
- boundaries: [0.003, 0.03, 0.1, 0.3, 1.5, 10],
116
- recordMinMax: true,
117
- },
118
- },
119
- },
120
- ],
121
- };
122
- telemetrySdk = new NodeSDK(customizer ? customizer(options) : options);
123
- telemetrySdk.start();
124
- }
125
- }
126
-
127
- export function getNodeTelemetrySdk() {
128
- return telemetrySdk;
129
- }
130
-
131
- export function getGlobalPrometheusExporter() {
132
- return prometheusExporter;
133
- }
134
-
135
- export async function shutdownGlobalTelemetry() {
136
- await telemetrySdk?.shutdown();
137
- telemetrySdk = undefined;
138
- prometheusExporter = undefined;
139
- }
140
-
141
- export async function startWithTelemetry<
142
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
143
- RLocals extends RequestLocals = RequestLocals,
144
- >(options: DelayLoadServiceStartOptions) {
145
- await startGlobalTelemetry(options.name, options.customizer);
146
-
147
- const { startApp, listen } = (await import('../express-app/app.js')) as {
148
- startApp: StartAppFn<SLocals, RLocals>;
149
- listen: ListenFn<SLocals>;
150
- };
151
- const serviceModule = await import(options.service);
152
- const service = serviceModule.default || serviceModule.service;
153
- const startOptions: ServiceStartOptions<SLocals> = {
154
- ...options,
155
- service,
156
- locals: { ...options.locals } as unknown as Partial<SLocals>,
157
- };
158
- const app = await startApp(startOptions);
159
- app.locals.logger.info('OpenTelemetry enabled');
160
-
161
- const server = await listen(app, async () => {
162
- await shutdownGlobalTelemetry();
163
- app.locals.logger.info('OpenTelemetry shut down');
164
- });
165
- return { app, codepath: options.codepath, server };
166
- }
167
-
168
- export { setTelemetryHooks } from './instrumentations.js';
@@ -1,103 +0,0 @@
1
- import type { Instrumentation } from '@opentelemetry/instrumentation';
2
- import { DnsInstrumentation } from '@opentelemetry/instrumentation-dns';
3
- import type { SpanNameHook } from '@opentelemetry/instrumentation-express';
4
- import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
5
- import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
6
- import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-generic-pool';
7
- import type { IgnoreIncomingRequestFunction } from '@opentelemetry/instrumentation-http';
8
- import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
9
- import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
10
- import { NetInstrumentation } from '@opentelemetry/instrumentation-net';
11
- import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
12
- import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
13
- import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';
14
-
15
- const InstrumentationMap = {
16
- '@opentelemetry/instrumentation-http': HttpInstrumentation,
17
- '@opentelemetry/instrumentation-dns': DnsInstrumentation,
18
- '@opentelemetry/instrumentation-express': ExpressInstrumentation,
19
- '@opentelemetry/instrumentation-graphql': GraphQLInstrumentation,
20
- '@opentelemetry/instrumentation-undici': UndiciInstrumentation,
21
- '@opentelemetry/instrumentation-generic-pool': GenericPoolInstrumentation,
22
- '@opentelemetry/instrumentation-redis': RedisInstrumentation,
23
- '@opentelemetry/instrumentation-net': NetInstrumentation,
24
- '@opentelemetry/instrumentation-pg': PgInstrumentation,
25
- '@opentelemetry/instrumentation-pino': PinoInstrumentation,
26
- };
27
-
28
- // Config types inferred automatically from the first argument of the constructor
29
- type ConfigArg<T> = T extends new (...args: infer U) => unknown ? U[0] : never;
30
- export type InstrumentationConfigMap = {
31
- [Name in keyof typeof InstrumentationMap]?: ConfigArg<(typeof InstrumentationMap)[Name]>;
32
- };
33
-
34
- let ignoreIncomingRequestHook: IgnoreIncomingRequestFunction | undefined = (req) => {
35
- return req.url === '/health' || req.url === '/metrics';
36
- };
37
-
38
- let spanNameHook: SpanNameHook | undefined;
39
-
40
- export function setTelemetryHooks(hooks: {
41
- ignoreIncomingRequestHook?: IgnoreIncomingRequestFunction;
42
- spanNameHook?: SpanNameHook;
43
- }) {
44
- if ('ignoreIncomingRequestHook' in hooks) {
45
- ignoreIncomingRequestHook = hooks.ignoreIncomingRequestHook;
46
- }
47
- if ('spanNameHook' in hooks) {
48
- spanNameHook = hooks.spanNameHook;
49
- }
50
- }
51
-
52
- const defaultConfigs: InstrumentationConfigMap = {
53
- '@opentelemetry/instrumentation-http': {
54
- ignoreIncomingRequestHook(req) {
55
- if (ignoreIncomingRequestHook) {
56
- return ignoreIncomingRequestHook(req);
57
- }
58
- return false;
59
- },
60
- },
61
- '@opentelemetry/instrumentation-express': {
62
- spanNameHook(info, defaultName) {
63
- if (spanNameHook) {
64
- return spanNameHook(info, defaultName);
65
- }
66
- return defaultName;
67
- },
68
- ignoreLayers: [
69
- 'middleware - serviceLogger',
70
- 'middleware - jsonParser',
71
- 'middleware - attachServiceLocals',
72
- 'middleware - cookieParser',
73
- 'middleware - corsMiddleware',
74
- 'middleware - addReturnHeaders',
75
- 'middleware - freezeQuery',
76
- 'middleware - pathParamsMiddleware',
77
- 'middleware - metadataMiddleware',
78
- 'middleware - multipartMiddleware',
79
- 'middleware - securityMiddleware',
80
- ],
81
- },
82
- };
83
-
84
- export function getAutoInstrumentations(
85
- inputConfigs: InstrumentationConfigMap = defaultConfigs,
86
- ): Instrumentation[] {
87
- const keys = Object.keys(InstrumentationMap) as (keyof typeof InstrumentationMap)[];
88
- return keys
89
- .map((name) => {
90
- const Instance = InstrumentationMap[name];
91
- // Defaults are defined by the instrumentation itself
92
- const userConfig = inputConfigs[name] ?? {};
93
-
94
- try {
95
- return new Instance(userConfig);
96
- } catch (e) {
97
- // eslint-disable-next-line no-console
98
- console.error(`Failed to load ${name}`, e);
99
- return null;
100
- }
101
- })
102
- .filter((i) => !!i) as Instrumentation[];
103
- }