Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,309 changes: 5,309 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

55 changes: 13 additions & 42 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
{
"name": "@openapi-ts/backend",
"version": "2.0.5",
"description": "",
"bin": {
"openapi-ts-backend": "dist/tools/cli.js"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"name": "openapi-ts-backend",
"private": true,
"homepage": "https://github.com/henhal/openapi-ts-backend#readme",
"bugs": {
"url": "https://github.com/henhal/openapi-ts-backend/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/henhal/openapi-ts-backend"
"url": "git+https://github.com/henhal/openapi-ts-backend.git"
},
"scripts": {
"build": "tsc",
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
"lint": "eslint src --ext js,ts --max-warnings 0",
"postbuild": "chmod u+x dist/tools/cli.js",
"prepublish": "yarn clean && yarn build && yarn test",
"prebuild": "rm -rf src/test/gen",
"pretest": "MODULE_PATH=../.. ts-node src/tools/cli.ts generate-types src/test/api.yml src/test/gen",
"test": "NODE_PATH=src LOG_LEVEL=${LOG_LEVEL:=error} jest --config src/test/jest.config.js"
"lint": "eslint src --ext js,ts --max-warnings 0"
},
"author": "[email protected]",
"files": [
"dist/"
"dist/",
"README.md"
],
"keywords": [
"openapi",
Expand All @@ -46,31 +34,14 @@
"lambda"
],
"dependencies": {
"@openapi-ts/request-types": "^1.0.5",
"ajv": "^7.1.1",
"ajv-formats": "^2.0.1",
"js-yaml": "^4.0.0",
"loglevel": "^1.7.1",
"openapi-backend": "^3.9.0",
"openapi-types": "^7.2.3",
"openapi-typescript": "^3.0.1"
"typescript": "^5.9.3"
},
"workspaces": [
"packages/lib",
"packages/cli",
"packages/test"
],
"devDependencies": {
"@types/jest": "^26.0.20",
"@types/js-yaml": "^4.0.0",
"@types/node": "^14.14.31",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"eslint": "^7.21.0",
"jest": "^26.6.3",
"ts-jest": "^26.5.3",
"ts-node": "^9.1.1",
"typescript": ">=3"
},
"peerDependencies": {
"typescript": ">=3"
},
"publishConfig": {
"access": "public"
"@tsconfig/node22": "^22.0.2"
}
}
21 changes: 21 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@openapi-ts/cli",
"main": "src/cli.ts",
"version": "3.0.1",
"bin": {
"openapi-ts-backend": "dist/cli.js"
},
"scripts": {
"build": "tsc",
"postbuild": "chmod u+x dist/cli.js"
},
"dependencies": {
"openapi-typescript": "^7.9.1"
},
"peerDependencies": {
"typescript": "^5.9.3"
},
"files": [
"dist"
]
}
2 changes: 1 addition & 1 deletion src/tools/cli.ts → packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import typegen from './typegen';
import typegen from './typegen/typegen';

type Handler = (program: string, command: string, args: string[]) => Promise<void>;

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ export const utils = `
/**
* Get property K in T if it exists, otherwise D.
*/
export type Property<T, K extends keyof any, D = unknown> = (K extends keyof T ? T[K] : D);
export type Property<T, K extends keyof any, D = unknown> =
K extends keyof T
? ([T[K]] extends [never | undefined]
? D
: T[K])
: D;

/**
* Like keyof but values. Get all values of T if it is an object, otherwise D.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import * as yaml from 'js-yaml';
import generateOpenApiTypes from 'openapi-typescript';
import {readFileSync, writeFileSync, mkdirSync, existsSync} from 'fs';
import openapiTS, { astToString } from "openapi-typescript";
import {writeFileSync, mkdirSync, existsSync} from 'fs';
import path from 'path';

import {getApiOperationIds} from './parser';
import * as templates from './templates';

const YAML_EXTENSION = /\.ya?ml$/;
import {pathToFileURL} from "node:url";

function write(dirName: string, fileName: string, data: string) {
const outputPath = path.resolve(dirName, fileName);
Expand All @@ -17,10 +15,10 @@ function write(dirName: string, fileName: string, data: string) {
}

async function createSpecTypes(specPath: string, outputDir: string) {
const raw = readFileSync(specPath).toString();
const schema = (YAML_EXTENSION.test(specPath)) ? yaml.load(raw) : JSON.parse(raw);

const ts = generateOpenApiTypes(schema);
const absolutePath = path.resolve(specPath);
const url = pathToFileURL(absolutePath)
const ast = await openapiTS(url);
const ts = astToString(ast);

return write(outputDir, 'spec.ts', ts);
}
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "dist",
},
}
34 changes: 34 additions & 0 deletions packages/lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@openapi-ts/backend",
"version": "3.0.1",
"scripts": {
"build": "tsc"
},
"main": "dist/index.js",
"files": [
"dist"
],
"types": "dist/index.d.ts",
"dependencies": {
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"js-yaml": "^4.0.0",
"json-schema-to-ts": "^3.1.1",
"loglevel": "^1.7.1",
"openapi-backend": "^5.15.0",
"openapi-types": "^12.1.3",
"openapi-typescript": "^7.9.1"
},
"devDependencies": {
"@openapi-ts/request-types": "^1.0.5",
"@types/js-yaml": "^4.0.0",
"@types/node": "^24.7.0",
"@typescript-eslint/eslint-plugin": "^8.45.0",
"@typescript-eslint/parser": "^8.45.0",
"eslint": "^9.37.0",
"ts-node": "^10.9.2"
},
"peerDependencies": {
"typescript": "^5.9.3"
}
}
7 changes: 3 additions & 4 deletions src/errors.ts → packages/lib/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ function formatOperationName(request: RawRequest) {
export abstract class ApiError extends Error {
protected constructor(readonly request: RawRequest, message?: string) {
super(message);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this line removed? It's simply so that sub-classes of this error are named properly, causing logs to show e.g. BadRequestError: some message instead of Error: some message

Copy link
Contributor Author

@luqasn luqasn Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was my understanding that newer javascript targets (ES6 and up) no longer need this.
And we rely on a newer feature anyways here (the cause for wrapping exceptions), so I think there is no gain in having this backwards compatible.
But open to change any of this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked my expectations... and they were wrong. Logging the error name indeed spelled Error. So I put the line back in. I guess we can still only get rid of half of the hacks (the prototype chain) even in modern JavaScript, which baffles me.

this.name = this.constructor.name;
}
}

Expand Down Expand Up @@ -49,9 +48,9 @@ export class HttpError<Data extends Record<string, any> = any> extends Error {
function toHttpError(err: Error, {logger}: OpenApi<unknown>): HttpError {
if (err instanceof BadRequestError) {
return new HttpError(`Invalid request`, 400, {
errors: err.errors.map(({dataPath, message, keyword, params}) => ({
message: `${dataPath || 'Request'} ${message}`,
data: {keyword, dataPath, params}
errors: err.errors.map(({instancePath, message, keyword, params}) => ({
message: `${instancePath || 'Request'} ${message}`,
data: {keyword, instancePath, params}
}))
});
}
Expand Down
File renamed without changes.
File renamed without changes.
20 changes: 12 additions & 8 deletions src/openapi.ts → packages/lib/src/openapi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Ajv from 'ajv';
import * as OpenAPI from 'openapi-backend';
import {ValidationContext} from 'openapi-backend';
import {OpenAPIV3} from 'openapi-types';
import {OpenAPIV3_1} from 'openapi-types';

import * as Errors from './errors';
import {
Expand All @@ -21,6 +21,7 @@ import {
StringParams,
} from './types';
import {
customizeAjv,
formatArray,
formatValidationError,
getAjv,
Expand Down Expand Up @@ -206,12 +207,14 @@ export class OpenApi<T> {
try {
results[name] = await authorizers[name](req, res, operationParams, {
name,
scheme: definition.components?.securitySchemes?.[name] as OpenAPIV3.SecuritySchemeObject,
scheme: definition.components?.securitySchemes?.[name] as OpenAPIV3_1.SecuritySchemeObject,
parameters: {scopes}
});
} catch (error) {
authorized = false;
errors.push(error);

const theError = error instanceof Error ? error : new Error('Unknown error', {cause: error})
errors.push(theError);
}
}

Expand Down Expand Up @@ -330,10 +333,10 @@ export class OpenApi<T> {
customizeAjv: (ajv, ajvOpts, validationContext) => {
if (validationContext === ValidationContext.Response) {
// Remove additional properties on response body only
ajv._opts.removeAdditional = this.responseBodyTrimming === 'none' ? false : this.responseBodyTrimming;
ajv.opts.removeAdditional = this.responseBodyTrimming === 'none' ? false : this.responseBodyTrimming;
}
// Invoke custom function as well if applicable
return ajv;

return customizeAjv(ajv);
}
}, operations, authorizers));

Expand Down Expand Up @@ -411,9 +414,10 @@ export class OpenApi<T> {
try {
await this.routeRequest(req, res, params);
} catch (err) {
this.logger.warn(`Error: ${id}: "${err.name}: ${err.message}"`);
const theError = err instanceof Error ? err : new Error('Unknown error', {cause: err})
this.logger.warn(`Error: ${id}: "${theError.name}: ${theError.message}"`);

await this.errorHandler(req, res, params, err);
await this.errorHandler(req, res, params, theError);
}

res.statusCode = res.statusCode ?? 500;
Expand Down
11 changes: 7 additions & 4 deletions src/types.ts → packages/lib/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import * as OpenAPI from "openapi-backend";

import {Request, Response, StringParams} from '@openapi-ts/request-types';
import {OpenApi} from './openapi';
import {OpenAPIV3} from 'openapi-types';
import {Document, Operation} from "openapi-backend";
import {OpenAPIV3, OpenAPIV3_1} from "openapi-types";

export * from '@openapi-ts/request-types';

Expand Down Expand Up @@ -72,8 +73,8 @@ export type Interceptor<T> = (
* The params provided to request handlers
*/
export type OperationParams<T = unknown> = RequestParams<T> & {
operation: OpenAPIV3.OperationObject;
definition: OpenAPIV3.Document;
operation: Operation;
definition: Document;
security: {
results: Record<string, unknown>;
};
Expand Down Expand Up @@ -105,12 +106,14 @@ export type RequestHandler<P = unknown,
res: Res,
params: P) => Awaitable<Res['body'] | void>;


type SecuritySchemeObject = OpenAPIV3_1.SecuritySchemeObject | OpenAPIV3.SecuritySchemeObject;
/**
* A security requirement to be fulfilled by an authorizer
*/
export type SecurityRequirement = {
name: string;
scheme: OpenAPIV3.SecuritySchemeObject;
scheme: SecuritySchemeObject;
parameters: {
scopes?: string[];
};
Expand Down
39 changes: 18 additions & 21 deletions src/utils.ts → packages/lib/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import Ajv, {ErrorObject, Options as AjvOptions} from 'ajv';
import addFormats from 'ajv-formats'
import {OpenAPIV3} from 'openapi-types';
import Ajv, {ErrorObject, Options as AjvOptions, } from 'ajv';
import addFormats, {type FormatsPluginOptions} from 'ajv-formats'
import {OpenAPIV3_1} from 'openapi-types';
import {OneOrMany} from '@openapi-ts/request-types';

// The "not a function restriction" solves TS2349 and enables using typeof === 'function' to determine if T is callable.
// eslint-disable-next-line @typescript-eslint/ban-types
export type Resolvable<T> = T extends Function ? never : T | (() => T);

export function resolve<T>(resolvable: Resolvable<T>): T {
return typeof resolvable === 'function' ? resolvable() : resolvable;
}
import {Operation} from "openapi-backend";

export function formatValidationError(error: ErrorObject): string {
return `At '${error.dataPath}': ${Object.entries(error.params)
return `At '${error.instancePath}': ${Object.entries(error.params)
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
.join(', ')}`;
}
Expand All @@ -24,10 +17,10 @@ export function formatArray<T>(items: T[], formatter: (item: T) => string, prefi
export type ParameterType = 'header' | 'query' | 'path' | 'cookie';

export function getParameterMap(
{parameters = []}: OpenAPIV3.OperationObject,
{parameters = []}: Operation,
type: ParameterType
): Record<string, OpenAPIV3.ParameterBaseObject> {
const result: Record<string, OpenAPIV3.ParameterBaseObject> = {};
): Record<string, OpenAPIV3_1.ParameterBaseObject> {
const result: Record<string, OpenAPIV3_1.ParameterBaseObject> = {};

for (const parameter of parameters) {
if ('in' in parameter && parameter.in === type) {
Expand All @@ -39,8 +32,8 @@ export function getParameterMap(
}

export function getParametersSchema(
parameters: Record<string, OpenAPIV3.ParameterBaseObject>
): OpenAPIV3.SchemaObject {
parameters: Record<string, OpenAPIV3_1.ParameterBaseObject>
): OpenAPIV3_1.SchemaObject {
const result = {
type: 'object',
required: [] as string[],
Expand All @@ -58,18 +51,22 @@ export function getParametersSchema(
}
}

return result as OpenAPIV3.SchemaObject;
return result as OpenAPIV3_1.SchemaObject;
}

export function customizeAjv (ajv: Ajv, opts: FormatsPluginOptions = {}): Ajv {
return addFormats(ajv, opts);
}

export function getAjv(ajvOptions?: AjvOptions) {
return addFormats(new Ajv(ajvOptions));
export function getAjv(ajvOptions?: AjvOptions): Ajv {
return customizeAjv(new Ajv(ajvOptions));
}

// Note that errors is an out parameter
export function matchSchema<T, U>(
ajv: Ajv,
source: Readonly<T>,
schema: OpenAPIV3.SchemaObject,
schema: OpenAPIV3_1.SchemaObject,
errors: ErrorObject[]): U {
// Ajv mutates the passed object so we pass a copy
const result = cloneObject(source);
Expand Down
Loading