diff --git a/server/dist/src/models/model.js b/server/dist/src/models/model.js index c5cb4030..55c93cc1 100644 --- a/server/dist/src/models/model.js +++ b/server/dist/src/models/model.js @@ -204,6 +204,7 @@ class Model { var foreignModel; var optionalDependencies = instance.dbEntry._optionalDependencies; var requiredDependencies = instance.dbEntry._requiredDependencies; + var arrayDependencies = instance.dbEntry._arrayDependencies; var promises = []; // Nullify all dependencies that are optional if (optionalDependencies) @@ -211,10 +212,20 @@ class Model { foreignModel = Model.getByName(optionalDependencies[i].collection); if (!foreignModel) continue; - var setToken = { $set: {} }; + let setToken = { $set: {} }; setToken.$set[optionalDependencies[i].propertyName] = null; promises.push(foreignModel.collection.updateOne({ _id: optionalDependencies[i]._id }, setToken)); } + // Remove any dependencies that are in arrays + if (arrayDependencies) + for (var i = 0, l = arrayDependencies.length; i < l; i++) { + foreignModel = Model.getByName(arrayDependencies[i].collection); + if (!foreignModel) + continue; + let pullToken = { $pull: {} }; + pullToken.$pull[arrayDependencies[i].propertyName] = instance._id; + promises.push(foreignModel.collection.updateMany({ _id: arrayDependencies[i]._id }, pullToken)); + } // For those dependencies that are required, we delete the instances if (requiredDependencies) for (var i = 0, l = requiredDependencies.length; i < l; i++) { diff --git a/server/dist/src/models/schema-items/schema-foreign-key.js b/server/dist/src/models/schema-items/schema-foreign-key.js index cacabbf9..01ea3f6b 100644 --- a/server/dist/src/models/schema-items/schema-foreign-key.js +++ b/server/dist/src/models/schema-items/schema-foreign-key.js @@ -11,6 +11,7 @@ const schema_item_1 = require("./schema-item"); const model_1 = require("../model"); const mongodb_1 = require("mongodb"); const utils_1 = require("../../utils"); +const schema_id_array_1 = require("./schema-id-array"); /** * Represents a mongodb ObjectID of a document in separate collection. * Foreign keys are used as a way of relating models to one another. They can be required or optional. @@ -116,32 +117,30 @@ class SchemaForeignKey extends schema_item_1.SchemaItem { */ getValue(options) { return __awaiter(this, void 0, Promise, function* () { + if (options.expandForeignKeys && options.expandMaxDepth === undefined) + throw new Error("You cannot set expandForeignKeys and not specify the expandMaxDepth"); if (!options.expandForeignKeys) return this.value; - else { - var model = model_1.Model.getByName(this.targetCollection); - if (model) { - if (!this.value) - return null; - // Make sure the current level is not beyond the max depth - if (options.expandMaxDepth !== undefined) { - if (this.curLevel > options.expandMaxDepth) - return this.value; - } - else - options.expandMaxDepth = 1; - var result = yield model.findOne({ _id: this.value }); - // Get the models items are increase their level - this ensures we dont go too deep - var items = result.schema.getItems(); - var nextLevel = this.curLevel + 1; - for (var i = 0, l = items.length; i < l; i++) - if (items[i] instanceof SchemaForeignKey) - items[i].curLevel = nextLevel; - return yield result.schema.getAsJson(result.dbEntry._id, options); - } - else - throw new Error(`${this.name} references a foreign key '${this.targetCollection}' which doesn't seem to exist`); + if (options.expandSchemaBlacklist && options.expandSchemaBlacklist.indexOf(this.name) != -1) + return this.value; + var model = model_1.Model.getByName(this.targetCollection); + if (!model) + throw new Error(`${this.name} references a foreign key '${this.targetCollection}' which doesn't seem to exist`); + if (!this.value) + return null; + // Make sure the current level is not beyond the max depth + if (options.expandMaxDepth !== undefined) { + if (this.curLevel > options.expandMaxDepth) + return this.value; } + var result = yield model.findOne({ _id: this.value }); + // Get the models items are increase their level - this ensures we dont go too deep + var items = result.schema.getItems(); + var nextLevel = this.curLevel + 1; + for (var i = 0, l = items.length; i < l; i++) + if (items[i] instanceof SchemaForeignKey || items[i] instanceof schema_id_array_1.SchemaIdArray) + items[i].curLevel = nextLevel; + return yield result.schema.getAsJson(result.dbEntry._id, options); }); } } diff --git a/server/dist/src/models/schema-items/schema-id-array.js b/server/dist/src/models/schema-items/schema-id-array.js index 5c285a04..348a4ba4 100644 --- a/server/dist/src/models/schema-items/schema-id-array.js +++ b/server/dist/src/models/schema-items/schema-id-array.js @@ -1,22 +1,40 @@ "use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments)).next()); + }); +}; const schema_item_1 = require("./schema-item"); +const schema_foreign_key_1 = require("./schema-foreign-key"); +const model_1 = require("../model"); const mongodb_1 = require("mongodb"); const utils_1 = require("../../utils"); /** -* A n ID array scheme item for use in Models -*/ + * An ID array scheme item for use in Models. Optionally can be used as a foreign key array + * and return objects of the specified ids. In order for the array to return objects you must + * specify the targetCollection property. This tells the schema from which model the ids belong to. + * Currently we only support Id lookups that exist in the same model - i.e. if the ids are of objects + * in different models we cannot get the object values. + */ class SchemaIdArray extends schema_item_1.SchemaItem { /** - * Creates a new schema item that holds an array of id items - * @param {string} name The name of this item - * @param {Array} val The array of ids for this schema item - * @param {number} minItems [Optional] Specify the minimum number of items that can be allowed - * @param {number} maxItems [Optional] Specify the maximum number of items that can be allowed - */ - constructor(name, val, minItems = 0, maxItems = 10000) { + * Creates a new schema item that holds an array of id items + * @param {string} name The name of this item + * @param {Array} val The array of ids for this schema item + * @param {number} minItems [Optional] Specify the minimum number of items that can be allowed + * @param {number} maxItems [Optional] Specify the maximum number of items that can be allowed + * @param {string} targetCollection [Optional] Specify the model name to which all the ids belong. If set + * the item can expand objects on retreival. + */ + constructor(name, val, minItems = 0, maxItems = 10000, targetCollection = "") { super(name, val); this.maxItems = maxItems; this.minItems = minItems; + this.targetCollection = targetCollection; + this.curLevel = 1; } /** * Creates a clone of this item @@ -28,6 +46,7 @@ class SchemaIdArray extends schema_item_1.SchemaItem { super.clone(copy); copy.maxItems = this.maxItems; copy.minItems = this.minItems; + copy.targetCollection = this.targetCollection; return copy; } /** @@ -35,22 +54,112 @@ class SchemaIdArray extends schema_item_1.SchemaItem { * @returns {Promise} Returns true if successful or an error message string if unsuccessful */ validate() { - var transformedValue = this.value; - for (var i = 0, l = transformedValue.length; i < l; i++) { - if (typeof this.value[i] == "string") { - if (utils_1.Utils.isValidObjectID(this.value[i])) - transformedValue[i] = new mongodb_1.ObjectID(this.value[i]); - else if (this.value[i].trim() != "") - return Promise.reject(new Error(`Please use a valid ID for '${this.name}'`)); - else - return Promise.reject(new Error(`Please use a valid ID for '${this.name}'`)); + return __awaiter(this, void 0, Promise, function* () { + var transformedValue = this.value; + for (var i = 0, l = transformedValue.length; i < l; i++) { + if (typeof this.value[i] == "string") { + if (utils_1.Utils.isValidObjectID(this.value[i])) + transformedValue[i] = new mongodb_1.ObjectID(this.value[i]); + else if (this.value[i].trim() != "") + throw new Error(`Please use a valid ID for '${this.name}'`); + else + throw new Error(`Please use a valid ID for '${this.name}'`); + } + } + if (transformedValue.length < this.minItems) + throw new Error(`You must select at least ${this.minItems} item${(this.minItems == 1 ? "" : "s")} for ${this.name}`); + if (transformedValue.length > this.maxItems) + throw new Error(`You have selected too many items for ${this.name}, please only use up to ${this.maxItems}`); + // If no collection - then return + if (this.targetCollection == "") + return true; + if (this.value.length == 0) + return true; + // If they collection is not empty, then it must exist + var model = model_1.Model.getByName(this.targetCollection); + if (!model) + throw new Error(`${this.name} references a foreign key '${this.targetCollection}' which doesn't seem to exist`); + // We can assume the value is object id by this point + var query = { $or: [] }; + var arr = this.value; + for (var i = 0, l = arr.length; i < l; i++) + query.$or.push({ _id: arr[i] }); + var result = yield model.findInstances(query); + this._targetDocs = result; + return true; + }); + } + /** + * Called once a schema has been validated and inserted into the database. Useful for + * doing any post update/insert operations + * @param {ModelInstance} instance The model instance that was inserted or updated + * @param {string} collection The DB collection that the model was inserted into + */ + postValidation(instance, collection) { + return __awaiter(this, void 0, Promise, function* () { + if (!this._targetDocs) + return; + // If they key is required then it must exist + var model = model_1.Model.getByName(this.targetCollection); + var promises = []; + for (var i = 0, l = this._targetDocs.length; i < l; i++) { + let arrDeps = this._targetDocs[i].dbEntry._arrayDependencies || []; + arrDeps.push({ _id: instance.dbEntry._id, collection: collection, propertyName: this.name }); + promises.push(model.collection.updateOne({ _id: this._targetDocs[i].dbEntry._id }, { + $set: { _arrayDependencies: arrDeps } + })); + } + yield Promise.all(promises); + // Nullify the target doc cache + this._targetDocs = null; + return; + }); + } + /** + * Gets the value of this item + * @param {ISchemaOptions} options [Optional] A set of options that can be passed to control how the data must be returned + * @returns {Promise>} + */ + getValue(options) { + return __awaiter(this, void 0, Promise, function* () { + if (options.expandForeignKeys && options.expandMaxDepth === undefined) + throw new Error("You cannot set expandForeignKeys and not specify the expandMaxDepth"); + if (!options.expandForeignKeys) + return this.value; + if (options.expandSchemaBlacklist && options.expandSchemaBlacklist.indexOf(this.name) != -1) + return this.value; + if (this.targetCollection == "") + return this.value; + var model = model_1.Model.getByName(this.targetCollection); + if (!model) + throw new Error(`${this.name} references a foreign key '${this.targetCollection}' which doesn't seem to exist`); + // Make sure the current level is not beyond the max depth + if (options.expandMaxDepth !== undefined) { + if (this.curLevel > options.expandMaxDepth) + return this.value; + } + if (this.value.length == 0) + return this.value; + // Create the query for fetching the instances + var query = { $or: [] }; + for (var i = 0, l = this.value.length; i < l; i++) + query.$or.push({ _id: this.value[i] }); + var instances = yield model.findInstances(query); + var instance; + var toReturn = []; + var promises = []; + // Get the models items are increase their level - this ensures we dont go too deep + for (var i = 0, l = instances.length; i < l; i++) { + instance = instances[i]; + var items = instance.schema.getItems(); + var nextLevel = this.curLevel + 1; + for (var ii = 0, il = items.length; ii < il; ii++) + if (items[ii] instanceof schema_foreign_key_1.SchemaForeignKey || items[ii] instanceof SchemaIdArray) + items[ii].curLevel = nextLevel; + promises.push(instance.schema.getAsJson(instance.dbEntry._id, options)); } - } - if (transformedValue.length < this.minItems) - return Promise.reject(new Error(`You must select at least ${this.minItems} item${(this.minItems == 1 ? "" : "s")} for ${this.name}`)); - if (transformedValue.length > this.maxItems) - return Promise.reject(new Error(`You have selected too many items for ${this.name}, please only use up to ${this.maxItems}`)); - return Promise.resolve(true); + return yield Promise.all(promises); + }); } } exports.SchemaIdArray = SchemaIdArray; diff --git a/server/src/models/model.ts b/server/src/models/model.ts index 4ba80470..6d69d29f 100644 --- a/server/src/models/model.ts +++ b/server/src/models/model.ts @@ -261,6 +261,8 @@ export abstract class Model var foreignModel : Model; var optionalDependencies = instance.dbEntry._optionalDependencies; var requiredDependencies = instance.dbEntry._requiredDependencies; + var arrayDependencies = instance.dbEntry._arrayDependencies; + var promises : Array> = []; // Nullify all dependencies that are optional @@ -271,11 +273,24 @@ export abstract class Model if (!foreignModel) continue; - var setToken = { $set : {} }; + let setToken = { $set : {} }; setToken.$set[optionalDependencies[i].propertyName] = null; promises.push( foreignModel.collection.updateOne( { _id : optionalDependencies[i]._id }, setToken ) ); } + // Remove any dependencies that are in arrays + if (arrayDependencies) + for ( var i = 0, l = arrayDependencies.length; i < l; i++ ) + { + foreignModel = Model.getByName( arrayDependencies[i].collection ); + if (!foreignModel) + continue; + + let pullToken = { $pull : {} }; + pullToken.$pull[arrayDependencies[i].propertyName] = instance._id; + promises.push( foreignModel.collection.updateMany( { _id : arrayDependencies[i]._id }, pullToken ) ); + } + // For those dependencies that are required, we delete the instances if (requiredDependencies) for ( var i = 0, l = requiredDependencies.length; i < l; i++ ) @@ -287,6 +302,8 @@ export abstract class Model promises.push( foreignModel.deleteInstances( { _id : requiredDependencies[i]._id } ) ); } + + var dependenciesResults = await Promise.all(promises); // Remove the original instance from the DB diff --git a/server/src/models/schema-items/schema-foreign-key.ts b/server/src/models/schema-items/schema-foreign-key.ts index 2f180673..b58410ff 100644 --- a/server/src/models/schema-items/schema-foreign-key.ts +++ b/server/src/models/schema-items/schema-foreign-key.ts @@ -2,7 +2,8 @@ import {ISchemaOptions} from "modepress-api"; import {Model, ModelInstance} from "../model"; import {ObjectID} from "mongodb"; -import {Utils} from "../../utils" +import {Utils} from "../../utils"; +import {SchemaIdArray} from "./schema-id-array"; /** * Represents a mongodb ObjectID of a document in separate collection. @@ -139,39 +140,39 @@ export class SchemaForeignKey extends SchemaItem { + if (options.expandForeignKeys && options.expandMaxDepth === undefined) + throw new Error("You cannot set expandForeignKeys and not specify the expandMaxDepth"); + if (!options.expandForeignKeys) return this.value; - else + + if (options.expandSchemaBlacklist && options.expandSchemaBlacklist.indexOf(this.name) != -1) + return this.value; + + var model = Model.getByName(this.targetCollection); + if (!model) + throw new Error(`${this.name} references a foreign key '${this.targetCollection}' which doesn't seem to exist`); + + if (!this.value) + return null; + + // Make sure the current level is not beyond the max depth + if (options.expandMaxDepth !== undefined) { - var model = Model.getByName(this.targetCollection); - if (model) - { - if (!this.value) - return null; - - // Make sure the current level is not beyond the max depth - if (options.expandMaxDepth !== undefined) - { - if ( this.curLevel > options.expandMaxDepth ) - return this.value; - } - else - options.expandMaxDepth = 1; - - var result = await model.findOne( { _id : this.value } ); - - // Get the models items are increase their level - this ensures we dont go too deep - var items = result.schema.getItems(); - var nextLevel = this.curLevel + 1; - - for (var i = 0, l = items.length; i < l; i++) - if ( items[i] instanceof SchemaForeignKey ) - (items[i]).curLevel = nextLevel; - - return await result.schema.getAsJson( result.dbEntry._id, options ); - } - else - throw new Error(`${this.name} references a foreign key '${this.targetCollection}' which doesn't seem to exist`); + if ( this.curLevel > options.expandMaxDepth ) + return this.value; } + + var result = await model.findOne( { _id : this.value } ); + + // Get the models items are increase their level - this ensures we dont go too deep + var items = result.schema.getItems(); + var nextLevel = this.curLevel + 1; + + for (var i = 0, l = items.length; i < l; i++) + if ( items[i] instanceof SchemaForeignKey || items[i] instanceof SchemaIdArray ) + (items[i]).curLevel = nextLevel; + + return await result.schema.getAsJson( result.dbEntry._id, options ); } } \ No newline at end of file diff --git a/server/src/models/schema-items/schema-id-array.ts b/server/src/models/schema-items/schema-id-array.ts index 69dfc6f8..67c2d8fd 100644 --- a/server/src/models/schema-items/schema-id-array.ts +++ b/server/src/models/schema-items/schema-id-array.ts @@ -1,29 +1,42 @@ import {SchemaItem} from "./schema-item"; +import {SchemaForeignKey} from "./schema-foreign-key"; +import {Model, ModelInstance} from "../model"; import {ISchemaOptions} from "modepress-api"; import sanitizeHtml = require("sanitize-html"); -import {ObjectID} from "mongodb"; +import {ObjectID, UpdateWriteOpResult} from "mongodb"; import {Utils} from "../../utils" /** -* A n ID array scheme item for use in Models -*/ -export class SchemaIdArray extends SchemaItem> + * An ID array scheme item for use in Models. Optionally can be used as a foreign key array + * and return objects of the specified ids. In order for the array to return objects you must + * specify the targetCollection property. This tells the schema from which model the ids belong to. + * Currently we only support Id lookups that exist in the same model - i.e. if the ids are of objects + * in different models we cannot get the object values. + */ +export class SchemaIdArray extends SchemaItem> { + public targetCollection : string; public minItems: number; public maxItems: number; + public curLevel: number; + private _targetDocs : Array>; /** - * Creates a new schema item that holds an array of id items - * @param {string} name The name of this item - * @param {Array} val The array of ids for this schema item - * @param {number} minItems [Optional] Specify the minimum number of items that can be allowed - * @param {number} maxItems [Optional] Specify the maximum number of items that can be allowed - */ - constructor(name: string, val: Array, minItems: number = 0, maxItems: number = 10000) + * Creates a new schema item that holds an array of id items + * @param {string} name The name of this item + * @param {Array} val The array of ids for this schema item + * @param {number} minItems [Optional] Specify the minimum number of items that can be allowed + * @param {number} maxItems [Optional] Specify the maximum number of items that can be allowed + * @param {string} targetCollection [Optional] Specify the model name to which all the ids belong. If set + * the item can expand objects on retreival. + */ + constructor(name: string, val: Array, minItems: number = 0, maxItems: number = 10000, targetCollection: string = "") { super(name, val); this.maxItems = maxItems; this.minItems = minItems; + this.targetCollection = targetCollection; + this.curLevel = 1; } /** @@ -37,6 +50,7 @@ export class SchemaIdArray extends SchemaItem> super.clone(copy); copy.maxItems = this.maxItems; copy.minItems = this.minItems; + copy.targetCollection = this.targetCollection; return copy; } @@ -44,7 +58,7 @@ export class SchemaIdArray extends SchemaItem> * Checks the value stored to see if its correct in its current form * @returns {Promise} Returns true if successful or an error message string if unsuccessful */ - public validate(): Promise + public async validate(): Promise { var transformedValue = this.value; @@ -55,18 +69,132 @@ export class SchemaIdArray extends SchemaItem> if (Utils.isValidObjectID(this.value[i])) transformedValue[i] = new ObjectID(this.value[i]); else if ((this.value[i]).trim() != "") - return Promise.reject( new Error(`Please use a valid ID for '${this.name}'`)); + throw new Error(`Please use a valid ID for '${this.name}'`); else - return Promise.reject( new Error(`Please use a valid ID for '${this.name}'`)); + throw new Error(`Please use a valid ID for '${this.name}'`); } } - if (transformedValue.length < this.minItems) - return Promise.reject( new Error(`You must select at least ${this.minItems} item${(this.minItems == 1 ? "" : "s") } for ${this.name}`)); + throw new Error(`You must select at least ${this.minItems} item${(this.minItems == 1 ? "" : "s") } for ${this.name}`); if (transformedValue.length > this.maxItems) - return Promise.reject( new Error(`You have selected too many items for ${this.name}, please only use up to ${this.maxItems}`)); + throw new Error(`You have selected too many items for ${this.name}, please only use up to ${this.maxItems}`); + + // If no collection - then return + if (this.targetCollection == "") + return true; + + if (this.value.length == 0) + return true; + + // If they collection is not empty, then it must exist + var model = Model.getByName(this.targetCollection); + + if (!model) + throw new Error(`${this.name} references a foreign key '${this.targetCollection}' which doesn't seem to exist`); + + // We can assume the value is object id by this point + var query = { $or : [] }; + var arr = this.value; + + for (var i = 0, l = arr.length; i < l; i++) + query.$or.push( { _id :arr[i] } ); + + var result = await model.findInstances( query ); + this._targetDocs = result; + + return true; + } + + /** + * Called once a schema has been validated and inserted into the database. Useful for + * doing any post update/insert operations + * @param {ModelInstance} instance The model instance that was inserted or updated + * @param {string} collection The DB collection that the model was inserted into + */ + public async postValidation( instance: ModelInstance, collection : string ): Promise + { + if (!this._targetDocs) + return; + + // If they key is required then it must exist + var model = Model.getByName(this.targetCollection); + var promises : Array> = []; + + for (var i = 0, l = this._targetDocs.length; i < l; i++) + { + let arrDeps = this._targetDocs[i].dbEntry._arrayDependencies || []; + arrDeps.push( { _id : instance.dbEntry._id, collection: collection, propertyName: this.name } ); + promises.push( model.collection.updateOne( { _id : this._targetDocs[i].dbEntry._id }, { + $set : { _arrayDependencies: arrDeps } + })); + } + + await Promise.all( promises ); + + // Nullify the target doc cache + this._targetDocs = null; + return; + } + + /** + * Gets the value of this item + * @param {ISchemaOptions} options [Optional] A set of options that can be passed to control how the data must be returned + * @returns {Promise>} + */ + public async getValue(options? : ISchemaOptions): Promise> + { + if (options.expandForeignKeys && options.expandMaxDepth === undefined) + throw new Error("You cannot set expandForeignKeys and not specify the expandMaxDepth"); + + if (!options.expandForeignKeys) + return this.value; + + if (options.expandSchemaBlacklist && options.expandSchemaBlacklist.indexOf(this.name) != -1) + return this.value; + + if (this.targetCollection == "") + return this.value; + + var model = Model.getByName(this.targetCollection); + if (!model) + throw new Error(`${this.name} references a foreign key '${this.targetCollection}' which doesn't seem to exist`); + + // Make sure the current level is not beyond the max depth + if (options.expandMaxDepth !== undefined) + { + if ( this.curLevel > options.expandMaxDepth ) + return this.value; + } + + if (this.value.length == 0) + return this.value; + + // Create the query for fetching the instances + var query = { $or : [] }; + for (var i = 0, l = this.value.length; i < l; i++) + query.$or.push( { _id : this.value[i] } ); + + var instances = await model.findInstances( query ); + var instance: ModelInstance; + + var toReturn : Array = []; + var promises : Array> = []; + + // Get the models items are increase their level - this ensures we dont go too deep + for (var i = 0, l = instances.length; i < l; i++) + { + instance = instances[i]; + var items = instance.schema.getItems(); + var nextLevel = this.curLevel + 1; + + for (var ii = 0, il = items.length; ii < il; ii++) + if ( items[ii] instanceof SchemaForeignKey || items[ii] instanceof SchemaIdArray ) + (items[ii]).curLevel = nextLevel; + + promises.push( instance.schema.getAsJson( instance.dbEntry._id, options ) ); + } - return Promise.resolve(true); + return await Promise.all(promises); } } \ No newline at end of file