Created
October 4, 2016 17:16
-
-
Save jhyland87/6a1617fe959561e9013ee72806021c0e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Asset [Mongoose]{@link http://mongoosejs.com/} Model - Used to create/edit/delete any Asset documents, as well as MDB | |
* documents in the Field, Partition and Revision collections that may be associated to the the Asset(s) | |
* | |
* @module AssetModel | |
* @see {@link http://mongoosejs.com/docs/middleware.html|Mongoose Middleware} | |
* @see {@link http://mongoosejs.com/docs/guide.html|Mongoose Schema} | |
* @see {@link http://mongoosejs.com/docs/schematypes.html|Mongoose Schema Types} | |
*/ | |
'use strict' | |
//const _m = AppRoot.require('./dist/lib/helpers/mongoose-helper') | |
const modelName = _m.file2Model( path.basename( __filename ).match( /(.*)\.js$/ )[ 1 ] ) | |
const log = AppRoot.require('./dist/lib/utils/logger')({ model: `${modelName}`}) | |
const accountLib = AppRoot.require('./dist/lib/account') | |
/** | |
* @param {Mongoose} Mongoose Mongoose instance | |
* @property {Object} Mongoose.Schema Schema thingy | |
* @property {Object} Mongoose.Types Mongoose document attribute types | |
* @returns {Mongoose.model} Asset Mongoose model | |
*/ | |
module.exports = Mongoose => { | |
// Return this model, if it already exists | |
if( ! _.isUndefined( Mongoose.models[ modelName ] ) ) | |
return Mongoose.models[ modelName ] | |
/** | |
* Reference to the Mongoose Schema object | |
* | |
* @name Schema | |
* @member {Mongoose.Schema} | |
* @see http://mongoosejs.com/docs/guide.html | |
* @constant | |
* @readonly | |
*/ | |
const Schema = Mongoose.Schema | |
/** | |
* Reference to the Mongoose Types object | |
* | |
* @name Types | |
* @inner | |
* @instanceof Mongoose.Types | |
* @see http://mongoosejs.com/docs/schematypes.html | |
* @constant | |
* @readonly | |
*/ | |
const Types = Mongoose.Types | |
/** | |
* Schema settings for Asset documents | |
* @member {Mongoose.Schema} AssetSchema | |
* @property {string} status Status of asset. This can be 'locked' (meaning locked, | |
* but unsecured with no password), 'unlocked' (meaning | |
* anyone with access can do anything), or a 108 character | |
* password, which means the asset is 'secured' | |
* @property {Mongoose.Types.ObjectId} _createdBy Reference to a document in the Account collection | |
* of the user who created the asset | |
* @property {Mongoose.Types.ObjectId} _updatedBy Reference to a document in the Account collection | |
* of whoever updated the asset last | |
* @property {Schema} attrCache This is a sub-schema with no format or property | |
* restrictions, it holds a cached version of the assets | |
* attribute values (updated via Asset/AttrCache middleware | |
* plugin, which updates it via the documents 'save' event) | |
* @property {array} attributes Array of documents containing the assets attributes and values | |
* @property {Mongoose.Types.ObjectId} attributes[]._field Reference to the attributes field document | |
* @property {*} attributes[].value Value of the attribute | |
* @property {Mongoose.Types.ObjectId} _partition Reference document to the parent partition (Assets | |
* are associated to the partitions, not the other way around) | |
* @instanceof {Mongoose.Schema} | |
*/ | |
const AssetSchema = new Schema({ | |
status: { | |
type: Schema.Types.String, | |
default: 'unlocked', | |
select: true, | |
minlength: 6, // Length of 'locked' | |
maxlength: 108, // Length of _.passwordHash value | |
// Validate that its locked, unlocked, or a 108 character hash (made by _.passwordHash) | |
validate: status => _.includes( ['unlocked','locked'], status ) || status.length === 108 | |
}, | |
_createdBy: { | |
//required: true, | |
type: Schema.Types.ObjectId, | |
ref: 'Account' | |
}, | |
_updatedBy: { | |
type: Schema.Types.ObjectId, | |
ref: 'Account' | |
}, | |
// The attribute cache object is used for quick searches, it gets updated after any asset document save/update | |
attrCache: new Schema({},{ strict: false }), | |
attributes: [{ | |
_field: { | |
type: Schema.Types.ObjectId, | |
ref: 'Field', | |
required: true | |
}, | |
value: { | |
type: Schema.Types.Mixed, | |
required: true | |
} | |
}], | |
/** | |
* Instead of the partition containing a list of assets it owns, the assets will dictate what partition they | |
* belong to. I decided this because Mongoose documents have a size limit of 16MB, so that would limit how many | |
* references there can be, even if thats a large limit, id rather not tell people they can only have n assets | |
* per a partition | |
* @ignore | |
*/ | |
_partition: { | |
type: Schema.Types.ObjectId, | |
ref: 'Partition', | |
required: true | |
} | |
}, { | |
timestamps: { | |
createdAt: 'createdAt', | |
updatedAt: 'updatedAt' | |
} | |
}) | |
// PLUGINS/MIDDLEWARE PACKAGES----------------------------------------- | |
// MIDDLEWARE --------------------------------------------------------- | |
_.forEach( [ 'find', 'findOne', 'findOneById', 'findOneAndRemove', 'findOneAndUpdate' ], query => { | |
AssetSchema.pre( query, function() { | |
this.populate( 'attributes._field' ).populate( { path: '_partition' } ) | |
}) | |
}) | |
// VIRTUAL PROPERTIES ------------------------------------------------- | |
/** | |
* Retrieve the primary value of an asset. The main goal of this is to quickly be able to reference | |
* assets by their primary value, if one is set. This defaults back to the assets _id string | |
* | |
* NOTE: For the identifier virtual property to work properly, the _partition and _partition._fields | |
* both need to be populated, otherwise, the assets ID will be returned | |
* | |
* @this AssetSchema | |
* @instance | |
* @readonly | |
* @name module:AssetModel#identifier | |
* @memberof module:AssetModel | |
* @returns {string} Whatever the value of the primary field is for this asset, or if there isn't a | |
* primary field, or it isn't populated, return the assetID | |
* @example // In partition without a primary field, the documents ObjectID is the identifier | |
* AssetModel.create( partitionId, {}, ( err, assetDoc ) => { | |
* assetDoc.identifier // The Mongoose ObjectID of the new asset document | |
* }) | |
* | |
* @example // In a partition with a primary field, the primaryField value is the identifier | |
* AssetModel.create( partitionId, { | |
* primaryAttribute: 'Some-Unique-Value' | |
* }, ( err, assetDoc ) => { | |
* assetDoc.identifier // Some-Unique-Value | |
* }) | |
*/ | |
AssetSchema.virtual('identifier').get(function () { | |
// Value to return if no primary field is configured or no value is found | |
const assetID = this._id | |
// If the partition or partition fields werent loaded, then return null | |
// (Should only happen if they weren't populated via the query) | |
if( ! this._partition || ! this._partition._fields ) | |
return assetID | |
// Filter for the primary field.. | |
const primaryField = _.find( this._partition._fields, { primary: true } ) | |
// If no primary field is found, just return null | |
if( _.isUndefined( primaryField ) ) | |
return assetID | |
// Filter through the assets attributes for the primary field attribute | |
const primaryVal = _.find( this.attributes, f => { | |
// If the field is an object, then the attributes._field has been populated, | |
// so check the _id of the field, rather than the field itself.. | |
if( _.isObject( f._field ) ) | |
return f._field._id.toString() === primaryField._id.toString() | |
// Otherwise, just do a simple comparison | |
else | |
return f._field.toString() === primaryField._id.toString() | |
} ) | |
// If its not found, return null. This should only happen if the primary field was | |
// set/configured after the asset was already made | |
if( ! primaryVal ) | |
return assetID | |
// If the primary value is empty/null (which should only happen if the asset was | |
// created before the primary field was configured), then return the assets ID | |
return _.isEmpty( primaryVal.value ) | |
? assetID | |
: primaryVal.value | |
}) | |
// INSTANCE METHODS --------------------------------------------------- | |
/** | |
* Delete an asset - This is meant to be more complicated than just removing the document from the collection, as | |
* there should be certain other actions taken, and if it's a soft delete, just update the doc | |
* | |
* @function module:AssetModel#delete | |
* @name module:AssetModel#delete | |
* @instance | |
* @param {?(string|function)=} commentOrCb Any comment to add for the logs, such as why the asset was deleted | |
* @param {module:AssetModel~deleteCb=} callback Callback to fire when asset gets deleted successfully (If | |
* undefined, then a Promise will be returned from this method) | |
* @returns {Promise} Returns a Bluebird promise, but only if the callback param is undefined | |
* @example // Query for an asset, then delete it (with a comment) | |
* AssetModel.getAsset( assetsObjectId ) | |
* .then( asset => { return asset.delete( `Deleting asset ${asset.identifier} (duplicate)`) }) | |
* .then( data => console.log( 'Asset successfully deleted' ) ) | |
* | |
* @example // Query for an asset, then delete it (without a comment) | |
* AssetModel.getAsset( assetsObjectId ) | |
* .then( asset => asset.delete ) | |
* .then( data => console.log( 'Asset successfully deleted' ) ) | |
*/ | |
AssetSchema.methods.delete = function ( commentOrCb, callback ){ | |
return new Promise( ( res, rej ) => { | |
}).asCallback( ( args => _.findLast( args, a => _.isFunction( a ) ) )( arguments ) ) | |
} | |
/** | |
* Callback executed when an asset document is created. | |
* | |
* @function | |
* @type function | |
* @name deleteCb | |
* @callback module:AssetModel~deleteCb | |
* @param {?(string|Exception)} error Error that was thrown, or null for no error | |
* @param {Object} data Uhm... not sure | |
* @param {string} data.fooBar Baz quux | |
* @todo What should the 'data' value for the callback be? hm.. | |
*/ | |
// -------------------------------------------------------------------- | |
/** | |
* Create an entry in the Revisions collection with a copy of the assets current values | |
* | |
* @this AssetSchema | |
* @function module:AssetModel#createRevisionHistory | |
* @name module:AssetModel#createRevisionHistory | |
* @param {module:AssetModel~createRevisionCb=} callback Callback to fire (Optional, or promise returned) | |
* @returns {Promise} Returns a Bluebird promise, but only if the callback param is undefined | |
*/ | |
AssetSchema.methods.createRevisionHistory = function( callback ) { | |
return new Promise( ( res, rej ) => { | |
}).asCallback( callback ) | |
} | |
/** | |
* Callback executed when the assets document revision history entry is created | |
* | |
* @function | |
* @type function | |
* @name createRevisionCb | |
* @callback module:AssetModel~createRevisionCb | |
* @param {?(string|Exception)} error Error that was thrown, or null for no error | |
* @param {Object} data Document containing any | |
* @param {Types.ObjectId} data._id ID of the new revision entry | |
*/ | |
// STATIC METHODS ----------------------------------------------------- | |
/** | |
* Create a *single* asset, associating it to the specified partition. This is much easier than inserting a new | |
* asset document manually, since the `attributes` parameter here can be a simple object, and the static/dynamic | |
* attributes are extracted by grabbing the partition fields and parsing the object | |
* | |
* @function module:AssetModel.create | |
* @memberof module:AssetModel | |
* @this AssetSchema | |
* @name module:AssetModel.create | |
* @param {ObjectId} partitionId Partition ID to add asset to | |
* @param {?(Object|function)=} attributesOrCb Either the Assets attribute values (static AND dynamic - The | |
* static and dynamic attributes are separated, and the proper | |
* asset document structure is constructed); Or a callback function | |
* @param {module:AssetModel~createCb=} callback Callback to fire when asset gets created successfully (If | |
* undefined, then a Promise will be returned from this method) | |
* @returns {Promise} Returns a Bluebird promise, unless a callback is specified | |
* @note I HIGHLY recommend that the assets be created from the Partition instance methods, as they do most of | |
* the validation, this should only be used after the asset attributes are already validated | |
* @todo Validate the asset attributes against the partition field settings | |
* @todo Validate the partition ID exists (by getting the data) | |
* @todo Use above data to verify the primary attibute is populated | |
* @example // Create an asset in a partition with the primary field 'primaryAttr' | |
* AssetModel.create( PartitionsObjectId, { | |
* primaryAttr: 'Some-Unique-Value', // Primary (string) attribute (thus, unique value) | |
* booleanAttr: false, // Boolean attribute | |
* numericAttr: 123 // Numeric attribute | |
* }, ( err, assetDoc ) => { | |
* console.log( `The asset ${assetDoc.identifier} was successfully created` ) | |
* // The asset Some-Unique-Value was successfully created | |
* }) | |
*/ | |
AssetSchema.statics.create = function( partitionId, attributesOrCb, callback ) { | |
return new Promise( ( res, rej ) => { | |
// Validate the partition ID is a valid Mongoose ID | |
if( ! Types.ObjectId.isValid( partitionId.toString() ) ) | |
return rej( 'Need a partition ID to add the assets to' ) | |
// If the attributes are not unset AND not an object, then throw a hissy fit | |
if( ! attributes && ! _.isObject( attributes ) ) | |
return rej( 'Asset attributes need to be in an object' ) | |
attributes = attributes || {} | |
// Values to pull out of the attributes object, as they aren't partition fields | |
let statics = [ 'status' ] | |
// Separate the static fields from the attributes object (Since they are stored differently in the DB) | |
statics = _.removeObj( attributes, statics ) | |
// Since the asset attributes need to be inserted using the field IDs (as opposed to names), get those | |
// first, then use the attribute names to filter for the needed key | |
Mongoose.models.Partition.getFieldIdsByName( partitionId, Object.keys( attributes ) ) | |
.then( fields => { | |
// Construct the attribute values in the right format (Array of objects like: | |
// [ { _field: fieldId, value: 'Attr Val' } ] | |
const assetAttrs = _.map( attributes, ( value, key ) => ( { _field: fields[ key ], value } ) ) | |
// Create the new asset with several merged objects, should result in something like: | |
// { _partition: partitionId, | |
// status: 'locked', | |
// attributes: [ { _field: someFieldId, value: 'Some Value' } ] } | |
// @todo If this is broken, its probably because of me adding the 'return' | |
return new this( | |
_.merge( { _partition: partitionId }, statics, { attributes: assetAttrs } ) | |
).save() | |
.then( assetData => { | |
log.info( `Asset ID ${assetData._id.toString()} successfully created` ) | |
res( assetData ) | |
} ) | |
.catch( err => rej( err ) ) | |
}) | |
.catch( err => rej( _.setException( err ) ) ) | |
}).asCallback( ( args => _.findLast( args, a => _.isFunction( a ) ) )( arguments ) ) | |
} | |
/** | |
* Callback executed when a Asset document is created and added to the MDB collection | |
* | |
* @function | |
* @type function | |
* @name createCb | |
* @callback module:AssetModel~createCb | |
* @param {?(string|Exception)} error Error that was thrown, or null for no error | |
* @param {Object} data MDB document of the newly created Asset | |
* @param {Types.ObjectId} data.partitionId ID of partition document this asset is associated to | |
*/ | |
// -------------------------------------------------------------------- | |
log.debug('Returning compiled model schema') | |
return Mongoose.model( modelName, AssetSchema ) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment