diff --git a/app/imports/@types/mongo.d.ts b/app/imports/@types/mongo.d.ts index a5623ebc..9b7f223a 100644 --- a/app/imports/@types/mongo.d.ts +++ b/app/imports/@types/mongo.d.ts @@ -1,7 +1,34 @@ +import { TypedSimpleSchema } from 'imports/api/utility/TypedSimpleSchema'; +import SimpleSchema from 'simpl-schema'; + declare namespace Mongo { interface CollectionStatic { get: ( collectionName: string, options?: { connection: Meteor.Connection } ) => Mongo.Collection; } + type SchemaOptions = { + /** + * Set to `true` if your document must be passed through the collection's transform to properly validate + */ + transform: boolean, + /** + * Set to `true` to replace any existing schema instead of combining + */ + replace: boolean + } + + interface Collection { + schema?: TypedSimpleSchema; + /** + * Use this method to attach a schema to a collection created by another package, + * such as Meteor.users. It is most likely unsafe to call this method more than + * once for a single collection, or to call this for a collection that had a + * schema object passed to its constructor. + * @param ss SimpleSchema instance or a schema definition object from which to create a new SimpleSchema instance + * @param options Options + * + */ + attachSchema(ss: SimpleSchema | TypedSimpleSchema, options?: SchemaOptions): void; + } } diff --git a/app/imports/api/engine/computation/CreatureComputation.ts b/app/imports/api/engine/computation/CreatureComputation.ts index c704b6b1..9c7c1f4e 100644 --- a/app/imports/api/engine/computation/CreatureComputation.ts +++ b/app/imports/api/engine/computation/CreatureComputation.ts @@ -4,7 +4,7 @@ import getEffectivePropTags from '/imports/api/engine/computation/utility/getEff import type { Creature } from '/imports/api/creature/creatures/Creatures'; import type { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; -type ComputationProperty = CreatureProperty & { +export type ComputationProperty = CreatureProperty & { _computationDetails: { calculations: any[], emptyCalculations: any[], diff --git a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts index 06361748..f05d3e74 100644 --- a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts +++ b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts @@ -1,11 +1,11 @@ import walkDown from '/imports/api/engine/computation/utility/walkdown'; import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies'; import { Forest, TreeNode } from '/imports/api/parenting/parentingFunctions'; -import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { ComputationProperty } from '/imports/api/engine/computation/CreatureComputation'; import CreatureComputation from '/imports/api/engine/computation/CreatureComputation'; export default function computeToggleDependencies( - node: TreeNode, computation: CreatureComputation, forest: Forest + node: TreeNode, computation: CreatureComputation, forest: Forest ) { const prop = node.doc // Only for toggles diff --git a/app/imports/api/engine/computation/buildCreatureComputation.ts b/app/imports/api/engine/computation/buildCreatureComputation.ts index e7541f29..44dde3f6 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.ts +++ b/app/imports/api/engine/computation/buildCreatureComputation.ts @@ -32,7 +32,11 @@ import type { Creature } from '/imports/api/creature/creatures/Creatures'; export default function buildCreatureComputation(creatureId: string) { const creature = getCreature(creatureId); - if (!creature) return; + if (!creature) { + throw new Meteor.Error('not-found', + 'Build computation failed, the creature was not found' + ); + } const variables = getVariables(creatureId); const properties = getProperties(creatureId); const computation = buildComputationFromProps(properties, creature, variables); @@ -81,7 +85,7 @@ export function buildComputationFromProps( }); // Get all the properties as a forest, with their nested set properties set - const forest = applyNestedSetProperties(properties); + const forest = applyNestedSetProperties(computation.props); // Walk the property trees computing things that need to be inherited walkDown(forest.trees, node => { @@ -98,7 +102,7 @@ export function buildComputationFromProps( linkInventory(forest, dependencyGraph); // Link functions that require the above to be complete - properties.forEach(prop => { + computation.props.forEach(prop => { linkTypeDependencies(dependencyGraph, prop, computation); linkCalculationDependencies(dependencyGraph, prop, computation); }); diff --git a/app/imports/api/engine/computation/computeCreatureComputation.test.js b/app/imports/api/engine/computation/computeCreatureComputation.test.ts similarity index 54% rename from app/imports/api/engine/computation/computeCreatureComputation.test.js rename to app/imports/api/engine/computation/computeCreatureComputation.test.ts index 45018d3f..754db290 100644 --- a/app/imports/api/engine/computation/computeCreatureComputation.test.js +++ b/app/imports/api/engine/computation/computeCreatureComputation.test.ts @@ -1,19 +1,21 @@ import computeCreatureComputation from './computeCreatureComputation'; import { buildComputationFromProps } from './buildCreatureComputation'; import { assert } from 'chai'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureProperties, { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; import computeTests from './computeComputation/tests/index'; +import Creatures, { Creature } from 'imports/api/creature/creatures/Creatures'; -describe('Compute compuation', function () { +describe('Compute computation', function () { it('Computes something at all', function () { - let computation = buildComputationFromProps(testProperties); + const creature: Creature = Creatures.schema.clean({}); + const computation = buildComputationFromProps(testProperties, creature, {}); computeCreatureComputation(computation); assert.exists(computation); }); computeTests.forEach(test => it(test.text, test.fn)); }); -var testProperties = [ +const testProperties = [ clean({ _id: 'attributeId123', type: 'attribute', @@ -28,7 +30,8 @@ var testProperties = [ }), ]; -function clean(prop) { - let schema = CreatureProperties.simpleSchema(prop); +function clean(prop: Partial): CreatureProperty { + // @ts-expect-error don't have types for .simpleSchema + const schema = CreatureProperties.simpleSchema(prop); return schema.clean(prop); } diff --git a/app/imports/api/properties/subSchemas/computedField.ts b/app/imports/api/properties/subSchemas/computedField.ts index 397708c8..ba409142 100644 --- a/app/imports/api/properties/subSchemas/computedField.ts +++ b/app/imports/api/properties/subSchemas/computedField.ts @@ -10,7 +10,7 @@ export type FieldToCalculate = { export type CalculatedOnlyField = { value?: ConstantValueType; - valueNode: ParseNode; + valueNode?: ParseNode; effectIds?: string[]; proficiencyIds?: string[]; unaffected?: ConstantValueType; diff --git a/app/imports/api/properties/subSchemas/inlineCalculationField.ts b/app/imports/api/properties/subSchemas/inlineCalculationField.ts index 6826a78b..2e1586c5 100644 --- a/app/imports/api/properties/subSchemas/inlineCalculationField.ts +++ b/app/imports/api/properties/subSchemas/inlineCalculationField.ts @@ -11,7 +11,7 @@ export type ComputedOnlyInlineCalculationField = { text?: string, hash?: number, value?: string, - inlineCalculations: CalculatedField[], + inlineCalculations?: CalculatedField[], } // Get schemas that apply fields directly so they can be gracefully extended diff --git a/app/packages/collection2/.npm/package/.gitignore b/app/packages/collection2/.npm/package/.gitignore deleted file mode 100644 index 3c3629e6..00000000 --- a/app/packages/collection2/.npm/package/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/app/packages/collection2/.npm/package/README b/app/packages/collection2/.npm/package/README deleted file mode 100644 index 3d492553..00000000 --- a/app/packages/collection2/.npm/package/README +++ /dev/null @@ -1,7 +0,0 @@ -This directory and the files immediately inside it are automatically generated -when you change this package's NPM dependencies. Commit the files in this -directory (npm-shrinkwrap.json, .gitignore, and this README) to source control -so that others run the same versions of sub-dependencies. - -You should NOT check in the node_modules directory that Meteor automatically -creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/app/packages/collection2/.npm/package/npm-shrinkwrap.json b/app/packages/collection2/.npm/package/npm-shrinkwrap.json deleted file mode 100644 index 7c7e4cf6..00000000 --- a/app/packages/collection2/.npm/package/npm-shrinkwrap.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "lockfileVersion": 1, - "dependencies": { - "lodash.isempty": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", - "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=" - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, - "lodash.isobject": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", - "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" - } - } -} diff --git a/app/packages/collection2/.versions b/app/packages/collection2/.versions deleted file mode 100644 index 3b51faec..00000000 --- a/app/packages/collection2/.versions +++ /dev/null @@ -1,50 +0,0 @@ -aldeed:collection2@3.5.0 -allow-deny@1.1.0 -babel-compiler@7.7.0 -babel-runtime@1.5.0 -base64@1.0.12 -binary-heap@1.0.11 -boilerplate-generator@1.7.1 -callback-hook@1.3.1 -check@1.3.1 -ddp@1.4.0 -ddp-client@2.5.0 -ddp-common@1.4.0 -ddp-server@2.4.0 -diff-sequence@1.1.1 -dynamic-import@0.7.1 -ecmascript@0.15.3 -ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.11.1 -ecmascript-runtime-server@0.10.1 -ejson@1.1.1 -fetch@0.1.1 -geojson-utils@1.0.10 -id-map@1.1.1 -inter-process-messaging@0.1.1 -logging@1.2.0 -meteor@1.9.3 -minimongo@1.7.0 -modern-browsers@0.1.5 -modules@0.16.0 -modules-runtime@0.12.0 -mongo@1.12.0 -mongo-decimal@0.1.2 -mongo-dev-server@1.1.0 -mongo-id@1.0.8 -npm-mongo@3.9.1 -ordered-dict@1.1.0 -promise@0.12.0 -raix:eventemitter@1.0.0 -random@1.2.0 -react-fast-refresh@0.1.1 -reload@1.3.1 -retry@1.1.0 -routepolicy@1.1.1 -socket-stream-client@0.4.0 -tmeasday:check-npm-versions@1.0.2 -tracker@1.2.0 -typescript@4.3.5 -underscore@1.0.10 -webapp@1.11.1 -webapp-hashing@1.1.0 diff --git a/app/packages/collection2/collection2.js b/app/packages/collection2/collection2.js deleted file mode 100644 index 422dbf95..00000000 --- a/app/packages/collection2/collection2.js +++ /dev/null @@ -1,739 +0,0 @@ -import { EventEmitter } from 'meteor/raix:eventemitter'; -import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; -import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; -import { EJSON } from 'meteor/ejson'; -import isEmpty from 'lodash.isempty'; -import isEqual from 'lodash.isequal'; -import isObject from 'lodash.isobject'; -import { flattenSelector } from './lib'; - -checkNpmVersions({ 'simpl-schema': '>=0.0.0' }, 'aldeed:collection2'); - -const SimpleSchema = require('simpl-schema').default; - -// Exported only for listening to events -const Collection2 = new EventEmitter(); - -Collection2.cleanOptions = { - filter: true, - autoConvert: true, - removeEmptyStrings: true, - trimStrings: true, - removeNullsFromArrays: false, -}; - -/** - * Mongo.Collection.prototype.attachSchema - * @param {SimpleSchema|Object} ss - SimpleSchema instance or a schema definition object - * from which to create a new SimpleSchema instance - * @param {Object} [options] - * @param {Boolean} [options.transform=false] Set to `true` if your document must be passed - * through the collection's transform to properly validate. - * @param {Boolean} [options.replace=false] Set to `true` to replace any existing schema instead of combining - * @return {undefined} - * - * Use this method to attach a schema to a collection created by another package, - * such as Meteor.users. It is most likely unsafe to call this method more than - * once for a single collection, or to call this for a collection that had a - * schema object passed to its constructor. - */ -Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { - options = options || {}; - - // Allow passing just the schema object - if (!SimpleSchema.isSimpleSchema(ss)) { - ss = new SimpleSchema(ss); - } - - function attachTo(obj) { - // we need an array to hold multiple schemas - // position 0 is reserved for the "base" schema - obj._c2 = obj._c2 || {}; - obj._c2._simpleSchemas = obj._c2._simpleSchemas || [ null ]; - - if (typeof options.selector === 'object') { - // Selector Schemas - - // Extend selector schema with base schema - const baseSchema = obj._c2._simpleSchemas[0]; - if (baseSchema) { - ss = extendSchema(baseSchema.schema, ss); - } - - // Index of existing schema with identical selector - let schemaIndex; - - // Loop through existing schemas with selectors, - for (schemaIndex = obj._c2._simpleSchemas.length - 1; 0 < schemaIndex; schemaIndex--) { - const schema = obj._c2._simpleSchemas[schemaIndex]; - if (schema && isEqual(schema.selector, options.selector)) break; - } - - if (schemaIndex <= 0) { - // We didn't find the schema in our array - push it into the array - obj._c2._simpleSchemas.push({ - schema: ss, - selector: options.selector, - }); - } else { - // We found a schema with an identical selector in our array, - if (options.replace === true) { - // Replace existing selector schema with new selector schema - obj._c2._simpleSchemas[schemaIndex].schema = ss; - } else { - // Extend existing selector schema with new selector schema. - obj._c2._simpleSchemas[schemaIndex].schema = extendSchema(obj._c2._simpleSchemas[schemaIndex].schema, ss); - } - } - } else { - // Base Schema - if (options.replace === true) { - // Replace base schema and delete all other schemas - obj._c2._simpleSchemas = [{ - schema: ss, - selector: options.selector, - }]; - } else { - // Set base schema if not yet set - if (!obj._c2._simpleSchemas[0]) { - return obj._c2._simpleSchemas[0] = { schema: ss, selector: undefined }; - } - // Extend base schema and therefore extend all schemas - obj._c2._simpleSchemas.forEach((schema, index) => { - if (obj._c2._simpleSchemas[index]) { - obj._c2._simpleSchemas[index].schema = extendSchema(obj._c2._simpleSchemas[index].schema, ss); - } - }); - } - } - } - - attachTo(this); - // Attach the schema to the underlying LocalCollection, too - if (this._collection instanceof LocalCollection) { - this._collection._c2 = this._collection._c2 || {}; - attachTo(this._collection); - } - - defineDeny(this, options); - keepInsecure(this); - - Collection2.emit('schema.attached', this, ss, options); -}; - -[Mongo.Collection, LocalCollection].forEach((obj) => { - /** - * simpleSchema - * @description function detect the correct schema by given params. If it - * detect multi-schema presence in the collection, then it made an attempt to find a - * `selector` in args - * @param {Object} doc - It could be on update/upsert or document - * itself on insert/remove - * @param {Object} [options] - It could be on update/upsert etc - * @param {Object} [query] - it could be on update/upsert - * @return {Object} Schema - */ - obj.prototype.simpleSchema = function (doc, options, query) { - if (!this._c2) return null; - if (this._c2._simpleSchema) return this._c2._simpleSchema; - - const schemas = this._c2._simpleSchemas; - if (schemas && schemas.length > 0) { - - let schema, selector, target; - // Position 0 reserved for base schema - for (var i = 1; i < schemas.length; i++) { - schema = schemas[i]; - selector = Object.keys(schema.selector)[0]; - - // We will set this to undefined because in theory you might want to select - // on a null value. - target = undefined; - // here we are looking for selector in different places - // $set should have more priority here - if (doc.$set && typeof doc.$set[selector] !== 'undefined') { - target = doc.$set[selector]; - } else if (typeof doc[selector] !== 'undefined') { - target = doc[selector]; - } else if (options && options.selector) { - target = options.selector[selector]; - } else if (query && query[selector]) { // on upsert/update operations - target = query[selector]; - } - - // we need to compare given selector with doc property or option to - // find right schema - if (target !== undefined && target === schema.selector[selector]) { - return schema.schema; - } - } - if (schemas[0]) { - return schemas[0].schema; - } else { - throw new Error('No default schema'); - } - } - - return null; - }; -}); - -// Wrap DB write operation methods -['insert', 'update'].forEach((methodName) => { - const _super = Mongo.Collection.prototype[methodName]; - Mongo.Collection.prototype[methodName] = function(...args) { - let options = (methodName === 'insert') ? args[1] : args[2]; - - // Support missing options arg - if (!options || typeof options === 'function') { - options = {}; - } - - if (this._c2 && options.bypassCollection2 !== true) { - let userId = null; - try { // https://github.com/aldeed/meteor-collection2/issues/175 - userId = Meteor.userId(); - } catch (err) {} - - args = doValidate( - this, - methodName, - args, - Meteor.isServer || this._connection === null, // getAutoValues - userId, - Meteor.isServer // isFromTrustedCode - ); - if (!args) { - // doValidate already called the callback or threw the error so we're done. - // But insert should always return an ID to match core behavior. - return methodName === 'insert' ? this._makeNewID() : undefined; - } - } else { - // We still need to adjust args because insert does not take options - if (methodName === 'insert' && typeof args[1] !== 'function') args.splice(1, 1); - } - - return _super.apply(this, args); - }; -}); - -/* - * Private - */ - -function doValidate(collection, type, args, getAutoValues, userId, isFromTrustedCode) { - let doc, callback, error, options, isUpsert, selector, last, hasCallback; - - if (!args.length) { - throw new Error(type + ' requires an argument'); - } - - // Gather arguments and cache the selector - if (type === 'insert') { - doc = args[0]; - options = args[1]; - callback = args[2]; - - // The real insert doesn't take options - if (typeof options === 'function') { - args = [doc, options]; - } else if (typeof callback === 'function') { - args = [doc, callback]; - } else { - args = [doc]; - } - } else if (type === 'update') { - selector = args[0]; - doc = args[1]; - options = args[2]; - callback = args[3]; - } else { - throw new Error('invalid type argument'); - } - - const validatedObjectWasInitiallyEmpty = isEmpty(doc); - - // Support missing options arg - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } - options = options || {}; - - last = args.length - 1; - - hasCallback = (typeof args[last] === 'function'); - - // If update was called with upsert:true, flag as an upsert - isUpsert = (type === 'update' && options.upsert === true); - - // we need to pass `doc` and `options` to `simpleSchema` method, that's why - // schema declaration moved here - let schema = collection.simpleSchema(doc, options, selector); - const isLocalCollection = (collection._connection === null); - - // On the server and for local collections, we allow passing `getAutoValues: false` to disable autoValue functions - if ((Meteor.isServer || isLocalCollection) && options.getAutoValues === false) { - getAutoValues = false; - } - - // Process pick/omit options if they are present - const picks = Array.isArray(options.pick) ? options.pick : null; - const omits = Array.isArray(options.omit) ? options.omit : null; - - if (picks && omits) { - // Pick and omit cannot both be present in the options - throw new Error('pick and omit options are mutually exclusive'); - } else if (picks) { - schema = schema.pick(...picks); - } else if (omits) { - schema = schema.omit(...omits); - } - - // Determine validation context - let validationContext = options.validationContext; - if (validationContext) { - if (typeof validationContext === 'string') { - validationContext = schema.namedContext(validationContext); - } - } else { - validationContext = schema.namedContext(); - } - - // Add a default callback function if we're on the client and no callback was given - /* - if (Meteor.isClient && !callback) { - // Client can't block, so it can't report errors by exception, - // only by callback. If they forget the callback, give them a - // default one that logs the error, so they aren't totally - // baffled if their writes don't work because their database is - // down. - callback = function(err) { - if (err) { - Meteor._debug(type + " failed: " + (err.reason || err.stack)); - } - }; - } - */ - - // If client validation is fine or is skipped but then something - // is found to be invalid on the server, we get that error back - // as a special Meteor.Error that we need to parse. - if (Meteor.isClient && hasCallback) { - callback = args[last] = wrapCallbackForParsingServerErrors(validationContext, callback); - } - - const schemaAllowsId = schema.allowsKey('_id'); - if (type === 'insert' && !doc._id && schemaAllowsId) { - doc._id = collection._makeNewID(); - } - - // Get the docId for passing in the autoValue/custom context - let docId; - if (type === 'insert') { - docId = doc._id; // might be undefined - } else if (type === 'update' && selector) { - docId = typeof selector === 'string' || selector instanceof Mongo.ObjectID ? selector : selector._id; - } - - // If _id has already been added, remove it temporarily if it's - // not explicitly defined in the schema. - let cachedId; - if (doc._id && !schemaAllowsId) { - cachedId = doc._id; - delete doc._id; - } - - const autoValueContext = { - isInsert: (type === 'insert'), - isUpdate: (type === 'update' && options.upsert !== true), - isUpsert, - userId, - isFromTrustedCode, - docId, - isLocalCollection - }; - - const extendAutoValueContext = { - ...((schema._cleanOptions || {}).extendAutoValueContext || {}), - ...autoValueContext, - ...options.extendAutoValueContext, - }; - - const cleanOptionsForThisOperation = {}; - ['autoConvert', 'filter', 'removeEmptyStrings', 'removeNullsFromArrays', 'trimStrings'].forEach(prop => { - if (typeof options[prop] === 'boolean') { - cleanOptionsForThisOperation[prop] = options[prop]; - } - }); - - // Preliminary cleaning on both client and server. On the server and for local - // collections, automatic values will also be set at this point. - schema.clean(doc, { - mutate: true, // Clean the doc/modifier in place - isModifier: (type !== 'insert'), - // Start with some Collection2 defaults, which will usually be overwritten - ...Collection2.cleanOptions, - // The extend with the schema-level defaults (from SimpleSchema constructor options) - ...(schema._cleanOptions || {}), - // Finally, options for this specific operation should take precedence - ...cleanOptionsForThisOperation, - extendAutoValueContext, // This was extended separately above - getAutoValues, // Force this override - }); - - // We clone before validating because in some cases we need to adjust the - // object a bit before validating it. If we adjusted `doc` itself, our - // changes would persist into the database. - let docToValidate = {}; - for (var prop in doc) { - // We omit prototype properties when cloning because they will not be valid - // and mongo omits them when saving to the database anyway. - if (Object.prototype.hasOwnProperty.call(doc, prop)) { - docToValidate[prop] = doc[prop]; - } - } - - // On the server, upserts are possible; SimpleSchema handles upserts pretty - // well by default, but it will not know about the fields in the selector, - // which are also stored in the database if an insert is performed. So we - // will allow these fields to be considered for validation by adding them - // to the $set in the modifier, while stripping out query selectors as these - // don't make it into the upserted document and break validation. - // This is no doubt prone to errors, but there probably isn't any better way - // right now. - if (Meteor.isServer && isUpsert && isObject(selector)) { - const set = docToValidate.$set || {}; - docToValidate.$set = flattenSelector(selector); - - if (!schemaAllowsId) delete docToValidate.$set._id; - Object.assign(docToValidate.$set, set); - } - // Set automatic values for validation on the client. - // On the server, we already updated doc with auto values, but on the client, - // we will add them to docToValidate for validation purposes only. - // This is because we want all actual values generated on the server. - if (Meteor.isClient && !isLocalCollection) { - schema.clean(docToValidate, { - autoConvert: false, - extendAutoValueContext, - filter: false, - getAutoValues: true, - isModifier: (type !== 'insert'), - mutate: true, // Clean the doc/modifier in place - removeEmptyStrings: false, - removeNullsFromArrays: false, - trimStrings: false, - }); - } - - // XXX Maybe move this into SimpleSchema - if (!validatedObjectWasInitiallyEmpty && isEmpty(docToValidate)) { - throw new Error('After filtering out keys not in the schema, your ' + - (type === 'update' ? 'modifier' : 'object') + - ' is now empty'); - } - - // Validate doc - let isValid; - if (options.validate === false) { - isValid = true; - } else { - isValid = validationContext.validate(docToValidate, { - modifier: (type === 'update' || type === 'upsert'), - upsert: isUpsert, - extendedCustomContext: { - isInsert: (type === 'insert'), - isUpdate: (type === 'update' && options.upsert !== true), - isUpsert, - userId, - isFromTrustedCode, - docId, - isLocalCollection, - ...(options.extendedCustomContext || {}), - }, - }); - } - - if (isValid) { - // Add the ID back - if (cachedId) { - doc._id = cachedId; - } - - // Update the args to reflect the cleaned doc - // XXX not sure this is necessary since we mutate - if (type === 'insert') { - args[0] = doc; - } else { - args[1] = doc; - } - - // If callback, set invalidKey when we get a mongo unique error - if (Meteor.isServer && hasCallback) { - args[last] = wrapCallbackForParsingMongoValidationErrors(validationContext, args[last]); - } - - return args; - } else { - error = getErrorObject(validationContext, Meteor.settings?.packages?.collection2?.disableCollectionNamesInValidation ? '' : `in ${collection._name} ${type}`); - if (callback) { - // insert/update/upsert pass `false` when there's an error, so we do that - callback(error, false); - } else { - throw error; - } - } -} - -function getErrorObject(context, appendToMessage = '') { - let message; - const invalidKeys = (typeof context.validationErrors === 'function') ? context.validationErrors() : context.invalidKeys(); - if (invalidKeys.length) { - const firstErrorKey = invalidKeys[0].name; - const firstErrorMessage = context.keyErrorMessage(firstErrorKey); - - // If the error is in a nested key, add the full key to the error message - // to be more helpful. - if (firstErrorKey.indexOf('.') === -1) { - message = firstErrorMessage; - } else { - message = `${firstErrorMessage} (${firstErrorKey})`; - } - } else { - message = 'Failed validation'; - } - message = `${message} ${appendToMessage}`.trim(); - const error = new Error(message); - error.invalidKeys = invalidKeys; - error.validationContext = context; - // If on the server, we add a sanitized error, too, in case we're - // called from a method. - if (Meteor.isServer) { - error.sanitizedError = new Meteor.Error(400, message, EJSON.stringify(error.invalidKeys)); - } - return error; -} - -function addUniqueError(context, errorMessage) { - const name = errorMessage.split('c2_')[1].split(' ')[0]; - const val = errorMessage.split('dup key:')[1].split('"')[1]; - - const addValidationErrorsPropName = (typeof context.addValidationErrors === 'function') ? 'addValidationErrors' : 'addInvalidKeys'; - context[addValidationErrorsPropName]([{ - name: name, - type: 'notUnique', - value: val - }]); -} - -function wrapCallbackForParsingMongoValidationErrors(validationContext, cb) { - return function wrappedCallbackForParsingMongoValidationErrors(...args) { - const error = args[0]; - if (error && - ((error.name === 'MongoError' && error.code === 11001) || error.message.indexOf('MongoError: E11000') !== -1) && - error.message.indexOf('c2_') !== -1) { - addUniqueError(validationContext, error.message); - args[0] = getErrorObject(validationContext); - } - return cb.apply(this, args); - }; -} - -function wrapCallbackForParsingServerErrors(validationContext, cb) { - const addValidationErrorsPropName = (typeof validationContext.addValidationErrors === 'function') ? 'addValidationErrors' : 'addInvalidKeys'; - return function wrappedCallbackForParsingServerErrors(...args) { - const error = args[0]; - // Handle our own validation errors - if (error instanceof Meteor.Error && - error.error === 400 && - error.reason === 'INVALID' && - typeof error.details === 'string') { - const invalidKeysFromServer = EJSON.parse(error.details); - validationContext[addValidationErrorsPropName](invalidKeysFromServer); - args[0] = getErrorObject(validationContext); - } - // Handle Mongo unique index errors, which are forwarded to the client as 409 errors - else if (error instanceof Meteor.Error && - error.error === 409 && - error.reason && - error.reason.indexOf('E11000') !== -1 && - error.reason.indexOf('c2_') !== -1) { - addUniqueError(validationContext, error.reason); - args[0] = getErrorObject(validationContext); - } - return cb.apply(this, args); - }; -} - -let alreadyInsecure = {}; -function keepInsecure(c) { - // If insecure package is in use, we need to add allow rules that return - // true. Otherwise, it would seemingly turn off insecure mode. - if (Package && Package.insecure && !alreadyInsecure[c._name]) { - c.allow({ - insert: function() { - return true; - }, - update: function() { - return true; - }, - remove: function () { - return true; - }, - fetch: [], - transform: null - }); - alreadyInsecure[c._name] = true; - } - // If insecure package is NOT in use, then adding the two deny functions - // does not have any effect on the main app's security paradigm. The - // user will still be required to add at least one allow function of her - // own for each operation for this collection. And the user may still add - // additional deny functions, but does not have to. -} - -let alreadyDefined = {}; -function defineDeny(c, options) { - if (!alreadyDefined[c._name]) { - - const isLocalCollection = (c._connection === null); - - // First define deny functions to extend doc with the results of clean - // and auto-values. This must be done with "transform: null" or we would be - // extending a clone of doc and therefore have no effect. - c.deny({ - insert: function(userId, doc) { - // Referenced doc is cleaned in place - c.simpleSchema(doc).clean(doc, { - mutate: true, - isModifier: false, - // We don't do these here because they are done on the client if desired - filter: false, - autoConvert: false, - removeEmptyStrings: false, - trimStrings: false, - extendAutoValueContext: { - isInsert: true, - isUpdate: false, - isUpsert: false, - userId: userId, - isFromTrustedCode: false, - docId: doc._id, - isLocalCollection: isLocalCollection - } - }); - - return false; - }, - update: function(userId, doc, fields, modifier) { - // Referenced modifier is cleaned in place - c.simpleSchema(modifier).clean(modifier, { - mutate: true, - isModifier: true, - // We don't do these here because they are done on the client if desired - filter: false, - autoConvert: false, - removeEmptyStrings: false, - trimStrings: false, - extendAutoValueContext: { - isInsert: false, - isUpdate: true, - isUpsert: false, - userId: userId, - isFromTrustedCode: false, - docId: doc && doc._id, - isLocalCollection: isLocalCollection - } - }); - - return false; - }, - fetch: ['_id'], - transform: null - }); - - // Second define deny functions to validate again on the server - // for client-initiated inserts and updates. These should be - // called after the clean/auto-value functions since we're adding - // them after. These must *not* have "transform: null" if options.transform is true because - // we need to pass the doc through any transforms to be sure - // that custom types are properly recognized for type validation. - c.deny({ - insert: function(userId, doc) { - // We pass the false options because we will have done them on client if desired - doValidate( - c, - 'insert', - [ - doc, - { - trimStrings: false, - removeEmptyStrings: false, - filter: false, - autoConvert: false - }, - function(error) { - if (error) { - throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); - } - } - ], - false, // getAutoValues - userId, - false // isFromTrustedCode - ); - - return false; - }, - update: function(userId, doc, fields, modifier) { - // NOTE: This will never be an upsert because client-side upserts - // are not allowed once you define allow/deny functions. - // We pass the false options because we will have done them on client if desired - doValidate( - c, - 'update', - [ - {_id: doc && doc._id}, - modifier, - { - trimStrings: false, - removeEmptyStrings: false, - filter: false, - autoConvert: false - }, - function(error) { - if (error) { - throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); - } - } - ], - false, // getAutoValues - userId, - false // isFromTrustedCode - ); - - return false; - }, - fetch: ['_id'], - ...(options.transform === true ? {} : {transform: null}), - }); - - // note that we've already done this collection so that we don't do it again - // if attachSchema is called again - alreadyDefined[c._name] = true; - } -} - -function extendSchema(s1, s2) { - if (s2.version >= 2) { - const ss = new SimpleSchema(s1); - ss.extend(s2); - return ss; - } else { - return new SimpleSchema([ s1, s2 ]); - } -} - -export default Collection2; diff --git a/app/packages/collection2/lib.js b/app/packages/collection2/lib.js deleted file mode 100644 index 057195d2..00000000 --- a/app/packages/collection2/lib.js +++ /dev/null @@ -1,31 +0,0 @@ -export function flattenSelector(selector) { - // If selector uses $and format, convert to plain object selector - if (Array.isArray(selector.$and)) { - selector.$and.forEach(sel => { - Object.assign(selector, flattenSelector(sel)); - }); - - delete selector.$and - } - - const obj = {} - - Object.entries(selector).forEach(([key, value]) => { - // Ignoring logical selectors (https://docs.mongodb.com/manual/reference/operator/query/#logical) - if (!key.startsWith("$")) { - if (typeof value === 'object' && value !== null) { - if (value.$eq !== undefined) { - obj[key] = value.$eq - } else if (Array.isArray(value.$in) && value.$in.length === 1) { - obj[key] = value.$in[0] - } else if (Object.keys(value).every(v => !(typeof v === "string" && v.startsWith("$")))) { - obj[key] = value - } - } else { - obj[key] = value - } - } - }) - - return obj -} diff --git a/app/packages/collection2/package.js b/app/packages/collection2/package.js deleted file mode 100644 index 4177ad10..00000000 --- a/app/packages/collection2/package.js +++ /dev/null @@ -1,33 +0,0 @@ -/* global Package */ - -Package.describe({ - name: "aldeed:collection2", - summary: "Automatic validation of Meteor Mongo insert and update operations on the client and server", - version: "3.5.0", - documentation: "../../README.md", - git: "https://github.com/aldeed/meteor-collection2.git" -}); - -Npm.depends({ - 'lodash.isempty': '4.4.0', - 'lodash.isequal': '4.5.0', - 'lodash.isobject': '3.0.2', -}); - -Package.onUse(function(api) { - api.versionsFrom(['1.12.1', '2.3']); - api.use('mongo'); - api.imply('mongo'); - api.use('minimongo'); - api.use('ejson'); - api.use('raix:eventemitter@1.0.0'); - api.use('ecmascript'); - api.use('tmeasday:check-npm-versions@1.0.2'); - - // Allow us to detect 'insecure'. - api.use('insecure@1.0.7', {weak: true}); - - api.mainModule('collection2.js'); - - api.export('Collection2'); -});