Skip to content

Instantly share code, notes, and snippets.

@jhyland87
Created October 4, 2016 17:16
Show Gist options
  • Save jhyland87/6a1617fe959561e9013ee72806021c0e to your computer and use it in GitHub Desktop.
Save jhyland87/6a1617fe959561e9013ee72806021c0e to your computer and use it in GitHub Desktop.
/**
* 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