@jrmc/adonis-attachment
Version:
Turn any field on your Lucid model to an attachment data type
289 lines (288 loc) • 11.6 kB
JavaScript
import logger from '@adonisjs/core/services/logger';
import attachmentManager from '../../services/main.js';
import { defaultStateAttributeMixin } from '../utils/default_values.js';
import { Attachment } from '../attachments/attachment.js';
import { optionsSym } from '../utils/symbols.js';
import { ConverterManager } from '../converter_manager.js';
import { E_CANNOT_CREATE_VARIANT } from '../errors.js';
export default class RecordWithAttachment {
#row;
constructor(row) {
this.#row = row;
if (!this.#row.$attachments) {
/**
* Empty previous $attachments
*/
this.#row.$attachments = structuredClone(defaultStateAttributeMixin);
}
}
/**
* During commit, we should cleanup the old detached files
*/
async commit() {
await Promise.allSettled(this.#row.$attachments.detached.map((attachment) => attachmentManager.remove(attachment)));
}
/**
* During rollback we should remove the attached files.
*/
async rollback() {
await Promise.allSettled(this.#row.$attachments.attached.map(async (attachment) => {
await attachment.rollbackMoveFileForDelete();
await attachmentManager.remove(attachment);
}));
}
async persist() {
const attachmentAttributeNames = this.#getDirtyAttributeNamesOfAttachment();
/**
* Persist attachments before saving the row to the database. This
* way if file saving fails we will not write anything to the
* database
*/
await Promise.all(attachmentAttributeNames.map(async (name) => {
const originalAttachments = this.#getOriginalAttachmentsByAttributeName(name);
const newAttachments = this.#getAttachmentsByAttributeName(name);
const options = this.#getOptionsByAttributeName(name);
/**
* Skip when the attachment attributeName hasn't been updated
*/
if (!originalAttachments && !newAttachments) {
return;
}
/**
* memorise attribute name for generate variants
*/
this.#row.$attachments.dirtied.push(name);
for (let i = 0; i < newAttachments.length; i++) {
if (originalAttachments.includes(newAttachments[i])) {
continue;
}
/**
* If there is a new file and its local then we must save this
* file.
*/
if (newAttachments[i]) {
newAttachments[i].setOptions(options).makeFolder(this.#row);
this.#row.$attachments.attached.push(newAttachments[i]);
/**
* Also write the file to the disk right away
*/
await attachmentManager.write(newAttachments[i]);
}
}
}));
}
async transaction(options = { enabledRollback: true }) {
try {
if (this.#row.$trx) {
this.#row.$trx.after('commit', () => this.commit());
if (options.enabledRollback) {
this.#row.$trx.after('rollback', () => this.rollback());
}
}
else {
await this.commit();
}
}
catch (error) {
if (options.enabledRollback) {
await this.rollback();
}
throw error;
}
}
async preComputeUrl() {
const attachmentAttributeNames = this.#getAttributeNamesOfAttachment();
await Promise.all(attachmentAttributeNames.map(async (name) => {
const options = this.#getOptionsByAttributeName(name);
if (this.#row.$attributes[name]) {
const attachments = this.#getAttachmentsByAttributeName(name);
for (let i = 0; i < attachments.length; i++) {
attachments[i].setOptions(options);
await attachmentManager.preComputeUrl(attachments[i]);
}
}
}));
}
async generateVariants() {
/* this.#row.$dirty is not avalable in afterSave hooks */
const attachmentAttributeNames = this.#row.$attachments.dirtied;
/**
* For all properties Attachment
* Launch async generation variants
*/
for await (const name of attachmentAttributeNames) {
if (!this.#row.$attributes[name]) {
continue;
}
const record = this;
attachmentManager.queue.push({
name: `${this.#row.constructor.name}-${name}`,
async run() {
const converterManager = new ConverterManager({
record,
attributeName: name,
options: record.#getOptionsByAttributeName(name),
});
await converterManager.run();
},
})
.onError = function (error) {
if (error.message) {
logger.error(error.message);
}
else {
throw new E_CANNOT_CREATE_VARIANT([error]);
}
};
}
}
async regenerateVariants(options = {}) {
let attachmentAttributeNames;
if (options.attributes?.length) {
attachmentAttributeNames = options.attributes;
}
else {
attachmentAttributeNames = this.#getAttributeNamesOfAttachment();
}
for await (const name of attachmentAttributeNames) {
if (!this.#row.$attributes[name]) {
continue;
}
const record = this;
attachmentManager.queue.push({
name: `${this.#row.constructor.name}-${name}`,
async run() {
const converterManager = new ConverterManager({
record,
attributeName: name,
options: record.#getOptionsByAttributeName(name),
filters: {
variants: options.variants
}
});
await converterManager.run();
},
})
.onError = function (error) {
if (error.message) {
logger.error(error.message);
}
else {
throw new E_CANNOT_CREATE_VARIANT([error]);
}
};
}
}
async detach() {
const attachmentAttributeNames = this.#getDirtyAttributeNamesOfAttachment();
/**
* Mark all original attachments for deletion
*/
return Promise.allSettled(attachmentAttributeNames.map(async (name) => {
let attachments = [];
const options = this.#getOptionsByAttributeName(name);
if (this.#row.$dirty[name] === null) {
attachments = this.#getOriginalAttachmentsByAttributeName(name);
}
else {
const originalAttachments = this.#getOriginalAttachmentsByAttributeName(name);
const newAttachments = this.#getAttachmentsByAttributeName(name);
/**
* Clean Attachments changed
*/
for (let i = 0; i < originalAttachments.length; i++) {
if (newAttachments.includes(originalAttachments[i])) {
continue;
}
/**
* If there was an existing file, then we must get rid of it
*/
if (originalAttachments[i]) {
originalAttachments[i].setOptions(options);
attachments.push(originalAttachments[i]);
}
}
}
await Promise.allSettled(attachments.map(async (attachment) => {
attachment.setOptions(options);
await attachment.moveFileForDelete();
this.#row.$attachments.detached.push(attachment);
}));
}));
}
async detachAll() {
const attachmentAttributeNames = this.#getAttributeNamesOfAttachment();
/**
* Mark all attachments for deletion
*/
return Promise.allSettled(attachmentAttributeNames.map((name) => {
const options = this.#getOptionsByAttributeName(name);
const attachments = this.#getAttachmentsByAttributeName(name);
for (let i = 0; i < attachments.length; i++) {
attachments[i].setOptions(options);
this.#row.$attachments.detached.push(attachments[i]);
}
}));
}
get row() {
return this.#row;
}
getAttachments(options) {
let attachments;
if (options.requiredOriginal) {
attachments = this.#getOriginalAttachmentsByAttributeName(options.attributeName);
}
else if (options.requiredDirty) {
attachments = this.#getDirtyAttachmentsByAttributeName(options.attributeName);
}
else {
attachments = this.#getAttachmentsByAttributeName(options.attributeName);
}
const opts = this.#getOptionsByAttributeName(options.attributeName);
attachments.map((attachment) => attachment.setOptions(opts).makeFolder(this.#row));
return attachments;
}
#getAttachmentsByAttributeName(name) {
if (Array.isArray(this.#row.$attributes[name])) {
return this.#row.$attributes[name];
}
return [this.#row.$attributes[name]];
}
#getOriginalAttachmentsByAttributeName(name) {
if (Array.isArray(this.#row.$original[name])) {
return this.#row.$original[name];
}
return [this.#row.$original[name]];
}
#getDirtyAttachmentsByAttributeName(name) {
if (Array.isArray(this.#row.$dirty[name])) {
return this.#row.$dirty[name];
}
return [this.#row.$dirty[name]];
}
#getOptionsByAttributeName(name) {
return this.#row.constructor.prototype[optionsSym]?.[name];
}
#getAttributeNamesOfAttachment() {
return Object.keys(this.#row.$attributes).filter((name) => {
const value = this.#row.$attributes[name];
return (value instanceof Attachment ||
(Array.isArray(value) && value.every((item) => item instanceof Attachment)));
});
}
#getDirtyAttributeNamesOfAttachment() {
return Object.keys(this.#row.$dirty).filter((name) => {
const dirtyValue = this.#row.$dirty[name];
const originalValue = this.#row.$original[name]; // if dirtyValue is null, check original type
const isDirtyAttachment = dirtyValue instanceof Attachment ||
(Array.isArray(dirtyValue) &&
dirtyValue.length &&
dirtyValue.every((item) => item instanceof Attachment));
const isOriginalAttachment = originalValue instanceof Attachment ||
(Array.isArray(originalValue) &&
originalValue.length &&
originalValue.every((item) => item instanceof Attachment));
return isDirtyAttachment || isOriginalAttachment;
});
}
}