tachybase_todo/packages/plugins/@nocobase/plugin-api-doc/src/server/swagger/index.ts
jack zhang 705b7449f0
feat: new plugin manager, supports adding plugins through UI (#2430)
* refactor: plugin manager page

* fix: bug

* feat: addByNpm api

* fix: improve the addByNpm

* feat: improve applicationPlugins:list api

* fix: re-download npm package when restart app

* fix: plugin delete api

* feat: plugin detail api

* feat: zipUrl add api

* fix: upload api bug

* fix: plugin detail info

* feat: upgrade api

* fix: upload api

* feat: handle plugin load error

* feat: support authToken

* feat: muti lang

* fix: build error

* fix: self review

* Update plugin-manager.ts

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bugs

* fix: detail click and remove isOfficial

* fix: upgrade no refresh

* fix: file size and type check

* fix: bug

* fix: upgrade error

* fix: bug

* fix: bug

* fix: plugin card layout

* fix: handling exceptional cases

* fix: tgz file support

* fix: macos compress file

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: add upgrade npm type

* fix: bugs

* fix: bug

* fix: change plugins static expose url

* fix: api prefix

* fix: bug

* fix: add nginx `/static/plugin/` path

* fix: bugs and pr docker build no dts

* fix: bug

* fix: build tools bug

* fix: improve code

* fix: build bug

* feat: improve plugin info

* fix: ui bug

* fix: plugin document bug

* feat: improve code

* feat: improve code

* feat: process dev deps check

* feat: improve code

* feat: process.env.IS_DEV_CMD

* fix: do not delete the plugin package

* feat: plugin symlink

* fix: tsx watch --ignore=./storage/plugins/**

* fix: test error

* fix: improve code

* fix: improve code

* fix: emitStartedEvent

* fix: improve code

* fix: type error

* fix: test error

* test: console.log

* fix: createStoragePluginSymLink

* fix: clientStaticMiddleware rename to clientStaticUtils

* feat: build tools support plugins folder

* fix: 350px

* fix: error

* feat: client dev support plugin folder

* fix: clear cli options

* fix: typeError: Converting circular structure to JSON

* fix: plugin name

* chore: restart application after command

* feat: upgrade error & docs

* Update v14-changelog.md

* Update v14-changelog.md

* Update v14-changelog.md

* fix: gateway test

* refactor(plugin-workflow): add ready state for gracefully tearing down

* Revert "chore: restart application after command"

This reverts commit 5015274f8e4e06e506e15754b672330330e8c7f8.

* chore: stop application whe restart

* T 1218 change plugin folder (#2629)

* feat: change folder name

* feat: change `pm create` command

* feat:  revert plugin name change

* fix: delete samples

* feat: change plugins folder

* fix: pm create

* feat: update docs

* fix: link package error

* fix: docs

* fix: create command

* fix: pm add error

* fix: create  add build

* fix: pm creatre + add

* feat: add tar command

* fix: docs

* fix: bug

* fix: docs

---------

Co-authored-by: chenos <chenlinxh@gmail.com>

* feat: docs

* Update your-fisrt-plugin.md

* Update your-fisrt-plugin.md

* chore: application reload

* chore: test

* fix: pm add error

* chore: preset install skip exists plugin

* fix: createIfNotExists

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
Co-authored-by: chareice <chareice@live.com>
Co-authored-by: Zhou <zhou.working@gmail.com>
Co-authored-by: mytharcher <mytharcher@gmail.com>
2023-09-12 22:39:23 +08:00

235 lines
6.6 KiB
TypeScript

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<string, any> = {};
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);
}
}