From d42d2a724e774e8cbc467a4e15b012f2385c1755 Mon Sep 17 00:00:00 2001 From: ThaumRystra Date: Fri, 2 May 2025 13:37:11 +0200 Subject: [PATCH] Fixed library nodes not having a default schema --- app/.gitignore | 1 + .../@types/ddp-rate-limiter-mixin.d.ts | 3 + app/imports/@types/meteor.d.ts | 10 +-- app/imports/@types/mongo.d.ts | 13 +++ .../creatureProperties/CreatureProperties.ts | 2 +- .../{LibraryNodes.js => LibraryNodes.ts} | 90 ++++++++++--------- app/imports/api/parenting/ChildSchema.ts | 1 - app/imports/api/parenting/organizeMethods.js | 2 +- .../api/parenting/parentingFunctions.ts | 8 +- ...ngPermissions.js => sharingPermissions.ts} | 80 ++++++++++------- .../client/ui/library/LibraryAndNode.vue | 2 +- app/imports/migrations/server/dbv3/dbv3.ts | 10 ++- app/tsconfig.json | 3 +- 13 files changed, 134 insertions(+), 91 deletions(-) create mode 100644 app/imports/@types/ddp-rate-limiter-mixin.d.ts rename app/imports/api/library/{LibraryNodes.js => LibraryNodes.ts} (78%) rename app/imports/api/sharing/{sharingPermissions.js => sharingPermissions.ts} (64%) diff --git a/app/.gitignore b/app/.gitignore index 67d0a229..2778084e 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -14,3 +14,4 @@ private/oldClient nohup.out node_modules dump +*.crt diff --git a/app/imports/@types/ddp-rate-limiter-mixin.d.ts b/app/imports/@types/ddp-rate-limiter-mixin.d.ts new file mode 100644 index 00000000..2f5b5609 --- /dev/null +++ b/app/imports/@types/ddp-rate-limiter-mixin.d.ts @@ -0,0 +1,3 @@ +declare module 'ddp-rate-limiter-mixin' { + export const RateLimiterMixin: (options: T) => T; +} diff --git a/app/imports/@types/meteor.d.ts b/app/imports/@types/meteor.d.ts index d590b723..65c6438e 100644 --- a/app/imports/@types/meteor.d.ts +++ b/app/imports/@types/meteor.d.ts @@ -1,7 +1,5 @@ -declare module 'meteor/meteor' { - namespace Meteor { - interface User { - roles?: string[]; - } +declare namespace Meteor { + interface User { + roles?: string[]; } -} \ No newline at end of file +} diff --git a/app/imports/@types/mongo.d.ts b/app/imports/@types/mongo.d.ts index 5a59ebe3..0cadf013 100644 --- a/app/imports/@types/mongo.d.ts +++ b/app/imports/@types/mongo.d.ts @@ -32,5 +32,18 @@ declare namespace Mongo { * */ attachSchema(ss: SimpleSchema | TypedSimpleSchema, options?: SchemaOptions): void; + update( + selector: Selector | ObjectID | string, + modifier: Modifier, + options?: { + multi?: boolean | undefined; + upsert?: boolean | undefined; + arrayFilters?: Array<{ [identifier: string]: any }> | undefined; + // Add Collection2 options + selector?: Record; + getAutoValues?: boolean; + }, + callback?: FunctionConstructor, + ): number; } } diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.ts b/app/imports/api/creature/creatureProperties/CreatureProperties.ts index 2bde333a..3042158e 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.ts +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.ts @@ -143,7 +143,7 @@ export type CreaturePropertyTypes = { > } -export type CreatureProperty = ConvertToUnion; +export type CreatureProperty = Simplify>; const CreatureProperties = new Mongo.Collection('creatureProperties'); diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.ts similarity index 78% rename from app/imports/api/library/LibraryNodes.js rename to app/imports/api/library/LibraryNodes.ts index 0231c9e0..05bf30f3 100644 --- a/app/imports/api/library/LibraryNodes.js +++ b/app/imports/api/library/LibraryNodes.ts @@ -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'; @@ -7,7 +6,7 @@ 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 { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +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'; @@ -15,12 +14,13 @@ 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, getAncestry } from '/imports/api/parenting/parentingFunctions'; +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'; -let LibraryNodes = new Mongo.Collection('libraryNodes'); - -let LibraryNodeSchema = new SimpleSchema({ +const LibraryNodeSchema = TypedSimpleSchema.from({ _id: { type: String, max: 32, @@ -51,13 +51,11 @@ let LibraryNodeSchema = new SimpleSchema({ fillSlots: { type: Boolean, optional: true, - index: 1, }, // Will this property show up in the insert-from-library dialog searchable: { type: Boolean, optional: true, - index: 1, }, libraryTags: { type: Array, @@ -100,39 +98,50 @@ let LibraryNodeSchema = new SimpleSchema({ }, }); +export type LibraryNodeTypes = { + [T in PropertyType]: Simplify< + { type: T } + & InferType + > & Simplify< + Exclude, 'type'> + & InferType + & InferType + & InferType + > +} + +export type LibraryNode = ConvertToUnion; + +const LibraryNodes = new Mongo.Collection('libraryNodes'); + // Set up server side search index if (Meteor.isServer) { - LibraryNodes._ensureIndex({ + LibraryNodes.createIndexAsync({ 'name': 'text', 'tags': 'text', }); } -for (let key in propertySchemasIndex) { - let schema = new SimpleSchema({}); - schema.extend(LibraryNodeSchema); - schema.extend(ColorSchema); - schema.extend(propertySchemasIndex[key]); - schema.extend(ChildSchema); - schema.extend(SoftRemovableSchema); - // @ts-expect-error don't have types for .attachSchema +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 } }); } -function getLibrary(node) { - if (!node) throw new Meteor.Error('No node provided'); - let library = Libraries.findOne(node.root.id); - if (!library) throw new Meteor.Error('Library does not exist'); - return library; -} - -function assertNodeEditPermission(node, userId) { - let lib = getLibrary(node); - return assertEditPermission(lib, userId); -} - const insertNode = new ValidatedMethod({ name: 'libraryNodes.insert', validate: new SimpleSchema({ @@ -209,8 +218,8 @@ const updateLibraryNode = new ValidatedMethod({ }, run({ _id, path, value }) { let node = LibraryNodes.findOne(_id); - assertNodeEditPermission(node, this.userId); - let pathString = path.join('.'); + assertDocEditPermission(node, this.userId); + const pathString = path.join('.'); let modifier; // unset empty values if (value === null || value === undefined) { @@ -218,7 +227,7 @@ const updateLibraryNode = new ValidatedMethod({ } else { modifier = { $set: { [pathString]: value } }; } - let numUpdated = LibraryNodes.update(_id, modifier, { + const numUpdated = LibraryNodes.update(_id, modifier, { selector: { type: node.type }, }); if (node.type == 'reference') { @@ -238,8 +247,8 @@ const pushToLibraryNode = new ValidatedMethod({ timeInterval: 5000, }, run({ _id, path, value }) { - let node = LibraryNodes.findOne(_id); - assertNodeEditPermission(node, this.userId); + const node = LibraryNodes.findOne(_id); + assertDocEditPermission(node, this.userId); return LibraryNodes.update(_id, { $push: { [path.join('.')]: value }, }, { @@ -257,8 +266,8 @@ const pullFromLibraryNode = new ValidatedMethod({ timeInterval: 5000, }, run({ _id, path, itemId }) { - let node = LibraryNodes.findOne(_id); - assertNodeEditPermission(node, this.userId); + const node = LibraryNodes.findOne(_id); + assertDocEditPermission(node, this.userId); return LibraryNodes.update(_id, { $pull: { [path.join('.')]: { _id: itemId } }, }, { @@ -279,8 +288,8 @@ const softRemoveLibraryNode = new ValidatedMethod({ timeInterval: 5000, }, run({ _id }) { - let node = LibraryNodes.findOne(_id); - assertNodeEditPermission(node, this.userId); + const node = LibraryNodes.findOne(_id); + assertDocEditPermission(node, this.userId); softRemove(LibraryNodes, node); } }); @@ -297,8 +306,9 @@ const restoreLibraryNode = new ValidatedMethod({ }, run({ _id }) { // Permissions - let node = LibraryNodes.findOne(_id); - assertNodeEditPermission(node, this.userId); + const node = LibraryNodes.findOne(_id); + if (!node) return; + assertDocEditPermission(node, this.userId); // Do work restore(LibraryNodes, node); } diff --git a/app/imports/api/parenting/ChildSchema.ts b/app/imports/api/parenting/ChildSchema.ts index 2200b3b7..bafe1378 100644 --- a/app/imports/api/parenting/ChildSchema.ts +++ b/app/imports/api/parenting/ChildSchema.ts @@ -1,4 +1,3 @@ -import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; import { InferType, TypedSimpleSchema } from '/imports/api/utility/TypedSimpleSchema'; import type { Simplify } from 'type-fest'; diff --git a/app/imports/api/parenting/organizeMethods.js b/app/imports/api/parenting/organizeMethods.js index 31d93fce..1adde4b8 100644 --- a/app/imports/api/parenting/organizeMethods.js +++ b/app/imports/api/parenting/organizeMethods.js @@ -2,7 +2,7 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RefSchema } from '/imports/api/parenting/ChildSchema'; -import { assertDocEditPermission, assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import { assertDocEditPermission, assertEditPermission } from '/imports/api/sharing/sharingPermissions'; import { compact } from 'lodash'; import Creatures from '/imports/api/creature/creatures/Creatures'; import { fetchDocByRefAsync, getCollectionByName, moveDocBetweenRoots, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions'; diff --git a/app/imports/api/parenting/parentingFunctions.ts b/app/imports/api/parenting/parentingFunctions.ts index efbab728..fb55673b 100644 --- a/app/imports/api/parenting/parentingFunctions.ts +++ b/app/imports/api/parenting/parentingFunctions.ts @@ -3,8 +3,8 @@ import { TreeDoc, treeDocFields, Reference } from '/imports/api/parenting/ChildS import { getProperties } from '/imports/api/engine/loadCreatures'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; -export function getCollectionByName(name: string): Mongo.Collection { - const collection = Mongo.Collection.get(name) +export function getCollectionByName(name: string): Mongo.Collection { + const collection = Mongo.Collection.get(name) if (!collection) { throw new Meteor.Error('bad-collection-reference', `Parent references collection ${name}, which does not exist` @@ -27,8 +27,8 @@ export function fetchDocByRefAsync(ref: Reference, options?: Mongo.Options): TreeDoc { - const doc: TreeDoc = getCollectionByName(ref.collection).findOne(ref.id, options); +export function fetchDocByRef(ref: Reference, options?: Mongo.Options): T { + const doc: T = getCollectionByName(ref.collection).findOne(ref.id, options); assertDocFound(doc, ref); return doc; } diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.ts similarity index 64% rename from app/imports/api/sharing/sharingPermissions.js rename to app/imports/api/sharing/sharingPermissions.ts index 8c600f92..fd3b110c 100644 --- a/app/imports/api/sharing/sharingPermissions.js +++ b/app/imports/api/sharing/sharingPermissions.ts @@ -1,26 +1,28 @@ import { includes } from 'lodash'; import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; +import type { Shared } from '/imports/api/sharing/SharingSchema'; +import type { TreeDoc } from '/imports/api/parenting/ChildSchema'; -function assertIdValid(userId) { +function assertIdValid(userId: string | undefined | null): asserts userId { if (!userId || typeof userId !== 'string') { throw new Meteor.Error('Permission denied', 'No user ID. Are you logged in?'); } } -function assertdocExists(doc) { +function assertDocExists(doc: Record | undefined): asserts doc { if (!doc) { throw new Meteor.Error('Permission denied', 'Permission denied: No such document exists'); } } -export function assertOwnership(doc, userId) { +export function assertOwnership(doc: Shared, userId: string): asserts doc { assertIdValid(userId); - assertdocExists(doc); + assertDocExists(doc); if (doc.owner === userId) { - return true; + return; } else { throw new Meteor.Error('Permission denied', 'You are not the owner of this document'); @@ -33,18 +35,22 @@ export function assertOwnership(doc, userId) { * * Warning: the doc and userId must be set by a trusted source */ -export function assertEditPermission(doc, userId) { +export function assertEditPermission(doc: Shared | undefined, userId: string | undefined | null): asserts doc { assertIdValid(userId); - assertdocExists(doc); + assertDocExists(doc); const user = Meteor.users.findOne(userId, { fields: { 'roles': 1, } }); + if (!user) { + throw new Meteor.Error('Edit permission denied', + 'No such user exists'); + } // Admin override if (user.roles && user.roles.includes('admin')) { - return true; + return; } // Ensure the user is authorized for this specific document @@ -52,7 +58,7 @@ export function assertEditPermission(doc, userId) { doc.owner === userId || includes(doc.writers, userId) ) { - return true; + return; } else { throw new Meteor.Error('Edit permission denied', 'You do not have permission to edit this document'); @@ -65,18 +71,23 @@ export function assertEditPermission(doc, userId) { * * Warning: the doc and userId must be set by a trusted source */ -export function assertCopyPermission(doc, userId) { +export function assertCopyPermission(doc: Shared, userId): asserts doc { assertIdValid(userId); - assertdocExists(doc); + assertDocExists(doc); const user = Meteor.users.findOne(userId, { fields: { 'roles': 1, } }); + if (!user) { + throw new Meteor.Error('Edit permission denied', + 'No such user exists'); + } + // Admin override if (user.roles && user.roles.includes('admin')) { - return true; + return; } // Ensure the user is authorized for this specific document @@ -84,22 +95,22 @@ export function assertCopyPermission(doc, userId) { doc.owner === userId || includes(doc.writers, userId) ) { - return true; + return; } else if ( (includes(doc.readers, userId) || doc.public) && doc.readersCanCopy ) { - return true; + return; } else { throw new Meteor.Error('Copy permission denied', 'You do not have permission to copy this document'); } } -function getRoot(doc) { - assertdocExists(doc); - if (doc.root) { - return fetchDocByRef(doc.root); +function getRoot(doc: TreeDoc | Shared | undefined) { + assertDocExists(doc); + if ('root' in doc) { + return fetchDocByRef(doc.root); } else { return doc; } @@ -111,8 +122,8 @@ function getRoot(doc) { * * Warning: the doc and userId must be set by a trusted source */ -export function assertDocEditPermission(doc, userId) { - let root = getRoot(doc); +export function assertDocEditPermission(doc: TreeDoc | Shared | undefined, userId: string | null): asserts doc { + const root = getRoot(doc); assertEditPermission(root, userId); } @@ -122,14 +133,14 @@ export function assertDocEditPermission(doc, userId) { * * Warning: the doc and userId must be set by a trusted source */ -export function assertDocCopyPermission(doc, userId) { - let root = getRoot(doc); +export function assertDocCopyPermission(doc, userId): asserts doc { + const root = getRoot(doc); assertCopyPermission(root, userId); } -export function assertViewPermission(doc, userId) { - assertdocExists(doc); - if (doc.public) return true; +export function assertViewPermission(doc, userId): asserts doc { + assertDocExists(doc); + if (doc.public) return; assertIdValid(userId); if ( @@ -137,7 +148,7 @@ export function assertViewPermission(doc, userId) { includes(doc.readers, userId) || includes(doc.writers, userId) ) { - return true; + return; } else { // Admin override @@ -146,8 +157,13 @@ export function assertViewPermission(doc, userId) { 'roles': 1, } }); + if (!user) { + throw new Meteor.Error('Edit permission denied', + 'No such user exists'); + } + if (user.roles && user.roles.includes('admin')) { - return true; + return; } throw new Meteor.Error('View permission denied', @@ -161,19 +177,19 @@ export function assertViewPermission(doc, userId) { * * Warning: the doc and userId must be set by a trusted source */ -export function assertDocViewPermission(doc, userId) { - let root = getRoot(doc); +export function assertDocViewPermission(doc, userId): asserts doc { + const root = getRoot(doc); assertViewPermission(root, userId); } -export function assertAdmin(userId) { +export function assertAdmin(userId): asserts userId { assertIdValid(userId); - let user = Meteor.users.findOne(userId, { fields: { roles: 1 } }); + const user = Meteor.users.findOne(userId, { fields: { roles: 1 } }); if (!user) { throw new Meteor.Error('Permission denied', 'UserId does not match any existing user'); } - let isAdmin = user.roles && user.roles.includes('admin') + const isAdmin = user.roles && user.roles.includes('admin') if (!isAdmin) { throw new Meteor.Error('Permission denied', 'User does not have the admin role'); diff --git a/app/imports/client/ui/library/LibraryAndNode.vue b/app/imports/client/ui/library/LibraryAndNode.vue index a050837d..8638749e 100644 --- a/app/imports/client/ui/library/LibraryAndNode.vue +++ b/app/imports/client/ui/library/LibraryAndNode.vue @@ -28,8 +28,8 @@ ref="searchBox" v-model="filter" class="mx-4" - @extra-fields-changed="val => extraFields = val" :is-library="true" + @extra-fields-changed="val => extraFields = val" /> diff --git a/app/imports/migrations/server/dbv3/dbv3.ts b/app/imports/migrations/server/dbv3/dbv3.ts index ae07197c..d5842d6d 100644 --- a/app/imports/migrations/server/dbv3/dbv3.ts +++ b/app/imports/migrations/server/dbv3/dbv3.ts @@ -13,11 +13,11 @@ Migrations.add({ 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'); + await migrateCollection('libraryNodes'); console.log('migrating up creature props 2 -> 3'); - migrateCollection('creatureProperties'); + await migrateCollection('creatureProperties'); console.log('migrating up docs 2 -> 3'); - migrateCollection('docs'); + await migrateCollection('docs'); console.log('New parenting schema fields added, if it was done correctly remove the old fields manually'); console.log('removing all CreatureVariables, creatures will add them back the next time they recalculate'); @@ -52,7 +52,9 @@ export function migrateCollection(collectionName: string) { // Copy the parent id field and the root ancestor to the new structure // Using the mongo aggregation API // Waring: This will destroy parenting data if the old parenting fields are deleted - return collection.rawCollection().updateMany({}, [ + return collection.rawCollection().updateMany({ + 'root.id': undefined, + }, [ { $addFields: { 'root': { $arrayElemAt: ['$ancestors', 0] }, diff --git a/app/tsconfig.json b/app/tsconfig.json index f81d14b9..a41b7f9b 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -12,6 +12,7 @@ "strict": true, "strictNullChecks": true, "strictFunctionTypes": true, + "noImplicitAny": false, "baseUrl": ".", "preserveSymlinks": true, "allowJs": true, @@ -20,7 +21,7 @@ "outDir": "build", "paths": { "/*": [ - "./*" + "*" ], "meteor/*": [ "node_modules/@types/meteor/*",