From 86f4e206f47f1b250346052a06ae7774fd6910c4 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 22 May 2023 04:50:18 +0000
Subject: [PATCH] wip

---
 .../backend/src/server/api/endpoint-base.ts   | 41 ++++++++------
 .../src/server/api/openapi/gen-spec.ts        | 55 ++++++++-----------
 .../backend/src/server/api/openapi/schemas.ts | 33 +----------
 packages/misskey-js/.eslintrc.cjs             |  2 +-
 packages/misskey-js/package.json              |  1 +
 packages/misskey-js/src/endpoints.ts          | 14 +++++
 packages/misskey-js/src/index.ts              |  3 -
 packages/misskey-js/test/api.ts               | 26 +++++++++
 packages/misskey-js/test/tsconfig.json        | 42 ++++++++++++++
 pnpm-lock.yaml                                |  6 +-
 10 files changed, 135 insertions(+), 88 deletions(-)
 create mode 100644 packages/misskey-js/test/tsconfig.json

diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts
index 6cd40ad564..9365051647 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<E extends keyof Endpoints, T extends IEndpointMet
 
 	constructor(cb: Executor<T>) {
 		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<E extends keyof Endpoints, T extends IEndpointMet
 					id: '4267801e-70d1-416a-b011-4ee502885d8b',
 				}));
 			}
-	
-			const valid = validate(params);
-			if (!valid) {
-				if (file) cleanup!();
-	
-				const errors = validate.errors!;
-				const err = new ApiError({
-					message: 'Invalid param.',
-					code: 'INVALID_PARAM',
-					id: '3d81ceae-475f-4600-b2a8-2bc116157532',
-				}, {
-					param: errors[0].schemaPath,
-					reason: errors[0].message,
-				});
-				return Promise.reject(err);
+
+			if (validate) {
+				const valid = validate(params);
+
+				if (!valid) {
+					if (file) cleanup!();
+		
+					const errors = validate.errors!;
+					const err = new ApiError({
+						message: 'Invalid param.',
+						code: 'INVALID_PARAM',
+						id: '3d81ceae-475f-4600-b2a8-2bc116157532',
+					}, {
+						param: errors[0].schemaPath,
+						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);
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
index fa62480c02..fc6dadc23a 100644
--- a/packages/backend/src/server/api/openapi/gen-spec.ts
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -1,7 +1,8 @@
 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 { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
+import { schemas } from './schemas.js';
+import { Endpoints } from 'misskey-js';
 
 export function genOpenapiSpec(config: Config) {
 	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;
 
-		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 ecde38b1ea..bfffddbf88 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 e2e31e9e33..9f35f95ebc 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 026ce4a1c7..871875b3a6 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 991ba037fd..355a2f2b70 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 457a2742f5..2e1703985a 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 84b1fc0933..c816608a22 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 0000000000..88262e0cf1
--- /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 adebac5daa..f7b1b8b052 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==}