Skip to content

Commit cdce372

Browse files
authored
feat: new api to create subject (#719)
1 parent a1db2ce commit cdce372

File tree

11 files changed

+853
-9
lines changed

11 files changed

+853
-9
lines changed

.eslintrc.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ module.exports = {
4949
],
5050
plugins: ['@typescript-eslint', 'tsdoc'],
5151
rules: {
52+
'@typescript-eslint/restrict-template-expressions': 'off',
5253
'unicorn/import-style': 'off',
5354
'rulesdir/no-relative-parent-import': 'error',
5455
'unused-imports/no-unused-imports': 'error',

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "vendor/common"]
2+
path = vendor/common
3+
url = https://github.com/Bangumi/common

lib/subject/type.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ export enum SubjectType {
55
Game = 4, // 游戏
66
Real = 6, // 三次元
77
}
8+
9+
export const SubjectTypeValues = new Set([1, 2, 3, 4, 6]);

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"scripts": {
1313
"format": "prettier --list-different -w ./",
1414
"tsc": "tsc",
15+
"build-common": "node scripts/build-common.cjs",
1516
"lint": "eslint --ext ts,cjs,js ./",
1617
"start": "nodemon ./bin/main.ts",
1718
"test": "vitest --run",
@@ -65,7 +66,8 @@
6566
"socket.io": "^4.7.5",
6667
"socks-proxy-agent": "^8.0.4",
6768
"tsx": "^4.19.0",
68-
"typeorm": "^0.3.20"
69+
"typeorm": "^0.3.20",
70+
"yaml": "^2.5.1"
6971
},
7072
"devDependencies": {
7173
"@shopify/prettier-plugin-liquid": "^1.5.0",

pnpm-lock.yaml

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

routes/private/routes/wiki/subject/index.ts

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
import { parseToMap } from '@bgm38/wiki';
12
import type { Static } from '@sinclair/typebox';
23
import { Type as t } from '@sinclair/typebox';
4+
import { StatusCodes } from 'http-status-codes';
5+
import { DateTime } from 'luxon';
6+
import type { ResultSetHeader } from 'mysql2';
37

48
import { NotAllowedError } from '@app/lib/auth/index.ts';
59
import { BadRequestError, NotFoundError } from '@app/lib/error.ts';
610
import { Security, Tag } from '@app/lib/openapi/index.ts';
7-
import { SubjectRevRepo } from '@app/lib/orm/index.ts';
11+
import * as entity from '@app/lib/orm/entity';
12+
import { AppDataSource, SubjectRevRepo } from '@app/lib/orm/index.ts';
813
import * as orm from '@app/lib/orm/index.ts';
914
import * as Subject from '@app/lib/subject/index.ts';
10-
import { InvalidWikiSyntaxError, platforms } from '@app/lib/subject/index.ts';
15+
import { InvalidWikiSyntaxError, platforms, SubjectType } from '@app/lib/subject/index.ts';
16+
import PlatformConfig from '@app/lib/subject/platform.ts';
17+
import { SubjectTypeValues } from '@app/lib/subject/type.ts';
1118
import * as res from '@app/lib/types/res.ts';
1219
import { formatErrors } from '@app/lib/types/res.ts';
1320
import { requireLogin } from '@app/routes/hooks/pre-handler.ts';
@@ -48,6 +55,21 @@ const exampleSubjectEdit = {
4855
https://bgm.tv/group/topic/366812#post_1923517`,
4956
};
5057

58+
export type ISubjectNew = Static<typeof SubjectNew>;
59+
export const SubjectNew = t.Object(
60+
{
61+
name: t.String({ minLength: 1 }),
62+
type: t.Enum(SubjectType),
63+
platform: t.Integer(),
64+
infobox: t.String({ minLength: 1 }),
65+
nsfw: t.Boolean(),
66+
summary: t.String(),
67+
},
68+
{
69+
$id: 'SubjectNew',
70+
},
71+
);
72+
5173
export type ISubjectEdit = Static<typeof SubjectEdit>;
5274
export const SubjectEdit = t.Object(
5375
{
@@ -163,6 +185,125 @@ export async function setup(app: App) {
163185
},
164186
);
165187

188+
app.addSchema(SubjectNew);
189+
190+
app.post(
191+
'/subjects',
192+
{
193+
schema: {
194+
tags: [Tag.Wiki],
195+
operationId: 'createNewSubject',
196+
description: '创建新条目',
197+
security: [{ [Security.CookiesSession]: [], [Security.HTTPBearer]: [] }],
198+
body: SubjectNew,
199+
response: {
200+
200: t.Object({ subjectID: t.Number() }),
201+
[StatusCodes.BAD_REQUEST]: t.Ref(res.Error, {
202+
'x-examples': formatErrors(InvalidWikiSyntaxError()),
203+
}),
204+
401: t.Ref(res.Error, {}),
205+
},
206+
},
207+
},
208+
async ({ auth, body }) => {
209+
if (!auth.permission.subject_edit) {
210+
throw new NotAllowedError('edit subject');
211+
}
212+
213+
if (!SubjectTypeValues.has(body.type)) {
214+
throw new BadRequestError(`条目类型错误`);
215+
}
216+
217+
if (!(body.platform in PlatformConfig[body.type])) {
218+
throw new BadRequestError(`条目分类错误`);
219+
}
220+
221+
let w;
222+
try {
223+
w = parseToMap(body.infobox);
224+
} catch (error) {
225+
throw new BadRequestError(`infobox 包含语法错误 ${error}`);
226+
}
227+
228+
let eps = 0;
229+
if (body.type === SubjectType.Anime) {
230+
eps = Number.parseInt(w.data.get('话数')?.value ?? '0') || 0;
231+
} else if (body.type === SubjectType.Real) {
232+
eps = Number.parseInt(w.data.get('集数')?.value ?? '0') || 0;
233+
}
234+
235+
const newSubject = {
236+
name: body.name,
237+
nameCN: w.data.get('中文名')?.value ?? '',
238+
platform: body.platform,
239+
fieldInfobox: body.infobox,
240+
typeID: body.type,
241+
fieldSummary: body.summary,
242+
fieldEps: eps,
243+
updatedAt: DateTime.now().toUnixInteger(),
244+
};
245+
246+
const subjectID = await AppDataSource.transaction(async (txn) => {
247+
const s = await txn
248+
.getRepository(entity.Subject)
249+
.createQueryBuilder()
250+
.insert()
251+
.values(newSubject)
252+
.execute();
253+
254+
const r = s.raw as ResultSetHeader;
255+
256+
await txn
257+
.getRepository(entity.SubjectFields)
258+
.createQueryBuilder()
259+
.insert()
260+
.values({ subjectID: r.insertId })
261+
.execute();
262+
263+
if (eps) {
264+
const episodes = Array.from({ length: eps })
265+
.fill(null)
266+
.map((_, index) => {
267+
return {
268+
subjectID: r.insertId,
269+
sort: index + 1,
270+
type: 0,
271+
};
272+
});
273+
274+
await txn
275+
.getRepository(entity.Episode)
276+
.createQueryBuilder()
277+
.insert()
278+
.values(episodes)
279+
.execute();
280+
}
281+
282+
await txn
283+
.getRepository(entity.SubjectRev)
284+
.createQueryBuilder()
285+
.insert()
286+
.values({
287+
subjectID: r.insertId,
288+
name: newSubject.name,
289+
nameCN: newSubject.nameCN,
290+
infobox: newSubject.fieldInfobox,
291+
summary: newSubject.fieldSummary,
292+
createdAt: newSubject.updatedAt,
293+
typeID: newSubject.typeID,
294+
platform: newSubject.platform,
295+
eps: eps,
296+
creatorID: auth.userID,
297+
})
298+
.execute();
299+
300+
return r.insertId;
301+
});
302+
303+
return { subjectID };
304+
},
305+
);
306+
166307
type IHistorySummary = Static<typeof HistorySummary>;
167308
const HistorySummary = t.Object(
168309
{

routes/private/routes/wiki/subject/subject.test.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import * as fs from 'node:fs/promises';
22
import * as path from 'node:path';
33

4+
import { StatusCodes } from 'http-status-codes';
45
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
56

67
import { UserGroup } from '@app/lib/auth/index.ts';
78
import { projectRoot } from '@app/lib/config.ts';
89
import * as image from '@app/lib/image/index.ts';
910
import type { IImaginary, Info } from '@app/lib/services/imaginary.ts';
1011
import * as Subject from '@app/lib/subject/index.ts';
11-
import type { ISubjectEdit } from '@app/routes/private/routes/wiki/subject/index.ts';
12+
import { SubjectType } from '@app/lib/subject/index.ts';
13+
import type { ISubjectEdit, ISubjectNew } from '@app/routes/private/routes/wiki/subject/index.ts';
1214
import { setup } from '@app/routes/private/routes/wiki/subject/index.ts';
1315
import { createTestServer } from '@app/tests/utils.ts';
1416

@@ -18,6 +20,102 @@ async function testApp(...args: Parameters<typeof createTestServer>) {
1820
return app;
1921
}
2022

23+
const newSubjectApp = () =>
24+
testApp({
25+
auth: {
26+
groupID: UserGroup.Normal,
27+
login: true,
28+
permission: { subject_edit: true },
29+
allowNsfw: true,
30+
regTime: 0,
31+
userID: 100,
32+
},
33+
});
34+
35+
describe('create subject', () => {
36+
test('create new subject', async () => {
37+
const app = await newSubjectApp();
38+
const res = await app.inject({
39+
url: '/subjects',
40+
method: 'post',
41+
payload: {
42+
name: 'New Subject',
43+
infobox: `{{Infobox
44+
| 话数 = 10
45+
}}`,
46+
type: SubjectType.Anime,
47+
platform: 0,
48+
summary: 'A brief summary of the subject',
49+
nsfw: false,
50+
} satisfies ISubjectNew,
51+
});
52+
53+
expect(res.statusCode).toBe(200);
54+
55+
expect(res.json()).toEqual({
56+
subjectID: expect.any(Number),
57+
});
58+
});
59+
60+
test('create type', async () => {
61+
const app = await newSubjectApp();
62+
const res = await app.inject({
63+
url: '/subjects',
64+
method: 'post',
65+
payload: {
66+
name: 'New Subject',
67+
infobox: `{{Infobox
68+
| 话数 = 10
69+
}}`,
70+
type: 0,
71+
platform: 0,
72+
summary: 'A brief summary of the subject',
73+
nsfw: false,
74+
},
75+
});
76+
77+
expect(res.statusCode).toBe(StatusCodes.BAD_REQUEST);
78+
79+
expect(res.json()).toMatchInlineSnapshot(`
80+
Object {
81+
"code": "REQUEST_VALIDATION_ERROR",
82+
"error": "Bad Request",
83+
"message": "body/type must be equal to constant, body/type must be equal to constant, body/type must be equal to constant, body/type must be equal to constant, body/type must be equal to constant, body/type must match a schema in anyOf",
84+
"statusCode": 400,
85+
}
86+
`);
87+
});
88+
89+
test('invalid platform', async () => {
90+
const app = await newSubjectApp();
91+
const res = await app.inject({
92+
url: '/subjects',
93+
method: 'post',
94+
payload: {
95+
name: 'New Subject',
96+
infobox: `{{Infobox
97+
| 话数 = 10
98+
}}`,
99+
type: SubjectType.Anime,
100+
platform: 777777888,
101+
summary: 'A brief summary of the subject',
102+
nsfw: false,
103+
},
104+
});
105+
106+
expect(res.statusCode).toBe(StatusCodes.BAD_REQUEST);
107+
108+
expect(res.json()).toMatchInlineSnapshot(`
109+
Object {
110+
"code": "BAD_REQUEST",
111+
"error": "Bad Request",
112+
"message": "条目分类错误",
113+
"statusCode": 400,
114+
}
115+
`);
116+
});
117+
});
118+
21119
describe('edit subject ', () => {
22120
const editSubject = vi.fn();
23121

0 commit comments

Comments
 (0)