Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(backend): 虚無ノートを投稿できる問題の修正と api.json の OpenAPI Specification 3.1.0 への対応 #12969

Merged
merged 22 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e537f91
fix(backend): `text: null`だけのノートは投稿できないように
zyoshoka Jan 11, 2024
e896481
add test
zyoshoka Jan 11, 2024
0222508
Update CHANGELOG.md
zyoshoka Jan 11, 2024
3d58ca9
chore: bump OpenAPI Specification from 3.0.0 to 3.1.0
zyoshoka Jan 11, 2024
a4f47e4
chore: テストがすでにコメントで記述されていたのでそっちを使うことにする
zyoshoka Jan 11, 2024
b92c972
fix test
zyoshoka Jan 11, 2024
d83c01a
fix(backend): prohibit posting whitespace-only notes
zyoshoka Jan 11, 2024
dc38118
Update CHANGELOG.md
zyoshoka Jan 11, 2024
9930881
fix(backend): `renoteId`または`fileIds`(`mediaIds`)または`poll`が`null`でない場合…
zyoshoka Jan 12, 2024
5dd8c68
test(backend): 引用renoteで空白文字のみで構成されたtextにするとレスポンスが`text: null`になることをチ…
zyoshoka Jan 12, 2024
ab15af3
Merge branch 'develop' into prohibit-posting-kyomu-note
zyoshoka Jan 12, 2024
a9ca553
fix(frontend): `text`が`null`であって`renoteId`と`replyId`が`null`でないようなノートは…
zyoshoka Jan 12, 2024
7288851
fix(misskey-js): OpenAPI 3.1に対応
zyoshoka Jan 13, 2024
788072c
fix(misskey-js): 型生成をOpenAPI Specification 3.1.0に対応
zyoshoka Jan 13, 2024
a28531b
fix(ci): `validate-api.json`をOpenAPI Specification 3.1.0に対応
zyoshoka Jan 13, 2024
4499190
Merge branch 'develop' into prohibit-posting-kyomu-note
zyoshoka Jan 13, 2024
b61d7c8
fix(ci): スキーマ書き換えの際のミスを修正
zyoshoka Jan 13, 2024
c6c668d
Revert "fix(frontend): `text`が`null`であって`renoteId`と`replyId`が`null`でな…
zyoshoka Jan 13, 2024
8654589
fix(misskey-js): `build-misskey-js-with-types`時は`api.json`のGETをスキップするように
zyoshoka Jan 13, 2024
7c14bbb
Revert "fix(misskey-js): `build-misskey-js-with-types`時は`api.json`のGE…
zyoshoka Jan 13, 2024
0492b12
fix(misskey-js): `openapi-parser`で`validate`のかわりに`parse`を用いるように
zyoshoka Jan 13, 2024
cb224e6
Update CHANGELOG.md
zyoshoka Jan 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/validate-api-json.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install swagger-cli
run: npm i -g swagger-cli
- name: Install Redocly CLI
run: npm i -g @redocly/cli
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
Expand All @@ -44,4 +44,4 @@ jobs:
- name: Build and generate
run: pnpm build && pnpm --filter backend generate-api-json
- name: Validation
run: swagger-cli validate ./packages/backend/built/api.json
run: npx @redocly/cli lint --extends=minimal ./packages/backend/built/api.json
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swagger-cli は OpenAPI Specification 3.1.0 に対応せず deprecated となり、Redocly CLI への移行が公式に推奨されていた1ので、そちらへの移行作業を行いました。

このコマンドは公式ドキュメントに載っていたマイグレーション・ガイド2に基づいています。

Footnotes

  1. https://github.com/APIDevTools/swagger-cli#swaggeropenapi-cli

  2. https://redocly.com/docs/cli/guides/migrate-from-swagger-cli/#migrate-to-redocly-cli-from-swagger-cli

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zyoshoka#4 にて validate-api-json が通っているのを確認済みです。

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
- Enhance: クリップをエクスポートできるように
- Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
- Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更
- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更

## 2023.12.2

Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ export class NoteCreateService implements OnApplicationShutdown {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
}
data.text = data.text.trim();
if (data.text === '') {
data.text = null;
}
} else {
data.text = null;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/server/api/endpoints/drive/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openapi-typescripttype: ['string', 'null'] と記述しても Union 型に null がつかないようになっている(仕様?)なので enum かつ nullable: true となっていた数箇所について enum への null 追記を行いました。

},
required: [],
} as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const paramDef = {
'-firstRetrievedAt',
'+latestRequestReceivedAt',
'-latestRequestReceivedAt',
null,
],
},
},
Expand Down
33 changes: 17 additions & 16 deletions packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,13 @@ export const meta = {
items: {
type: 'string',
enum: [
"ble",
"cable",
"hybrid",
"internal",
"nfc",
"smart-card",
"usb",
'ble',
'cable',
'hybrid',
'internal',
'nfc',
'smart-card',
'usb',
],
},
},
Expand All @@ -123,8 +123,8 @@ export const meta = {
authenticatorAttachment: {
type: 'string',
enum: [
"cross-platform",
"platform",
'cross-platform',
'platform',
],
},
requireResidentKey: {
Expand All @@ -133,9 +133,9 @@ export const meta = {
userVerification: {
type: 'string',
enum: [
"discouraged",
"preferred",
"required",
'discouraged',
'preferred',
'required',
],
},
},
Expand All @@ -144,10 +144,11 @@ export const meta = {
type: 'string',
nullable: true,
enum: [
"direct",
"enterprise",
"indirect",
"none",
'direct',
'enterprise',
'indirect',
'none',
null,
],
},
extensions: {
Expand Down
14 changes: 9 additions & 5 deletions packages/backend/src/server/api/endpoints/notes/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ describe('api:notes/create', () => {
.toBe(VALID);
});

// TODO
//test('null post', () => {
// expect(v({ text: null }))
// .toBe(INVALID);
//});
test('null post', () => {
expect(v({ text: null }))
.toBe(INVALID);
});

test('0 characters post', () => {
expect(v({ text: '' }))
Expand All @@ -49,6 +48,11 @@ describe('api:notes/create', () => {
expect(v({ text: await tooLong }))
.toBe(INVALID);
});

test('whitespace-only post', () => {
expect(v({ text: ' ' }))
.toBe(INVALID);
});
});

describe('cw', () => {
Expand Down
34 changes: 27 additions & 7 deletions packages/backend/src/server/api/endpoints/notes/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,33 @@ export const paramDef = {
},
},
// (re)note with text, files and poll are optional
anyOf: [
{ required: ['text'] },
{ required: ['renoteId'] },
{ required: ['fileIds'] },
{ required: ['mediaIds'] },
{ required: ['poll'] },
],
if: {
properties: {
renoteId: {
type: 'null',
},
fileIds: {
type: 'null',
},
mediaIds: {
type: 'null',
},
poll: {
type: 'null',
},
},
},
then: {
properties: {
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
pattern: '[^\\s]+',
},
},
required: ['text'],
},
} as const;

@Injectable()
Expand Down
10 changes: 6 additions & 4 deletions packages/backend/src/server/api/openapi/gen-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';

export function genOpenapiSpec(config: Config) {
const spec = {
openapi: '3.0.0',
openapi: '3.1.0',

info: {
version: config.version,
Expand Down Expand Up @@ -56,7 +56,7 @@ export function genOpenapiSpec(config: Config) {
}
}

const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {};
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res') : {};

let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';

Expand All @@ -71,7 +71,7 @@ export function genOpenapiSpec(config: Config) {
}

const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
const schema = { ...endpoint.params };
const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param') };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAPI Specification 3.1.0 では nullable が廃止され、type: null といった風の書式に変更されました1。リクエストのスキーマを修正する必要がありましたが変更点があまりにも大きすぎてしまうので、その場しのぎの対処として convertSchemaToOpenApiSchema にかけるようにして書き換えを行っています。

Footnotes

  1. 3.1.0-rc0 のリリースノート (https://github.com/OAI/OpenAPI-Specification/releases/tag/3.1.0-rc0) の Breaking Change を参照してください。


if (endpoint.meta.requireFile) {
schema.properties = {
Expand Down Expand Up @@ -210,7 +210,9 @@ export function genOpenapiSpec(config: Config) {
};

spec.paths['/' + endpoint.name] = {
...(endpoint.meta.allowGet ? { get: info } : {}),
...(endpoint.meta.allowGet ? {
get: info,
} : {}),
post: info,
};
}
Expand Down
37 changes: 24 additions & 13 deletions packages/backend/src/server/api/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,35 @@
import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js';

export function convertSchemaToOpenApiSchema(schema: Schema) {
// optional, refはスキーマ定義に含まれないので分離しておく
export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res') {
// optional, nullable, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { optional, ref, ...res }: any = schema;
const { optional, nullable, ref, ...res }: any = schema;

if (schema.type === 'object' && schema.properties) {
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
if (required.length > 0) {
if (type === 'res') {
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
if (required.length > 0) {
// 空配列は許可されない
res.required = required;
res.required = required;
}
}

for (const k of Object.keys(schema.properties)) {
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type);
}
}

if (schema.type === 'array' && schema.items) {
res.items = convertSchemaToOpenApiSchema(schema.items);
res.items = convertSchemaToOpenApiSchema(schema.items, type);
}

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);
for (const o of ['anyOf', 'oneOf', 'allOf'] as const) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type));
}

if (schema.ref) {
if (type === 'res' && schema.ref) {
const $ref = `#/components/schemas/${schema.ref}`;
if (schema.nullable || schema.optional) {
res.allOf = [{ $ref }];
Expand All @@ -40,6 +43,14 @@ export function convertSchemaToOpenApiSchema(schema: Schema) {
}
}

if (schema.nullable) {
if (Array.isArray(schema.type) && !schema.type.includes('null')) {
res.type.push('null');
} else if (typeof schema.type === 'string') {
res.type = [res.type, 'null'];
}
}

return res;
}

Expand Down Expand Up @@ -72,6 +83,6 @@ export const schemas = {
},

...Object.fromEntries(
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]),
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res')]),
),
};
13 changes: 13 additions & 0 deletions packages/backend/test/e2e/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
});

test('引用renoteで空白文字のみで構成されたtextにするとレスポンスがtext: nullになる', async () => {
const bobPost = await post(bob, {
text: 'test',
});
const res = await api('/notes/create', {
text: ' ',
renoteId: bobPost.id,
}, alice);

assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.createdNote.text, null);
});

test('visibility: followersでrenoteできる', async () => {
const createRes = await api('/notes/create', {
text: 'test',
Expand Down
4 changes: 2 additions & 2 deletions packages/misskey-js/generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
"generate": "tsx src/generator.ts && eslint ./built/**/* --ext .ts --fix"
},
"devDependencies": {
"@apidevtools/swagger-parser": "10.1.0",
"@misskey-dev/eslint-plugin": "^1.0.0",
"@readme/openapi-parser": "2.5.0",
"@types/node": "20.9.1",
"@typescript-eslint/eslint-plugin": "6.11.0",
"@typescript-eslint/parser": "6.11.0",
"eslint": "8.53.0",
"openapi-types": "12.1.3",
"openapi-typescript": "6.7.1",
"openapi-typescript": "6.7.3",
"ts-case-convert": "2.0.2",
"tsx": "4.4.0",
"typescript": "5.3.3"
Expand Down
Loading
Loading