diff --git a/src/prelude/schema.ts b/src/prelude/schema.ts new file mode 100644 index 0000000000..7c3d6aac8f --- /dev/null +++ b/src/prelude/schema.ts @@ -0,0 +1,38 @@ +export type Schema = { + type: 'number' | 'string' | 'array' | 'object' | any; + optional?: boolean; + items?: Schema; + properties?: Obj; + description?: string; +}; + +export type Obj = { [key: string]: Schema }; + +export type ObjType = { [P in keyof s]: SchemaType }; + +// https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2 +type MyType = { + 0: any; + 1: SchemaType; +}[T extends Schema ? 1 : 0]; + +export type SchemaType

= + p['type'] extends 'number' ? number : + p['type'] extends 'string' ? string : + p['type'] extends 'array' ? MyType[] : + p['type'] extends 'object' ? ObjType : + any; + +export function convertOpenApiSchema(schema: Schema) { + const x = JSON.parse(JSON.stringify(schema)); // copy + if (!['string', 'number', 'boolean', 'array', 'object'].includes(x.type)) { + x['$ref'] = `#/components/schemas/${x.type}`; + } + if (x.type === 'object' && x.properties) { + x.required = Object.entries(x.properties).filter(([k, v]: any) => !v.isOptional).map(([k, v]: any) => k); + for (const k of Object.keys(x.properties)) { + x.properties[k] = convertOpenApiSchema(x.properties[k]); + } + } + return x; +} diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index 2873dd3c1e..7abee95a51 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -1,6 +1,7 @@ import { Context } from 'cafy'; import * as path from 'path'; import * as glob from 'glob'; +import { Schema } from '../../prelude/schema'; export type Param = { validator: Context; @@ -29,7 +30,7 @@ export interface IEndpointMeta { }; }; - res?: any; + res?: Schema; /** * このエンドポイントにリクエストするのにユーザー情報が必須か否か diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts index d254bb854c..cc0ca8bef7 100644 --- a/src/server/api/endpoints/charts/notes.ts +++ b/src/server/api/endpoints/charts/notes.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import notesChart from '../../../../services/chart/notes'; +import notesChart, { notesLogSchema } from '../../../../services/chart/notes'; +import { convertLog } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -28,12 +29,7 @@ export const meta = { }, }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(notesLogSchema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index a4f262bdad..7df86625d6 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -175,12 +175,10 @@ export const meta = { res: { type: 'object', - props: { + properties: { createdNote: { type: 'Note', - desc: { - 'ja-JP': '作成した投稿' - } + description: '作成した投稿' } } }, diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts index ad46eb20a4..7487918231 100644 --- a/src/server/api/openapi/gen-spec.ts +++ b/src/server/api/openapi/gen-spec.ts @@ -4,6 +4,7 @@ import config from '../../../config'; import { errors as basicErrors } from './errors'; import { schemas } from './schemas'; import { description } from './description'; +import { convertOpenApiSchema } from '../../../prelude/schema'; export function genOpenapiSpec(lang = 'ja-JP') { const spec = { @@ -104,33 +105,7 @@ export function genOpenapiSpec(lang = 'ja-JP') { const required = endpoint.meta.params ? Object.entries(endpoint.meta.params).filter(([k, v]) => !v.validator.isOptional).map(([k, v]) => k) : []; - const resSchema = endpoint.meta.res ? renderType(endpoint.meta.res) : {}; - - function renderType(x: any) { - const res = {} as any; - - if (['User', 'Note', 'DriveFile'].includes(x.type)) { - res['$ref'] = `#/components/schemas/${x.type}`; - } else if (x.type === 'object') { - res['type'] = 'object'; - if (x.props) { - const props = {} as any; - for (const kv of Object.entries(x.props)) { - props[kv[0]] = renderType(kv[1]); - } - res['properties'] = props; - } - } else if (x.type === 'array') { - res['type'] = 'array'; - if (x.items) { - res['items'] = renderType(x.items); - } - } else { - res['type'] = x.type; - } - - return res; - } + const resSchema = endpoint.meta.res ? convertOpenApiSchema(endpoint.meta.res) : {}; const info = { operationId: endpoint.name, diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts index 30ef2847d6..1e6ff0ca97 100644 --- a/src/services/chart/index.ts +++ b/src/services/chart/index.ts @@ -9,6 +9,7 @@ import * as mongo from 'mongodb'; import db from '../../db/mongodb'; import { ICollection } from 'monk'; import Logger from '../../misc/logger'; +import { Schema } from '../../prelude/schema'; const logger = new Logger('chart'); @@ -346,3 +347,18 @@ export default abstract class Chart { return res; } } + +export function convertLog(logSchema: Schema): Schema { + const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy + if (v.type === 'number') { + v.type = 'array'; + v.items = { + type: 'number' + }; + } else if (v.type === 'object') { + for (const k of Object.keys(v.properties)) { + v.properties[k] = convertLog(v.properties[k]); + } + } + return v; +} diff --git a/src/services/chart/notes.ts b/src/services/chart/notes.ts index 8f95f63638..d3704fed9a 100644 --- a/src/services/chart/notes.ts +++ b/src/services/chart/notes.ts @@ -2,48 +2,61 @@ import autobind from 'autobind-decorator'; import Chart, { Obj } from '.'; import Note, { INote } from '../../models/note'; import { isLocalUser } from '../../models/user'; +import { SchemaType } from '../../prelude/schema'; -/** - * 投稿に関するチャート - */ -type NotesLog = { - local: { - /** - * 集計期間時点での、全投稿数 - */ - total: number; +const logSchema = { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全投稿数' + }, - /** - * 増加した投稿数 - */ - inc: number; + inc: { + type: 'number' as 'number', + description: '増加した投稿数' + }, - /** - * 減少した投稿数 - */ - dec: number; + dec: { + type: 'number' as 'number', + description: '減少した投稿数' + }, - diffs: { - /** - * 通常の投稿数の差分 - */ - normal: number; + diffs: { + type: 'object' as 'object', + properties: { + normal: { + type: 'number' as 'number', + description: '通常の投稿数の差分' + }, - /** - * リプライの投稿数の差分 - */ - reply: number; + reply: { + type: 'number' as 'number', + description: 'リプライの投稿数の差分' + }, - /** - * Renoteの投稿数の差分 - */ - renote: number; - }; - }; - - remote: NotesLog['local']; + renote: { + type: 'number' as 'number', + description: 'Renoteの投稿数の差分' + }, + } + }, }; +export const notesLogSchema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +type NotesLog = SchemaType; + class NotesChart extends Chart { constructor() { super('notes');