import APIDocPlugin from '../server'; import baseSwagger from './base-swagger'; import { SchemaTypeMapping } from './constants'; import { createDefaultActionSwagger, getInterfaceCollection } from './helpers'; import { getPluginsSwagger, getSwaggerDocument, loadSwagger } from './loader'; import { merge } from './merge'; import collectionToSwaggerObject from './collections'; export class SwaggerManager { private plugin: APIDocPlugin; constructor(plugin: APIDocPlugin) { this.plugin = plugin; } private get app() { return this.plugin.app; } private get db() { return this.plugin.db; } async generateSwagger(options: { plugins?: string[] } = {}) { const base = await this.getBaseSwagger(); const core = options.plugins ? {} : await loadSwagger('@nocobase/server'); const plugins = await this.loadSwaggers(options.plugins); return merge(merge(core, plugins), base); } async getSwagger() { return this.generateSwagger(); } async collection2Swagger(collectionName: string, withAssociation = true) { const collection = this.db.getCollection(collectionName); return await (async () => { await new Promise((resolve) => setTimeout(resolve, 0)); return collectionToSwaggerObject(collection, { withAssociation, }); })(); } async getCollectionsSwagger(name?: string) { const base = await this.getBaseSwagger(); let others = {}; if (name) { const collectionSwagger = await this.collection2Swagger(name); others = merge(others, collectionSwagger); } else { const collections = await this.db.getRepository('collections').find({ filter: { 'name.$ne': ['roles', 'users'], 'hidden.$isFalsy': true, }, }); for (const collection of collections) { if (collection.name === 'roles') { continue; } try { others = merge(others, await this.collection2Swagger(collection.name, false)); } catch (e) { this.app.log.error(e); } } } return merge(base, others); } async getPluginsSwagger(pluginName?: string) { return this.generateSwagger({ plugins: pluginName ? [pluginName] : [], }); } async getCoreSwagger() { return merge(await this.getBaseSwagger(), await loadSwagger('@nocobase/server')); } async getUrls() { const plugins = await getPluginsSwagger(this.db) .then((res) => { return Object.keys(res).map((name) => { const schema = res[name]; return { name: schema.info?.title || name, url: `/api/swagger:get?ns=${encodeURIComponent(`plugins/${name}`)}`, }; }); }) .catch(() => []); const collections = await this.db.getRepository('collections').find({ filter: { 'name.$ne': ['roles', 'users'], 'hidden.$isFalsy': true, }, }); return [ { name: 'NocoBase API', url: '/api/swagger:get', }, { name: 'NocoBase API - Core', url: '/api/swagger:get?ns=core', }, { name: 'NocoBase API - All plugins', url: '/api/swagger:get?ns=plugins', }, { name: 'NocoBase API - Custom collections', url: '/api/swagger:get?ns=collections', }, ...plugins, ...collections.map((collection) => { return { name: `Collection API - ${collection.title}`, url: `/api/swagger:get?ns=${encodeURIComponent('collections/' + collection.name)}`, }; }), ]; } private async getBaseSwagger() { return merge(baseSwagger, { info: { version: await this.app.version.get(), }, servers: [ { url: (this.app.resourcer.options.prefix || '/').replace(/^[^/]/, '/$1'), }, ], }); } private generateSchemas() { const schemas = {}; this.app.i18n; this.db.collections.forEach((collection) => { const properties = {}; const model = { type: 'object', properties, }; collection.forEachField((field) => { const { type, target } = field; const property: Record = {}; if (type === 'hasMany' || type === 'belongsToMany') { property['type'] = 'array'; property['items'] = { $ref: `#/components/schemas/${target}`, }; } else if (type === 'belongsTo' || type === 'hasOne') { property['type'] = 'object'; property['$ref'] = `#/components/schemas/${target}`; } else { property.type = SchemaTypeMapping[type] || type; if (property.type === 'array' && !property.items) { property.items = { type: 'object', }; } property.example = field.get('defaultValue'); } properties[field.name] = property; }); schemas[collection.name] = model; }); return schemas; } private generateCollectionBuiltInInterface() { const paths = {}; const IC = getInterfaceCollection(this.app.resourcer.options); this.db.collections.forEach((collection) => { const { name } = collection; let actions; if (this.app.resourcer.isDefined(name)) { actions = this.app.resourcer.getResource(name).actions; } else { actions = { has: () => true, }; } const collectionMethods = {}; Object.entries(IC).forEach(([path, actionNames]) => { actionNames.forEach((actionName) => { const actionMethods = createDefaultActionSwagger({ collection, }); const { method = 'post', ...swaggerArgs } = actionMethods[actionName]; const parameters = []; ['associatedName', 'associatedIndex', 'resourceIndex'].forEach((name) => { if (path.includes(`{${name}}`)) { parameters.push({ name, required: true, schema: { type: 'string', }, }); } }); if (actions.has(actionName)) { collectionMethods[path.replace('{resourceName}', `${name}:${actionName}`)] = { [method]: { tags: [name], description: `${name}:${actionName}`, parameters, ...swaggerArgs, }, }; } }); }); Object.assign(paths, collectionMethods); }); return paths; } private loadSwaggers(plugins: string[]) { return getSwaggerDocument(this.db, plugins); } }