Files
DiceCloud/app/imports/api/library/LibraryNodes.ts
2025-05-02 13:37:11 +02:00

327 lines
8.9 KiB
TypeScript

import { Meteor } from 'meteor/meteor';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema';
import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema';
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex';
import Libraries from '/imports/api/library/Libraries';
import { assertDocEditPermission, assertEditPermission } from '/imports/api/sharing/sharingPermissions';
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 { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import { restore } from '/imports/api/parenting/softRemove';
import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions';
import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions';
import { ConvertToUnion, InferType, TypedSimpleSchema } from '/imports/api/utility/TypedSimpleSchema';
import type { PropertyType } from '/imports/api/properties/PropertyType.type';
import { Simplify } from 'type-fest';
const LibraryNodeSchema = TypedSimpleSchema.from({
_id: {
type: String,
max: 32,
},
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
icon: {
type: storedIconsSchema,
optional: true,
max: STORAGE_LIMITS.icon,
},
// Library-specific properties, these can be stripped from the resulting
// creature properties
// Will this property show up in the slot-fill dialog
fillSlots: {
type: Boolean,
optional: true,
},
// Will this property show up in the insert-from-library dialog
searchable: {
type: Boolean,
optional: true,
},
libraryTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.tagCount,
},
'libraryTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
// Overrides the type when searching for properties
slotFillerType: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// Image to display when filling the slot
slotFillImage: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
// Fill more than one quantity in a slot, like feats and ability score
// improvements, filtered out of UI if there isn't space in quantityExpected
slotQuantityFilled: {
type: SimpleSchema.Integer,
optional: true, // Undefined implies 1
},
// Filters out of UI if condition isn't met, but isn't otherwise enforced
slotFillerCondition: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
// Text to display if slot filler condition fails
slotFillerConditionNote: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
});
export type LibraryNodeTypes = {
[T in PropertyType]: Simplify<
{ type: T }
& InferType<typeof propertySchemasIndex[T]>
> & Simplify<
Exclude<InferType<typeof LibraryNodeSchema>, 'type'>
& InferType<typeof ColorSchema>
& InferType<typeof ChildSchema>
& InferType<typeof SoftRemovableSchema>
>
}
export type LibraryNode = ConvertToUnion<LibraryNodeTypes>;
const LibraryNodes = new Mongo.Collection<LibraryNode>('libraryNodes');
// Set up server side search index
if (Meteor.isServer) {
LibraryNodes.createIndexAsync({
'name': 'text',
'tags': 'text',
});
}
const genericLibraryNodeSchema = TypedSimpleSchema.from({})
.extend(LibraryNodeSchema)
.extend(ColorSchema)
.extend(ChildSchema)
.extend(SoftRemovableSchema);
// Attach the default schema
LibraryNodes.attachSchema(genericLibraryNodeSchema);
// Attach the schemas for each type
let key: keyof typeof propertySchemasIndex;
for (key in propertySchemasIndex) {
const schema = TypedSimpleSchema.from({})
.extend(propertySchemasIndex[key])
.extend(genericLibraryNodeSchema);
LibraryNodes.attachSchema(schema, {
selector: { type: key }
});
}
const insertNode = new ValidatedMethod({
name: 'libraryNodes.insert',
validate: new SimpleSchema({
libraryNode: {
type: Object,
blackbox: true,
},
parentRef: RefSchema,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ libraryNode, parentRef }) {
// get the new ancestry
const parentDoc = fetchDocByRef(parentRef);
// Check permission to edit
let rootLibrary;
if (parentRef.collection === 'libraries') {
rootLibrary = parentDoc;
} else if (parentRef.collection === 'libraryNodes') {
rootLibrary = Libraries.findOne(parentDoc.root.id);
libraryNode.parentId = parentRef.id;
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootLibrary, this.userId);
// Set the root of the node we are inserting
libraryNode.root = { collection: 'libraries', id: rootLibrary._id };
// Remove its ID if it came with one to force a random one to be generated
// server-side
delete libraryNode._id;
// Insert the node
const nodeId = LibraryNodes.insert(libraryNode);
// Update the node if it was a reference node
if (libraryNode.type == 'reference') {
libraryNode._id = nodeId;
updateReferenceNodeWork(libraryNode, this.userId);
}
// Tree structure changed by insert, reorder the tree
rebuildNestedSets(LibraryNodes, rootLibrary._id);
// Return the id of the inserted node
return nodeId;
},
});
const updateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.update',
validate({ _id, path }) {
if (!_id) return false;
// We cannot change these fields with a simple update
switch (path[0]) {
case 'type':
case 'order':
case 'parent':
case 'ancestors':
case 'parentId':
case 'root':
return false;
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 15,
timeInterval: 5000,
},
run({ _id, path, value }) {
let node = LibraryNodes.findOne(_id);
assertDocEditPermission(node, this.userId);
const pathString = path.join('.');
let modifier;
// unset empty values
if (value === null || value === undefined) {
modifier = { $unset: { [pathString]: 1 } };
} else {
modifier = { $set: { [pathString]: value } };
}
const numUpdated = LibraryNodes.update(_id, modifier, {
selector: { type: node.type },
});
if (node.type == 'reference') {
node = LibraryNodes.findOne(_id);
updateReferenceNodeWork(node, this.userId);
}
return numUpdated;
},
});
const pushToLibraryNode = new ValidatedMethod({
name: 'libraryNodes.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, path, value }) {
const node = LibraryNodes.findOne(_id);
assertDocEditPermission(node, this.userId);
return LibraryNodes.update(_id, {
$push: { [path.join('.')]: value },
}, {
selector: { type: node.type },
});
}
});
const pullFromLibraryNode = new ValidatedMethod({
name: 'libraryNodes.pull',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, path, itemId }) {
const node = LibraryNodes.findOne(_id);
assertDocEditPermission(node, this.userId);
return LibraryNodes.update(_id, {
$pull: { [path.join('.')]: { _id: itemId } },
}, {
selector: { type: node.type },
getAutoValues: false,
});
}
});
const softRemoveLibraryNode = new ValidatedMethod({
name: 'libraryNodes.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id }) {
const node = LibraryNodes.findOne(_id);
assertDocEditPermission(node, this.userId);
softRemove(LibraryNodes, node);
}
});
const restoreLibraryNode = new ValidatedMethod({
name: 'libraryNodes.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id }) {
// Permissions
const node = LibraryNodes.findOne(_id);
if (!node) return;
assertDocEditPermission(node, this.userId);
// Do work
restore(LibraryNodes, node);
}
});
export default LibraryNodes;
export {
LibraryNodeSchema,
insertNode,
updateLibraryNode,
pullFromLibraryNode,
pushToLibraryNode,
softRemoveLibraryNode,
restoreLibraryNode,
};