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
JavaScript
;
/*
* 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;