UNPKG

we-plugin-file

Version:

We.js file plugin with suport to storages

424 lines (373 loc) 12.7 kB
/** * File model * * @module :: Model */ module.exports = function FileModel (we) { const _ = we.utils._, async = we.utils.async; // set sequelize model define and options const model = { definition: { // - user given data text label: { type: we.db.Sequelize.STRING }, description: { type: we.db.Sequelize.TEXT }, // - data get from file name: { type: we.db.Sequelize.STRING, allowNull: false, unique: true }, size: { type: we.db.Sequelize.INTEGER }, encoding: { type: we.db.Sequelize.STRING }, active: { type: we.db.Sequelize.BOOLEAN, defaultValue: true }, originalname: { type: we.db.Sequelize.STRING }, mime: { type: we.db.Sequelize.STRING(255) }, extension: { type: we.db.Sequelize.STRING(10) }, storageName: { type: we.db.Sequelize.STRING }, isLocalStorage: { type: we.db.Sequelize.BOOLEAN, defaultValue: true }, urls: { type: we.db.Sequelize.BLOB, allowNull: false, skipSanitizer: true, get() { let v = this.getDataValue('urls'); if (!v) return {}; if (v instanceof Buffer) { try { return JSON.parse(v.toString('utf8')); } catch (e) { we.log.error('error on parse file urls from db', e); return {}; } } else if (typeof v == 'string') { return JSON.parse(v); } else { return v; } }, set(v) { if (!v) v = {}; if (typeof v != 'object') { throw new Error('file:urls:need_be_object'); } this.setDataValue('urls', JSON.stringify(v)); } }, extraData: { type: we.db.Sequelize.BLOB, skipSanitizer: true, get() { let v = this.getDataValue('extraData'); if (!v) return {}; if (v instanceof Buffer) { try { return JSON.parse(v.toString('utf8')); } catch (e) { we.log.error('error on parse file extraData from db', e); return {}; } } else if (typeof v == 'string') { return JSON.parse(v); } else { return v; } }, set(v) { if (!v) v = {}; if (typeof v != 'object') { throw new Error('file:extraData:need_be_object'); } this.setDataValue('extraData', JSON.stringify(v)); } } }, associations: { creator: { type: 'belongsTo', model: 'user' } } } // after define all models add term field hooks in models how have terms we.hooks.on('we:models:set:joins', function (we, done) { const models = we.db.models for (let modelName in models) { let fileFields = we.file.file.getModelFileFields( we.db.modelsConfigs[modelName] ); if (_.isEmpty(fileFields)) continue; const m = models[modelName]; m.addHook('afterFind', 'loadFiles', we.file.file.afterFind); m.addHook('afterCreate', 'createFile', we.file.file.afterCreatedRecord); m.addHook('afterUpdate', 'updateFile', we.file.file.afterUpdatedRecord); m.addHook('afterDestroy', 'destroyFile', we.file.file.afterDeleteRecord); } done(); }) // use we:after:load:plugins for set file object and methods we.events.on('we:after:load:plugins', function (we) { if (!we.file) we.file = {}; if (!we.file.file) we.file.file = {}; const db = we.db; we.file.file.getModelFileFields = function getModelFileFields (Model) { if (!Model || !Model.options || !Model.options.fileFields) return null; return Model.options.fileFields; } we.file.file.afterFind = function (r, opts) { return new Promise( (resolve, reject)=> { const Model = this; // skip if is raw query that dont preload need model attrs and methods if (opts.raw) return resolve(); if (_.isArray(r)) { async.eachSeries(r, (r1, next)=> { // we.db.models.fileassoc we.file.file.afterFindRecord.bind(Model)(r1, opts, next); }, (err)=> { if (err) return reject(err); resolve(); }); } else { we.file.file.afterFindRecord.bind(Model)(r, opts, (err)=> { if (err) return reject(err); resolve(); }); } }); } we.file.file.afterFindRecord = function (r, opts, done) { const functions = []; const Model = this; // found 0 results if (!r) return done(); // skip if is raw query that dont preload need model attrs and methods if (opts.raw || !r.setDataValue) return done(); // get fields let fields = we.file.file.getModelFileFields(this); if (!fields) return done(); // set cache objects if (!r._salvedFiles) r._salvedFiles = {}; if (!r._salvedFileAssocs) r._salvedFileAssocs = {}; let fieldNames = Object.keys(fields); // for each file field fieldNames.forEach( (fieldName)=> { functions.push( (next)=> { return db.models.fileassoc .findAll({ where: { modelName: Model.name, modelId: r.id, field: fieldName }, include: [{ all: true }] }) .then( (flAssocs)=> { if (_.isEmpty(flAssocs)) { next(); return null; } r._salvedFiles = flAssocs.map( (flAssoc)=> { return flAssoc.file.toJSON(); }); r.setDataValue(fieldName, r._salvedFiles); // salved terms cache r._salvedFileAssocs[fieldName] = flAssocs; next(); return null; }) .catch(next); }); }) // run all file field find records in parallel async.parallel(functions, done); } // after create one record with file fields we.file.file.afterCreatedRecord = function (r, opts) { return new Promise( (resolve, reject)=> { const functions = []; const Model = this; // skip if is raw query that dont preload need model attrs and methods if (opts.raw || !r.setDataValue) return resolve(); let fields = we.file.file.getModelFileFields(this); if (!fields) return resolve(); let fileFields = Object.keys(fields); if (!r._salvedFiles) r._salvedFiles = {}; if (!r._salvedFileAssocs) r._salvedFileAssocs = {}; fileFields.forEach( (fieldName)=> { let values = r.get(fieldName); if (_.isEmpty(values)) return; let filesToSave = []; let newFileAssocs = []; functions.push( (nextField)=> { async.each(values, (value, next)=> { if (!value || (value === 'null')) return next(); // check if the file exists db.models.file.findOne({ where: { id: value.id || value } }) .then( (i)=> { if (!i) { next(); return null; } db.models.fileassoc .create({ modelName: Model.name, modelId: r.id, field: fieldName, fileId: value.id || value }) .then( (r)=> { we.log.verbose('File assoc created:', r.id); filesToSave.push(i); newFileAssocs.push(r); next(); return null; }) .catch(next); }) .catch(next); }, (err)=> { if (err) return nextField(err); r._salvedFileAssocs[fieldName] = newFileAssocs; r._salvedFiles[fieldName] = filesToSave; r.setDataValue(fieldName, filesToSave.map( (im)=> { return im.toJSON(); })); nextField(); }) }) }) // TODO check if is better to run this in parallel async.series(functions, (err)=> { if (err) return reject(err); resolve(); }); }); } // after update one record with file fields we.file.file.afterUpdatedRecord = function (r, opts) { return new Promise( (resolve, reject)=> { const Model = this; // skip if is raw query that dont preload need model attrs and methods if (opts.raw || !r.setDataValue) return resolve(); const fields = we.file.file.getModelFileFields(this); if (!fields) return resolve(); let fieldNames = Object.keys(fields); async.eachSeries(fieldNames, (fieldName, nextField)=> { // check if user whant update this field if (opts.fields.indexOf(fieldName) === -1) return nextField() let filesToSave = _.clone(r.get(fieldName)); let newFileAssocs = []; let newFileAssocsIds = []; async.series([ function findOrCreateAllAssocs (done) { let preloadedFilesAssocsToSave = []; async.each(filesToSave, (its, next)=> { if (_.isEmpty(its) || its === 'null') return next(); let values = { modelName: Model.name, modelId: r.id, field: fieldName, fileId: its.id || its }; // check if this file exits db.models.file .findOne({ where: { id: its.id || its } }) .then( (i)=> { if (!i) { done(); return null; } // find of create the assoc return db.models.fileassoc .findOrCreate({ where: values, defaults: values }) .then( (r)=> { r[0].file = i; preloadedFilesAssocsToSave.push(r[0]); next(); return null; }); }) .catch(done); }, (err)=> { if (err) return done(err); filesToSave = preloadedFilesAssocsToSave.map( (r)=> { newFileAssocsIds.push(r.id); return r.file; }); newFileAssocs = preloadedFilesAssocsToSave; done(); }); }, // delete removed file assocs function deleteAssocs (done) { let query = { where: { modelName: Model.name, modelId: r.id, field: fieldName } }; if (!_.isEmpty(newFileAssocsIds)) { query.where.id = { [we.Op.notIn]: newFileAssocsIds }; } db.models.fileassoc .destroy(query) .then(function afterDelete (result) { we.log.verbose('Result from deleted file assocs: ', result, fieldName, Model.name); done(); return null; }) .catch(done); }, function setRecorValues (done) { r._salvedFiles[fieldName] = filesToSave; r._salvedFileAssocs[fieldName] = newFileAssocs; r.setDataValue(fieldName, filesToSave.map( (im)=> { return im.toJSON(); })); done(); } ], nextField); }, (err)=> { if (err) return reject(err); resolve(); }); }); } // delete the file associations after delete related model we.file.file.afterDeleteRecord = function (r) { return new Promise( (resolve, reject)=> { const Model = this; db.models.fileassoc .destroy({ where: { modelName: Model.name, modelId: r.id } }) .then( (result)=> { we.log.debug('Deleted ' + result + ' file assocs from record with id: ' + r.id); resolve(); return null; }) .catch(reject); }); } }); return model; }