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 { 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<Doc> & {
getJsonDocs?: () => string
} = new Mongo.Collection<Doc>('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;

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 { 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<TreeDoc> {
const collection: Mongo.Collection<TreeDoc> = Mongo.Collection.get(name)
const collection = Mongo.Collection.get<TreeDoc>(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,
}
}

View File

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

View File

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

View File

@@ -10,20 +10,18 @@
:children="docs"
:organize="true"
:selected-node="undefined"
:root="{collection: 'docs', id: 'DDDDDDDDDDDDDDDDD'}"
group="docs"
@move-within-root="moveWithinRoot"
@selected="selected"
@reordered="reordered"
@reorganized="reorganized"
/>
</v-navigation-drawer>
</template>
<script lang="js">
import Docs, { organizeDoc } from '/imports/api/docs/Docs';
import { docsToForest } from '/imports/api/parenting/parentingFunctions';
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 {
components: {
TreeNodeList,
@@ -47,7 +45,7 @@ export default {
return Session.get('editingDocs');
},
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);
},
},
@@ -55,25 +53,12 @@ export default {
selected(docId) {
const doc = Docs.findOne(docId);
if (!doc) return;
console.log(doc.href);
this.$router.push(doc.href);
},
reordered({ doc, newIndex }) {
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;
}
moveWithinRoot({ doc, newPosition }) {
organizeDoc.callAsync({
docId: doc._id,
parentId: parent?._id,
order: newIndex,
newPosition,
});
},
}

View File

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

View File

@@ -1,12 +1,15 @@
import { Migrations } from 'meteor/percolate:migrations';
import Libraries from '/imports/api/library/Libraries';
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
// Database version 3
Migrations.add({
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) => {
console.log('migrating up library nodes 2 -> 3');
migrateCollection('libraryNodes');
@@ -16,12 +19,21 @@ Migrations.add({
migrateCollection('docs');
console.log('New schema fields added, if it was done correctly remove the old fields manually');
console.log('Rebuilding nested sets for all libraries');
const libraryIds = await Mongo.Collection.get('libraries').find().mapAsync((library) => library._id);
console.log('Rebuilding nested sets for all libraries'); // Characters rebuild themselves on recompute
const libraryIds = await Libraries.find().mapAsync((library) => library._id);
for (const [index, libraryId] of libraryIds.entries()) {
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();
}),
@@ -32,7 +44,6 @@ Migrations.add({
});
export function migrateCollection(collectionName: string) {
// @ts-expect-error Collection.get is not defined
const collection = Mongo.Collection.get(collectionName);
// Copy the parent id field and the root ancestor to the new structure
// Using the mongo aggregation API

File diff suppressed because one or more lines are too long