Updated documentation to new parenting format

This commit is contained in:
Thaum Rystra
2024-05-28 13:05:19 +02:00
parent 772e55ece5
commit b98a8b1ddf
12 changed files with 570 additions and 574 deletions

3
app/imports/@types/ddp.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare namespace DDP {
function randomStream(seed: string): typeof Random;
}

7
app/imports/@types/meteor.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare module 'meteor/meteor' {
namespace Meteor {
interface User {
roles?: string[];
}
}
}

7
app/imports/@types/mongo.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare namespace Mongo {
interface CollectionStatic {
get: <T>(
collectionName: string, options?: { connection: Meteor.Connection }
) => Mongo.Collection<T>;
}
}

View File

@@ -0,0 +1,27 @@
declare module 'meteor/mdg:validated-method' {
interface ValidatedMethodOptionsMixinFields<TRunArg, TRunReturn> {
rateLimit: {
numRequests: number,
timeInterval: number,
}
}
type Return<TFunc> = TFunc extends (...args: any[]) => infer TReturn ? TReturn : never;
type Argument<TFunc> = TFunc extends (...args: infer TArgs) => any ? TArgs extends [infer TArg] ? TArg
: NoArguments
: never;
interface ValidatedMethod<TName extends string, TRun extends (...args: any[]) => any> {
callAsync: Argument<TRun> extends NoArguments
// methods with no argument can be called with () or just a callback
?
& ((unusedArg: any, callback: (error: Meteor.Error, result: Return<TRun>) => void) => void)
& ((callback: (error: Meteor.Error | undefined, result: Return<TRun>) => void) => void)
& (() => Return<TRun>)
// methods with arguments require those arguments to be called
:
& ((
arg: Argument<TRun>,
callback: (error: Meteor.Error | undefined, result: Return<TRun>) => void,
) => void)
& ((arg: Argument<TRun>) => Return<TRun>);
}
}

View File

@@ -1,5 +1,4 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
@@ -9,12 +8,30 @@ import { storedIconsSchema } from '/imports/api/icons/Icons';
import '/imports/api/library/methods/index'; import '/imports/api/library/methods/index';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import { restore } from '/imports/api/parenting/softRemove'; import { restore } from '/imports/api/parenting/softRemove';
import { getFilter, rebuildNestedSets, changeParent } from '/imports/api/parenting/parentingFunctions'; import { getFilter, rebuildNestedSets, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions';
import ChildSchema from '/imports/api/parenting/ChildSchema'; 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<Doc> & {
getJsonDocs?: () => string
} = new Mongo.Collection<Doc>('docs');
const DocSchema = new SimpleSchema({
_id: { _id: {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, 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(DocSchema);
schema.extend(ChildSchema); schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema); schema.extend(SoftRemovableSchema);
// @ts-expect-error No attach schema in types
Docs.attachSchema(schema); Docs.attachSchema(schema);
function assertDocsEditPermission(userId) { function assertDocsEditPermission(userId) {
@@ -60,7 +78,7 @@ function assertDocsEditPermission(userId) {
if (!user?.roles?.includes?.('docsWriter')) throw ('Permission denied') if (!user?.roles?.includes?.('docsWriter')) throw ('Permission denied')
} }
function getDocLink(doc, urlName) { function getDocLink(doc: Doc, urlName?: string) {
if (!urlName) urlName = doc.urlName; if (!urlName) urlName = doc.urlName;
const address = ['/docs']; const address = ['/docs'];
const ancestorDocs = Docs.find(getFilter.ancestors(doc)); const ancestorDocs = Docs.find(getFilter.ancestors(doc));
@@ -79,11 +97,11 @@ if (Meteor.isClient) {
} else if (Meteor.isServer) { } else if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
if (!Docs.findOne()) { if (!Docs.findOne()) {
console.warn('Default documents must be updated to new parenting format'); console.log('No docs found, filling documentation with defaults');
return;
Assets.getText('docs/defaultDocs.json', (error, string) => { Assets.getText('docs/defaultDocs.json', (error, string) => {
const docs = JSON.parse(string) const docs = JSON.parse(string)
docs.forEach(doc => Docs.insert(doc)); docs.forEach(doc => Docs.insert(doc));
rebuildNestedSets(Docs, DOC_ROOT_ID);
}); });
} }
}); });
@@ -102,18 +120,20 @@ const insertDoc = new ValidatedMethod({
assertDocsEditPermission(this.userId); assertDocsEditPermission(this.userId);
doc.parentId = parentId; doc.parentId = parentId;
doc.root = {
collection: 'docs',
id: DOC_ROOT_ID,
};
const lastOrder = Docs.find({}, { sort: { left: -1 } }).fetch()[0]?.order || 0; const lastOrder = Docs.find({}, { sort: { left: -1 }, limit: 1 }).fetch()[0]?.left || 0;
doc.order = lastOrder + 1;
doc.urlName = 'new-doc-' + (lastOrder + 1); doc.urlName = 'new-doc-' + (lastOrder + 1);
doc.href = getDocLink(doc); doc.href = getDocLink(doc);
if (Docs.findOne({ href: doc.href })) { if (Docs.findOne({ href: doc.href })) {
throw new Meteor.Error('Link collision', 'A document with the same URL already exists'); throw new Meteor.Error('Link collision', 'A document with the same URL already exists');
} }
const docId = Docs.insert(doc); const docId = Docs.insert(doc);
rebuildNestedSets(Docs); rebuildNestedSets(Docs, DOC_ROOT_ID);
return docId; return docId;
}, },
}); });
@@ -135,7 +155,7 @@ const updateDoc = new ValidatedMethod({
}, },
run({ _id, path, value }) { run({ _id, path, value }) {
assertDocsEditPermission(this.userId); assertDocsEditPermission(this.userId);
let pathString = path.join('.'); const pathString = path.join('.');
let modifier; let modifier;
// unset empty values // unset empty values
if (value === null || value === undefined) { if (value === null || value === undefined) {
@@ -145,6 +165,7 @@ const updateDoc = new ValidatedMethod({
} }
if (pathString === 'urlName') { if (pathString === 'urlName') {
const doc = Docs.findOne(_id); 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); const newLink = getDocLink(doc, value);
if (Docs.findOne({ href: newLink })) { if (Docs.findOne({ href: newLink })) {
throw new Meteor.Error('Link collision', 'A document with the same URL already exists'); 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; modifier.$set.href = newLink;
} }
const updates = Docs.update(_id, modifier); const updates = Docs.update(_id, modifier);
rebuildNestedSets(Docs); rebuildNestedSets(Docs, DOC_ROOT_ID);
return updates; return updates;
}, },
}); });
@@ -202,8 +223,8 @@ const softRemoveDoc = new ValidatedMethod({
}, },
run({ _id }) { run({ _id }) {
assertDocsEditPermission(this.userId); assertDocsEditPermission(this.userId);
softRemove({ _id, collection: Docs }); softRemove(Docs, _id);
rebuildNestedSets(Docs); rebuildNestedSets(Docs, DOC_ROOT_ID);
} }
}); });
@@ -220,7 +241,7 @@ const restoreDoc = new ValidatedMethod({
run({ _id }) { run({ _id }) {
assertDocsEditPermission(this.userId); assertDocsEditPermission(this.userId);
restore('docs', _id); restore('docs', _id);
rebuildNestedSets(Docs); rebuildNestedSets(Docs, DOC_ROOT_ID);
} }
}); });
@@ -228,53 +249,27 @@ const organizeDoc = new ValidatedMethod({
name: 'docs.organizeDoc', name: 'docs.organizeDoc',
validate: new SimpleSchema({ validate: new SimpleSchema({
docId: String, docId: String,
parentId: String, newPosition: Number,
order: { skipClient: {
type: Number, type: Boolean,
// Should end in 0.5 to place it reliably between two existing documents optional: true,
}, }
}).validator(), }).validator(),
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
rateLimit: { rateLimit: {
numRequests: 5, numRequests: 5,
timeInterval: 5000, 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 doc = Docs.findOne(docId);
const parent = Docs.findOne(parentId); if (!doc) throw new Meteor.Error('not found', 'The doc you are moving was not found');
// The user must be able to edit both the doc and its parent to move it // Move the doc
// successfully await moveDocWithinRoot(doc, Docs, newPosition);
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);
}, },
}); });
@@ -287,7 +282,6 @@ export {
softRemoveDoc, softRemoveDoc,
restoreDoc, restoreDoc,
organizeDoc, organizeDoc,
reorderDoc,
}; };
export default Docs; export default Docs;

View File

@@ -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 { TreeDoc, treeDocFields, Reference } from '/imports/api/parenting/ChildSchema';
import { getProperties } from '/imports/api/engine/loadCreatures'; import { getProperties } from '/imports/api/engine/loadCreatures';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { Mongo } from 'meteor/mongo';
export function getCollectionByName(name: string): Mongo.Collection<TreeDoc> { export function getCollectionByName(name: string): Mongo.Collection<TreeDoc> {
const collection: Mongo.Collection<TreeDoc> = Mongo.Collection.get(name) const collection = Mongo.Collection.get<TreeDoc>(name)
if (!collection) { if (!collection) {
throw new Meteor.Error('bad-collection-reference', throw new Meteor.Error('bad-collection-reference',
`Parent references collection ${name}, which does not exist` `Parent references collection ${name}, which does not exist`
@@ -119,7 +118,6 @@ export function filterToForest(
if (options.sort) { if (options.sort) {
collectionSort = { collectionSort = {
...collectionSort, ...collectionSort,
// @ts-expect-error go home typescript you're drunk
...options.sort, ...options.sort,
} }
} }

View File

@@ -6,6 +6,8 @@
</template> </template>
<script lang="js"> <script lang="js">
import Docs from '/imports/api/docs/Docs';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
export default { export default {
props: { props: {
@@ -17,24 +19,27 @@ export default {
computed: { computed: {
items() { items() {
const items = [{ const items = [{
text: 'Home', text: 'Docs',
to: '/docs', to: '/docs',
exact: true, exact: true,
}]; }];
if (!this.doc?.ancestors) return items; if (!this.doc) return items;
const address = ['/docs']
this.doc.ancestors?.forEach(a => { const ancestors = Docs.find({
address.push(a.urlName); ...getFilter.ancestors(this.doc)
}).fetch();
ancestors.forEach(a => {
items.push({ items.push({
text: a.name, text: a.name,
to: address.join('/'), to: a.href,
exact: true, exact: true,
}); });
}); });
address.push(this.doc.urlName);
items.push({ items.push({
text: this.doc.name, text: this.doc.name,
to: address.join('/'), to: this.doc.href,
exact: true, exact: true,
}); });
return items; return items;

View File

@@ -183,7 +183,7 @@ export default {
parentId: this.docId, parentId: this.docId,
}, ack); }, ack);
}, },
remove({ ack } = {}) { remove({ ack }) {
const _id = this.docId; const _id = this.docId;
const docName = this.doc.name; const docName = this.doc.name;
let parentHref = '/docs'; let parentHref = '/docs';

View File

@@ -10,20 +10,18 @@
:children="docs" :children="docs"
:organize="true" :organize="true"
:selected-node="undefined" :selected-node="undefined"
:root="{collection: 'docs', id: 'DDDDDDDDDDDDDDDDD'}"
group="docs" group="docs"
@move-within-root="moveWithinRoot"
@selected="selected" @selected="selected"
@reordered="reordered"
@reorganized="reorganized"
/> />
</v-navigation-drawer> </v-navigation-drawer>
</template> </template>
<script lang="js"> <script lang="js">
import Docs, { organizeDoc } from '/imports/api/docs/Docs';
import { docsToForest } from '/imports/api/parenting/parentingFunctions'; import { docsToForest } from '/imports/api/parenting/parentingFunctions';
import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue'; import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
import { organizeDoc, reorderDoc } from '/imports/api/docs/Docs.js';
import Docs from '/imports/api/docs/Docs.js';
export default { export default {
components: { components: {
TreeNodeList, TreeNodeList,
@@ -47,7 +45,7 @@ export default {
return Session.get('editingDocs'); return Session.get('editingDocs');
}, },
docs() { docs() {
const docs = Docs.find({ removed: {$ne: true} }, { sort: {left: 1} }).fetch(); const docs = Docs.find({ removed: { $ne: true } }, { sort: { left: 1 } }).fetch();
return docsToForest(docs); return docsToForest(docs);
}, },
}, },
@@ -55,25 +53,12 @@ export default {
selected(docId) { selected(docId) {
const doc = Docs.findOne(docId); const doc = Docs.findOne(docId);
if (!doc) return; if (!doc) return;
console.log(doc.href);
this.$router.push(doc.href); this.$router.push(doc.href);
}, },
reordered({ doc, newIndex }) { moveWithinRoot({ doc, newPosition }) {
reorderDoc.callAsync({
docId: doc._id,
order: newIndex,
});
},
reorganized({ doc, parent, newIndex }) {
if (!parent) {
this.refreshTree += 1;
console.error('Moving docs to root level isn\'t implemented');
return;
}
organizeDoc.callAsync({ organizeDoc.callAsync({
docId: doc._id, docId: doc._id,
parentId: parent?._id, newPosition,
order: newIndex,
}); });
}, },
} }

View File

@@ -110,7 +110,7 @@ export default {
}, },
childDocs() { childDocs() {
if (!this.doc) return Docs.find({ if (!this.doc) return Docs.find({
'parent': undefined, 'parentId': undefined,
removed: { $ne: true }, removed: { $ne: true },
}, { }, {
sort: { left: 1 } sort: { left: 1 }
@@ -125,7 +125,7 @@ export default {
siblingDocs() { siblingDocs() {
if (!this.doc) return []; if (!this.doc) return [];
return Docs.find({ return Docs.find({
'parentId': this.doc.parent?.id, 'parentId': this.doc.parentId,
removed: { $ne: true }, removed: { $ne: true },
}, { }, {
sort: { left: 1 } sort: { left: 1 }

View File

@@ -1,12 +1,15 @@
import { Migrations } from 'meteor/percolate:migrations'; import { Migrations } from 'meteor/percolate:migrations';
import Libraries from '/imports/api/library/Libraries'; import Libraries from '/imports/api/library/Libraries';
import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions';
import Docs, { DOC_ROOT_ID } from '/imports/api/docs/Docs';
import LibraryNodes from '/imports/api/library/LibraryNodes';
import { TreeDoc } from '/imports/api/parenting/ChildSchema';
// Git version 2.0.59 // Git version 2.0.59
// Database version 3 // Database version 3
Migrations.add({ Migrations.add({
version: 3, version: 3,
name: 'Separates creature property tags from library tags', name: 'Changes parenting from array of ancestors to nested sets',
up: Meteor.wrapAsync(async (_, next) => { up: Meteor.wrapAsync(async (_, next) => {
console.log('migrating up library nodes 2 -> 3'); console.log('migrating up library nodes 2 -> 3');
migrateCollection('libraryNodes'); migrateCollection('libraryNodes');
@@ -16,12 +19,21 @@ Migrations.add({
migrateCollection('docs'); migrateCollection('docs');
console.log('New schema fields added, if it was done correctly remove the old fields manually'); console.log('New schema fields added, if it was done correctly remove the old fields manually');
console.log('Rebuilding nested sets for all libraries'); console.log('Rebuilding nested sets for all libraries'); // Characters rebuild themselves on recompute
const libraryIds = await Mongo.Collection.get('libraries').find().mapAsync((library) => library._id); const libraryIds = await Libraries.find().mapAsync((library) => library._id);
for (const [index, libraryId] of libraryIds.entries()) { for (const [index, libraryId] of libraryIds.entries()) {
console.log('Rebuilding nested sets for library', index + 1, 'of', libraryIds.length); console.log('Rebuilding nested sets for library', index + 1, 'of', libraryIds.length);
await rebuildNestedSets(Mongo.Collection.get('libraryNodes'), libraryId); await rebuildNestedSets(LibraryNodes as Mongo.Collection<TreeDoc>, libraryId);
} }
console.log('Removing all docs and replacing them with default docs');
Docs.remove({});
Assets.getText('docs/defaultDocs.json', (error, string) => {
const docs = JSON.parse(string)
docs.forEach(doc => Docs.insert(doc));
});
rebuildNestedSets(Docs, DOC_ROOT_ID);
next(); next();
}), }),
@@ -32,7 +44,6 @@ Migrations.add({
}); });
export function migrateCollection(collectionName: string) { export function migrateCollection(collectionName: string) {
// @ts-expect-error Collection.get is not defined
const collection = Mongo.Collection.get(collectionName); const collection = Mongo.Collection.get(collectionName);
// Copy the parent id field and the root ancestor to the new structure // Copy the parent id field and the root ancestor to the new structure
// Using the mongo aggregation API // Using the mongo aggregation API

File diff suppressed because one or more lines are too long