Updated documentation to new parenting format
This commit is contained in:
3
app/imports/@types/ddp.d.ts
vendored
Normal file
3
app/imports/@types/ddp.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare namespace DDP {
|
||||
function randomStream(seed: string): typeof Random;
|
||||
}
|
||||
7
app/imports/@types/meteor.d.ts
vendored
Normal file
7
app/imports/@types/meteor.d.ts
vendored
Normal 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
7
app/imports/@types/mongo.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare namespace Mongo {
|
||||
interface CollectionStatic {
|
||||
get: <T>(
|
||||
collectionName: string, options?: { connection: Meteor.Connection }
|
||||
) => Mongo.Collection<T>;
|
||||
}
|
||||
}
|
||||
27
app/imports/@types/validated-method.d.ts
vendored
Normal file
27
app/imports/@types/validated-method.d.ts
vendored
Normal 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>);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user