wip
This commit is contained in:
parent
e689dcdd73
commit
86f4e206f4
10 changed files with 135 additions and 88 deletions
|
@ -3,7 +3,7 @@ import Ajv from 'ajv';
|
||||||
import type { LocalUser } from '@/models/entities/User.js';
|
import type { LocalUser } from '@/models/entities/User.js';
|
||||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||||
import { ApiError } from './error.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 { IEndpointMeta, ResponseOf, SchemaOrUndefined } from 'misskey-js/built/endpoints.types.js';
|
||||||
import type { Endpoints } from 'misskey-js';
|
import type { Endpoints } from 'misskey-js';
|
||||||
import { WeakSerialized } from 'schema-type';
|
import { WeakSerialized } from 'schema-type';
|
||||||
|
@ -50,7 +50,8 @@ export abstract class Endpoint<E extends keyof Endpoints, T extends IEndpointMet
|
||||||
|
|
||||||
constructor(cb: Executor<T>) {
|
constructor(cb: Executor<T>) {
|
||||||
this.meta = endpoints[this.name];
|
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) => {
|
this.exec = (params, user, token, file, ip, headers) => {
|
||||||
let cleanup: undefined | (() => void) = undefined;
|
let cleanup: undefined | (() => void) = undefined;
|
||||||
|
@ -66,21 +67,27 @@ export abstract class Endpoint<E extends keyof Endpoints, T extends IEndpointMet
|
||||||
id: '4267801e-70d1-416a-b011-4ee502885d8b',
|
id: '4267801e-70d1-416a-b011-4ee502885d8b',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = validate(params);
|
if (validate) {
|
||||||
if (!valid) {
|
const valid = validate(params);
|
||||||
if (file) cleanup!();
|
|
||||||
|
if (!valid) {
|
||||||
const errors = validate.errors!;
|
if (file) cleanup!();
|
||||||
const err = new ApiError({
|
|
||||||
message: 'Invalid param.',
|
const errors = validate.errors!;
|
||||||
code: 'INVALID_PARAM',
|
const err = new ApiError({
|
||||||
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
message: 'Invalid param.',
|
||||||
}, {
|
code: 'INVALID_PARAM',
|
||||||
param: errors[0].schemaPath,
|
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||||
reason: errors[0].message,
|
}, {
|
||||||
});
|
param: errors[0].schemaPath,
|
||||||
return Promise.reject(err);
|
reason: errors[0].message,
|
||||||
|
});
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// validateがnullである場合、paramsがnullや空オブジェクトであるべきではあるが、
|
||||||
|
// 特にチェックはしない
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(params as any, user as any, token, file, cleanup, ip, headers);
|
return cb(params as any, user as any, token, file, cleanup, ip, headers);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import endpoints from '../endpoints.js';
|
import { endpoints, getEndpointSchema } from 'misskey-js/built/endpoints.js';
|
||||||
import { errors as basicErrors } from './errors.js';
|
import { errors as basicErrors } from './errors.js';
|
||||||
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
import { schemas } from './schemas.js';
|
||||||
|
import { Endpoints } from 'misskey-js';
|
||||||
|
|
||||||
export function genOpenapiSpec(config: Config) {
|
export function genOpenapiSpec(config: Config) {
|
||||||
const spec = {
|
const spec = {
|
||||||
|
@ -37,11 +38,11 @@ export function genOpenapiSpec(config: Config) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
|
for (const [name, endpoint] of Object.entries(endpoints).filter(([name, ep]) => !ep.secure)) {
|
||||||
const errors = {} as any;
|
const errors = {} as any;
|
||||||
|
|
||||||
if (endpoint.meta.errors) {
|
if ('errors' in endpoint && endpoint.errors) {
|
||||||
for (const e of Object.values(endpoint.meta.errors)) {
|
for (const e of Object.values(endpoint.errors)) {
|
||||||
errors[e.code] = {
|
errors[e.code] = {
|
||||||
value: {
|
value: {
|
||||||
error: e,
|
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';
|
let desc = ('description' in endpoint ? endpoint.description : 'No description provided.') + '\n\n';
|
||||||
desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`;
|
desc += `**Credential required**: *${('requireCredential' in endpoint && endpoint.requireCredential) ? 'Yes' : 'No'}*`;
|
||||||
if (endpoint.meta.kind) {
|
if ('kind' in endpoint && endpoint.kind) {
|
||||||
const kind = endpoint.meta.kind;
|
const kind = endpoint.kind;
|
||||||
desc += ` / **Permission**: *${kind}*`;
|
desc += ` / **Permission**: *${kind}*`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
|
const requestType = ('requireFile' in endpoint && endpoint.requireFile) ? 'multipart/form-data' : 'application/json';
|
||||||
const schema = { ...endpoint.params };
|
const schema = getEndpointSchema('req', name as keyof Endpoints) ?? {};
|
||||||
|
|
||||||
if (endpoint.meta.requireFile) {
|
|
||||||
schema.properties = {
|
|
||||||
...schema.properties,
|
|
||||||
file: {
|
|
||||||
type: 'string',
|
|
||||||
format: 'binary',
|
|
||||||
description: 'The file contents.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
schema.required = [...schema.required ?? [], 'file'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
operationId: endpoint.name,
|
operationId: name,
|
||||||
summary: endpoint.name,
|
summary: name,
|
||||||
description: desc,
|
description: desc,
|
||||||
externalDocs: {
|
externalDocs: {
|
||||||
description: 'Source code',
|
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' in endpoint && endpoint.tags) ? {
|
||||||
tags: [endpoint.meta.tags[0]],
|
tags: [endpoint.tags[0]],
|
||||||
} : {}),
|
} : {}),
|
||||||
...(endpoint.meta.requireCredential ? {
|
...('requireCredential' in endpoint && endpoint.requireCredential ? {
|
||||||
security: [{
|
security: [{
|
||||||
ApiKeyAuth: [],
|
ApiKeyAuth: [],
|
||||||
}],
|
}],
|
||||||
|
@ -99,7 +88,7 @@ export function genOpenapiSpec(config: Config) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
...(endpoint.meta.res ? {
|
...(resSchema ? {
|
||||||
'200': {
|
'200': {
|
||||||
description: 'OK (with results)',
|
description: 'OK (with results)',
|
||||||
content: {
|
content: {
|
||||||
|
@ -157,7 +146,7 @@ export function genOpenapiSpec(config: Config) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...(endpoint.meta.limit ? {
|
...(('limit' in endpoint && endpoint.limit) ? {
|
||||||
'429': {
|
'429': {
|
||||||
description: 'To many requests',
|
description: 'To many requests',
|
||||||
content: {
|
content: {
|
||||||
|
@ -184,7 +173,7 @@ export function genOpenapiSpec(config: Config) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
spec.paths['/' + endpoint.name] = {
|
spec.paths['/' + name] = {
|
||||||
post: info,
|
post: info,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,4 @@
|
||||||
import type { JSONSchema7 } from 'schema-type';
|
import { refs } from 'misskey-js/built/schemas.js';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const schemas = {
|
export const schemas = {
|
||||||
Error: {
|
Error: {
|
||||||
|
@ -55,7 +28,5 @@ export const schemas = {
|
||||||
required: ['error'],
|
required: ['error'],
|
||||||
},
|
},
|
||||||
|
|
||||||
...Object.fromEntries(
|
...refs,
|
||||||
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: ['./tsconfig.json'],
|
project: ['./tsconfig.json', './test/tsconfig.json'],
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'../shared/.eslintrc.js',
|
'../shared/.eslintrc.js',
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"@types/node": "18.16.3",
|
"@types/node": "18.16.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||||
"@typescript-eslint/parser": "5.59.5",
|
"@typescript-eslint/parser": "5.59.5",
|
||||||
|
"ajv": "8.12.0",
|
||||||
"eslint": "8.40.0",
|
"eslint": "8.40.0",
|
||||||
"jest": "29.5.0",
|
"jest": "29.5.0",
|
||||||
"jest-fetch-mock": "3.0.3",
|
"jest-fetch-mock": "3.0.3",
|
||||||
|
|
|
@ -446,3 +446,17 @@ export const endpoints = {
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
} as const satisfies { [x: string]: IEndpointMeta; };
|
} 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import Stream, { Connection } from './streaming.js';
|
||||||
import { Channels } from './streaming.types.js';
|
import { Channels } from './streaming.types.js';
|
||||||
import { Acct } from './acct.js';
|
import { Acct } from './acct.js';
|
||||||
import type { Packed, Def } from './schemas.js';
|
import type { Packed, Def } from './schemas.js';
|
||||||
import { refs as _refs } from './schemas.js';
|
|
||||||
import * as consts from './consts.js';
|
import * as consts from './consts.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -15,8 +14,6 @@ export {
|
||||||
Packed, Def,
|
Packed, Def,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const refs = _refs;
|
|
||||||
|
|
||||||
export const permissions = consts.permissions;
|
export const permissions = consts.permissions;
|
||||||
export const notificationTypes = consts.notificationTypes;
|
export const notificationTypes = consts.notificationTypes;
|
||||||
export const obsoleteNotificationTypes = consts.obsoleteNotificationTypes;
|
export const obsoleteNotificationTypes = consts.obsoleteNotificationTypes;
|
||||||
|
|
|
@ -1,5 +1,31 @@
|
||||||
import { enableFetchMocks } from 'jest-fetch-mock';
|
import { enableFetchMocks } from 'jest-fetch-mock';
|
||||||
import { APIClient, isAPIError } from '../src/api';
|
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();
|
enableFetchMocks();
|
||||||
|
|
||||||
|
|
42
packages/misskey-js/test/tsconfig.json
Normal file
42
packages/misskey-js/test/tsconfig.json
Normal file
|
@ -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",
|
||||||
|
]
|
||||||
|
}
|
|
@ -1049,6 +1049,9 @@ importers:
|
||||||
'@typescript-eslint/parser':
|
'@typescript-eslint/parser':
|
||||||
specifier: 5.59.5
|
specifier: 5.59.5
|
||||||
version: 5.59.5(eslint@8.40.0)(typescript@5.0.4)
|
version: 5.59.5(eslint@8.40.0)(typescript@5.0.4)
|
||||||
|
ajv:
|
||||||
|
specifier: 8.12.0
|
||||||
|
version: 8.12.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 8.40.0
|
specifier: 8.40.0
|
||||||
version: 8.40.0
|
version: 8.40.0
|
||||||
|
@ -7796,7 +7799,6 @@ packages:
|
||||||
json-schema-traverse: 1.0.0
|
json-schema-traverse: 1.0.0
|
||||||
require-from-string: 2.0.2
|
require-from-string: 2.0.2
|
||||||
uri-js: 4.4.1
|
uri-js: 4.4.1
|
||||||
dev: false
|
|
||||||
|
|
||||||
/alphanum-sort@1.0.2:
|
/alphanum-sort@1.0.2:
|
||||||
resolution: {integrity: sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==}
|
resolution: {integrity: sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==}
|
||||||
|
@ -13932,7 +13934,6 @@ packages:
|
||||||
|
|
||||||
/json-schema-traverse@1.0.0:
|
/json-schema-traverse@1.0.0:
|
||||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/json-schema@0.4.0:
|
/json-schema@0.4.0:
|
||||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||||
|
@ -17386,7 +17387,6 @@ packages:
|
||||||
/require-from-string@2.0.2:
|
/require-from-string@2.0.2:
|
||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/require-main-filename@1.0.1:
|
/require-main-filename@1.0.1:
|
||||||
resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==}
|
resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==}
|
||||||
|
|
Loading…
Reference in a new issue