Fixed library nodes not having a default schema

This commit is contained in:
ThaumRystra
2025-05-02 13:37:11 +02:00
parent 8453bd9d86
commit d42d2a724e
13 changed files with 134 additions and 91 deletions

1
app/.gitignore vendored
View File

@@ -14,3 +14,4 @@ private/oldClient
nohup.out nohup.out
node_modules node_modules
dump dump
*.crt

View File

@@ -0,0 +1,3 @@
declare module 'ddp-rate-limiter-mixin' {
export const RateLimiterMixin: <T>(options: T) => T;
}

View File

@@ -1,7 +1,5 @@
declare module 'meteor/meteor' { declare namespace Meteor {
namespace Meteor { interface User {
interface User { roles?: string[];
roles?: string[];
}
} }
} }

View File

@@ -32,5 +32,18 @@ declare namespace Mongo {
* *
*/ */
attachSchema(ss: SimpleSchema | TypedSimpleSchema<T>, options?: SchemaOptions): void; attachSchema(ss: SimpleSchema | TypedSimpleSchema<T>, options?: SchemaOptions): void;
update(
selector: Selector<T> | ObjectID | string,
modifier: Modifier<T>,
options?: {
multi?: boolean | undefined;
upsert?: boolean | undefined;
arrayFilters?: Array<{ [identifier: string]: any }> | undefined;
// Add Collection2 options
selector?: Record<string, any>;
getAutoValues?: boolean;
},
callback?: FunctionConstructor,
): number;
} }
} }

View File

@@ -143,7 +143,7 @@ export type CreaturePropertyTypes = {
> >
} }
export type CreatureProperty = ConvertToUnion<CreaturePropertyTypes>; export type CreatureProperty = Simplify<ConvertToUnion<CreaturePropertyTypes>>;
const CreatureProperties = new Mongo.Collection<CreatureProperty>('creatureProperties'); const CreatureProperties = new Mongo.Collection<CreatureProperty>('creatureProperties');

View File

@@ -1,5 +1,4 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema'; 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 ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema';
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex'; import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex';
import Libraries from '/imports/api/library/Libraries'; 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 { softRemove } from '/imports/api/parenting/softRemove';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema'; import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema';
import { storedIconsSchema } from '/imports/api/icons/Icons'; 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 { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import { restore } from '/imports/api/parenting/softRemove'; 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 { 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'); const LibraryNodeSchema = TypedSimpleSchema.from({
let LibraryNodeSchema = new SimpleSchema({
_id: { _id: {
type: String, type: String,
max: 32, max: 32,
@@ -51,13 +51,11 @@ let LibraryNodeSchema = new SimpleSchema({
fillSlots: { fillSlots: {
type: Boolean, type: Boolean,
optional: true, optional: true,
index: 1,
}, },
// Will this property show up in the insert-from-library dialog // Will this property show up in the insert-from-library dialog
searchable: { searchable: {
type: Boolean, type: Boolean,
optional: true, optional: true,
index: 1,
}, },
libraryTags: { libraryTags: {
type: Array, type: Array,
@@ -100,39 +98,50 @@ let LibraryNodeSchema = new SimpleSchema({
}, },
}); });
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 // Set up server side search index
if (Meteor.isServer) { if (Meteor.isServer) {
LibraryNodes._ensureIndex({ LibraryNodes.createIndexAsync({
'name': 'text', 'name': 'text',
'tags': 'text', 'tags': 'text',
}); });
} }
for (let key in propertySchemasIndex) { const genericLibraryNodeSchema = TypedSimpleSchema.from({})
let schema = new SimpleSchema({}); .extend(LibraryNodeSchema)
schema.extend(LibraryNodeSchema); .extend(ColorSchema)
schema.extend(ColorSchema); .extend(ChildSchema)
schema.extend(propertySchemasIndex[key]); .extend(SoftRemovableSchema);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema); // Attach the default schema
// @ts-expect-error don't have types for .attachSchema 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, { LibraryNodes.attachSchema(schema, {
selector: { type: key } 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({ const insertNode = new ValidatedMethod({
name: 'libraryNodes.insert', name: 'libraryNodes.insert',
validate: new SimpleSchema({ validate: new SimpleSchema({
@@ -209,8 +218,8 @@ const updateLibraryNode = new ValidatedMethod({
}, },
run({ _id, path, value }) { run({ _id, path, value }) {
let node = LibraryNodes.findOne(_id); let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); assertDocEditPermission(node, this.userId);
let pathString = path.join('.'); const pathString = path.join('.');
let modifier; let modifier;
// unset empty values // unset empty values
if (value === null || value === undefined) { if (value === null || value === undefined) {
@@ -218,7 +227,7 @@ const updateLibraryNode = new ValidatedMethod({
} else { } else {
modifier = { $set: { [pathString]: value } }; modifier = { $set: { [pathString]: value } };
} }
let numUpdated = LibraryNodes.update(_id, modifier, { const numUpdated = LibraryNodes.update(_id, modifier, {
selector: { type: node.type }, selector: { type: node.type },
}); });
if (node.type == 'reference') { if (node.type == 'reference') {
@@ -238,8 +247,8 @@ const pushToLibraryNode = new ValidatedMethod({
timeInterval: 5000, timeInterval: 5000,
}, },
run({ _id, path, value }) { run({ _id, path, value }) {
let node = LibraryNodes.findOne(_id); const node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); assertDocEditPermission(node, this.userId);
return LibraryNodes.update(_id, { return LibraryNodes.update(_id, {
$push: { [path.join('.')]: value }, $push: { [path.join('.')]: value },
}, { }, {
@@ -257,8 +266,8 @@ const pullFromLibraryNode = new ValidatedMethod({
timeInterval: 5000, timeInterval: 5000,
}, },
run({ _id, path, itemId }) { run({ _id, path, itemId }) {
let node = LibraryNodes.findOne(_id); const node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); assertDocEditPermission(node, this.userId);
return LibraryNodes.update(_id, { return LibraryNodes.update(_id, {
$pull: { [path.join('.')]: { _id: itemId } }, $pull: { [path.join('.')]: { _id: itemId } },
}, { }, {
@@ -279,8 +288,8 @@ const softRemoveLibraryNode = new ValidatedMethod({
timeInterval: 5000, timeInterval: 5000,
}, },
run({ _id }) { run({ _id }) {
let node = LibraryNodes.findOne(_id); const node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); assertDocEditPermission(node, this.userId);
softRemove(LibraryNodes, node); softRemove(LibraryNodes, node);
} }
}); });
@@ -297,8 +306,9 @@ const restoreLibraryNode = new ValidatedMethod({
}, },
run({ _id }) { run({ _id }) {
// Permissions // Permissions
let node = LibraryNodes.findOne(_id); const node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); if (!node) return;
assertDocEditPermission(node, this.userId);
// Do work // Do work
restore(LibraryNodes, node); restore(LibraryNodes, node);
} }

View File

@@ -1,4 +1,3 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import { InferType, TypedSimpleSchema } from '/imports/api/utility/TypedSimpleSchema'; import { InferType, TypedSimpleSchema } from '/imports/api/utility/TypedSimpleSchema';
import type { Simplify } from 'type-fest'; import type { Simplify } from 'type-fest';

View File

@@ -2,7 +2,7 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { RefSchema } from '/imports/api/parenting/ChildSchema'; 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 { compact } from 'lodash';
import Creatures from '/imports/api/creature/creatures/Creatures'; import Creatures from '/imports/api/creature/creatures/Creatures';
import { fetchDocByRefAsync, getCollectionByName, moveDocBetweenRoots, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions'; import { fetchDocByRefAsync, getCollectionByName, moveDocBetweenRoots, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions';

View File

@@ -3,8 +3,8 @@ import { TreeDoc, treeDocFields, Reference } from '/imports/api/parenting/ChildS
import { getProperties } from '/imports/api/engine/loadCreatures'; import { getProperties } from '/imports/api/engine/loadCreatures';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
export function getCollectionByName(name: string): Mongo.Collection<TreeDoc> { export function getCollectionByName<T = TreeDoc>(name: string): Mongo.Collection<T> {
const collection = Mongo.Collection.get<TreeDoc>(name) const collection = Mongo.Collection.get<T>(name)
if (!collection) { if (!collection) {
throw new Meteor.Error('bad-collection-reference', throw new Meteor.Error('bad-collection-reference',
`Parent references collection ${name}, which does not exist` `Parent references collection ${name}, which does not exist`
@@ -27,8 +27,8 @@ export function fetchDocByRefAsync(ref: Reference, options?: Mongo.Options<objec
return doc; return doc;
} }
export function fetchDocByRef(ref: Reference, options?: Mongo.Options<object>): TreeDoc { export function fetchDocByRef<T extends object = TreeDoc>(ref: Reference, options?: Mongo.Options<object>): T {
const doc: TreeDoc = getCollectionByName(ref.collection).findOne(ref.id, options); const doc: T = getCollectionByName<T>(ref.collection).findOne(ref.id, options);
assertDocFound(doc, ref); assertDocFound(doc, ref);
return doc; return doc;
} }

View File

@@ -1,26 +1,28 @@
import { includes } from 'lodash'; import { includes } from 'lodash';
import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; 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') { if (!userId || typeof userId !== 'string') {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'No user ID. Are you logged in?'); 'No user ID. Are you logged in?');
} }
} }
function assertdocExists(doc) { function assertDocExists(doc: Record<string, any> | undefined): asserts doc {
if (!doc) { if (!doc) {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'Permission denied: No such document exists'); 'Permission denied: No such document exists');
} }
} }
export function assertOwnership(doc, userId) { export function assertOwnership(doc: Shared, userId: string): asserts doc {
assertIdValid(userId); assertIdValid(userId);
assertdocExists(doc); assertDocExists(doc);
if (doc.owner === userId) { if (doc.owner === userId) {
return true; return;
} else { } else {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'You are not the owner of this document'); '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 * 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); assertIdValid(userId);
assertdocExists(doc); assertDocExists(doc);
const user = Meteor.users.findOne(userId, { const user = Meteor.users.findOne(userId, {
fields: { fields: {
'roles': 1, 'roles': 1,
} }
}); });
if (!user) {
throw new Meteor.Error('Edit permission denied',
'No such user exists');
}
// Admin override // Admin override
if (user.roles && user.roles.includes('admin')) { if (user.roles && user.roles.includes('admin')) {
return true; return;
} }
// Ensure the user is authorized for this specific document // Ensure the user is authorized for this specific document
@@ -52,7 +58,7 @@ export function assertEditPermission(doc, userId) {
doc.owner === userId || doc.owner === userId ||
includes(doc.writers, userId) includes(doc.writers, userId)
) { ) {
return true; return;
} else { } else {
throw new Meteor.Error('Edit permission denied', throw new Meteor.Error('Edit permission denied',
'You do not have permission to edit this document'); '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 * 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); assertIdValid(userId);
assertdocExists(doc); assertDocExists(doc);
const user = Meteor.users.findOne(userId, { const user = Meteor.users.findOne(userId, {
fields: { fields: {
'roles': 1, 'roles': 1,
} }
}); });
if (!user) {
throw new Meteor.Error('Edit permission denied',
'No such user exists');
}
// Admin override // Admin override
if (user.roles && user.roles.includes('admin')) { if (user.roles && user.roles.includes('admin')) {
return true; return;
} }
// Ensure the user is authorized for this specific document // Ensure the user is authorized for this specific document
@@ -84,22 +95,22 @@ export function assertCopyPermission(doc, userId) {
doc.owner === userId || doc.owner === userId ||
includes(doc.writers, userId) includes(doc.writers, userId)
) { ) {
return true; return;
} else if ( } else if (
(includes(doc.readers, userId) || doc.public) && (includes(doc.readers, userId) || doc.public) &&
doc.readersCanCopy doc.readersCanCopy
) { ) {
return true; return;
} else { } else {
throw new Meteor.Error('Copy permission denied', throw new Meteor.Error('Copy permission denied',
'You do not have permission to copy this document'); 'You do not have permission to copy this document');
} }
} }
function getRoot(doc) { function getRoot(doc: TreeDoc | Shared | undefined) {
assertdocExists(doc); assertDocExists(doc);
if (doc.root) { if ('root' in doc) {
return fetchDocByRef(doc.root); return fetchDocByRef<Shared>(doc.root);
} else { } else {
return doc; return doc;
} }
@@ -111,8 +122,8 @@ function getRoot(doc) {
* *
* Warning: the doc and userId must be set by a trusted source * Warning: the doc and userId must be set by a trusted source
*/ */
export function assertDocEditPermission(doc, userId) { export function assertDocEditPermission(doc: TreeDoc | Shared | undefined, userId: string | null): asserts doc {
let root = getRoot(doc); const root = getRoot(doc);
assertEditPermission(root, userId); assertEditPermission(root, userId);
} }
@@ -122,14 +133,14 @@ export function assertDocEditPermission(doc, userId) {
* *
* Warning: the doc and userId must be set by a trusted source * Warning: the doc and userId must be set by a trusted source
*/ */
export function assertDocCopyPermission(doc, userId) { export function assertDocCopyPermission(doc, userId): asserts doc {
let root = getRoot(doc); const root = getRoot(doc);
assertCopyPermission(root, userId); assertCopyPermission(root, userId);
} }
export function assertViewPermission(doc, userId) { export function assertViewPermission(doc, userId): asserts doc {
assertdocExists(doc); assertDocExists(doc);
if (doc.public) return true; if (doc.public) return;
assertIdValid(userId); assertIdValid(userId);
if ( if (
@@ -137,7 +148,7 @@ export function assertViewPermission(doc, userId) {
includes(doc.readers, userId) || includes(doc.readers, userId) ||
includes(doc.writers, userId) includes(doc.writers, userId)
) { ) {
return true; return;
} else { } else {
// Admin override // Admin override
@@ -146,8 +157,13 @@ export function assertViewPermission(doc, userId) {
'roles': 1, 'roles': 1,
} }
}); });
if (!user) {
throw new Meteor.Error('Edit permission denied',
'No such user exists');
}
if (user.roles && user.roles.includes('admin')) { if (user.roles && user.roles.includes('admin')) {
return true; return;
} }
throw new Meteor.Error('View permission denied', 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 * Warning: the doc and userId must be set by a trusted source
*/ */
export function assertDocViewPermission(doc, userId) { export function assertDocViewPermission(doc, userId): asserts doc {
let root = getRoot(doc); const root = getRoot(doc);
assertViewPermission(root, userId); assertViewPermission(root, userId);
} }
export function assertAdmin(userId) { export function assertAdmin(userId): asserts userId {
assertIdValid(userId); assertIdValid(userId);
let user = Meteor.users.findOne(userId, { fields: { roles: 1 } }); const user = Meteor.users.findOne(userId, { fields: { roles: 1 } });
if (!user) { if (!user) {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'UserId does not match any existing user'); '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) { if (!isAdmin) {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'User does not have the admin role'); 'User does not have the admin role');

View File

@@ -28,8 +28,8 @@
ref="searchBox" ref="searchBox"
v-model="filter" v-model="filter"
class="mx-4" class="mx-4"
@extra-fields-changed="val => extraFields = val"
:is-library="true" :is-library="true"
@extra-fields-changed="val => extraFields = val"
/> />
<v-spacer /> <v-spacer />
<v-fade-transition> <v-fade-transition>

View File

@@ -13,11 +13,11 @@ Migrations.add({
name: 'Changes parenting from array of ancestors to nested sets', name: 'Changes parenting from array of ancestors to nested sets',
up: Meteor.wrapAsync(async (_, next) => { up: Meteor.wrapAsync(async (_, next) => {
console.log('migrating up library nodes 2 -> 3'); console.log('migrating up library nodes 2 -> 3');
migrateCollection('libraryNodes'); await migrateCollection('libraryNodes');
console.log('migrating up creature props 2 -> 3'); console.log('migrating up creature props 2 -> 3');
migrateCollection('creatureProperties'); await migrateCollection('creatureProperties');
console.log('migrating up docs 2 -> 3'); 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('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'); 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 // Copy the parent id field and the root ancestor to the new structure
// Using the mongo aggregation API // Using the mongo aggregation API
// Waring: This will destroy parenting data if the old parenting fields are deleted // 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: { $addFields: {
'root': { $arrayElemAt: ['$ancestors', 0] }, 'root': { $arrayElemAt: ['$ancestors', 0] },

View File

@@ -12,6 +12,7 @@
"strict": true, "strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"strictFunctionTypes": true, "strictFunctionTypes": true,
"noImplicitAny": false,
"baseUrl": ".", "baseUrl": ".",
"preserveSymlinks": true, "preserveSymlinks": true,
"allowJs": true, "allowJs": true,
@@ -20,7 +21,7 @@
"outDir": "build", "outDir": "build",
"paths": { "paths": {
"/*": [ "/*": [
"./*" "*"
], ],
"meteor/*": [ "meteor/*": [
"node_modules/@types/meteor/*", "node_modules/@types/meteor/*",