|
| 1 | +import { parseToMap } from '@bgm38/wiki'; |
1 | 2 | import type { Static } from '@sinclair/typebox'; |
2 | 3 | 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'; |
3 | 7 |
|
4 | 8 | import { NotAllowedError } from '@app/lib/auth/index.ts'; |
5 | 9 | import { BadRequestError, NotFoundError } from '@app/lib/error.ts'; |
6 | 10 | 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'; |
8 | 13 | import * as orm from '@app/lib/orm/index.ts'; |
9 | 14 | 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'; |
11 | 18 | import * as res from '@app/lib/types/res.ts'; |
12 | 19 | import { formatErrors } from '@app/lib/types/res.ts'; |
13 | 20 | import { requireLogin } from '@app/routes/hooks/pre-handler.ts'; |
@@ -48,6 +55,21 @@ const exampleSubjectEdit = { |
48 | 55 | https://bgm.tv/group/topic/366812#post_1923517`, |
49 | 56 | }; |
50 | 57 |
|
| 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 | + |
51 | 73 | export type ISubjectEdit = Static<typeof SubjectEdit>; |
52 | 74 | export const SubjectEdit = t.Object( |
53 | 75 | { |
@@ -163,6 +185,125 @@ export async function setup(app: App) { |
163 | 185 | }, |
164 | 186 | ); |
165 | 187 |
|
| 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 | + |
166 | 307 | type IHistorySummary = Static<typeof HistorySummary>; |
167 | 308 | const HistorySummary = t.Object( |
168 | 309 | { |
|
0 commit comments