From 508ec77a73601a34cc97c69c48f51c229812ee1a Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 8 Nov 2024 17:37:50 +0800 Subject: [PATCH] core: content manager --- packages/hydrooj/src/interface.ts | 11 +++++- packages/hydrooj/src/model/document.ts | 34 ++++++++++++++++- packages/hydrooj/src/model/problem.ts | 52 +++++++++++++++++++++----- packages/hydrooj/src/service/bus.ts | 2 +- 4 files changed, 85 insertions(+), 14 deletions(-) diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 35cff9dc2..80e266d2e 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -223,7 +223,8 @@ export interface ProblemConfig { hackable?: boolean; } -export type Content = string | Record; +export type LocalizedContent = ({ id: string, name: string, lang: string } | { from: string, name: string, lang: string })[]; +export type Content = string | LocalizedContent; export interface Document { _id: ObjectId; @@ -241,7 +242,7 @@ declare module './model/problem' { docId: number; pid: string; title: string; - content: ({ id: string, name: string, lang: string } | { from: string, name: string, lang: string })[]; + content: LocalizedContent; nSubmit: number; nAccept: number; tag: string[]; @@ -678,6 +679,11 @@ export interface ContestBalloonDoc { sentAt?: Date; } +export interface ContentDoc { + _id: string; + content: string; +} + declare module './service/db' { interface Collections { 'blacklist': BlacklistDoc; @@ -689,6 +695,7 @@ declare module './service/db' { 'document.status': StatusDocBase & { [K in keyof DocStatusType]: { docType: K } & DocStatusType[K]; }[keyof DocStatusType]; + 'content': ContentDoc; 'discussion.history': DiscussionHistoryDoc; 'user': Udoc; 'user.preference': UserPreferenceDoc; diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index f2f72ef5c..06b29bdac 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -19,6 +19,7 @@ type NormalArrayKeys = Exclude, Symbol>; export const coll = db.collection('document'); export const collStatus = db.collection('document.status'); +export const collContent = db.collection('content'); export const TYPE_PROBLEM: 10 = 10; export const TYPE_PROBLEM_SOLUTION: 11 = 11; @@ -51,13 +52,13 @@ export interface DocStatusType { } export async function add( - domainId: string, content: Content, owner: number, + domainId: string, content: DocType[T]['content'], owner: number, docType: T, docId: K, parentType?: DocType[T]['parentType'], parentId?: DocType[T]['parentId'], args?: Partial, ): Promise; export async function add( - domainId: string, content: Content, owner: number, + domainId: string, content: DocType[T]['content'], owner: number, docType: T, docId: null, parentType?: DocType[T]['parentType'], parentId?: DocType[T]['parentId'], args?: Partial, @@ -425,6 +426,29 @@ export async function revSetStatus( return res.value; } +export async function getContent(contentId: string) { + return await collContent.findOne({ _id: contentId }); +} + +export async function getContents(contentIds: string[]) { + return await collContent.find({ _id: { $in: contentIds } }).toArray(); +} + +export async function addContent(content: string) { + const id = String.random(32); + await collContent.insertOne({ _id: id, content }); + return id; +} + +export async function setContent(contentId: string, content: string) { + const res = await collContent.updateOne({ _id: contentId }, { $set: { content } }); + return !!res.matchedCount; +} + +export async function deleteContent(contentId: string) { + return await collContent.deleteOne({ _id: contentId }); +} + export async function apply(ctx: Context) { ctx.on('domain/delete', (domainId) => Promise.all([ coll.deleteMany({ domainId }), @@ -463,6 +487,7 @@ export async function apply(ctx: Context) { global.Hydro.model.document = { coll, + collContent, collStatus, add, @@ -494,6 +519,11 @@ global.Hydro.model.document = { setStatus, setMultiStatus, setSub, + getContent, + getContents, + addContent, + setContent, + deleteContent, TYPE_CONTEST, TYPE_CONTEST_CLARIFICATION, diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index e8c533d16..94323fd16 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -12,7 +12,7 @@ import { Logger, size, streamToBuffer } from '@hydrooj/utils/lib/utils'; import { Context } from '../context'; import { FileUploadError, ProblemNotFoundError, ValidationError } from '../error'; import type { - Document, ProblemConfigFile, ProblemDict, ProblemStatusDoc, User, + Document, LocalizedContent, ProblemConfigFile, ProblemDict, ProblemStatusDoc, User, } from '../interface'; import { parseConfig } from '../lib/testdataConfig'; import * as bus from '../service/bus'; @@ -90,8 +90,6 @@ export class ProblemModel { 'reference', 'maintainer', ]; - static collContent = db.collection('problem.content'); - static default = { _id: new ObjectId(), domainId: 'system', @@ -135,15 +133,29 @@ export class ProblemModel { }; static async add( - domainId: string, pid: string = '', title: string, content: ProblemDoc['content'], owner: number, - tag: string[] = [], meta: ProblemCreateOptions = {}, + domainId: string, pid: string = '', title: string, + content: { content?: string, from?: string, lang: string, name: string }[] | string, + owner: number, tag: string[] = [], meta: ProblemCreateOptions = {}, ) { + let c: LocalizedContent; + if (typeof content === 'string') { + c.push({ id: await document.addContent(content), name: 'default', lang: 'en' }); + } else { + for (const entry of content) { + if (entry.from) { + c.push(entry as any); + } else { + const id = await document.addContent(entry.content); + c.push({ id, name: entry.name, lang: entry.lang }); + } + } + } const [doc] = await ProblemModel.getMulti(domainId, {}) .sort({ docId: -1 }).limit(1).project({ docId: 1 }) .toArray(); const result = await ProblemModel.addWithId( domainId, (doc?.docId || 0) + 1, pid, - title, content, owner, tag, meta, + title, c, owner, tag, meta, ); return result; } @@ -191,7 +203,7 @@ export class ProblemModel { } static async getContent(contentIds: string[]) { - const content = await ProblemModel.collContent.find({ _id: { $in: contentIds } }).toArray(); + const content = await document.getContents(contentIds); return Object.fromEntries(content.map((i) => [i._id.toString(), i.content])); } @@ -219,7 +231,10 @@ export class ProblemModel { return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query); } - static async edit(domainId: string, _id: number, $set: Partial): Promise { + static async edit( + domainId: string, _id: number, + $set: (Partial & ({ content?: { content: string }[] })), + ): Promise { const delpid = $set.pid === ''; const ddoc = await DomainModel.get(domainId); if (delpid) { @@ -229,6 +244,23 @@ export class ProblemModel { $set.sort = sortable($set.pid, ddoc.namespaces); } await bus.parallel('problem/before-edit', $set); + if ($set.content) { + const update = $set.content instanceof Array ? $set.content : [{ content: $set.content, lang: 'en', name: 'default' }]; + const existing = await document.get(domainId, document.TYPE_PROBLEM, _id); + if (!existing) throw new ProblemNotFoundError(domainId, _id); + const contents = existing.content; + const final = []; + for (const entry of update) { + if ('id' in entry && contents.find((i) => i.id === entry.id)) { + await document.setContent(entry.id, entry.content); + final.push({ id: entry.id, name: entry.name, lang: entry.lang }); + } else { + const cid = await document.addContent(entry.content); + final.push({ id: cid, name: entry.name, lang: entry.lang }); + } + } + $set.content = final; + } const result = await document.set(domainId, document.TYPE_PROBLEM, _id, $set, delpid ? { pid: '' } : undefined); await bus.emit('problem/edit', result); return result; @@ -542,7 +574,9 @@ export class ProblemModel { const docId = overridePid ? (await ProblemModel.edit(domainId, overridePid, { title: pdoc.title.trim(), - content: overrideContent || pdoc.content.toString() || 'No content', + content: [{ + id: '', name: 'default', content: overrideContent || pdoc.content.toString() || 'No content', lang: 'en', + }], tag, difficulty: pdoc.difficulty, })).docId diff --git a/packages/hydrooj/src/service/bus.ts b/packages/hydrooj/src/service/bus.ts index b58000c8d..fda1370a2 100644 --- a/packages/hydrooj/src/service/bus.ts +++ b/packages/hydrooj/src/service/bus.ts @@ -68,7 +68,7 @@ export interface EventMap extends KnownHandlerEvents { 'discussion/before-add': (payload: Partial) => VoidReturn 'discussion/add': (payload: Partial) => VoidReturn - 'problem/before-add': (domainId: string, content: string, owner: number, docId: number, doc: Partial) => VoidReturn + 'problem/before-add': (domainId: string, content: ProblemDoc['content'], owner: number, docId: number, doc: Partial) => VoidReturn 'problem/add': (doc: Partial, docId: number) => VoidReturn 'problem/before-edit': (doc: Partial) => VoidReturn 'problem/edit': (doc: ProblemDoc) => VoidReturn