UNPKG

adonis-responsive-attachment

Version:

Generate and persist optimised and responsive breakpoint images on the fly in your AdonisJS application.

257 lines (256 loc) 8.81 kB
"use strict"; /* * adonis-responsive-attachment * * (c) Ndianabasi Udonkang <ndianabasi@furnish.ng> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.responsiveAttachment = exports.DEFAULT_BREAKPOINTS = void 0; /// <reference path="../../adonis-typings/index.ts" /> const lodash_1 = require("lodash"); const index_1 = require("./index"); const image_manipulation_helper_1 = require("../Helpers/image_manipulation_helper"); /** * Default breakpoint options */ exports.DEFAULT_BREAKPOINTS = { large: 1000, medium: 750, small: 500, }; /** * Persist attachment for a given attachment property */ async function persistAttachment(modelInstance, property, options) { const existingFile = modelInstance.$original[property]; const newFile = modelInstance[property]; /** * Skip when the attachment property hasn't been updated */ if (existingFile === newFile) { return; } /** * There was an existing file, but there is no new file. Hence we must * remove the existing file. */ if (existingFile && !newFile) { existingFile.setOptions((0, lodash_1.merge)(options, existingFile.getOptions)); modelInstance['attachments'].detached.push(existingFile); return; } /** * If there is a new file and its local then we must save this * file. */ if (newFile && newFile.isLocal) { newFile.setOptions((0, lodash_1.merge)(options, newFile.getOptions)); modelInstance['attachments'].attached.push(newFile); /** * If there was an existing file, then we must get rid of it */ if (existingFile && !newFile.getOptions?.persistentFileNames) { existingFile.setOptions((0, lodash_1.merge)(options, newFile.getOptions)); modelInstance['attachments'].detached.push(existingFile); } /** * Also write the file to the disk right away */ const finalImageData = await newFile.save(); /** * Use this `finalImageData` as the value to be persisted * on the column for the `property` */ modelInstance[property] = finalImageData; } } /** * During commit, we should cleanup the old detached files */ async function commit(modelInstance) { await Promise.allSettled(modelInstance['attachments'].detached.map((attachment) => { return attachment.delete(); })); } /** * During rollback we should remove the attached files. */ async function rollback(modelInstance) { await Promise.allSettled(modelInstance['attachments'].attached.map((attachment) => { return attachment.delete(); })); } /** * Implementation of the model save method. */ async function saveWithAttachments() { this['attachments'] = this['attachments'] || { attached: [], detached: [], }; /** * Persist attachments before saving the model to the database. This * way if file saving fails we will not write anything to the * database */ await Promise.all(this.constructor['attachments'].map((attachmentField) => persistAttachment(this, attachmentField.property, attachmentField.options))); try { await this['originalSave'](); /** * If model is using transaction, then wait for the transaction * to settle */ if (this.$trx) { this.$trx.after('commit', () => commit(this)); this.$trx.after('rollback', () => rollback(this)); } else { await commit(this); } } catch (error) { await rollback(this); throw error; } } /** * Implementation of the model delete method. */ async function deleteWithAttachments() { this['attachments'] = this['attachments'] || { attached: [], detached: [], }; /** * Mark all attachments for deletion */ this.constructor['attachments'].forEach((attachmentField) => { if (this[attachmentField.property]) { this['attachments'].detached.push(this[attachmentField.property]); } }); await this['originalDelete'](); /** * If model is using transaction, then wait for the transaction * to settle */ if (this.$trx) { this.$trx.after('commit', () => commit(this)); } else { await commit(this); } } /** * Pre-compute URLs after a row has been fetched from the database */ async function afterFind(modelInstance) { await Promise.all(modelInstance.constructor['attachments'].map((attachmentField) => { if (modelInstance[attachmentField.property]) { ; modelInstance[attachmentField.property].setOptions(attachmentField.options); if (modelInstance[attachmentField.property] instanceof index_1.ResponsiveAttachment) { return modelInstance[attachmentField.property] .computeUrls() .catch((error) => { const logger = modelInstance[attachmentField.property].loggerInstance; logger.error('Adonis Responsive Attachment error: %o', error); }); } return null; } })); } /** * Pre-compute URLs after more than one rows are fetched */ async function afterFetch(modelInstances) { await Promise.all(modelInstances.map((row) => afterFind(row))); } /** * Attachment decorator */ const responsiveAttachment = (options) => { return function (target, property) { const Model = target.constructor; Model.boot(); const enableBlurhash = (0, image_manipulation_helper_1.getDefaultBlurhashOptions)(options); /** * Separate attachment options from the column options */ const { disk, folder, keepOriginal = true, preComputeUrls = false, breakpoints = exports.DEFAULT_BREAKPOINTS, forceFormat, optimizeOrientation = true, optimizeSize = true, responsiveDimensions = true, disableThumbnail = false, persistentFileNames = false, ...columnOptions } = options || {}; /** * Define attachments array on the model constructor */ Model.$defineProperty('attachments', [], 'inherit'); /** * Push current column (the one using the @attachment decorator) to * the attachments array */ Model['attachments'].push({ property, options: { disk, folder, keepOriginal, preComputeUrls, breakpoints, forceFormat, optimizeOrientation, optimizeSize, responsiveDimensions, disableThumbnail, blurhash: enableBlurhash, persistentFileNames, }, }); /** * Define the property as a column too */ Model.$addColumn(property, { ...columnOptions, consume: (value) => (value ? index_1.ResponsiveAttachment.fromDbResponse(value) : null), prepare: (value) => (value ? JSON.stringify(value.toObject()) : null), serialize: (value) => (value ? value.toJSON() : null), }); /** * Overwrite the "save" method to save the model with attachments. We * will get rid of it once models will have middleware support */ if (!Model.prototype['originalSave']) { Model.prototype.originalSave = Model.prototype.save; Model.prototype.save = saveWithAttachments; } /** * Overwrite the "delete" method to delete files when row is removed. We * will get rid of it once models will have middleware support */ if (!Model.prototype['originalDelete']) { Model.prototype.originalDelete = Model.prototype.delete; Model.prototype.delete = deleteWithAttachments; } /** * Do not register hooks when "preComputeUrl" is not defined * inside the options */ if (!options?.preComputeUrls) { return; } /** * Registering all hooks only once */ if (!Model.$hooks.has('after', 'find', afterFind)) { Model.after('find', afterFind); } if (!Model.$hooks.has('after', 'fetch', afterFetch)) { Model.after('fetch', afterFetch); } if (!Model.$hooks.has('after', 'paginate', afterFetch)) { Model.after('paginate', afterFetch); } }; }; exports.responsiveAttachment = responsiveAttachment;