mirror of
https://github.com/diced/zipline.git
synced 2026-06-12 19:01:18 -07:00
fix: add errors to spec
This commit is contained in:
@@ -78,13 +78,12 @@ jobs:
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Run app
|
- name: Run generator
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: postgres://zipline:zipline@localhost:5432/zipline
|
DATABASE_URL: postgres://zipline:zipline@localhost:5432/zipline
|
||||||
CORE_SECRET: ${{ steps.secret.outputs.secret }}
|
CORE_SECRET: ${{ steps.secret.outputs.secret }}
|
||||||
ZIPLINE_OUTPUT_OPENAPI: true
|
NODE_ENV: production
|
||||||
|
run: pnpm openapi
|
||||||
run: pnpm start
|
|
||||||
|
|
||||||
- name: Verify openapi.json exists
|
- name: Verify openapi.json exists
|
||||||
run: |
|
run: |
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"start:inspector": "cross-env NODE_ENV=production node --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./build/server",
|
"start:inspector": "cross-env NODE_ENV=production node --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./build/server",
|
||||||
"ctl": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/ctl",
|
"ctl": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/ctl",
|
||||||
"validate": "tsx scripts/validate.ts",
|
"validate": "tsx scripts/validate.ts",
|
||||||
|
"openapi": "tsx scripts/openapi.ts",
|
||||||
"db:prototype": "prisma db push --skip-generate && prisma generate --no-hints",
|
"db:prototype": "prisma db push --skip-generate && prisma generate --no-hints",
|
||||||
"db:migrate": "prisma migrate dev --create-only",
|
"db:migrate": "prisma migrate dev --create-only",
|
||||||
"docker:engine": "colima start --mount $PWD/themes:w --mount $PWD/uploads:w --mount $PWD/public:w",
|
"docker:engine": "colima start --mount $PWD/themes:w --mount $PWD/uploads:w --mount $PWD/public:w",
|
||||||
|
|||||||
+8
-2
@@ -1,4 +1,6 @@
|
|||||||
export function step(name: string, command: string, condition: () => boolean = () => true) {
|
type StepCommand = string | (() => void | Promise<void>);
|
||||||
|
|
||||||
|
export function step(name: string, command: StepCommand, condition: () => boolean = () => true) {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
command,
|
command,
|
||||||
@@ -35,7 +37,11 @@ export async function run(name: string, ...steps: Step[]) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
log(`> Running step "${name}/${step.name}"...`);
|
log(`> Running step "${name}/${step.name}"...`);
|
||||||
execSync(step.command, { stdio: 'inherit' });
|
if (typeof step.command === 'string') {
|
||||||
|
execSync(step.command, { stdio: 'inherit' });
|
||||||
|
} else {
|
||||||
|
await step.command();
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
console.error(`x Step "${name}/${step.name}" failed.`);
|
console.error(`x Step "${name}/${step.name}" failed.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import { run, step } from '.';
|
||||||
|
import { API_ERRORS, ApiError, ApiErrorCode } from '../src/lib/api/errors';
|
||||||
|
|
||||||
|
const ALL_METHODS = ['delete', 'get', 'head', 'patch', 'post', 'put'];
|
||||||
|
const GEN_PATH = path.resolve(__dirname, '..', 'openapi.json');
|
||||||
|
|
||||||
|
const ALL_ERRORS = Object.keys(API_ERRORS)
|
||||||
|
.map((code) => new ApiError(Number(code) as ApiErrorCode).toJSON())
|
||||||
|
.sort((a, b) => a.code - b.code);
|
||||||
|
|
||||||
|
const ERROR_SCHEMA = {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Generic error for API endpoints.',
|
||||||
|
properties: {
|
||||||
|
error: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Message for the error. This may differ from the standard message for the error code, but the error code should be used to figure out the type of error.',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
type: 'integer',
|
||||||
|
format: 'int32',
|
||||||
|
description:
|
||||||
|
'Zipline API error code. Ranges: 1xxx validation, 2xxx session, 3xxx permission, 4xxx not-found, 5xxx constraint, 6xxx internal, 9xxx generic.',
|
||||||
|
enum: ALL_ERRORS.map((entry) => entry.code),
|
||||||
|
'x-enumDescriptions': ALL_ERRORS.map((entry) => entry.message),
|
||||||
|
},
|
||||||
|
statusCode: {
|
||||||
|
type: 'integer',
|
||||||
|
format: 'int32',
|
||||||
|
description: 'HTTP status code returned alongside this error payload.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['error', 'code', 'statusCode'],
|
||||||
|
additionalProperties: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ERROR_EXAMPLES = ALL_ERRORS.reduce<Record<string, unknown>>((examples, entry) => {
|
||||||
|
examples[`E${entry.code}`] = {
|
||||||
|
summary: `${entry.error}`,
|
||||||
|
value: entry,
|
||||||
|
};
|
||||||
|
|
||||||
|
return examples;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const generic4xxResponse = {
|
||||||
|
description: 'API error response (4xx)',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: ERROR_SCHEMA,
|
||||||
|
examples: ERROR_EXAMPLES,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function addErrorResponse(responses: Record<string, any>): void {
|
||||||
|
const response = (responses['4xx'] ??= structuredClone(generic4xxResponse));
|
||||||
|
|
||||||
|
response.description ??= generic4xxResponse.description;
|
||||||
|
response.content ??= {};
|
||||||
|
|
||||||
|
const jsonContent = (response.content['application/json'] ??= {});
|
||||||
|
jsonContent.schema ??= structuredClone(ERROR_SCHEMA);
|
||||||
|
jsonContent.examples ??= structuredClone(generic4xxResponse.content['application/json'].examples);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterRoutes(paths = {}): Record<string, any> {
|
||||||
|
return Object.fromEntries(Object.entries(paths).filter(([route]) => route.startsWith('/api')));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fixSpec() {
|
||||||
|
const spec = JSON.parse(await readFile(GEN_PATH, 'utf8'));
|
||||||
|
|
||||||
|
spec.paths = filterRoutes(spec.paths);
|
||||||
|
|
||||||
|
for (const [, pathItem] of Object.entries(spec.paths ?? {})) {
|
||||||
|
if (!pathItem) continue;
|
||||||
|
|
||||||
|
for (const method of ALL_METHODS) {
|
||||||
|
const operation = (<any>pathItem)[method];
|
||||||
|
if (!operation) continue;
|
||||||
|
|
||||||
|
operation.responses ??= {};
|
||||||
|
addErrorResponse(operation.responses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(GEN_PATH, JSON.stringify(spec));
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.ZIPLINE_OUTPUT_OPENAPI = 'true';
|
||||||
|
|
||||||
|
run(
|
||||||
|
'openapi',
|
||||||
|
step('run-prod', 'pnpm start', () => process.env.NODE_ENV === 'production'),
|
||||||
|
step('run-dev', 'pnpm dev', () => process.env.NODE_ENV !== 'production'),
|
||||||
|
step('check', async () => {
|
||||||
|
try {
|
||||||
|
await readFile(GEN_PATH);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('\nSomething went wrong...', e);
|
||||||
|
|
||||||
|
throw new Error('No OpenAPI spec found at ./openapi.json');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
step('fix', fixSpec),
|
||||||
|
);
|
||||||
@@ -148,7 +148,7 @@ export class ApiError extends Error {
|
|||||||
|
|
||||||
toJSON(): ApiErrorPayload {
|
toJSON(): ApiErrorPayload {
|
||||||
const formattedMessage = API_ERRORS[this.code]
|
const formattedMessage = API_ERRORS[this.code]
|
||||||
? `${this.code}${this.message ? `: ${this.message}` : ''}`
|
? `E${this.code}${this.message ? `: ${this.message}` : ''}`
|
||||||
: this.message;
|
: this.message;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -163,7 +163,7 @@ export class ApiError extends Error {
|
|||||||
return payload.code === code;
|
return payload.code === code;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static codeToHttpStatus(code: ApiErrorCode): number {
|
public static codeToHttpStatus(code: ApiErrorCode): number {
|
||||||
const override = {
|
const override = {
|
||||||
9000: 400,
|
9000: 400,
|
||||||
9001: 403,
|
9001: 403,
|
||||||
|
|||||||
Reference in New Issue
Block a user