@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
@@ -1,267 +0,0 @@
1
- import type { RequestHandler, Request, Response, ErrorRequestHandler } from 'express';
2
- import { getClientIp } from 'request-ip';
3
- import type { Histogram } from '@opentelemetry/api';
4
- import cleanStack from 'clean-stack';
5
-
6
- import { ServiceError } from '../error.js';
7
- import type { AnyServiceLocals, RequestWithApp, ServiceExpress, ServiceLocals } from '../types.js';
8
- import type { ServiceHandler } from '../express-app/types.js';
9
- import type { ConfigurationSchema } from '../config/schema.js';
10
- import { getNodeEnv } from '../env.js';
11
-
12
- const LOG_PREFS = Symbol('Logging information');
13
- const LOGGED_SEMAPHORE = Symbol('Logged semaphore');
14
-
15
- interface LogPrefs {
16
- start: [number, number];
17
- logRequests?: boolean;
18
- chunks?: Buffer[];
19
- logged: boolean;
20
- }
21
-
22
- interface WithLogPrefs {
23
- [LOG_PREFS]: LogPrefs;
24
- }
25
-
26
- interface WithIdentifiedSession {
27
- session?: {
28
- id?: string;
29
- };
30
- }
31
-
32
- interface ErrorWithStatus extends Error {
33
- status?: number;
34
- expected_error?: boolean;
35
- }
36
-
37
- function getBasicInfo(req: Request): [string, Record<string, string | number>] {
38
- const url = req.originalUrl || req.url;
39
-
40
- const preInfo: Record<string, string> = {
41
- ip: getClientIp(req) || '',
42
- m: req.method,
43
- };
44
-
45
- if (req.headers['user-agent']) {
46
- preInfo.ua = req.headers['user-agent'];
47
- }
48
-
49
- const sessionReq = req as WithIdentifiedSession;
50
- if (sessionReq.session?.id) {
51
- preInfo.sid = sessionReq.session.id;
52
- }
53
-
54
- return [url, preInfo];
55
- }
56
-
57
- function finishLog<SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>>(
58
- app: ServiceExpress<SLocals>,
59
- error: Error | undefined,
60
- req: Request,
61
- res: Response & { [LOGGED_SEMAPHORE]?: boolean },
62
- histogram: Histogram,
63
- ) {
64
- if (res[LOGGED_SEMAPHORE]) {
65
- return;
66
- }
67
-
68
- const prefs = (res.locals as WithLogPrefs)[LOG_PREFS] || {};
69
- if (prefs.logged) {
70
- // This happens when error handler runs, but onEnd hasn't fired yet. We only log the first one.
71
- return;
72
- }
73
-
74
- const { logger, service } = app.locals;
75
-
76
- let dur = 0;
77
- if (prefs.start) {
78
- const hrdur = process.hrtime(prefs.start);
79
- dur = hrdur[0] + hrdur[1] / 1000000000;
80
- }
81
- const [url, preInfo] = getBasicInfo(req);
82
-
83
- let responseType = 'unknown';
84
-
85
- if (res.writableFinished) {
86
- responseType = 'finished';
87
- } else if (req.aborted) {
88
- responseType = 'aborted';
89
- } else if (req.destroyed) {
90
- responseType = 'destroyed';
91
- } else if (error) {
92
- responseType = 'errored';
93
- }
94
-
95
- const endLog: Record<string, string | string[] | number | undefined> = {
96
- ...preInfo,
97
- t: 'req',
98
- r: responseType,
99
- s: (error as ErrorWithStatus)?.status || res.statusCode || 0,
100
- dur,
101
- };
102
-
103
- const path = req.route ? { path: req.route.path } : undefined;
104
- histogram.record(dur, {
105
- status_code: endLog.s,
106
- method: endLog.m,
107
- ...path,
108
- service: app.locals.name,
109
- });
110
-
111
- if (res.locals.user?.id) {
112
- endLog.u = res.locals.user.id;
113
- }
114
-
115
- let unexpectedError = false;
116
- if (error) {
117
- endLog.e = error.message;
118
- if (!(error instanceof ServiceError) || error.log_stack) {
119
- endLog.st = cleanStack(error.stack);
120
- }
121
- if (!(error as ErrorWithStatus).expected_error) {
122
- unexpectedError = true;
123
- }
124
- }
125
-
126
- if (prefs.logRequests) {
127
- endLog.h = JSON.stringify(req.headers);
128
- if (Buffer.isBuffer(req.body)) {
129
- endLog.b = req.body.toString('base64');
130
- } else if (typeof req.body !== 'string') {
131
- endLog.b = JSON.stringify(req.body);
132
- } else if (req.body) {
133
- endLog.b = req.body;
134
- }
135
- }
136
-
137
- if (prefs.chunks?.length) {
138
- const bodyString = Buffer.concat(prefs.chunks).toString('utf8');
139
- if (bodyString) {
140
- endLog.resBody = bodyString;
141
- }
142
- }
143
- const msg = service.getLogFields?.(req as RequestWithApp<SLocals>, endLog);
144
- if (msg === false) {
145
- res[LOGGED_SEMAPHORE] = true;
146
- return;
147
- }
148
- if (unexpectedError) {
149
- logger.error(endLog, msg || url);
150
- } else {
151
- logger.info(endLog, msg || url);
152
- }
153
-
154
- res[LOGGED_SEMAPHORE] = true;
155
- }
156
-
157
- export function loggerMiddleware<
158
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
159
- >(
160
- app: ServiceExpress<SLocals>,
161
- histogram: Histogram,
162
- config?: ConfigurationSchema['logging'],
163
- ): RequestHandler {
164
- const nonProd = getNodeEnv() !== 'production';
165
- const { logger, service } = app.locals;
166
- return function serviceLogger(req, res, next) {
167
- const logResponse =
168
- config?.logResponseBody || (nonProd && req.headers['x-log']?.includes('res'));
169
- const logRequest = config?.logRequestBody || (nonProd && req.headers['x-log']?.includes('req'));
170
- const prefs: LogPrefs = {
171
- start: process.hrtime(),
172
- logRequests: logRequest,
173
- chunks: logResponse ? [] : undefined,
174
- logged: false,
175
- };
176
-
177
- (res.locals as WithLogPrefs)[LOG_PREFS] = prefs;
178
-
179
- if (logResponse) {
180
- // res is a read-only stream, so the only way to intercept response
181
- // data is to monkey-patch.
182
- const oldWrite = res.write;
183
- const oldEnd = res.end;
184
- res.write = ((...args: Parameters<(typeof res)['write']>) => {
185
- if (prefs.chunks) {
186
- prefs.chunks.push(Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0] as string));
187
- }
188
- return (oldWrite as (typeof res)['write']).apply(res, args);
189
- }) as (typeof res)['write'];
190
- res.end = ((...args: Parameters<(typeof res)['end']>) => {
191
- if (args[0] && prefs.chunks) {
192
- prefs.chunks.push(Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0] as string));
193
- }
194
- return oldEnd.apply(res, args);
195
- }) as (typeof res)['end'];
196
- }
197
-
198
- if (config?.preLog) {
199
- const [url, preInfo] = getBasicInfo(req);
200
- const preLog: Record<string, string | string[] | number | undefined> = {
201
- ...preInfo,
202
- t: 'pre',
203
- ref: req.headers.referer || undefined,
204
- sid: (req as WithIdentifiedSession).session?.id,
205
- c: req.headers.correlationid || undefined,
206
- };
207
- const msg = service.getLogFields?.(req as RequestWithApp<SLocals>, preLog);
208
- if (msg !== false) {
209
- logger.info(preLog, msg || url);
210
- }
211
- }
212
-
213
- const logWriter = (err?: Error) => finishLog(app, err, req, res, histogram);
214
- res.on('finish', logWriter);
215
- res.on('close', logWriter);
216
- res.on('error', logWriter);
217
- next();
218
- };
219
- }
220
-
221
- export function errorHandlerMiddleware<
222
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
223
- >(app: ServiceExpress<SLocals>, histogram: Histogram, unnest?: boolean, returnError?: boolean) {
224
- const svcErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
225
- let loggable: Partial<ServiceError> = error;
226
- const body = error.response?.body || error.body;
227
- if (unnest && body?.domain && body?.code && body?.message) {
228
- loggable = {
229
- status: error.status,
230
- message: body.message,
231
- domain: body.domain,
232
- code: body.code,
233
- display_message: body.display_message,
234
- ...loggable.client_metadata,
235
- };
236
- }
237
- // Set the status to error, even if we aren't going to render the error.
238
- res.status(loggable.status || 500);
239
- if (returnError) {
240
- finishLog(app, error as Error, req, res, histogram);
241
- const prefs = (res.locals as WithLogPrefs)[LOG_PREFS];
242
- prefs.logged = true;
243
- res.json({
244
- code: loggable.code,
245
- message: loggable.message,
246
- domain: loggable.domain,
247
- display_message: loggable.display_message,
248
- ...loggable.client_metadata,
249
- });
250
- } else {
251
- next(error);
252
- }
253
- };
254
- return svcErrorHandler;
255
- }
256
-
257
- export function notFoundMiddleware() {
258
- const serviceNotFoundHandler: ServiceHandler = (req, res, next) => {
259
- const error = new ServiceError(req.app, `Cannot ${req.method} ${req.path}`, {
260
- status: 404,
261
- code: 'NotFound',
262
- domain: 'http',
263
- });
264
- next(error);
265
- };
266
- return serviceNotFoundHandler as RequestHandler;
267
- }
package/src/tsx.d.ts DELETED
@@ -1 +0,0 @@
1
- declare module 'tsx/esm';
package/src/types.ts DELETED
@@ -1,223 +0,0 @@
1
- import type { Server } from 'node:http';
2
- import type { REPLServer } from 'node:repl';
3
-
4
- import type { BaseLogger, Logger } from 'pino';
5
- import type { Request, Response } from 'express';
6
- import type { Application } from 'express-serve-static-core';
7
- import type { middleware } from 'express-openapi-validator';
8
- import type { Meter } from '@opentelemetry/api';
9
- import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
10
- import type { ShortstopHandler } from '@sesamecare-oss/confit';
11
-
12
- import type { ConfigurationSchema } from './config/schema.js';
13
-
14
- export interface InternalLocals<
15
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
16
- > extends Record<string, unknown> {
17
- server?: Server;
18
- mainApp: ServiceExpress<SLocals>;
19
- }
20
-
21
- export type ServiceLogger = BaseLogger & Pick<Logger, 'isLevelEnabled'>;
22
-
23
- // Vanilla express wants this to extend Record<string, any> but this is a mistake
24
- // because you lose type checking on it, even though I get that underneath it truly
25
- // is Record<string, any>
26
- export interface ServiceLocals<Config extends ConfigurationSchema = ConfigurationSchema> {
27
- service: Service;
28
- name: string;
29
- version: string;
30
- logger: ServiceLogger;
31
- config: Config;
32
- meter: Meter;
33
- internalApp: Application<InternalLocals<this>>;
34
- /**
35
- * This is the parsed OpenAPI spec we are hosting (if openapi is enabled)
36
- */
37
- openApiSpecification?: ReturnType<typeof JSON.parse>;
38
- }
39
-
40
- export interface RequestLocals {
41
- // Set this to true during the request "attachment" and if there is a body,
42
- // it will be set to the buffer before API and route handlers run.
43
- rawBody?: Buffer | true;
44
- logger: ServiceLogger;
45
- }
46
-
47
- export type ServiceExpress<Locals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>> =
48
- Application<Locals>;
49
-
50
- export type RequestWithApp<Locals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>> =
51
- Omit<Request, 'app'> & {
52
- app: Application<Locals>;
53
- };
54
-
55
- export type ResponseFromApp<
56
- ResBody = unknown,
57
- RLocals extends RequestLocals = RequestLocals,
58
- > = Response<ResBody, RLocals>;
59
-
60
- /**
61
- * This is the core type you need to implement to provide a service
62
- */
63
- export interface Service<
64
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
65
- RLocals extends RequestLocals = RequestLocals,
66
- > {
67
- name?: string;
68
-
69
- // Modify options used for application start
70
- configure?: (
71
- startOptions: ServiceStartOptions<SLocals, RLocals>,
72
- options: ServiceOptions,
73
- ) => ServiceOptions;
74
-
75
- // Run after configuration but before routes are loaded,
76
- // which is often a good place to add elements to the app locals
77
- // that are needed during route setup
78
- attach?: (app: ServiceExpress<SLocals>) => void | Promise<void>;
79
-
80
- // Called after a server is created but before the server starts listening
81
- attachServer?: (app: ServiceExpress<SLocals>, server: Server) => void | Promise<void>;
82
- // Called after the server is listening
83
- onListening?: (
84
- app: ServiceExpress<SLocals>,
85
- info: { port?: number; protocol: 'http' | 'https' },
86
- ) => void | Promise<void>;
87
-
88
- start(app: ServiceExpress<SLocals>): void | Promise<void>;
89
-
90
- stop?: (app: ServiceExpress<SLocals>) => void | Promise<void>;
91
-
92
- healthy?: (app: ServiceExpress<SLocals>) => boolean | Promise<boolean>;
93
-
94
- // This runs as middleware right BEFORE the body parsers.
95
- // If you want to run AFTER the body parsers, the current
96
- // way to do that would be via /routes/index.ts and router.use()
97
- // in that file.
98
- onRequest?(req: RequestWithApp<SLocals>, res: Response<unknown, RLocals>): void | Promise<void>;
99
-
100
- // This runs after body parsing but before routing
101
- authorize?(
102
- req: RequestWithApp<SLocals>,
103
- res: Response<unknown, RLocals>,
104
- ): boolean | Promise<boolean>;
105
-
106
- // Add or redact any fields for logging. Note this will be called twice per request,
107
- // once at the start and once at the end. Modify the values directly. Return a
108
- // string to control the "msg" field of the logs, or return undefined to leave it
109
- // as the default, which is the request URL. Return false to suppress the log entirely.
110
- getLogFields?(
111
- req: RequestWithApp<SLocals>,
112
- values: Record<string, string | string[] | number | undefined>,
113
- ): string | false | undefined;
114
-
115
- // The repl is a useful tool for diagnosing issues in non-dev environments.
116
- // The attachRepl method provides a way to add custom functionality
117
- // (typically with top level variables) to the repl.
118
- attachRepl?(app: ServiceExpress<SLocals>, repl: REPLServer): void;
119
- }
120
-
121
- export type ServiceFactory<
122
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
123
- RLocals extends RequestLocals = RequestLocals,
124
- > = () => Service<SLocals, RLocals>;
125
-
126
- export interface ServiceStartOptions<
127
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
128
- RLocals extends RequestLocals = RequestLocals,
129
- > {
130
- name: string;
131
- version: string;
132
- rootDirectory: string;
133
-
134
- // Defaults to "build", but can be set to "src" to run off non-built source
135
- codepath?: 'build' | 'src' | 'dist';
136
-
137
- // NOTE: if you use this, you need to cast it because of a Typescript error:
138
- // https://github.com/microsoft/TypeScript/issues/22229
139
- // locals: { stuff } as Partial<MyLocals>
140
- locals?: Partial<SLocals>;
141
-
142
- // And finally, the function that creates the service instance
143
- service: () => Service<SLocals, RLocals>;
144
- }
145
-
146
- export interface DelayLoadServiceStartOptions extends Omit<ServiceStartOptions, 'service'> {
147
- service: string;
148
- customizer?:
149
- | ((options: Partial<NodeSDKConfiguration>) => Partial<NodeSDKConfiguration>)
150
- | undefined;
151
- }
152
-
153
- // Handled by service.configure
154
- export interface ServiceOptions {
155
- // Used to resolve code paths flexibly during dev/prod
156
- codepath?: string;
157
- // Will be either .ts or .js depending on the runtime environment
158
- codeExtension?: string;
159
-
160
- // If you need multiple configuration directories, pass them here
161
- // in the desired order (later trumps earlier)
162
- configurationDirectories: string[];
163
-
164
- // Add or control OpenAPI options such as security handlers
165
- openApiOptions?: Partial<Parameters<typeof middleware>[0]>;
166
-
167
- shortstopHandlers: Record<string, ShortstopHandler<string, unknown>>;
168
- }
169
-
170
- export interface ServiceLike<
171
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
172
- > {
173
- locals: SLocals;
174
- }
175
-
176
- /**
177
- * This type should be used (or extended) to pass "context"
178
- * into functions not directly wired into the Express request
179
- * handling flow. It will allow "synthetic" requests to be
180
- * easily constructed without depending on things they should not,
181
- * like query strings or body or similar. Most often, you want the
182
- * logger.
183
- */
184
- export interface RequestLike<
185
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
186
- RLocals extends RequestLocals = RequestLocals,
187
- > {
188
- app: ServiceLike<SLocals>;
189
- res: {
190
- locals: RLocals;
191
- };
192
- }
193
-
194
- // Define some utility types to make it easier to put them all
195
- // in one export. This interface never actually is instantiated.
196
- // Typically you should export an interface that extends this one
197
- // and then access all your types through that.
198
- export interface ServiceTypes<
199
- SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
200
- RLocals extends RequestLocals = RequestLocals,
201
- ResBody = unknown,
202
- > {
203
- App: ServiceExpress<SLocals>;
204
- Handler: (
205
- req: RequestWithApp<SLocals>,
206
- res: ResponseFromApp<ResBody, RLocals>,
207
- ) => void | Promise<void>;
208
- Request: RequestWithApp<SLocals>;
209
- RequestLike: RequestLike<SLocals, RLocals>;
210
- RequestLocals: RLocals;
211
- Response: ResponseFromApp<ResBody, RLocals>;
212
- Service: Service<SLocals, RLocals>;
213
- ServiceFactory: ServiceFactory<SLocals, RLocals>;
214
- ServiceLocals: SLocals;
215
- }
216
-
217
- export type UnwrapServiceConfig<SLocals extends ServiceLocals> =
218
- SLocals extends ServiceLocals<infer C> ? C : never;
219
-
220
- // TODO this allows us to clean up the generics by having a loose parameter
221
- // but using the UnwrapServiceConfig to get the specific type back
222
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
223
- export type AnyServiceLocals = ServiceLocals<any>;