diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index 6cd40ad56..936505164 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -3,7 +3,7 @@ import Ajv from 'ajv'; import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import { ApiError } from './error.js'; -import { endpoints } from 'misskey-js/built/endpoints.js'; +import { endpoints, getEndpointSchema } from 'misskey-js/built/endpoints.js'; import type { IEndpointMeta, ResponseOf, SchemaOrUndefined } from 'misskey-js/built/endpoints.types.js'; import type { Endpoints } from 'misskey-js'; import { WeakSerialized } from 'schema-type'; @@ -50,7 +50,8 @@ export abstract class Endpoint) { this.meta = endpoints[this.name]; - const validate = ajv.compile({ oneOf: this.meta.defines.map(d => d.req) }); + const req = getEndpointSchema('req', this.name); + const validate = req ? ajv.compile(req) : null; this.exec = (params, user, token, file, ip, headers) => { let cleanup: undefined | (() => void) = undefined; @@ -66,21 +67,27 @@ export abstract class Endpoint !ep.meta.secure)) { + for (const [name, endpoint] of Object.entries(endpoints).filter(([name, ep]) => !ep.secure)) { const errors = {} as any; - if (endpoint.meta.errors) { - for (const e of Object.values(endpoint.meta.errors)) { + if ('errors' in endpoint && endpoint.errors) { + for (const e of Object.values(endpoint.errors)) { errors[e.code] = { value: { error: e, @@ -50,42 +51,30 @@ export function genOpenapiSpec(config: Config) { } } - const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; + const resSchema = getEndpointSchema('res', name as keyof Endpoints); - let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; - desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; - if (endpoint.meta.kind) { - const kind = endpoint.meta.kind; + let desc = ('description' in endpoint ? endpoint.description : 'No description provided.') + '\n\n'; + desc += `**Credential required**: *${('requireCredential' in endpoint && endpoint.requireCredential) ? 'Yes' : 'No'}*`; + if ('kind' in endpoint && endpoint.kind) { + const kind = endpoint.kind; desc += ` / **Permission**: *${kind}*`; } - const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; - const schema = { ...endpoint.params }; - - if (endpoint.meta.requireFile) { - schema.properties = { - ...schema.properties, - file: { - type: 'string', - format: 'binary', - description: 'The file contents.', - }, - }; - schema.required = [...schema.required ?? [], 'file']; - } + const requestType = ('requireFile' in endpoint && endpoint.requireFile) ? 'multipart/form-data' : 'application/json'; + const schema = getEndpointSchema('req', name as keyof Endpoints) ?? {}; const info = { - operationId: endpoint.name, - summary: endpoint.name, + operationId: name, + summary: name, description: desc, externalDocs: { description: 'Source code', - url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`, + url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${name}.ts`, }, - ...(endpoint.meta.tags ? { - tags: [endpoint.meta.tags[0]], + ...(('tags' in endpoint && endpoint.tags) ? { + tags: [endpoint.tags[0]], } : {}), - ...(endpoint.meta.requireCredential ? { + ...('requireCredential' in endpoint && endpoint.requireCredential ? { security: [{ ApiKeyAuth: [], }], @@ -99,7 +88,7 @@ export function genOpenapiSpec(config: Config) { }, }, responses: { - ...(endpoint.meta.res ? { + ...(resSchema ? { '200': { description: 'OK (with results)', content: { @@ -157,7 +146,7 @@ export function genOpenapiSpec(config: Config) { }, }, }, - ...(endpoint.meta.limit ? { + ...(('limit' in endpoint && endpoint.limit) ? { '429': { description: 'To many requests', content: { @@ -184,7 +173,7 @@ export function genOpenapiSpec(config: Config) { }, }; - spec.paths['/' + endpoint.name] = { + spec.paths['/' + name] = { post: info, }; } diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index ecde38b1e..bfffddbf8 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -1,31 +1,4 @@ -import type { JSONSchema7 } from 'schema-type'; -import { refs } from 'misskey-js'; - -export function convertSchemaToOpenApiSchema(schema: JSONSchema7) { - const res: any = schema; - - if (schema.type === 'object' && schema.properties) { - res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); - - for (const k of Object.keys(schema.properties)) { - res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); - } - } - - if (schema.type === 'array' && schema.items) { - res.items = convertSchemaToOpenApiSchema(schema.items); - } - - if (schema.anyOf) res.anyOf = schema.anyOf.map(convertSchemaToOpenApiSchema); - if (schema.oneOf) res.oneOf = schema.oneOf.map(convertSchemaToOpenApiSchema); - if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema); - - if (schema.ref) { - res.$ref = `#/components/schemas/${schema.ref}`; - } - - return res; -} +import { refs } from 'misskey-js/built/schemas.js'; export const schemas = { Error: { @@ -55,7 +28,5 @@ export const schemas = { required: ['error'], }, - ...Object.fromEntries( - Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]), - ), + ...refs, }; diff --git a/packages/misskey-js/.eslintrc.cjs b/packages/misskey-js/.eslintrc.cjs index e2e31e9e3..9f35f95eb 100644 --- a/packages/misskey-js/.eslintrc.cjs +++ b/packages/misskey-js/.eslintrc.cjs @@ -1,7 +1,7 @@ module.exports = { parserOptions: { tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], + project: ['./tsconfig.json', './test/tsconfig.json'], }, extends: [ '../shared/.eslintrc.js', diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 026ce4a1c..871875b3a 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -26,6 +26,7 @@ "@types/node": "18.16.3", "@typescript-eslint/eslint-plugin": "5.59.5", "@typescript-eslint/parser": "5.59.5", + "ajv": "8.12.0", "eslint": "8.40.0", "jest": "29.5.0", "jest-fetch-mock": "3.0.3", diff --git a/packages/misskey-js/src/endpoints.ts b/packages/misskey-js/src/endpoints.ts index 991ba037f..355a2f2b7 100644 --- a/packages/misskey-js/src/endpoints.ts +++ b/packages/misskey-js/src/endpoints.ts @@ -446,3 +446,17 @@ export const endpoints = { }], }, } as const satisfies { [x: string]: IEndpointMeta; }; + +export function getEndpointSchema(reqres: 'req' | 'res', key: keyof typeof endpoints) { + const endpoint = endpoints[key]; + const schemas = endpoint.defines.map(d => d[reqres]).filter(d => d !== undefined); + if (schemas.length === 0) { + return null; + } + if (schemas.length === 1) { + return schemas[0]; + } + return { + oneOf: schemas, + }; +} diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts index 457a2742f..2e1703985 100644 --- a/packages/misskey-js/src/index.ts +++ b/packages/misskey-js/src/index.ts @@ -3,7 +3,6 @@ import Stream, { Connection } from './streaming.js'; import { Channels } from './streaming.types.js'; import { Acct } from './acct.js'; import type { Packed, Def } from './schemas.js'; -import { refs as _refs } from './schemas.js'; import * as consts from './consts.js'; export { @@ -15,8 +14,6 @@ export { Packed, Def, }; -export const refs = _refs; - export const permissions = consts.permissions; export const notificationTypes = consts.notificationTypes; export const obsoleteNotificationTypes = consts.obsoleteNotificationTypes; diff --git a/packages/misskey-js/test/api.ts b/packages/misskey-js/test/api.ts index 84b1fc093..c816608a2 100644 --- a/packages/misskey-js/test/api.ts +++ b/packages/misskey-js/test/api.ts @@ -1,5 +1,31 @@ import { enableFetchMocks } from 'jest-fetch-mock'; import { APIClient, isAPIError } from '../src/api'; +import Ajv from 'ajv'; +import { endpoints, getEndpointSchema } from '../src/endpoints'; +import { Endpoints } from '@/endpoints.types'; + +describe('schemas', () => { + describe.each(Object.keys(endpoints))('validate schema of %s', async (key) => { + const ajv = new Ajv({ + useDefaults: true, + }); + + ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); + + const endpoint = (endpoints as any)[key] as unknown as Endpoints[keyof Endpoints]; + test('each schemas', async () => { + for (const def of endpoint.defines) { + if (def.res === undefined) continue; + ajv.compile(def.req); + } + }); + + test('jointed schema (oneOf)', () => { + const req = getEndpointSchema('req', key as keyof Endpoints); + if (req) ajv.compile(req); + }); + }); +}); enableFetchMocks(); diff --git a/packages/misskey-js/test/tsconfig.json b/packages/misskey-js/test/tsconfig.json new file mode 100644 index 000000000..88262e0cf --- /dev/null +++ b/packages/misskey-js/test/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "allowJs": true, + "noEmitOnError": false, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedParameters": false, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "declaration": false, + "sourceMap": true, + "target": "es2021", + "module": "es2020", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "removeComments": false, + "noLib": false, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": "./", + "paths": { + "@/*": ["../src/*"] + }, + "typeRoots": [ + "../node_modules/@types", + "../src/@types" + ], + "lib": [ + "esnext" + ], + "types": ["jest", "node"] + }, + "compileOnSave": false, + "include": [ + "./**/*.ts", + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adebac5da..f7b1b8b05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1049,6 +1049,9 @@ importers: '@typescript-eslint/parser': specifier: 5.59.5 version: 5.59.5(eslint@8.40.0)(typescript@5.0.4) + ajv: + specifier: 8.12.0 + version: 8.12.0 eslint: specifier: 8.40.0 version: 8.40.0 @@ -7796,7 +7799,6 @@ packages: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 uri-js: 4.4.1 - dev: false /alphanum-sort@1.0.2: resolution: {integrity: sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==} @@ -13932,7 +13934,6 @@ packages: /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: false /json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -17386,7 +17387,6 @@ packages: /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - dev: false /require-main-filename@1.0.1: resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==}