mirror of
https://github.com/zenstackhq/zenstack
synced 2026-05-24 10:08:55 +00:00
Co-authored-by: Augustin <43639468+Azzerty23@users.noreply.github.com> Co-authored-by: Jonathan Stevens <jonathan.stevens@resnovas.com> Co-authored-by: Jonathan S <jonathan.stevens@eventiva.co.uk> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ErikMCM <70036542+ErikMCM@users.noreply.github.com> Co-authored-by: Jason Kleinberg <ustice@gmail.com> Co-authored-by: Jonathan S <punk.gift9475@alias.org.uk> Co-authored-by: Jiasheng <jiashengguo@outlook.com>
156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
import { PluginError, getDataModels, hasAttribute, type PluginOptions, type PluginResult } from '@zenstackhq/sdk';
|
|
import { Model } from '@zenstackhq/sdk/ast';
|
|
import type { DMMF } from '@zenstackhq/sdk/prisma';
|
|
import type { OpenAPIV3_1 as OAPI } from 'openapi-types';
|
|
import semver from 'semver';
|
|
import { fromZodError } from 'zod-validation-error';
|
|
import { name } from '.';
|
|
import { SecuritySchemesSchema } from './schema';
|
|
|
|
export abstract class OpenAPIGeneratorBase {
|
|
protected readonly DEFAULT_SPEC_VERSION = '3.1.0';
|
|
|
|
constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) {}
|
|
|
|
abstract generate(): PluginResult;
|
|
|
|
protected get includedModels() {
|
|
return getDataModels(this.model).filter((d) => !hasAttribute(d, '@@openapi.ignore'));
|
|
}
|
|
|
|
protected wrapArray(
|
|
schema: OAPI.ReferenceObject | OAPI.SchemaObject,
|
|
isArray: boolean
|
|
): OAPI.ReferenceObject | OAPI.SchemaObject {
|
|
if (isArray) {
|
|
return { type: 'array', items: schema };
|
|
} else {
|
|
return schema;
|
|
}
|
|
}
|
|
|
|
protected wrapNullable(
|
|
schema: OAPI.ReferenceObject | OAPI.SchemaObject,
|
|
isNullable: boolean
|
|
): OAPI.ReferenceObject | OAPI.SchemaObject {
|
|
if (!isNullable) {
|
|
return schema;
|
|
}
|
|
|
|
const specVersion = this.getOption('specVersion', this.DEFAULT_SPEC_VERSION);
|
|
|
|
// https://stackoverflow.com/questions/48111459/how-to-define-a-property-that-can-be-string-or-null-in-openapi-swagger
|
|
// https://stackoverflow.com/questions/40920441/how-to-specify-a-property-can-be-null-or-a-reference-with-swagger
|
|
if (semver.gte(specVersion, '3.1.0')) {
|
|
// OAPI 3.1.0 and above has native 'null' type
|
|
if ((schema as OAPI.BaseSchemaObject).oneOf) {
|
|
// merge into existing 'oneOf'
|
|
return { oneOf: [...(schema as OAPI.BaseSchemaObject).oneOf!, { type: 'null' }] };
|
|
} else {
|
|
// wrap into a 'oneOf'
|
|
return { oneOf: [{ type: 'null' }, schema] };
|
|
}
|
|
} else {
|
|
if ((schema as OAPI.ReferenceObject).$ref) {
|
|
// nullable $ref needs to be represented as: { allOf: [{ $ref: ... }], nullable: true }
|
|
return {
|
|
allOf: [schema],
|
|
nullable: true,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any;
|
|
} else {
|
|
// nullable scalar: { type: ..., nullable: true }
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return { ...schema, nullable: true } as any;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected array(itemType: OAPI.SchemaObject | OAPI.ReferenceObject) {
|
|
return { type: 'array', items: itemType } as const;
|
|
}
|
|
|
|
protected oneOf(...schemas: (OAPI.SchemaObject | OAPI.ReferenceObject)[]) {
|
|
return { oneOf: schemas };
|
|
}
|
|
|
|
protected allOf(...schemas: (OAPI.SchemaObject | OAPI.ReferenceObject)[]) {
|
|
return { allOf: schemas };
|
|
}
|
|
|
|
protected getOption<T = string>(name: string): T | undefined;
|
|
protected getOption<T = string, D extends T = T>(name: string, defaultValue: D): T;
|
|
protected getOption<T = string>(name: string, defaultValue?: T): T | undefined {
|
|
const value = this.options[name];
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-expect-error
|
|
return value === undefined ? defaultValue : value;
|
|
}
|
|
|
|
protected generateSecuritySchemes() {
|
|
const securitySchemes = this.getOption<Record<string, string>[]>('securitySchemes');
|
|
if (securitySchemes) {
|
|
const parsed = SecuritySchemesSchema.safeParse(securitySchemes);
|
|
if (!parsed.success) {
|
|
throw new PluginError(name, `"securitySchemes" option is invalid: ${fromZodError(parsed.error)}`);
|
|
}
|
|
return parsed.data;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
protected pruneComponents(paths: OAPI.PathsObject, components: OAPI.ComponentsObject) {
|
|
const schemas = components.schemas;
|
|
if (schemas) {
|
|
const roots = new Set<string>();
|
|
for (const path of Object.values(paths)) {
|
|
this.collectUsedComponents(path, roots);
|
|
}
|
|
|
|
// build a transitive closure for all reachable schemas from roots
|
|
const allUsed = new Set<string>(roots);
|
|
|
|
let todo = [...allUsed];
|
|
while (todo.length > 0) {
|
|
const curr = new Set<string>(allUsed);
|
|
Object.entries(schemas)
|
|
.filter(([key]) => todo.includes(key))
|
|
.forEach(([, value]) => {
|
|
this.collectUsedComponents(value, allUsed);
|
|
});
|
|
todo = [...allUsed].filter((e) => !curr.has(e));
|
|
}
|
|
|
|
// prune unused schemas
|
|
Object.keys(schemas).forEach((key) => {
|
|
if (!allUsed.has(key)) {
|
|
delete schemas[key];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private collectUsedComponents(value: unknown, allUsed: Set<string>) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
value.forEach((item) => {
|
|
this.collectUsedComponents(item, allUsed);
|
|
});
|
|
} else if (typeof value === 'object') {
|
|
Object.entries(value).forEach(([subKey, subValue]) => {
|
|
if (subKey === '$ref') {
|
|
const ref = subValue as string;
|
|
const name = ref.split('/').pop();
|
|
if (name && !allUsed.has(name)) {
|
|
allUsed.add(name);
|
|
}
|
|
} else {
|
|
this.collectUsedComponents(subValue, allUsed);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|