/* eslint-disable object-curly-newline */ import assert from 'assert'; import { Cursor, FilterQuery, ObjectID, OnlyFieldsOfType, UpdateQuery, } from 'mongodb'; import { BlogDoc, Content, DiscussionDoc, DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc, Tdoc, TrainingDoc, } from '../interface'; import * as bus from '../service/bus'; import db from '../service/db'; import { ArrayKeys, MaybeArray, NumberKeys, Projection } from '../typeutils'; import { buildProjection } from '../utils'; type DocID = ObjectID | string | number; export const coll = db.collection('document'); export const collStatus = db.collection('document.status'); export const TYPE_PROBLEM: 10 = 10; export const TYPE_PROBLEM_SOLUTION: 11 = 11; export const TYPE_PROBLEM_LIST: 12 = 12; export const TYPE_DISCUSSION_NODE: 20 = 20; export const TYPE_DISCUSSION: 21 = 21; export const TYPE_DISCUSSION_REPLY: 22 = 22; export const TYPE_CONTEST: 30 = 30; export const TYPE_TRAINING: 40 = 40; /** @deprecated use `TYPE_CONTEST` with rule `homework` instead. */ export const TYPE_HOMEWORK: 60 = 60; export const TYPE_BLOG: 70 = 70; export interface DocType { [TYPE_PROBLEM]: ProblemDoc; [TYPE_PROBLEM_SOLUTION]: any; [TYPE_PROBLEM_LIST]: any; [TYPE_DISCUSSION_NODE]: any; [TYPE_DISCUSSION]: DiscussionDoc; [TYPE_DISCUSSION_REPLY]: DiscussionReplyDoc; [TYPE_CONTEST]: Tdoc; [TYPE_TRAINING]: TrainingDoc; [TYPE_BLOG]: BlogDoc; } export interface DocStatusType { [TYPE_PROBLEM]: ProblemStatusDoc, [key: number]: any } export async function add( domainId: string, content: 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, docType: T, docId: null, parentType?: DocType[T]['parentType'], parentId?: DocType[T]['parentId'], args?: Partial, ): Promise; export async function add( domainId: string, content: Content, owner: number, docType: number, docId: DocID = null, parentType: number | null = null, parentId: DocID = null, args: any = {}, ) { const _id = new ObjectID(); const doc: any = { _id, content, owner, domainId, docType, docId: docId || _id, ...args, }; if (parentType || parentId) { assert(parentType && parentId); doc.parentType = parentType; doc.parentId = parentId; } await bus.parallel('document/add', doc); const res = await coll.insertOne(doc); return docId || res.insertedId; } export async function get(domainId: string, docType: K, docId: DocType[K]['docId'], projection?: Projection) { let cursor = coll.find({ domainId, docType, docId }).limit(1); if (projection) cursor = cursor.project(buildProjection(projection)); const result = await cursor.toArray(); if (result.length) return result[0]; return null; } export async function set( domainId: string, docType: K, docId: DocType[K]['docId'], $set?: Partial, $unset?: OnlyFieldsOfType, ): Promise { await bus.parallel('document/set', domainId, docType, docId, $set, $unset); const update: UpdateQuery = {}; if ($set) update.$set = $set; if ($unset) update.$unset = $unset; const res = await coll.findOneAndUpdate( { domainId, docType, docId }, update, { returnDocument: 'after', upsert: true }, ); return res.value; } export function deleteOne( domainId: string, docType: K, docId: DocType[K]['docId'], ) { return Promise.all([ coll.deleteOne({ domainId, docType, docId }), collStatus.deleteMany({ domainId, docType, docId }), ]); } export function deleteMulti( domainId: string, docType: K, query?: FilterQuery, ) { return coll.deleteMany({ ...query, domainId, docType }); } export function deleteMultiStatus( domainId: string, docType: K, query?: FilterQuery, ) { return collStatus.deleteMany({ ...query, domainId, docType }); } export function getMulti( domainId: string, docType: K, query?: FilterQuery, projection?: Projection, ): Cursor { let cursor = coll.find({ docType, domainId, ...query }); if (projection) cursor = cursor.project(buildProjection(projection)); return cursor; } export async function inc( domainId: string, docType: K, docId: DocType[K]['docId'], key: NumberKeys, value: number, ) { const res = await coll.findOneAndUpdate( { domainId, docType, docId }, { $inc: { [key]: value } }, { returnDocument: 'after' }, ); return res.value; } export async function incAndSet( domainId: string, docType: K, docId: DocType[K]['docId'], key: NumberKeys, value: number, args: Partial, ) { const res = await coll.findOneAndUpdate( { domainId, docType, docId }, { $inc: { [key]: value }, $set: args }, { returnDocument: 'after' }, ); return res.value; } export function count( domainId: string, docType: K, query?: FilterQuery, ) { return coll.find({ ...query, docType, domainId }).count(); } export async function push( domainId: string, docType: K, docId: DocType[K]['docId'], key: ArrayKeys, value: DocType[K][0], ): Promise<[DocType[K], ObjectID]>; export async function push( domainId: string, docType: K, docId: DocType[K]['docId'], key: keyof DocType[K], content: string, owner: number, args?: DocType[K][T][0], ): Promise<[DocType[K], ObjectID]>; export async function push( domainId: string, docType: number, docId: DocID, key: string, arg0: any, arg1?: any, arg2?: any, ) { const _id = arg2?._id || arg0?._id || new ObjectID(); const v = arg1 ? { _id, ...arg2, content: arg0, owner: arg1 } : { _id, ...arg0 }; const doc = await coll.findOneAndUpdate( { domainId, docType, docId }, { $push: { [key]: v } }, { returnDocument: 'after' }, ); return [doc.value, _id]; } export async function pull>( domainId: string, docType: K, docId: DocType[K]['docId'], setKey: T, contents: FilterQuery, ): Promise { const res = await coll.findOneAndUpdate( { domainId, docType, docId }, { $pull: { [setKey]: { $in: contents } } }, { returnDocument: 'after' }, ); return res.value; } export async function deleteSub>( domainId: string, docType: T, docId: DocType[T]['docId'], key: K, subId: MaybeArray, ): Promise { subId = (subId instanceof Array) ? subId : [subId]; const res = await coll.findOneAndUpdate( { domainId, docType, docId }, { $pull: { [key]: { _id: { $in: subId } } } }, { returnDocument: 'after' }, ); return res.value; } export async function getSub>( domainId: string, docType: T, docId: DocType[T]['docId'], key: K, subId: DocType[T][K][0]['_id'], ): Promise<[DocType[T], DocType[T][K][0]]> { const doc = await coll.findOne({ domainId, docType, docId, [key]: { $elemMatch: { _id: subId } }, }); if (!doc) return [null, null]; for (const sdoc of doc[key] || []) { if (sdoc._id.toString() === subId.toString()) return [doc, sdoc]; } return [doc, null]; } export async function setSub>( domainId: string, docType: T, docId: DocType[T]['docId'], key: K, subId: DocType[T][K][0]['_id'], args: Partial, ): Promise { const $set: Record = {}; for (const k in args) $set[`${key}.$.${k}`] = args[k]; const res = await coll.findOneAndUpdate( { domainId, docType, docId, [key]: { $elemMatch: { _id: subId } }, }, { $set }, { returnDocument: 'after' }, ); return res.value; } export async function addToSet>( domainId: string, docType: T, docId: DocType[T]['docId'], setKey: K, content: string, ) { const res = await coll.findOneAndUpdate( { domainId, docType, docId }, { $addToSet: { [setKey]: content } }, { returnDocument: 'after' }, ); return res.value; } export async function getStatus( domainId: string, docType: K, docId: DocStatusType[K]['docId'], uid: number, ): Promise { return await collStatus.findOne({ domainId, docType, docId, uid }); } export function getMultiStatus( domainId: string, docType: K, args: FilterQuery, ): Cursor { return collStatus.find({ ...args, docType, domainId }); } export function getMultiStatusWithoutDomain( docType: K, args: FilterQuery, ): Cursor { return collStatus.find({ ...args, docType }); } export async function setStatus( domainId: string, docType: K, docId: DocStatusType[K]['docId'], uid: number, args: UpdateQuery['$set'], ): Promise { const res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid }, { $set: args }, { upsert: true, returnDocument: 'after' }, ); return res.value; } export async function setMultiStatus( domainId: string, docType: K, query: FilterQuery, args: Partial, ) { return await collStatus.updateMany( { domainId, docType, ...query }, { $set: args }, ); } export async function setIfNotStatus( domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number, key: K, value: DocStatusType[T][K], ifNot: DocStatusType[T][K], args: Partial, ): Promise { const current: Partial = await collStatus.findOne({ domainId, docType, docId, uid }) || {}; if (typeof key === 'string' && key.includes('.')) { const acc = key.split('.'); let c = current; for (const i of acc) { if (!(i in c)) break; c = c[i]; if (c === null || c === undefined) break; } if (c === ifNot) return null; } else if (current[key] === ifNot) return null; const res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid }, { $set: { [key]: value, ...args } }, { upsert: true, returnDocument: 'after' }, ); return res.value; } export async function setStatusIfNotCondition( domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number, filter: FilterQuery, args: Partial, ): Promise { const match = await collStatus.findOne({ domainId, docType, docId, uid, ...filter }); if (match) return null; const res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid }, { $set: args }, { upsert: true, returnDocument: 'after' }, ); return res.value; } export async function cappedIncStatus( domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number, key: NumberKeys, value: number, minValue = -1, maxValue = 1, ): Promise { assert(value !== 0); const $not = value > 0 ? { $gte: maxValue } : { $lte: minValue }; const res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid, [key]: { $not } }, { $inc: { [key]: value } }, { upsert: true, returnDocument: 'after' }, ); return res.value; } export async function incStatus( domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number, key: NumberKeys, value: number, ): Promise { const res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid }, { $inc: { [key]: value } }, { upsert: true, returnDocument: 'after' }, ); return res.value; } export async function revPushStatus( domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number, key: string & ArrayKeys, value: any, id = '_id', ): Promise { let res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid, [`${key}.${id}`]: value[id] }, { $set: { [`${key}.$`]: value }, $inc: { rev: 1 } }, { returnDocument: 'after' }, ); if (!res.value) { res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid }, { $push: { [key]: value }, $inc: { rev: 1 } }, { upsert: true, returnDocument: 'after' }, ); } return res.value; } export async function revInitStatus( domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number, ): Promise { const res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid }, { $inc: { rev: 1 } }, { upsert: true, returnDocument: 'after' }, ); return res.value; } export async function revSetStatus( domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number, rev: number, args: Partial, ): Promise { const filter = { domainId, docType, docId, uid, rev }; const update = { $set: args, $inc: { rev: 1 } }; const res = await collStatus.findOneAndUpdate(filter, update, { returnDocument: 'after' }); return res.value; } bus.on('app/started', async () => { await db.ensureIndexes( coll, { key: { domainId: 1, docType: 1, docId: 1 }, name: 'basic', unique: true }, { key: { domainId: 1, docType: 1, owner: 1, docId: -1 }, name: 'owner' }, // For problem { key: { domainId: 1, docType: 1, search: 'text', title: 'text' }, name: 'search', sparse: true }, { key: { domainId: 1, docType: 1, sort: 1 }, name: 'sort', sparse: true }, { key: { domainId: 1, docType: 1, tag: 1, sort: 1 }, name: 'tag', sparse: true }, { key: { domainId: 1, docType: 1, hidden: 1, tag: 1, sort: 1 }, name: 'hidden', sparse: true }, // For problem solution { key: { domainId: 1, docType: 1, parentType: 1, parentId: 1, vote: -1, docId: -1 }, name: 'solution', sparse: true }, // For discussion { key: { domainId: 1, docType: 1, pin: -1, sort: -1, docId: -1 }, name: 'discussionSort', sparse: true }, { key: { domainId: 1, docType: 1, parentType: 1, parentId: 1, pin: -1, sort: -1, docId: -1 }, name: 'discussionNodeSort', sparse: true }, // Hidden doc { key: { domainId: 1, docType: 1, hidden: 1, docId: -1 }, name: 'hiddenDoc', sparse: true }, // For contest { key: { domainId: 1, docType: 1, pids: 1 }, name: 'contest', sparse: true }, { key: { domainId: 1, docType: 1, rule: 1, docId: -1 }, name: 'contestRule', sparse: true }, // For training { key: { domainId: 1, docType: 1, 'dag.pids': 1 }, name: 'training', sparse: true }, ); await db.ensureIndexes( collStatus, { key: { domainId: 1, docType: 1, docId: 1, uid: 1 }, name: 'basic', unique: true }, { key: { domainId: 1, docType: 1, docId: 1, status: 1, rid: 1, rp: 1 }, name: 'rp', sparse: true }, { key: { domainId: 1, docType: 1, docId: 1, score: -1 }, name: 'contestRuleOI', sparse: true }, { key: { domainId: 1, docType: 1, docId: 1, accept: -1, time: 1 }, name: 'contestRuleACM', sparse: true }, { key: { domainId: 1, docType: 1, uid: 1, enroll: 1, docId: 1 }, name: 'training', sparse: true }, ); }); bus.on('domain/delete', (domainId) => Promise.all([ coll.deleteMany({ domainId }), collStatus.deleteMany({ domainId }), ])); global.Hydro.model.document = { coll, collStatus, add, addToSet, cappedIncStatus, count, deleteMulti, deleteMultiStatus, deleteOne, deleteSub, get, getMulti, getMultiStatus, getMultiStatusWithoutDomain, getStatus, getSub, inc, incAndSet, incStatus, pull, push, revInitStatus, revPushStatus, revSetStatus, set, setIfNotStatus, setStatusIfNotCondition, setStatus, setMultiStatus, setSub, TYPE_CONTEST, TYPE_DISCUSSION, TYPE_DISCUSSION_NODE, TYPE_DISCUSSION_REPLY, TYPE_HOMEWORK, TYPE_PROBLEM, TYPE_PROBLEM_LIST, TYPE_PROBLEM_SOLUTION, TYPE_TRAINING, TYPE_BLOG, };