From b98a8b1ddf35ba57a5b7539795df376f707a2df7 Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Tue, 28 May 2024 13:05:19 +0200 Subject: [PATCH] Updated documentation to new parenting format --- app/imports/@types/ddp.d.ts | 3 + app/imports/@types/meteor.d.ts | 7 + app/imports/@types/mongo.d.ts | 7 + app/imports/@types/validated-method.d.ts | 27 + app/imports/api/docs/{Docs.js => Docs.ts} | 112 +-- .../api/parenting/parentingFunctions.ts | 6 +- app/imports/client/ui/docs/DocBreadcrumbs.vue | 21 +- app/imports/client/ui/docs/DocEditForm.vue | 2 +- .../client/ui/docs/DocsRightDrawer.vue | 27 +- app/imports/client/ui/pages/DocsPage.vue | 4 +- app/imports/migrations/server/dbv3/dbv3.ts | 21 +- app/private/docs/defaultDocs.json | 907 +++++++++--------- 12 files changed, 570 insertions(+), 574 deletions(-) create mode 100644 app/imports/@types/ddp.d.ts create mode 100644 app/imports/@types/meteor.d.ts create mode 100644 app/imports/@types/mongo.d.ts create mode 100644 app/imports/@types/validated-method.d.ts rename app/imports/api/docs/{Docs.js => Docs.ts} (75%) diff --git a/app/imports/@types/ddp.d.ts b/app/imports/@types/ddp.d.ts new file mode 100644 index 00000000..483f0cc0 --- /dev/null +++ b/app/imports/@types/ddp.d.ts @@ -0,0 +1,3 @@ +declare namespace DDP { + function randomStream(seed: string): typeof Random; +} \ No newline at end of file diff --git a/app/imports/@types/meteor.d.ts b/app/imports/@types/meteor.d.ts new file mode 100644 index 00000000..d590b723 --- /dev/null +++ b/app/imports/@types/meteor.d.ts @@ -0,0 +1,7 @@ +declare module 'meteor/meteor' { + namespace Meteor { + interface User { + roles?: string[]; + } + } +} \ No newline at end of file diff --git a/app/imports/@types/mongo.d.ts b/app/imports/@types/mongo.d.ts new file mode 100644 index 00000000..a5623ebc --- /dev/null +++ b/app/imports/@types/mongo.d.ts @@ -0,0 +1,7 @@ +declare namespace Mongo { + interface CollectionStatic { + get: ( + collectionName: string, options?: { connection: Meteor.Connection } + ) => Mongo.Collection; + } +} diff --git a/app/imports/@types/validated-method.d.ts b/app/imports/@types/validated-method.d.ts new file mode 100644 index 00000000..aff6eae6 --- /dev/null +++ b/app/imports/@types/validated-method.d.ts @@ -0,0 +1,27 @@ +declare module 'meteor/mdg:validated-method' { + interface ValidatedMethodOptionsMixinFields { + rateLimit: { + numRequests: number, + timeInterval: number, + } + } + type Return = TFunc extends (...args: any[]) => infer TReturn ? TReturn : never; + type Argument = TFunc extends (...args: infer TArgs) => any ? TArgs extends [infer TArg] ? TArg + : NoArguments + : never; + interface ValidatedMethod any> { + callAsync: Argument extends NoArguments + // methods with no argument can be called with () or just a callback + ? + & ((unusedArg: any, callback: (error: Meteor.Error, result: Return) => void) => void) + & ((callback: (error: Meteor.Error | undefined, result: Return) => void) => void) + & (() => Return) + // methods with arguments require those arguments to be called + : + & (( + arg: Argument, + callback: (error: Meteor.Error | undefined, result: Return) => void, + ) => void) + & ((arg: Argument) => Return); + } +} \ No newline at end of file diff --git a/app/imports/api/docs/Docs.js b/app/imports/api/docs/Docs.ts similarity index 75% rename from app/imports/api/docs/Docs.js rename to app/imports/api/docs/Docs.ts index 291c045b..bb7de8f6 100644 --- a/app/imports/api/docs/Docs.js +++ b/app/imports/api/docs/Docs.ts @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; @@ -9,12 +8,30 @@ import { storedIconsSchema } from '/imports/api/icons/Icons'; import '/imports/api/library/methods/index'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; import { restore } from '/imports/api/parenting/softRemove'; -import { getFilter, rebuildNestedSets, changeParent } from '/imports/api/parenting/parentingFunctions'; -import ChildSchema from '/imports/api/parenting/ChildSchema'; +import { getFilter, rebuildNestedSets, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions'; +import ChildSchema, { TreeDoc } from '/imports/api/parenting/ChildSchema'; -const Docs = new Mongo.Collection('docs'); +// Give the docs a common root, so they can share parenting logic +export const DOC_ROOT_ID = 'DDDDDDDDDDDDDDDDD' -let DocSchema = new SimpleSchema({ +type Doc = { + _id: string, + name: string, + urlName: string, + href: string, + description?: string, + published?: true, + icon?: { + name: string, + shape: string, + }, +} & TreeDoc; + +const Docs: Mongo.Collection & { + getJsonDocs?: () => string +} = new Mongo.Collection('docs'); + +const DocSchema = new SimpleSchema({ _id: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -47,10 +64,11 @@ let DocSchema = new SimpleSchema({ }, }); -let schema = new SimpleSchema({}); +const schema = new SimpleSchema({}); schema.extend(DocSchema); schema.extend(ChildSchema); schema.extend(SoftRemovableSchema); +// @ts-expect-error No attach schema in types Docs.attachSchema(schema); function assertDocsEditPermission(userId) { @@ -60,7 +78,7 @@ function assertDocsEditPermission(userId) { if (!user?.roles?.includes?.('docsWriter')) throw ('Permission denied') } -function getDocLink(doc, urlName) { +function getDocLink(doc: Doc, urlName?: string) { if (!urlName) urlName = doc.urlName; const address = ['/docs']; const ancestorDocs = Docs.find(getFilter.ancestors(doc)); @@ -79,11 +97,11 @@ if (Meteor.isClient) { } else if (Meteor.isServer) { Meteor.startup(() => { if (!Docs.findOne()) { - console.warn('Default documents must be updated to new parenting format'); - return; + console.log('No docs found, filling documentation with defaults'); Assets.getText('docs/defaultDocs.json', (error, string) => { const docs = JSON.parse(string) docs.forEach(doc => Docs.insert(doc)); + rebuildNestedSets(Docs, DOC_ROOT_ID); }); } }); @@ -102,18 +120,20 @@ const insertDoc = new ValidatedMethod({ assertDocsEditPermission(this.userId); doc.parentId = parentId; + doc.root = { + collection: 'docs', + id: DOC_ROOT_ID, + }; - const lastOrder = Docs.find({}, { sort: { left: -1 } }).fetch()[0]?.order || 0; - doc.order = lastOrder + 1; + const lastOrder = Docs.find({}, { sort: { left: -1 }, limit: 1 }).fetch()[0]?.left || 0; doc.urlName = 'new-doc-' + (lastOrder + 1); - doc.href = getDocLink(doc); if (Docs.findOne({ href: doc.href })) { throw new Meteor.Error('Link collision', 'A document with the same URL already exists'); } const docId = Docs.insert(doc); - rebuildNestedSets(Docs); + rebuildNestedSets(Docs, DOC_ROOT_ID); return docId; }, }); @@ -135,7 +155,7 @@ const updateDoc = new ValidatedMethod({ }, run({ _id, path, value }) { assertDocsEditPermission(this.userId); - let pathString = path.join('.'); + const pathString = path.join('.'); let modifier; // unset empty values if (value === null || value === undefined) { @@ -145,6 +165,7 @@ const updateDoc = new ValidatedMethod({ } if (pathString === 'urlName') { const doc = Docs.findOne(_id); + if (!doc) throw new Meteor.Error('Not Found', 'The document you are trying to edit was not found'); const newLink = getDocLink(doc, value); if (Docs.findOne({ href: newLink })) { throw new Meteor.Error('Link collision', 'A document with the same URL already exists'); @@ -153,7 +174,7 @@ const updateDoc = new ValidatedMethod({ modifier.$set.href = newLink; } const updates = Docs.update(_id, modifier); - rebuildNestedSets(Docs); + rebuildNestedSets(Docs, DOC_ROOT_ID); return updates; }, }); @@ -202,8 +223,8 @@ const softRemoveDoc = new ValidatedMethod({ }, run({ _id }) { assertDocsEditPermission(this.userId); - softRemove({ _id, collection: Docs }); - rebuildNestedSets(Docs); + softRemove(Docs, _id); + rebuildNestedSets(Docs, DOC_ROOT_ID); } }); @@ -220,7 +241,7 @@ const restoreDoc = new ValidatedMethod({ run({ _id }) { assertDocsEditPermission(this.userId); restore('docs', _id); - rebuildNestedSets(Docs); + rebuildNestedSets(Docs, DOC_ROOT_ID); } }); @@ -228,53 +249,27 @@ const organizeDoc = new ValidatedMethod({ name: 'docs.organizeDoc', validate: new SimpleSchema({ docId: String, - parentId: String, - order: { - type: Number, - // Should end in 0.5 to place it reliably between two existing documents - }, + newPosition: Number, + skipClient: { + type: Boolean, + optional: true, + } }).validator(), mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, timeInterval: 5000, }, - run({ docId, parentId, order }) { + async run({ docId, newPosition, skipClient }: { docId: string, newPosition: number, skipClient?: boolean }) { + if (skipClient && this.isSimulation) { + return; + } + assertDocsEditPermission(this.userId); + const doc = Docs.findOne(docId); - const parent = Docs.findOne(parentId); - // The user must be able to edit both the doc and its parent to move it - // successfully - assertDocsEditPermission(this.userId); - - // Change the doc's parent - changeParent(doc, parent, Docs); - // Change the doc's order to be a half step ahead of its target location - Docs.update(doc._id, { $set: { order } }); - - rebuildNestedSets(Docs); - }, -}); - -const reorderDoc = new ValidatedMethod({ - name: 'docs.reorderDoc', - validate: new SimpleSchema({ - docId: String, - order: { - type: Number, - // Should end in 0.5 to place it reliably between two existing documents - }, - }).validator(), - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 5, - timeInterval: 5000, - }, - run({ docId, order }) { - assertDocsEditPermission(this.userId); - Docs.update(docId, { - $set: { order } - }); - rebuildNestedSets(Docs); + if (!doc) throw new Meteor.Error('not found', 'The doc you are moving was not found'); + // Move the doc + await moveDocWithinRoot(doc, Docs, newPosition); }, }); @@ -287,7 +282,6 @@ export { softRemoveDoc, restoreDoc, organizeDoc, - reorderDoc, }; export default Docs; diff --git a/app/imports/api/parenting/parentingFunctions.ts b/app/imports/api/parenting/parentingFunctions.ts index 96398e53..81c31a1b 100644 --- a/app/imports/api/parenting/parentingFunctions.ts +++ b/app/imports/api/parenting/parentingFunctions.ts @@ -1,11 +1,10 @@ -import { chain, reverse, set } from 'lodash'; +import { chain, reverse } from 'lodash'; import { TreeDoc, treeDocFields, Reference } from '/imports/api/parenting/ChildSchema'; import { getProperties } from '/imports/api/engine/loadCreatures'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; -import { Mongo } from 'meteor/mongo'; export function getCollectionByName(name: string): Mongo.Collection { - const collection: Mongo.Collection = Mongo.Collection.get(name) + const collection = Mongo.Collection.get(name) if (!collection) { throw new Meteor.Error('bad-collection-reference', `Parent references collection ${name}, which does not exist` @@ -119,7 +118,6 @@ export function filterToForest( if (options.sort) { collectionSort = { ...collectionSort, - // @ts-expect-error go home typescript you're drunk ...options.sort, } } diff --git a/app/imports/client/ui/docs/DocBreadcrumbs.vue b/app/imports/client/ui/docs/DocBreadcrumbs.vue index c7bde66f..1b4bea5b 100644 --- a/app/imports/client/ui/docs/DocBreadcrumbs.vue +++ b/app/imports/client/ui/docs/DocBreadcrumbs.vue @@ -6,6 +6,8 @@