Files
DiceCloud/app/imports/api/docs/Docs.js
2023-10-03 16:28:20 +02:00

236 lines
5.8 KiB
JavaScript

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';
import { softRemove } from '/imports/api/parenting/softRemove';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema';
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 } from '/imports/api/parenting/parentingFunctions';
import ChildSchema from '/imports/api/parenting/ChildSchema';
const Docs = new Mongo.Collection('docs');
let DocSchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
name: {
type: String,
max: STORAGE_LIMITS.description,
},
urlName: {
type: String,
regEx: /[a-z]+(?:[a-z]|-)+/,
min: 2,
max: STORAGE_LIMITS.variableName,
},
href: {
type: String,
},
description: {
type: String,
optional: true,
},
published: {
type: Boolean,
optional: true,
},
icon: {
type: storedIconsSchema,
optional: true,
max: STORAGE_LIMITS.icon,
},
});
let schema = new SimpleSchema({});
schema.extend(DocSchema);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
Docs.attachSchema(schema);
function assertDocsEditPermission(userId) {
if (!userId || typeof userId !== 'string') throw new Meteor.Error('No user id provided');
const user = Meteor.users.findOne(userId);
if (!user) throw new Meteor.Error('User does not exist');
if (!user?.roles?.includes?.('docsWriter')) throw ('Permission denied')
}
function getDocLink(doc, urlName) {
if (!urlName) urlName = doc.urlName;
const address = ['/docs'];
const ancestorDocs = Docs.find(getFilter.ancestors(doc));
ancestorDocs?.forEach(a => {
address.push(a.urlName);
});
address.push(urlName);
return address.join('/');
}
// Add a means of seeding new servers with documentation
if (Meteor.isClient) {
Docs.getJsonDocs = function () {
return JSON.stringify(Docs.find({}).fetch(), null, 2);
}
} else if (Meteor.isServer) {
Meteor.startup(() => {
if (!Docs.findOne()) {
Assets.getText('docs/defaultDocs.json', (error, string) => {
const docs = JSON.parse(string)
docs.forEach(doc => Docs.insert(doc));
});
}
});
}
const insertDoc = new ValidatedMethod({
name: 'docs.insert',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ doc, parentId }) {
delete doc._id;
assertDocsEditPermission(this.userId);
doc.parentId = parentId;
const lastOrder = Docs.find({}, { sort: { order: -1 } }).fetch()[0]?.order || 0;
doc.order = lastOrder + 1;
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);
return docId;
},
});
const updateDoc = new ValidatedMethod({
name: 'docs.update',
validate({ _id, path }) {
if (!_id) return false;
// We cannot change these fields with a simple update
switch (path[0]) {
case '_is':
return false;
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, path, value }) {
assertDocsEditPermission(this.userId);
let pathString = path.join('.');
let modifier;
// unset empty values
if (value === null || value === undefined) {
modifier = { $unset: { [pathString]: 1 } };
} else {
modifier = { $set: { [pathString]: value } };
}
if (pathString === 'urlName') {
const doc = Docs.findOne(_id);
const newLink = getDocLink(doc, value);
if (Docs.findOne({ href: newLink })) {
throw new Meteor.Error('Link collision', 'A document with the same URL already exists');
}
modifier.$set = modifier.$set || {};
modifier.$set.href = newLink;
}
const updates = Docs.update(_id, modifier);
rebuildNestedSets(Docs);
return updates;
},
});
const pushToDoc = new ValidatedMethod({
name: 'docs.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, path, value }) {
assertDocsEditPermission(this.userId);
return Docs.update(_id, {
$push: { [path.join('.')]: value },
});
}
});
const pullFromDoc = new ValidatedMethod({
name: 'docs.pull',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, path, itemId }) {
assertDocsEditPermission(this.userId);
return Docs.update(_id, {
$pull: { [path.join('.')]: { _id: itemId } },
});
}
});
const softRemoveDoc = new ValidatedMethod({
name: 'docs.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id }) {
assertDocsEditPermission(this.userId);
softRemove({ _id, collection: Docs });
rebuildNestedSets(Docs);
}
});
const restoreDoc = new ValidatedMethod({
name: 'docs.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id }) {
assertDocsEditPermission(this.userId);
restore('docs', _id);
rebuildNestedSets(Docs);
}
});
export {
DocSchema,
insertDoc,
updateDoc,
pushToDoc,
pullFromDoc,
softRemoveDoc,
restoreDoc,
};
export default Docs;