Created
September 14, 2016 16:08
-
-
Save jhyland87/9cef5a4ac2ccbbb8c4da0bf8f23dbbf0 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
// src/models/partition.js | |
'use strict' | |
import _ from 'moar-lodash' | |
import Promise from 'bluebird' | |
import Async from 'async' | |
import Util from 'util' | |
import * as AppRoot from 'app-root-path' | |
import path from 'path' | |
//const exception = AppRoot.require('./dist/lib/utils/exception').init('partitionException') | |
const AppError = AppRoot.require( "./dist/lib/exceptions" ).init | |
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') | |
log.debug( `Model file loaded` ) | |
/** | |
* Partition Model | |
* - Create/find/edit/delete partitions | |
* - Find assets within instances partition | |
* - Partition field management | |
*/ | |
module.exports = Mongoose => { | |
// Return this model, if it already exists | |
if( ! _.isUndefined( Mongoose.models[ modelName ] ) ) | |
return Mongoose.models[ modelName ] | |
const Schema = Mongoose.Schema | |
const partitionSchema = new Schema({ | |
name: { | |
type: Schema.Types.String, | |
trim: true, | |
select: true, | |
unique: true, | |
required: true, | |
minlength: 4, | |
maxlength: 30 | |
}, | |
description: Schema.Types.String, | |
status: { | |
type: Schema.Types.Boolean, | |
default: true | |
}, | |
_groups: [{ | |
type: Schema.Types.ObjectId, | |
ref: 'Group' | |
}], | |
_fields: [{ | |
type: Schema.Types.ObjectId, | |
ref: 'Field' | |
}] | |
}, { | |
timestamps: { | |
createdAt: 'createdAt', | |
updatedAt: 'updatedAt' | |
} | |
}) | |
//partitionSchema.plugin( require('mongoose-unique-validator') ) | |
// MIDDLEWARE --------------------------------------------------------- | |
/** | |
* Since most of the instance methods, static methods and virtual properties rely on the partition fields, | |
* automatically populate the _fields reference to the field model documents for every find-like query | |
*/ | |
_.forEach( [ 'find', 'findOne', 'findOneById', 'findOneAndRemove', 'findOneAndUpdate' ], query => { | |
partitionSchema.pre( query, function( next ) { | |
this.populate( '_fields' ) | |
next() | |
}) | |
}) | |
// -------------------------------------------------------------------- | |
// Middleware for any update-type queries | |
_.forEach( [ 'save', 'update', 'findOneAndUpdate' ], query => { | |
// Increment the Mongoose (__v)ersion for any updates | |
partitionSchema.pre( query, function( next ) { | |
this.increment() | |
next() | |
}) | |
}) | |
// VIRTUAL PROPERTIES ------------------------------------------------- | |
/** | |
* Retrieve the primary field for said partition | |
* | |
* @returns {object} Returns the entire field, if the primary is true | |
*/ | |
partitionSchema.virtual('primaryField').get(function() { | |
const primary = _.find( this._fields, f => !!f.primary ) | |
return ! _.isEmpty( primary ) | |
? primary | |
: undefined | |
}) | |
// INSTANCE METHODS --------------------------------------------------- | |
/** | |
* Verify an assets attribute value against the corresponding field settings in the assets partition | |
* | |
* @param {object|string} options Object containing the attribute/field name & value, or the attribute | |
* name/field ID (Which if provided as a string, value will be | |
* automatically nulled, since it cant be defined anywhere else | |
* @param {function} callback Callback to be executed (if passed) | |
* @var {string} options.attr Name attribute or ID of field | |
* @var {Mixed} options.value Value of attribute (Null if undefined) | |
* @var {string|ObjectId} options.assetId If the attribute value is being validated while updating the asset | |
* (as opposed to creating), then set the asset ID (This is required | |
* for some validations such as unique) | |
* @returns {boolean} | |
*/ | |
partitionSchema.methods.verifyAttr = function( options, callback ) { | |
return new Promise( ( res, rej ) => { | |
let assetId | |
// If the options.assetId is provided, then were updating an asset, not creating a new one, so verify that | |
// the asset ID provided is a valid ObjectId | |
// The validation that this is a real asset ID is done in the validation factory init | |
if( ! _.isUndefined( options.assetId ) ){ | |
if( _m.isObjectId( options.assetId ) ) | |
assetId = options.assetId | |
else | |
return rej( new AppError({ | |
code: 'partition.verifyAttr.options.badAssetId', | |
data: _.typeof(options.assetId) | |
}) ) | |
} | |
// If there's no fields in this partition, obviously its an invalid attribute | |
if( _.isEmpty( this._fields ) ) | |
return rej( new AppError( 'partition.verifyAttr.noPartitionFields' ) ) | |
// If the fields aren't populated, then reject | |
// @todo Should just populate them if they arent already populated | |
if( !_m.isPopulated( this._fields[0] ) ) | |
return rej( new AppError( 'partition.verifyAttr.fields.notPopulated' ) ) | |
let attr | |
// Static attributes (Need to be validated differently than dynamic) | |
const statics = [ 'status' ] | |
// If options is a string, then its the field/attr name or ID (and no value is provided) | |
if( _.isString( options ) ) | |
attr = options | |
// If options is an object, it should contain the attr and value | |
if( _.isObject( options ) && _.isString( options.attr ) ) | |
attr = options.attr | |
// Reject!.. No attribute was provided | |
else | |
return rej( new AppError( 'partition.verifyAttr.attributes.noAttributes' ) ) | |
// Default the value to null.. | |
const value = _.isUndefined( options.value ) ? null : options.value | |
// If its a static attr, just return true for now | |
// @todo Need to actually do some checking | |
if( _.includes( statics, attr ) ) | |
res( 'STATIC OK' ) | |
// Find the first field that matches the attribute string to the field ID or name | |
const field = _.find( this._fields, f => f._id === attr || f.name === attr ) | |
// Obviously no field found, then don't validate it | |
if( ! field ) | |
return rej( new AppError({ | |
code: 'partition.verifyAttr.fields.notFound', | |
data: [ attr, this.name, this._id ] | |
})) | |
// Is it required and populated? | |
if( ( field.required === true || field.primary === true ) && _.isEmpty( value ) ) | |
return rej( new AppError({ | |
code: 'partition.verifyAttr.attributes.noValue', | |
data: field.name | |
}) ) | |
// Array of promises to validate | |
const doValidations = [] | |
// Validation factory (Returns any validation functions, unique, regex, etc) | |
const validationFactory = Mongoose.models.Field.validations({ | |
partitionId: this._id, | |
value: value, | |
field: attr, | |
assetId: assetId | |
}) | |
// Add the unique check if this is primary or unique | |
if( field.primary === true || field.unique === true ) | |
doValidations.push( validationFactory.unique() ) | |
// Add the regex check if regex is populated | |
if( field.regex ) | |
doValidations.push( validationFactory.regex( field.regex ) ) | |
// If a 'max' size/length is set, | |
if( _.isNumber( field.max ) ){ | |
// If this is a numeric or timestamp, then use the max SIZE.. | |
if( _.includes( [ 'timestamp', 'numeric' ], field.type ) ) | |
doValidations.push( validationFactory.max( field.max ) ) | |
// Otherwise, use max LENGTH | |
else | |
doValidations.push( validationFactory.maxLength( field.max ) ) | |
} | |
// If a 'min' size/length is set, | |
if( _.isNumber( field.min ) ){ | |
// If this is a numeric or timestamp, then use the min SIZE.. | |
if( _.includes( [ 'timestamp', 'numeric' ], field.type ) ) | |
doValidations.push( validationFactory.min( field.min ) ) | |
// Otherwise, use min LENGTH | |
else | |
doValidations.push( validationFactory.minLength( field.min ) ) | |
} | |
// Run all the promises, and only resolve this promise if all of those were successful | |
Promise.all( doValidations ) | |
.then( data => res( true ) ) | |
.catch( err => rej( new AppError({ | |
code: 'partition.verifyAttr.validateAction', | |
data: err | |
}) ) ) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Retrieve select Partition Field ID's by the field names (ORM METHOD) | |
* | |
* @param {string|array} fieldNames String of field names, or array of multiple. | |
* @return {object} Object of field IDs and Names, with the field names as the keys | |
*/ | |
partitionSchema.methods.getFieldIdsByName = function( fieldNames ) { | |
return _.chain( this._fields ) | |
.filter( f => _.includes( fieldNames, f.name ) ) | |
.reduce( ( end, f ) => { | |
end[ f.name ] = f._id | |
return end | |
},{}) | |
.value() | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Add asset to associated partition | |
* | |
* @param {object} attributes Assets attributes (In AttrName: value format) | |
* @param {function} callback Callback to execute (As opposed to a Promise) | |
* @returns {Promise} | |
*/ | |
partitionSchema.methods.createAsset = function( attributes, callback ) { | |
return new Promise( ( res, rej ) => { | |
if( ! _.isObject( attributes ) ) | |
return rej( new AppError({ | |
code: 'partition.asset.create.attributes.badType', | |
data: _.typeof(attributes) | |
})) | |
// Object to be populated for any possible attribute validation errors | |
const validationErrors = {} | |
// The Async library expects an array, so turn the object into an array of objects | |
const validateAttrs = _.map( attributes, ( value, key ) => ( { attr: key, value: value } ) ) | |
// Look for a primary field in this partition | |
const primaryField = _.find( this._fields, f => f.primary === true ) | |
// Check if there's a primary field, if so, make sure there's an attribute for it | |
if( ! _.isEmpty( primaryField ) && _.isEmpty( attributes[ primaryField.name ] ) ) | |
return rej( new AppError({ | |
code: 'partition.asset.create.attributes.primaryDuplicate', | |
data: primaryField.name | |
}) ) | |
// Validate the assets attributes asynchronously | |
Async.each( validateAttrs, ( attrVal, done ) => { | |
// Validate the attributes value against the field settings.. | |
this.verifyAttr( attrVal ) | |
.catch( err => { | |
validationErrors[ attrVal.attr ] = err | |
}) | |
.finally( () => done() ) // Close every async call | |
//.catch( err => rej( _.setException( err ) ) ) | |
}, err => { | |
if( err ) | |
return rej( new AppError({ | |
code: 'partition.asset.create.attributes.validationsFailed', | |
data: err | |
}) ) | |
// Check for any validation errors | |
if( ! _.isEmpty( validationErrors ) ) { | |
return rej( new AppError({ | |
code: 'partition.asset.create.attributes.validationsFailed', | |
data: validationErrors | |
}) ) | |
} | |
// If it hasn't been stopped by an error yet, then it should be good to go | |
Mongoose.models.Asset.createAsset( this._id, attributes ) | |
.then( data => res( data ) ) | |
.catch( err => rej( new AppError({ | |
code: 'partition.asset.create.saveFailed', | |
data: err | |
}) ) ) | |
}) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Create multiple assets - These do not get created async currently, as they need to validate each asset as they | |
* get created, (eg: if two assets have the same primary value, async will create them, since the unique check will | |
* pass) | |
* @todo Add some checks in here (such as unique, etc), so this CAN execute asynchronously | |
* | |
* @param {array} attributes Array of asset attribute objects | |
* @param {function} callback Callback to execute, if not handling as a promise | |
* @returns {Promise} Bluebird promise, or callback gets executed | |
* @todo Should be able to provide a number for the attributes, which will attempt to create n assets with no attributes (which should work if partition has no primary/required fields) | |
* @todo If an asset fails to be created, it should still move on to the next | |
*/ | |
partitionSchema.methods.createAssets = function( attributes, callback ) { | |
return new Promise( ( res, rej ) => { | |
if( ! _.isArray( attributes ) ) | |
return rej( new AppError({ | |
code: 'partition.assets.create.attributes.badType', | |
data: _.typeof( attributes ) | |
}) ) | |
if( ! _.every( attributes, _.isObject ) ) | |
return rej( new AppError( 'partition.assets.create.attributes.badValueTypes' ) ) | |
// Execute the asset additions in order (Async, but not parallel) | |
Async.mapSeries( attributes, ( thisAsset, asyncCb ) => { | |
// The createAsset asynchronously validates the attribute values | |
this.createAsset( thisAsset, ( err, thisResult ) => asyncCb( err, thisResult ) ) | |
}, ( err, results ) => { | |
if( err ) | |
return rej( new AppError({ | |
code: 'partition.assets.create.createFailed', | |
data: err | |
}) ) | |
res( results ) | |
}) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Asynchronously delete multiple assets; This is really just a wrapper around asynchronously executing the | |
* Partition.deleteAsset() method | |
* | |
* @param {array|object} options Delete options (assets and other option(s)) | |
* @param {function} callback Callback to execute (or promise returned) | |
* @var {array} options.assets Array of asset IDs or identifiers | |
* @var {string} options.assets[*] Asset ID or identifier | |
* @var {boolean} options.requireDelete Require deletion of assets to be successful | |
* @return {Promise} Promise returned, or callback executed (if provided) | |
* @todo Need to hand down the asyncResults even for a failure, should map the array, using the asset ID as a key | |
* @todo This should delete the documents directly, as opposed to async executing the deleteAsset (maybe) | |
*/ | |
partitionSchema.methods.deleteAssets = function( options, callback ) { | |
return new Promise( ( res, rej ) => { | |
let assets | |
let requireDelete = false | |
// Of options is an object.. Check for the assets, and the requireDelete setting | |
if( _.isPlainObject( options ) ){ // This fails if its just _.isObject.. weird | |
if( _.isEmpty( options.assets ) ) | |
return rej( new AppError( 'partition.assets.delete.noAssetsSpecified' ) ) | |
assets = options.assets | |
if( _.isEmpty( options.requireDelete ) ) | |
requireDelete = !!options.requireDelete | |
} | |
// If its an array, just make sure its not empty | |
else if( _.isArray( options ) ){ | |
if( _.isEmpty( options ) ) | |
return rej( new AppError( 'partition.assets.delete.noAssetsSpecified' ) ) | |
if( ! _.every( options, _.isString ) ) | |
return rej( new AppError( 'partition.assets.delete.badAssetId' ) ) | |
assets = options | |
} | |
// Anything other than an array or object should be rejected | |
else { | |
return rej( new AppError( 'partition.assets.delete.noAssetsSpecified' ) ) | |
} | |
Async.mapSeries( assets, ( thisAsset, asyncCb ) => { | |
this.deleteAsset( { | |
asset: thisAsset, | |
requireDelete: requireDelete | |
}, ( err, result ) => asyncCb( err, result )) | |
}, ( err, results ) => ! _.isNull( err ) | |
? rej( new AppError({ | |
code: 'partition.assets.delete.deleteFailed', | |
data: err | |
}) ) | |
: res( results ) ) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Delete a single asset from associated partition, this will allow the deletion of the asset, without having to | |
* create a new asset object. The asset to be deleted can be identified by the assets ObjectId, or the assets | |
* identifier (If the partition has a primary field) | |
* | |
* @param {string|object} options String (assets ID or identifier), or an object with the options | |
* @param {function} callback Callback to execute (or promise returned) | |
* @var {string} options.asset Asset ID or assets Identifier | |
* @var {boolean} options.requireDelete If true, then a failure will be returned when an asset | |
* is not deleted (for any reason, such as it doesn't exist | |
* in the first place) | |
* @return {Promise} If an asset was deleted, then the assets data will be resolved, if no asset was deleted, | |
* and requireDelete = true, then the promise will be rejected | |
*/ | |
partitionSchema.methods.deleteAsset = function( options, callback ) { | |
return new Promise( ( res, rej ) => { | |
let asset | |
let requireDelete = false | |
if( ! _.isObject( options ) ){ | |
asset = options | |
} | |
else { | |
if( _.isEmpty( options.asset ) ) | |
return rej( new AppError( 'partition.asset.delete.noAssetId' ) ) | |
else | |
asset = options.asset | |
if( ! _.isUndefined( options.requireDelete ) ) | |
requireDelete = !!options.requireDelete | |
} | |
// If we were given something other than an asset ID, then try to find an asset with that hostname | |
if( ! _m.isObjectId( asset ) ){ | |
// This promise gets handled a little differently, since we need to differentiate between the | |
// findAssetByIdentifer failing, and the document remove failing | |
this.findAssetByIdentifier( asset ) | |
.then( assetDoc => { | |
Mongoose.models.Asset.where().findOneAndRemove( { | |
_partition: this._id, | |
_id: assetDoc._id.toString() | |
} ) | |
.then( data => res( data ) ) | |
.catch( err => rej( new AppError({ | |
code: 'partition.asset.delete.deleteFailed', | |
data: err | |
}) ) ) | |
}) | |
.catch( err => { | |
// If no asset was found with this identifier, and requireDelete is disabled, | |
// then resolve, otherwise, reject | |
return requireDelete === true | |
? rej( new AppError({ | |
code: 'partition.asset.delete.deleteFailed.notFound', | |
data: err | |
}) ) | |
: res( undefined ) | |
}) | |
} | |
// If we were given an asset ID, then handle that directly | |
else { | |
Mongoose.models.Asset.where().findOneAndRemove( { | |
_partition: this._id, | |
_id: asset | |
} ) | |
.then( data => { | |
if( _.isEmpty( data ) && requireDelete === true ) | |
return rej( new AppError({ | |
code: 'partition.asset.delete.deleteFailed.notFound', | |
data: err | |
}) ) | |
// If the data is empty, return undefined, otherwise, return the asset | |
return res( _.isEmpty( data ) | |
? undefined | |
: data ) | |
} ) | |
.catch( err => rej( new AppError({ | |
code: 'partition.asset.delete.deleteFailed', | |
data: err | |
}) ) ) | |
} | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Find an asset by its identifier - This isn't much different than the other asset searches, other than it just | |
* specifies the where object for you. If were provided an array of identifiers, then return an object, with the | |
* identifier value as the key, and only return the identifiers that were found to have assets - Meaning if an | |
* identifier was provided and no asset was found for it, then don't return it | |
* | |
* @param {string|number|array} identifier Identifier to search for (Usually a string), or an array of identifiers, | |
* if an array is provided, then an object will be provided when the | |
* callback/Promise is resolved, with the identifier as the key, and the | |
* value as the assets mongoose document | |
* @param {function} callback Callback to execute if needed, otherwise a Promise is returned | |
* @returns {Promise} A promise is returned, or the callback is executed (if defined) | |
* @note This is the same thing as Asset.findByIdentifier(), except since this is an instance method, the | |
* partition ID doesn't need to be defined, since it's already stored in the instance document | |
*/ | |
partitionSchema.methods.findAssetByIdentifier = function( identifier, callback ){ | |
return new Promise( ( res, rej ) => { | |
// Only allow strings or numeric values for the identifier (NOT meaning this is restricting to the field | |
// type, just the value itself) | |
if( ! _.isString( identifier ) | |
&& ! _.isNumeric( identifier ) | |
&& ! _.isArray( identifier ) ) | |
return rej( new Error( 'Valid identifier required (array, string or number type)' ) ) | |
// The primaryField virtual property will easily get the primary field for us (the entire object) | |
if( _.isEmpty( this.primaryField ) ) | |
return rej( new Error( 'No primary field found' ) ) | |
const primaryField = this.primaryField | |
const where = { 'attributes._field': primaryField._id } | |
// If were looking for more than one asset, use an array as the where clause | |
if( _.isArray( identifier ) ) | |
where[ 'attributes.value' ] = { $in: identifier } | |
// Or not | |
else | |
where[ 'attributes.value' ] = identifier | |
// Query for an asset with the value as the primary fields ID, and the specified identifier | |
// Keep it as a sub-promise, becuase we need the primaryField data for the error | |
Mongoose.models.Asset.find( where ) | |
.then( assetData => { | |
if( _.isEmpty( assetData ) ) | |
return rej( new Error( `No asset found with the ${primaryField.name} '${(_.isArray( identifier ) ? identifier.join("', '") : identifier)}' in this partition` ) ) | |
// If we were given an array, then structure an object, with the identifiers as the keys | |
if( _.isArray( identifier ) ){ | |
const result = {} | |
// Only add the assets that were found, so if there was an identifier provided that doesnt | |
// exist, then dont add it | |
_.forEach( assetData, ad => { | |
let primaryAttr = _.find( ad.attributes, a => a._field._id.toString() === primaryField._id.toString() ) | |
result[ primaryAttr.value ] = ad | |
}) | |
res( result ) | |
} | |
// Otherwise, just return the single asset | |
else { | |
return res( assetData[0] ) | |
} | |
}) | |
.catch( err => rej( _.setException( err ) ) ) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Get partition assets, optionally define a custom selection object. The callback can be defined as either the | |
* first or second parameter. If a selector is NOT defined, then all assets for said partition will be returned. | |
* This is basically just a quick wrapper to the Asset.getAssets() method, and auto populates the partition ID from | |
* the instances ObjectId | |
* | |
* @param {object|function} attributesOrCb Either a callback, or an object of attribute filters | |
* @param {function} callback Callback to execute, optionally | |
* @returns {Promise} Promise returned, or callback executed | |
*/ | |
partitionSchema.methods.getAssets = function( attributesOrCb, callback ){ | |
// Since hte Asset.getPartitionsAssets determines which param is the callback, just hand them all down | |
return Mongoose.models.Asset.getPartitionsAssets( this._id.toString(), attributesOrCb, callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Delete fields from a partition, as well as remove the field documents from the fields container | |
* | |
* @param {array|string|function} fieldsOrCb Single field (string), multiple (array), or all fields (undefined), | |
* If undefined, and using a callback instead of a promise, then | |
* this accepts a function as well | |
* @param {function} callback Callback to execute (Or promise) | |
* @returns {Promise} Promise, or a callback if provided (Promise gets resolved with field data) | |
* @note This needs to delete the field(s) from the partitions assets | |
* @note This should accept field names as well, using the getFieldIdsByNames | |
*/ | |
partitionSchema.methods.deleteFields = function( fieldsOrCb, callback ){ | |
return new Promise( ( res, rej ) => { | |
let toDelete | |
// Do fields even exist? | |
if( _.isEmpty( this._fields ) ) | |
return res( undefined ) | |
// Get the partition Field ID's | |
const partitionFields = _m.getObjectIds( this._fields ) | |
// If no field(s) are specified (meaning first arg is a callback or undefined), then grab all the fields | |
if( _.isFunction( fieldsOrCb ) || _.isUndefined( fieldsOrCb ) ) | |
toDelete = partitionFields | |
// If a single field ID was specified | |
else if( _.isString( fieldsOrCb ) || _m.isObjectId( fieldsOrCb ) ) | |
toDelete = _.find( partitionFields, pf => pf.toString() === fieldsOrCb.toString() ) | |
// If its an array, then just retrieve the fields that exist in this partition documents _fields | |
else if( _.isArray( fieldsOrCb ) ) | |
toDelete = _.filter( partitionFields, pf => _.findIndex( fieldsOrCb, f => f.toString() === pf.toString() ) !== -1 ) | |
// If it gets here, then that means fieldsOrCb is not an object ID, or an array... | |
else | |
return rej( `The field IDs specified can be either undefined, an array of ObjectIds, or a single ObjectId - but received a ${_.typeof( fieldsOrCb )}` ) | |
// If the field(s) provided were not found in the partitions fields, just resolve undefined (since they dont exist to delete) | |
if( _.isEmpty( toDelete ) ) | |
return res( undefined ) | |
Mongoose.models.Field.deleteFields( toDelete ) | |
.then( fieldData => { | |
const fieldIds = _.map( fieldData, d => d._id.toString() ) | |
console.log('#THIS:',this) | |
console.log('# Removing From _fields:',fieldIds) | |
// Remove the fields from the partition docs _fields element | |
_.remove( this._fields, f => _.includes( fieldIds, f._id.toString() ) ) | |
this.markModified('_fields') | |
this.save() | |
.then( data => res( fieldIds ) ) | |
.catch( err => rej( _.setException( err ) ) ) | |
}) | |
.catch( err => rej( _.setException( err ) ) ) | |
}).asCallback( ( args => _.findLast( args, a => _.isFunction( a ) ) )( arguments ) ) | |
} | |
// STATIC METHODS ----------------------------------------------------- | |
/** | |
* Delete a partition by partition ID | |
* | |
* @param {object|string} options Either the partition ID, or an object of the partition ID | |
* and the requireDelete boolean | |
* @param {function} callback Callback to execute, or Promise returned | |
* @var {string} options.partitionId Partition ID to delete | |
* @var {boolean} options.requireDelete If true, then a failure will be returned when an asset | |
* is not deleted (for any reason, such as it doesn't exist | |
* in the first place) | |
* @returns {Promise} Promise returned, or the callback is executed if it's defined | |
* @todo Allow deletion by partition name - Check if partitionId is not numeric, then go by name | |
*/ | |
partitionSchema.statics.deletePartition = function( options, callback ) { | |
// partitionId, requireDelete = false | |
return new Promise( ( res, rej ) => { | |
let requireDelete = false | |
let fields = true | |
let partitionId | |
// If were given just the partition ID | |
if( _m.isObjectId( options ) ){ | |
requireDelete = false | |
partitionId = options.toString() | |
} | |
// If were given an object, make sure it contains the partition ID, at the very least | |
else if( _.isObject( options ) ){ | |
// Check the partition ID | |
if( _m.isObjectId( options.partitionId ) ) | |
partitionId = options.partitionId.toString() | |
else | |
return rej( new AppError( 'partition.delete.noId' ) ) | |
// Check if were requiring the partition to be deleted to return success (meaning if it was never there | |
// TO delete, then still throw an error) | |
if( ! _.isUndefined( options.requireDelete ) ) | |
requireDelete = !!options.requireDelete | |
if( ! _.isUndefined( options.fields ) ) | |
fields = !!options.fields | |
} | |
else { | |
return rej( new AppError( 'partition.delete.options' ) ) | |
} | |
this.findById( partitionId ) | |
.then( partitionData => { | |
}) | |
.catch( err => rej( _.setException( err ) ) ) | |
// Execute the query! | |
this.findByIdAndRemove( partitionId ) | |
.then( partitionData => { | |
// If data is empty (happens when it didnt exist), and requireDelete is | |
// true, then throw a reject | |
// @todo Should add some better detail here as to why it failed.. | |
if( _.isEmpty( partitionData ) && requireDelete ) | |
rej( new AppError({ | |
code: 'partition.delete.partitionNotFound', | |
data: partitionId | |
}) ) | |
// Resolve the promise with the partition data | |
res( partitionData || null ) | |
}) | |
.catch( err => rej( new AppError({ | |
code: 'partition.delete.deleteFailed', | |
data: err | |
}) ) ) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Retrieve select Partition Field ID's by the field names (STATIC METHOD) | |
* | |
* @param {object} partitionId Partition object | |
* @param {string|array|object} fieldNames String of field names, or array of multiple | |
* @param {function} callback Callback to execute (As opposed to a Promise) | |
* @return {object} Object of field IDs and Names, with the field names as the keys | |
* @todo Should this be a methid instead of a static? | |
*/ | |
partitionSchema.statics.getFieldIdsByName = function( partitionId, fieldNames, callback ) { | |
return new Promise( ( res, rej ) => { | |
this.findById( partitionId ) | |
.then( data => { | |
const fields = _.chain( data._fields ) | |
.filter( f => _.includes( fieldNames, f.name ) ) | |
.reduce( ( end, f ) => { | |
end[ f.name ] = f._id | |
return end | |
},{}) | |
.value() | |
if( ! fields ) | |
return rej( new Error( 'Error getting fields' ) ) | |
res( fields ) | |
}) | |
.catch( err => rej( _.setException( err ) ) ) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Get partition data, with or without the fields populated | |
* | |
* @param {string} partitionId Partition ID to retrieve data for | |
* @param {function} callback Callback to execute, or Promise returned | |
* @return {Promise} Returns a promise with a Mongoose model as the only param | |
*/ | |
partitionSchema.statics.getPartition = function( partitionId, callback ) { | |
return new Promise( ( res, rej ) => { | |
if( ! _m.isObjectId( partitionId ) ){ | |
log.error( `Unable to find partition with value '${partitionId}' - Not a MongoDB ObjectId` ) | |
return rej( new AppError( 'partition.get.options.noId' ) ) | |
} | |
this.findById( partitionId ).exec( ( err, result ) => { | |
if( err ) | |
return rej( new AppError({ | |
code: 'partition.get.queryFailed', | |
data: err | |
}) ) | |
// Checking for null is the best way to check if an empty result was returned. | |
// Odd that even though (when nothing is returned) its null, but typeof is an | |
// object, and when results ARE found, the result.length is 0 | |
if( _.isEmpty( result ) ) { | |
//return rej( new Error( `No results found for Partition ID ${partitionId}` ) ) | |
//return rej( new exception( `No results found for Partition ID ${partitionId}`, exception.NOT_FOUND ) ) | |
return res( {} ) | |
} | |
res( result ) | |
} ) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Create a new Partition, including the partitions fields | |
* | |
* @param {object} settings Object of the partition data, requires atleast name, status and fields | |
* @param {function} callback Callback to execute, or Promise returned | |
* @var {string} settings.name Name of partition | |
* @var {boolean} settings.status Status of the partition (Default: true) | |
* @var {string} settings.description Partition description | |
* @var {array} settings.groups List of permitted group IDs | |
* @var {array} settings.fields Field configs (passed to Field model) | |
* @return {Promise} Promise returned, or callback executed (if defined) | |
*/ | |
partitionSchema.statics.createPartition = function( settings, callback ) { | |
return new Promise( ( res, rej ) => { | |
if( ! _.isObject( settings ) ) | |
return rej( | |
new AppError( 'partition.create.settings.missing' ) | |
//new Error( 'Must provide an object when creating a partition' ) | |
) | |
if( _.isEmpty( settings.name ) ) | |
return rej( new AppError( 'partition.create.settings.missing.name' ) ) | |
if( _.isEmpty( settings.fields ) ) | |
return rej( new AppError( 'partition.create.settings.missing.fields' ) ) | |
// Grab the fields.. | |
const fields = settings.fields | |
// Remove the fields from the partition data | |
_.unset( settings, 'fields' ) | |
// Verify there's only one primary field (or none) | |
if( _.filter( fields, f => f.primary === true ).length > 1 ) | |
return rej( new AppError( 'partition.create.settings.primaries' ) ) | |
// Verify that the field names are unique | |
if( _.isUniq( fields, 'name' ) === false ) | |
return rej( new AppError( 'partition.create.settings.duplicate.fieldName' ) ) | |
const newPartition = new this( settings ) | |
// Create/save new partition | |
newPartition.save() | |
.then( partitionData => { | |
const fieldIds = [] | |
// Asynchronously add the new fields, saving the field ID's to the fieldIds array | |
Async.each( fields, ( fld, done ) => { | |
new Mongoose.models.Field( fld ).save() | |
.then( data => { | |
fieldIds.push( data._id ) | |
done() | |
}) | |
.catch( err => { | |
log.error(`Failed to create the partition field ${fld.name || 'Unknown'}`) | |
done( err ) | |
}) | |
}, err => { | |
if( err ) | |
return rej( new AppError({ | |
code: 'partition.create.result.fields.saveFailed', | |
data: err.message | |
})) | |
partitionData._fields = fieldIds | |
partitionData.markModified('_fields') | |
return partitionData.save() | |
.then( data => { | |
// @todo This should be able to populate the _fields of the data document, without having to execute another find (via this.getPartition) | |
res( this.getPartition( data._id ) ) | |
} ) | |
.catch( err => rej( new AppError({ | |
code: 'partition.create.result.partition.updateFailed', | |
data: err.message | |
}) ) ) | |
}) | |
} ) | |
.catch( err => rej( new AppError({ | |
code: 'partition.create.result.partition.saveFailed', | |
data: err.message | |
}) ) ) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
/** | |
* Retrieve the assets in a specific partition by the partition ID . The difference between this and the | |
* Assets.getAssets() method is this takes the partition ID, not the asset ID(s) | |
* | |
* @param {Mixed} options Partition ID (string or ObjectId), or an options object | |
* @param {function} callback Callback to execute, or Promise returned | |
* @var {string|object|array} options.partitionId Single or multiple partition ID | |
* @var {string|boolean} options.indexed If `true`, then instead of returning an array of asset | |
* objects, an object of objects is returned, with the Asset ID | |
* as the indexes; If `identifier`, then the asset identifiers | |
* will be the object keys | |
* @returns {Promise} Promise returned, or callback executed if provided | |
*/ | |
partitionSchema.statics.getAssets = function( options, callback ) { | |
return new Promise( ( res, rej ) => { | |
let indexedBy | |
// Determine if the results should be an indexed object or not | |
if( _.isUndefined( options.indexed ) ) | |
indexedBy = false | |
else if( options.indexed === true ) | |
indexedBy = 'id' | |
else if( _.includes(['identifier','identifiers'], options.indexed ) ) | |
indexedBy = 'identifier' | |
else | |
indexedBy = false | |
// This where object will get populated, and used in the Mongoose find() query | |
const where = { } | |
// Function to process an array of partition ID, altering the where object directly | |
const processPartitionOpt = optVal => { | |
if( _m.isObjectId( optVal ) ) | |
where._partition = optVal | |
// Anything else shouldn't be accepted | |
else | |
return rej( new AppError({ | |
code: 'partition.assets.get.options.badPartitionId', | |
data: _m.typeof( optVal ) | |
}) ) | |
} | |
// If the options param is anything other than an object, handle it as the partitionId value | |
//if( _.includes([ 'string','objectid' ], _m.typeof( options ) ) ){ | |
if( _m.isObjectId( options ) ) { | |
processPartitionOpt( options ) | |
} | |
// If its an object, it can contain settings other than just the partitionId | |
else if( _.isPlainObject( options ) ){ | |
if( _.isEmpty( options.partitionId ) ) | |
return rej( new AppError( 'partition.assets.get.options.noPartitionId' ) ) | |
// Parse the partitionId object, modifying the where object | |
processPartitionOpt( options.partitionId ) | |
} | |
// If it gets here, then no partition ID was provided | |
else { | |
return rej( new AppError( 'partition.assets.get.options.noPartitionId' ) ) | |
} | |
Mongoose.models.Asset.find( where ) | |
/*.then( assetData => { | |
if( _.isEmpty( assetData ) ) | |
return rej( new Error( `No assets found using filter: ${JSON.stringify(where)}` ) ) | |
// Now populate ethe fields and resolve the assetDocs | |
const populate = Promise.promisify( this.populate ) | |
return populate.call(assetData._partition,{ path: '_fields' }) | |
}) | |
.then( assetData => { | |
res( assetData ) | |
})*/ | |
.then( assetData => { | |
// If no assets were found, don't bother populating anything | |
if( _.isEmpty( assetData ) ) | |
// Return the appropriate empty element type | |
return indexedBy ? res({}) : res([]) | |
// Now populate ethe fields and resolve the assetDocs | |
this.populate( assetData._partition, { path: '_fields' }, ( err, result ) => { | |
if( err ) | |
return rej( new AppError({ | |
code: 'partition.assets.get.populateFailed', | |
data: err | |
}) ) | |
if( indexedBy === 'id' ) | |
return res( _.mapKeys( assetData, a => a._id.toString() ) ) | |
if( indexedBy === 'identifier' ) | |
return res( _.mapKeys( assetData, a => a.identifier.toString() ) ) | |
return res( assetData ) | |
} ) | |
}) | |
.catch( err => rej( new AppError({ | |
code: 'partition.assets.get.failedQuery', | |
data: err | |
}) ) ) | |
}).asCallback( callback ) | |
} | |
// -------------------------------------------------------------------- | |
log.debug('Returning compiled model schema') | |
return Mongoose.model( modelName, partitionSchema ) | |
} | |
exports.createThing = ( name, data ) => { | |
return new Promise( ( res, rej ) => { | |
if( doesItExist( name ) ) | |
return rej( new AppError({ | |
code: 'partition.verifyAttr.options.badAssetId', | |
data: name | |
}) ) | |
// Other stuff... | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment