@jrmc/adonis-attachment 3.2.1 → 3.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import attachmentManager from './services/main.js';
2
2
  export { configure } from './configure.js';
3
3
  export { Attachment } from './src/attachments/attachment.js';
4
4
  export { attachment } from './src/decorators/attachment.js';
5
+ export { attachments } from './src/decorators/attachment.js';
5
6
  export { defineConfig } from './src/define_config.js';
6
7
  export { Attachmentable } from './src/mixins/attachmentable.js';
7
8
  export * as errors from './src/errors.js';
package/build/index.js CHANGED
@@ -2,6 +2,7 @@ import attachmentManager from './services/main.js';
2
2
  export { configure } from './configure.js';
3
3
  export { Attachment } from './src/attachments/attachment.js';
4
4
  export { attachment } from './src/decorators/attachment.js';
5
+ export { attachments } from './src/decorators/attachment.js';
5
6
  export { defineConfig } from './src/define_config.js';
6
7
  export { Attachmentable } from './src/mixins/attachmentable.js';
7
8
  export * as errors from './src/errors.js';
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { DriveService, SignedURLOptions } from '@adonisjs/drive/types';
8
8
  import type { MultipartFile } from '@adonisjs/core/bodyparser';
9
- import type { AttachmentBase, Attachment as AttachmentType } from './types/attachment.js';
9
+ import type { AttachmentAttributes, AttachmentBase, Attachment as AttachmentType } from './types/attachment.js';
10
10
  import { DeferQueue } from '@poppinss/defer';
11
11
  import Converter from './converters/converter.js';
12
12
  import { ResolvedAttachmentConfig } from './define_config.js';
@@ -15,8 +15,17 @@ export declare class AttachmentManager<KnownConverters extends Record<string, Co
15
15
  queue: DeferQueue;
16
16
  constructor(config: ResolvedAttachmentConfig<KnownConverters>, drive: DriveService);
17
17
  getConfig(): ResolvedAttachmentConfig<KnownConverters>;
18
- createFromDbResponse(response: any): AttachmentType | null;
18
+ createFromDbResponse(response?: string | JSON): AttachmentType | null;
19
19
  createFromFile(input: MultipartFile): Promise<AttachmentType>;
20
+ createFromFiles(inputs: MultipartFile[]): Promise<(AttachmentBase & {
21
+ originalName: string;
22
+ variants?: import("./types/attachment.js").Variant[];
23
+ createVariant(key: string, input: import("./types/input.js").Input): Promise<import("./types/attachment.js").Variant>;
24
+ getVariant(variantName: string): import("./types/attachment.js").Variant | undefined;
25
+ getUrl(variantName?: string): Promise<string>;
26
+ getSignedUrl(variantNameOrOptions?: string | SignedURLOptions, signedUrlOptions?: SignedURLOptions): Promise<string>;
27
+ toObject(): AttachmentAttributes;
28
+ })[]>;
20
29
  createFromPath(input: string, name?: string): Promise<AttachmentType>;
21
30
  createFromBuffer(input: Buffer, name?: string): Promise<AttachmentType>;
22
31
  createFromBase64(input: string, name?: string): Promise<AttachmentType>;
@@ -52,6 +52,9 @@ export class AttachmentManager {
52
52
  const attachment = new Attachment(this.#drive, attributes, input.tmpPath);
53
53
  return this.#configureAttachment(attachment);
54
54
  }
55
+ async createFromFiles(inputs) {
56
+ return Promise.all(inputs.map((input) => this.createFromFile(input)));
57
+ }
55
58
  async createFromPath(input, name) {
56
59
  const meta = await metaFormFile(input, name || input);
57
60
  if (meta.extname === '') {
@@ -142,15 +145,11 @@ export class AttachmentManager {
142
145
  await attachment.getDisk().delete(attachment.path);
143
146
  if (attachment instanceof Attachment) {
144
147
  if (attachment.variants) {
145
- if (attachment.options?.disk == 'fs') {
146
- const variantPath = attachment.variants[0].folder;
147
- await attachment.getDisk().deleteAll(variantPath);
148
- }
149
- else {
150
- for (const key in attachment.variants) {
151
- if (Object.prototype.hasOwnProperty.call(attachment.variants, key)) {
152
- await attachment.getDisk().delete(attachment.variants[key].path);
153
- }
148
+ const variantPath = attachment.variants[0].folder;
149
+ await attachment.getDisk().deleteAll(variantPath); // not compatible Minio, necessary for fs as not to leave an empty directory
150
+ for (const key in attachment.variants) {
151
+ if (Object.prototype.hasOwnProperty.call(attachment.variants, key)) {
152
+ await attachment.getDisk().delete(attachment.variants[key].path);
154
153
  }
155
154
  }
156
155
  }
@@ -1,6 +1,6 @@
1
1
  import type { ConverterInitializeAttributes } from './types/converter.js';
2
2
  export declare class ConverterManager {
3
3
  #private;
4
- constructor({ record, attributeName }: ConverterInitializeAttributes);
4
+ constructor({ record, attributeName, options }: ConverterInitializeAttributes);
5
5
  save(): Promise<void>;
6
6
  }
@@ -1,42 +1,54 @@
1
+ import string from '@adonisjs/core/helpers/string';
1
2
  import db from '@adonisjs/lucid/services/db';
2
3
  import attachmentManager from '../services/main.js';
3
4
  import * as errors from './errors.js';
4
- import { getOptions } from './utils/helpers.js';
5
5
  export class ConverterManager {
6
6
  #record;
7
7
  #attributeName;
8
- constructor({ record, attributeName }) {
8
+ #options;
9
+ constructor({ record, attributeName, options }) {
9
10
  this.#record = record;
10
11
  this.#attributeName = attributeName;
12
+ this.#options = options;
11
13
  }
12
14
  async save() {
13
- const options = getOptions(this.#record, this.#attributeName);
14
- const attachment = this.#record.$attributes[this.#attributeName];
15
- const input = attachment.input;
16
- const Model = this.#record.constructor;
17
- const id = this.#record.$attributes['id'];
15
+ let attachments = this.#record.getAttachments({
16
+ attributeName: this.#attributeName,
17
+ });
18
+ const Model = this.#record.model.constructor;
19
+ const id = this.#record.model.$attributes['id'];
18
20
  const data = {};
19
- if (options.variants) {
20
- for (const option of options.variants) {
21
+ if (this.#options.variants) {
22
+ for (const option of this.#options.variants) {
21
23
  const converter = (await attachmentManager.getConverter(option));
22
- if (attachment && converter) {
23
- const output = await converter.handle({
24
- input,
25
- options: converter.options,
26
- });
27
- if (output === undefined) {
28
- throw new errors.E_CANNOT_PATH_BY_CONVERTER();
24
+ if (attachments && converter) {
25
+ for (let i = 0; i < attachments.length; i++) {
26
+ const input = attachments[i].input;
27
+ const output = await converter.handle({
28
+ input,
29
+ options: converter.options,
30
+ });
31
+ if (output === undefined) {
32
+ throw new errors.E_CANNOT_PATH_BY_CONVERTER();
33
+ }
34
+ const variant = await attachments[i].createVariant(option, output);
35
+ await attachmentManager.save(variant);
29
36
  }
30
- const variant = await attachment.createVariant(option, output);
31
- await attachmentManager.save(variant);
32
37
  }
33
38
  }
34
39
  }
35
- data[this.#attributeName] = JSON.stringify(attachment.toObject());
40
+ if (Array.isArray(this.#record.model.$original[this.#attributeName])) {
41
+ data[string.snakeCase(this.#attributeName)] = JSON.stringify(attachments.map((att) => att.toObject()));
42
+ }
43
+ else {
44
+ data[string.snakeCase(this.#attributeName)] = JSON.stringify(attachments[0].toObject());
45
+ }
36
46
  const trx = await db.transaction();
37
47
  trx.after('rollback', () => {
38
- for (const variant of attachment.variants) {
39
- attachmentManager.delete(variant);
48
+ for (let i = 0; i < attachments.length; i++) {
49
+ for (const variant of attachments[i].variants) {
50
+ attachmentManager.delete(variant);
51
+ }
40
52
  }
41
53
  });
42
54
  try {
@@ -4,5 +4,11 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
+ import type { LucidModel } from '@adonisjs/lucid/types/model';
7
8
  import type { LucidOptions } from '../types/attachment.js';
9
+ import type { AttributeOfModelWithAttachment } from '../types/mixin.js';
10
+ export declare const bootModel: (model: LucidModel & {
11
+ $attachments: AttributeOfModelWithAttachment;
12
+ }) => void;
8
13
  export declare const attachment: (options?: LucidOptions) => (target: any, attributeName: string) => void;
14
+ export declare const attachments: (options?: LucidOptions) => (target: any, attributeName: string) => void;
@@ -8,65 +8,86 @@ import attachmentManager from '../../services/main.js';
8
8
  import { optionsSym } from '../utils/symbols.js';
9
9
  import { defaultOptionsDecorator } from '../utils/default_values.js';
10
10
  import { afterFindHook, afterFetchHook, beforeSaveHook, afterSaveHook, beforeDeleteHook, } from '../utils/hooks.js';
11
- import { clone } from '../utils/helpers.js';
12
11
  import { defaultStateAttributeMixin } from '../utils/default_values.js';
13
- export const attachment = (options) => {
12
+ export const bootModel = (model) => {
13
+ model.boot();
14
+ model.$attachments = structuredClone(defaultStateAttributeMixin);
15
+ /**
16
+ * Registering all hooks only once
17
+ */
18
+ if (!model.$hooks.has('find', afterFindHook)) {
19
+ model.after('find', afterFindHook);
20
+ }
21
+ if (!model.$hooks.has('fetch', afterFetchHook)) {
22
+ model.after('fetch', afterFetchHook);
23
+ }
24
+ if (!model.$hooks.has('paginate', afterFetchHook)) {
25
+ model.after('paginate', afterFetchHook);
26
+ }
27
+ if (!model.$hooks.has('save', beforeSaveHook)) {
28
+ model.before('save', beforeSaveHook);
29
+ }
30
+ if (!model.$hooks.has('save', afterSaveHook)) {
31
+ model.after('save', afterSaveHook);
32
+ }
33
+ if (!model.$hooks.has('delete', beforeDeleteHook)) {
34
+ model.before('delete', beforeDeleteHook);
35
+ }
36
+ };
37
+ const makeColumnOptions = (options) => {
38
+ const { disk, folder, variants, meta, rename, ...columnOptions } = {
39
+ ...defaultOptionsDecorator,
40
+ ...options,
41
+ };
42
+ return {
43
+ consume: (value) => {
44
+ if (value) {
45
+ const attachment = attachmentManager.createFromDbResponse(value);
46
+ attachment?.setOptions({ disk, folder, variants });
47
+ if (options && options?.meta !== undefined) {
48
+ attachment?.setOptions({ meta: options.meta });
49
+ }
50
+ if (options && options?.rename !== undefined) {
51
+ attachment?.setOptions({ rename: options.rename });
52
+ }
53
+ if (options && options?.preComputeUrl !== undefined) {
54
+ attachment?.setOptions({ preComputeUrl: options.preComputeUrl });
55
+ }
56
+ return attachment;
57
+ }
58
+ else {
59
+ return null;
60
+ }
61
+ },
62
+ prepare: (value) => (value ? JSON.stringify(value.toObject()) : null),
63
+ serialize: (value) => (value ? value.toJSON() : null),
64
+ ...columnOptions,
65
+ };
66
+ };
67
+ const makeAttachmentDecorator = (columnOptionsTransformer) => (options) => {
14
68
  return function (target, attributeName) {
15
69
  if (!target[optionsSym]) {
16
70
  target[optionsSym] = {};
17
71
  }
18
72
  target[optionsSym][attributeName] = options;
19
73
  const Model = target.constructor;
20
- Model.boot();
21
- Model.$attachments = clone(defaultStateAttributeMixin);
22
- /**
23
- * Registering all hooks only once
24
- */
25
- if (!Model.$hooks.has('find', afterFindHook)) {
26
- Model.after('find', afterFindHook);
27
- }
28
- if (!Model.$hooks.has('fetch', afterFetchHook)) {
29
- Model.after('fetch', afterFetchHook);
30
- }
31
- if (!Model.$hooks.has('paginate', afterFetchHook)) {
32
- Model.after('paginate', afterFetchHook);
33
- }
34
- if (!Model.$hooks.has('save', beforeSaveHook)) {
35
- Model.before('save', beforeSaveHook);
36
- }
37
- if (!Model.$hooks.has('save', afterSaveHook)) {
38
- Model.after('save', afterSaveHook);
39
- }
40
- if (!Model.$hooks.has('delete', beforeDeleteHook)) {
41
- Model.before('delete', beforeDeleteHook);
42
- }
43
- const { disk, folder, variants, meta, rename, ...columnOptions } = {
44
- ...defaultOptionsDecorator,
45
- ...options,
46
- };
47
- Model.$addColumn(attributeName, {
48
- consume: (value) => {
49
- if (value) {
50
- const attachment = attachmentManager.createFromDbResponse(value);
51
- attachment?.setOptions({ disk, folder, variants });
52
- if (options && options?.meta !== undefined) {
53
- attachment?.setOptions({ meta: options.meta });
54
- }
55
- if (options && options?.rename !== undefined) {
56
- attachment?.setOptions({ rename: options.rename });
57
- }
58
- if (options && options?.preComputeUrl !== undefined) {
59
- attachment?.setOptions({ preComputeUrl: options.preComputeUrl });
60
- }
61
- return attachment;
62
- }
63
- else {
64
- return null;
65
- }
66
- },
67
- prepare: (value) => (value ? JSON.stringify(value.toObject()) : null),
68
- serialize: (value) => (value ? value.toJSON() : null),
69
- ...columnOptions,
70
- });
74
+ bootModel(Model);
75
+ const columnOptions = makeColumnOptions(options);
76
+ const transformedColumnOptions = columnOptionsTransformer
77
+ ? columnOptionsTransformer(columnOptions)
78
+ : columnOptions;
79
+ Model.$addColumn(attributeName, transformedColumnOptions);
71
80
  };
72
81
  };
82
+ export const attachment = makeAttachmentDecorator();
83
+ export const attachments = makeAttachmentDecorator((columnOptions) => ({
84
+ consume: (value) => {
85
+ if (value) {
86
+ const data = typeof value === 'string' ? JSON.parse(value) : value;
87
+ return data.map(columnOptions.consume);
88
+ }
89
+ return null;
90
+ },
91
+ prepare: (value) => value ? JSON.stringify(value.map((v) => v.toObject())) : null,
92
+ serialize: (value) => (value ? value.map(columnOptions.serialize) : null),
93
+ }));
@@ -75,6 +75,11 @@ export declare function Attachmentable<T extends Constructor>(superclass: T): {
75
75
  find: <T_1 extends import("@adonisjs/lucid/types/model").LucidModel>(this: T_1, value: any, options?: import("@adonisjs/lucid/types/model").ModelAdapterOptions) => Promise<null | InstanceType<T_1>>;
76
76
  readonly booted: boolean;
77
77
  boot: () => void;
78
+ after: {
79
+ <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "fetch", handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>[], "fetch">): void;
80
+ <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelPaginatorContract<InstanceType<Model>>, "paginate">): void;
81
+ <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>, Event>): void;
82
+ };
78
83
  create: <T_1 extends import("@adonisjs/lucid/types/model").LucidModel>(this: T_1, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T_1>>>, options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T_1>>;
79
84
  all: <T_1 extends import("@adonisjs/lucid/types/model").LucidModel>(this: T_1, options?: import("@adonisjs/lucid/types/model").ModelAdapterOptions) => Promise<InstanceType<T_1>[]>;
80
85
  namingStrategy: import("@adonisjs/lucid/types/model").NamingStrategyContract;
@@ -116,11 +121,6 @@ export declare function Attachmentable<T extends Constructor>(superclass: T): {
116
121
  <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<[import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>, import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>], "paginate">): void;
117
122
  <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>, Event>): void;
118
123
  };
119
- after: {
120
- <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "fetch", handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>[], "fetch">): void;
121
- <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelPaginatorContract<InstanceType<Model>>, "paginate">): void;
122
- <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>, Event>): void;
123
- };
124
124
  createMany: <T_1 extends import("@adonisjs/lucid/types/model").LucidModel>(this: T_1, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T_1>>>[], options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T_1>[]>;
125
125
  findOrFail: <T_1 extends import("@adonisjs/lucid/types/model").LucidModel>(this: T_1, value: any, options?: import("@adonisjs/lucid/types/model").ModelAdapterOptions) => Promise<InstanceType<T_1>>;
126
126
  findBy: {
@@ -11,12 +11,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
11
11
  return c > 3 && r && Object.defineProperty(target, key, r), r;
12
12
  };
13
13
  import { beforeSave, beforeFind, beforeFetch, beforePaginate, beforeCreate, } from '@adonisjs/lucid/orm';
14
- import { clone } from '../utils/helpers.js';
15
14
  import { defaultStateAttributeMixin } from '../utils/default_values.js';
16
15
  import logger from '@adonisjs/core/services/logger';
17
16
  export function Attachmentable(superclass) {
18
17
  class ModelWithAttachment extends superclass {
19
- $attachments = clone(defaultStateAttributeMixin);
18
+ $attachments = structuredClone(defaultStateAttributeMixin);
20
19
  static async warn() {
21
20
  logger.warn(`The "Attachmentable" mixin is deprecated and may be removed in a future version.`);
22
21
  }
@@ -0,0 +1,28 @@
1
+ import type { ModelWithAttachment } from '../types/mixin.js';
2
+ import type { Attachment as AttachmentType } from '../types/attachment.js';
3
+ import type { Record as RecordImplementation } from '../types/service.js';
4
+ export default class Record implements RecordImplementation {
5
+ #private;
6
+ constructor(model: ModelWithAttachment);
7
+ /**
8
+ * During commit, we should cleanup the old detached files
9
+ */
10
+ commit(): Promise<void>;
11
+ /**
12
+ * During rollback we should remove the attached files.
13
+ */
14
+ rollback(): Promise<void>;
15
+ persist(): Promise<void>;
16
+ transaction(options?: {
17
+ enabledRollback: boolean;
18
+ }): Promise<void>;
19
+ preComputeUrl(): Promise<void>;
20
+ generateVariants(): Promise<void>;
21
+ detach(): Promise<PromiseSettledResult<void>[]>;
22
+ detachAll(): Promise<PromiseSettledResult<void>[]>;
23
+ get model(): ModelWithAttachment;
24
+ getAttachments(options: {
25
+ attributeName: string;
26
+ requiredOriginal?: boolean;
27
+ }): AttachmentType[];
28
+ }
@@ -0,0 +1,223 @@
1
+ import attachmentManager from '../../services/main.js';
2
+ import { defaultStateAttributeMixin } from '../utils/default_values.js';
3
+ import { Attachment } from '../attachments/attachment.js';
4
+ import { optionsSym } from '../utils/symbols.js';
5
+ import { ConverterManager } from '../converter_manager.js';
6
+ import { E_CANNOT_CREATE_VARIANT } from '../errors.js';
7
+ export default class Record {
8
+ #model;
9
+ constructor(model) {
10
+ this.#model = model;
11
+ if (!this.#model.$attachments) {
12
+ /**
13
+ * Empty previous $attachments
14
+ */
15
+ this.#model.$attachments = structuredClone(defaultStateAttributeMixin);
16
+ }
17
+ }
18
+ /**
19
+ * During commit, we should cleanup the old detached files
20
+ */
21
+ async commit() {
22
+ await Promise.allSettled(this.#model.$attachments.detached.map((attachment) => attachmentManager.delete(attachment)));
23
+ }
24
+ /**
25
+ * During rollback we should remove the attached files.
26
+ */
27
+ async rollback() {
28
+ await Promise.allSettled(this.#model.$attachments.attached.map((attachment) => attachmentManager.delete(attachment)));
29
+ }
30
+ async persist() {
31
+ const attachmentAttributeNames = this.#getDirtyAttributeNamesOfAttachment();
32
+ /**
33
+ * Persist attachments before saving the model to the database. This
34
+ * way if file saving fails we will not write anything to the
35
+ * database
36
+ */
37
+ await Promise.all(attachmentAttributeNames.map(async (name) => {
38
+ const originalAttachments = this.#getOriginalAttachmentsByAttributeName(name);
39
+ const newAttachments = this.#getAttachmentsByAttributeName(name);
40
+ const options = this.#getOptionsByAttributeName(name);
41
+ /**
42
+ * Skip when the attachment attributeName hasn't been updated
43
+ */
44
+ if (!originalAttachments && !newAttachments) {
45
+ return;
46
+ }
47
+ /**
48
+ * memorise attribute name for generate variants
49
+ */
50
+ this.#model.$attachments.dirtied.push(name);
51
+ for (let i = 0; i < newAttachments.length; i++) {
52
+ if (originalAttachments.includes(newAttachments[i])) {
53
+ continue;
54
+ }
55
+ /**
56
+ * If there is a new file and its local then we must save this
57
+ * file.
58
+ */
59
+ if (newAttachments[i]) {
60
+ newAttachments[i].setOptions(options);
61
+ this.#model.$attachments.attached.push(newAttachments[i]);
62
+ /**
63
+ * Also write the file to the disk right away
64
+ */
65
+ await attachmentManager.save(newAttachments[i]);
66
+ }
67
+ }
68
+ }));
69
+ }
70
+ async transaction(options = { enabledRollback: true }) {
71
+ try {
72
+ if (this.#model.$trx) {
73
+ this.#model.$trx.after('commit', () => this.commit());
74
+ if (options.enabledRollback) {
75
+ this.#model.$trx.after('rollback', () => this.rollback());
76
+ }
77
+ }
78
+ else {
79
+ await this.commit();
80
+ }
81
+ }
82
+ catch (error) {
83
+ if (options.enabledRollback) {
84
+ await this.rollback();
85
+ }
86
+ throw error;
87
+ }
88
+ }
89
+ async preComputeUrl() {
90
+ const attachmentAttributeNames = this.#getAttributeNamesOfAttachment();
91
+ await Promise.all(attachmentAttributeNames.map(async (name) => {
92
+ const options = this.#getOptionsByAttributeName(name);
93
+ if (this.#model.$attributes[name]) {
94
+ const attachments = this.#getAttachmentsByAttributeName(name);
95
+ for (let i = 0; i < attachments.length; i++) {
96
+ attachments[i].setOptions(options);
97
+ await attachmentManager.preComputeUrl(attachments[i]);
98
+ }
99
+ }
100
+ }));
101
+ }
102
+ async generateVariants() {
103
+ /* this.#model.$dirty is not avalable in afterSave hooks */
104
+ const attachmentAttributeNames = this.#model.$attachments.dirtied;
105
+ /**
106
+ * For all properties Attachment
107
+ * Launch async generation variants
108
+ */
109
+ await Promise.allSettled(attachmentAttributeNames.map((name) => {
110
+ const record = this;
111
+ attachmentManager.queue.push({
112
+ name: `${this.#model.constructor.name}-${name}`,
113
+ async run() {
114
+ try {
115
+ const converterManager = new ConverterManager({
116
+ record,
117
+ attributeName: name,
118
+ options: record.#getOptionsByAttributeName(name),
119
+ });
120
+ await converterManager.save();
121
+ }
122
+ catch (err) {
123
+ throw new E_CANNOT_CREATE_VARIANT([err.message]);
124
+ }
125
+ },
126
+ });
127
+ }));
128
+ }
129
+ async detach() {
130
+ const attachmentAttributeNames = this.#getDirtyAttributeNamesOfAttachment();
131
+ /**
132
+ * Mark all original attachments for deletion
133
+ */
134
+ return Promise.allSettled(attachmentAttributeNames.map((name) => {
135
+ let attachments = [];
136
+ const options = this.#getOptionsByAttributeName(name);
137
+ if (this.#model.$dirty[name] === null) {
138
+ attachments = this.#getOriginalAttachmentsByAttributeName(name);
139
+ }
140
+ else {
141
+ const originalAttachments = this.#getOriginalAttachmentsByAttributeName(name);
142
+ const newAttachments = this.#getAttachmentsByAttributeName(name);
143
+ /**
144
+ * Clean Attachments changed
145
+ */
146
+ for (let i = 0; i < originalAttachments.length; i++) {
147
+ if (newAttachments.includes(originalAttachments[i])) {
148
+ continue;
149
+ }
150
+ /**
151
+ * If there was an existing file, then we must get rid of it
152
+ */
153
+ if (originalAttachments[i]) {
154
+ originalAttachments[i].setOptions(options);
155
+ attachments.push(originalAttachments[i]);
156
+ }
157
+ }
158
+ }
159
+ for (let i = 0; i < attachments.length; i++) {
160
+ attachments[i].setOptions(options);
161
+ this.#model.$attachments.detached.push(attachments[i]);
162
+ }
163
+ }));
164
+ }
165
+ async detachAll() {
166
+ const attachmentAttributeNames = this.#getAttributeNamesOfAttachment();
167
+ /**
168
+ * Mark all attachments for deletion
169
+ */
170
+ return Promise.allSettled(attachmentAttributeNames.map((name) => {
171
+ const options = this.#getOptionsByAttributeName(name);
172
+ const attachments = this.#getAttachmentsByAttributeName(name);
173
+ for (let i = 0; i < attachments.length; i++) {
174
+ attachments[i].setOptions(options);
175
+ this.#model.$attachments.detached.push(attachments[i]);
176
+ }
177
+ }));
178
+ }
179
+ get model() {
180
+ return this.#model;
181
+ }
182
+ getAttachments(options) {
183
+ if (options.requiredOriginal) {
184
+ return this.#getOriginalAttachmentsByAttributeName(options.attributeName);
185
+ }
186
+ else {
187
+ return this.#getAttachmentsByAttributeName(options.attributeName);
188
+ }
189
+ }
190
+ #getAttachmentsByAttributeName(name) {
191
+ if (Array.isArray(this.#model.$attributes[name])) {
192
+ return this.#model.$attributes[name];
193
+ }
194
+ return [this.#model.$attributes[name]];
195
+ }
196
+ #getOriginalAttachmentsByAttributeName(name) {
197
+ if (Array.isArray(this.#model.$original[name])) {
198
+ return this.#model.$original[name];
199
+ }
200
+ return [this.#model.$original[name]];
201
+ }
202
+ #getOptionsByAttributeName(name) {
203
+ return this.#model.constructor.prototype[optionsSym]?.[name];
204
+ }
205
+ #getAttributeNamesOfAttachment() {
206
+ return Object.keys(this.#model.$attributes).filter((name) => {
207
+ const value = this.#model.$attributes[name];
208
+ return (value instanceof Attachment ||
209
+ (Array.isArray(value) && value.every((item) => item instanceof Attachment)));
210
+ });
211
+ }
212
+ #getDirtyAttributeNamesOfAttachment() {
213
+ return Object.keys(this.#model.$dirty).filter((name) => {
214
+ const dirtyValue = this.#model.$dirty[name];
215
+ const originalValue = this.#model.$original[name]; // if dirtyValue is null, check original type
216
+ const isDirtyAttachment = dirtyValue instanceof Attachment ||
217
+ (Array.isArray(dirtyValue) && dirtyValue.every((item) => item instanceof Attachment));
218
+ const isOriginalAttachment = originalValue instanceof Attachment ||
219
+ (Array.isArray(originalValue) && originalValue.every((item) => item instanceof Attachment));
220
+ return isDirtyAttachment || isOriginalAttachment;
221
+ });
222
+ }
223
+ }
@@ -4,17 +4,19 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
+ import type { Record } from './service.js';
7
8
  import type { BinPaths } from './config.js';
8
9
  import type { Input } from './input.js';
9
- import type { ModelWithAttachment } from './mixin.js';
10
+ import type { LucidOptions } from './attachment.js';
10
11
  export type Converter = {
11
12
  options?: ConverterOptions;
12
13
  binPaths?: BinPaths;
13
14
  handle?: (attributes: ConverterAttributes) => Promise<Input | undefined>;
14
15
  };
15
16
  export type ConverterInitializeAttributes = {
16
- record: ModelWithAttachment;
17
+ record: Record;
17
18
  attributeName: string;
19
+ options: LucidOptions;
18
20
  };
19
21
  export type ConverterAttributes = {
20
22
  input: Input;
@@ -3,3 +3,4 @@ export * from './config.js';
3
3
  export * from './converter.js';
4
4
  export * from './input.js';
5
5
  export * from './mixin.js';
6
+ export * from './service.js';
@@ -3,3 +3,4 @@ export * from './config.js';
3
3
  export * from './converter.js';
4
4
  export * from './input.js';
5
5
  export * from './mixin.js';
6
+ export * from './service.js';
@@ -9,7 +9,7 @@ import type { Attachment } from './attachment.js';
9
9
  export type AttributeOfModelWithAttachment = {
10
10
  attached: Attachment[];
11
11
  detached: Attachment[];
12
- attributesModified: string[];
12
+ dirtied: string[];
13
13
  };
14
14
  export type ModelWithAttachment = LucidRow & {
15
15
  $attachments: AttributeOfModelWithAttachment;
@@ -0,0 +1,17 @@
1
+ import type { Attachment } from './attachment.js';
2
+ import type { ModelWithAttachment } from './mixin.js';
3
+ export interface Record {
4
+ model: ModelWithAttachment;
5
+ commit(): Promise<void>;
6
+ rollback(): Promise<void>;
7
+ persist(): Promise<void>;
8
+ transaction(options?: {
9
+ enabledRollback: boolean;
10
+ }): Promise<void>;
11
+ preComputeUrl(): Promise<void>;
12
+ generateVariants(): Promise<void>;
13
+ getAttachments(options: {
14
+ attributeName: string;
15
+ requiredOriginal?: boolean;
16
+ }): Attachment[];
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -15,5 +15,5 @@ export declare const defaultOptionsDecorator: {
15
15
  export declare const defaultStateAttributeMixin: {
16
16
  attached: never[];
17
17
  detached: never[];
18
- attributesModified: never[];
18
+ dirtied: never[];
19
19
  };
@@ -15,5 +15,5 @@ export const defaultOptionsDecorator = {
15
15
  export const defaultStateAttributeMixin = {
16
16
  attached: [],
17
17
  detached: [],
18
- attributesModified: [],
18
+ dirtied: [],
19
19
  };
@@ -4,13 +4,7 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
- import type { LucidOptions } from '../types/attachment.js';
8
- import type { ModelWithAttachment } from '../types/mixin.js';
9
- export declare function getAttachmentAttributeNames(modelInstance: ModelWithAttachment): string[];
10
- export declare function getDirtyAttachmentAttributeNames(modelInstance: ModelWithAttachment): string[];
11
- export declare function getOptions(modelInstance: ModelWithAttachment, attributeName: string): LucidOptions;
12
7
  export declare function cleanObject(obj: any): any;
13
- export declare function clone(object: Object): any;
14
8
  export declare function use(module: string): Promise<any>;
15
9
  export declare function bufferToTempFile(input: Buffer): Promise<string>;
16
10
  export declare function streamToTempFile(input: NodeJS.ReadableStream): Promise<string>;
@@ -11,20 +11,8 @@ import fs from 'node:fs/promises';
11
11
  import { pipeline } from 'node:stream';
12
12
  import { promisify } from 'node:util';
13
13
  import { createWriteStream } from 'node:fs';
14
- import { Attachment } from '../attachments/attachment.js';
15
14
  import * as errors from '../errors.js';
16
- import { optionsSym } from './symbols.js';
17
15
  const streamPipeline = promisify(pipeline);
18
- export function getAttachmentAttributeNames(modelInstance) {
19
- return Object.keys(modelInstance.$attributes).filter((attr) => modelInstance.$attributes[attr] instanceof Attachment);
20
- }
21
- export function getDirtyAttachmentAttributeNames(modelInstance) {
22
- return Object.keys(modelInstance.$dirty).filter((attr) => modelInstance.$dirty[attr] instanceof Attachment ||
23
- modelInstance.$original[attr] instanceof Attachment);
24
- }
25
- export function getOptions(modelInstance, attributeName) {
26
- return modelInstance.constructor.prototype[optionsSym]?.[attributeName];
27
- }
28
16
  export function cleanObject(obj) {
29
17
  if (obj === null || typeof obj !== 'object') {
30
18
  return obj;
@@ -43,9 +31,6 @@ export function cleanObject(obj) {
43
31
  }
44
32
  return cleanedObj;
45
33
  }
46
- export function clone(object) {
47
- return JSON.parse(JSON.stringify(object));
48
- }
49
34
  export async function use(module) {
50
35
  try {
51
36
  const result = await import(module);
@@ -4,16 +4,12 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
- import { persistAttachment, commit, rollback, generateVariants, preComputeUrl, } from '../utils/actions.js';
8
- import { clone, getAttachmentAttributeNames, getDirtyAttachmentAttributeNames, } from '../utils/helpers.js';
9
- import { defaultStateAttributeMixin } from '../utils/default_values.js';
7
+ import Record from '../services/record_with_attachment.js';
10
8
  // @afterFind()
11
9
  export const afterFindHook = async (instance) => {
12
10
  const modelInstance = instance;
13
- const attachmentAttributeNames = getAttachmentAttributeNames(modelInstance);
14
- await Promise.all(attachmentAttributeNames.map((attributeName) => {
15
- return preComputeUrl(modelInstance, attributeName);
16
- }));
11
+ const model = new Record(modelInstance);
12
+ await model.preComputeUrl();
17
13
  };
18
14
  // @afterFetch()
19
15
  // @afterPaginate()
@@ -24,73 +20,21 @@ export const afterFetchHook = async (instance) => {
24
20
  // @beforeSave()
25
21
  export const beforeSaveHook = async (instance) => {
26
22
  const modelInstance = instance;
27
- const attachmentAttributeNames = getDirtyAttachmentAttributeNames(modelInstance);
28
- /**
29
- * Empty previous $attachments
30
- */
31
- modelInstance.$attachments = clone(defaultStateAttributeMixin);
32
- /**
33
- * Set attributes Attachment type modified
34
- */
35
- attachmentAttributeNames.forEach((attributeName) => modelInstance.$attachments.attributesModified.push(attributeName));
36
- /**
37
- * Persist attachments before saving the model to the database. This
38
- * way if file saving fails we will not write anything to the
39
- * database
40
- */
41
- await Promise.all(attachmentAttributeNames.map((attributeName) => persistAttachment(modelInstance, attributeName)));
42
- try {
43
- if (modelInstance.$trx) {
44
- modelInstance.$trx.after('commit', () => commit(modelInstance));
45
- modelInstance.$trx.after('rollback', () => rollback(modelInstance));
46
- }
47
- else {
48
- await commit(modelInstance);
49
- }
50
- }
51
- catch (error) {
52
- await rollback(modelInstance);
53
- throw error;
54
- }
23
+ const model = new Record(modelInstance);
24
+ await model.detach();
25
+ await model.persist();
26
+ await model.transaction();
55
27
  };
56
28
  // @afterSave()
57
29
  export const afterSaveHook = async (instance) => {
58
30
  const modelInstance = instance;
59
- const attachmentAttributeNames = getAttachmentAttributeNames(modelInstance);
60
- /**
61
- * For all properties Attachment
62
- * Launch async generation variants
63
- */
64
- await Promise.all(attachmentAttributeNames.map((attributeName) => {
65
- if (modelInstance.$attachments.attributesModified.includes(attributeName)) {
66
- return generateVariants(modelInstance, attributeName);
67
- }
68
- }));
31
+ const model = new Record(modelInstance);
32
+ await model.generateVariants();
69
33
  };
70
34
  // @beforeDelete()
71
35
  export const beforeDeleteHook = async (instance) => {
72
36
  const modelInstance = instance;
73
- const attachmentAttributeNames = getAttachmentAttributeNames(modelInstance);
74
- /**
75
- * create $attachments
76
- */
77
- modelInstance.$attachments = clone(defaultStateAttributeMixin);
78
- /**
79
- * Mark all attachments for deletion
80
- */
81
- attachmentAttributeNames.map((attributeName) => {
82
- if (modelInstance.$attributes[attributeName]) {
83
- modelInstance.$attachments.detached.push(modelInstance.$attributes[attributeName]);
84
- }
85
- });
86
- /**
87
- * If model is using transaction, then wait for the transaction
88
- * to settle
89
- */
90
- if (modelInstance.$trx) {
91
- modelInstance.$trx.after('commit', () => commit(modelInstance));
92
- }
93
- else {
94
- await commit(modelInstance);
95
- }
37
+ const model = new Record(modelInstance);
38
+ await model.detachAll();
39
+ await model.transaction({ enabledRollback: false });
96
40
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrmc/adonis-attachment",
3
- "version": "3.2.1",
3
+ "version": "3.3.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "Turn any field on your Lucid model to an attachment data type",
6
6
  "engines": {
@@ -115,7 +115,7 @@
115
115
  },
116
116
  "publishConfig": {
117
117
  "access": "public",
118
- "tag": "latest"
118
+ "tag": "beta"
119
119
  },
120
120
  "volta": {
121
121
  "node": "20.17.0"
@@ -1,24 +0,0 @@
1
- /**
2
- * @jrmc/adonis-attachment
3
- *
4
- * @license MIT
5
- * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
- */
7
- import type { ModelWithAttachment } from '../types/mixin.js';
8
- /**
9
- * During commit, we should cleanup the old detached files
10
- */
11
- export declare function commit(modelInstance: ModelWithAttachment): Promise<void>;
12
- /**
13
- * During rollback we should remove the attached files.
14
- */
15
- export declare function rollback(modelInstance: ModelWithAttachment): Promise<void>;
16
- /**
17
- * Persist attachment for a given attachment attributeName
18
- */
19
- export declare function persistAttachment(modelInstance: ModelWithAttachment, attributeName: string): Promise<void>;
20
- export declare function preComputeUrl(modelInstance: ModelWithAttachment, attributeName: string): Promise<void>;
21
- /**
22
- * Launch converter by variant option
23
- */
24
- export declare function generateVariants(modelInstance: ModelWithAttachment, attributeName: string): Promise<void>;
@@ -1,90 +0,0 @@
1
- /**
2
- * @jrmc/adonis-attachment
3
- *
4
- * @license MIT
5
- * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
- */
7
- import attachmentManager from '../../services/main.js';
8
- import { getOptions } from './helpers.js';
9
- import { ConverterManager } from '../converter_manager.js';
10
- import { E_CANNOT_CREATE_VARIANT } from '../errors.js';
11
- /**
12
- * During commit, we should cleanup the old detached files
13
- */
14
- export async function commit(modelInstance) {
15
- await Promise.allSettled(modelInstance.$attachments.detached.map((attachment) => attachmentManager.delete(attachment)));
16
- }
17
- /**
18
- * During rollback we should remove the attached files.
19
- */
20
- export async function rollback(modelInstance) {
21
- await Promise.allSettled(modelInstance.$attachments.attached.map((attachment) => attachmentManager.delete(attachment)));
22
- }
23
- /**
24
- * Persist attachment for a given attachment attributeName
25
- */
26
- export async function persistAttachment(modelInstance, attributeName) {
27
- const existingFile = modelInstance.$original[attributeName];
28
- const newFile = modelInstance.$attributes[attributeName];
29
- const options = getOptions(modelInstance, attributeName);
30
- /**
31
- * Skip when the attachment attributeName hasn't been updated
32
- */
33
- if (existingFile === newFile) {
34
- return;
35
- }
36
- /**
37
- * There was an existing file, but there is no new file. Hence we must
38
- * remove the existing file.
39
- */
40
- if (existingFile && !newFile) {
41
- existingFile.setOptions(options);
42
- modelInstance.$attachments.detached.push(existingFile);
43
- return;
44
- }
45
- /**
46
- * If there is a new file and its local then we must save this
47
- * file.
48
- */
49
- if (newFile) {
50
- newFile.setOptions(options);
51
- modelInstance.$attachments.attached.push(newFile);
52
- /**
53
- * If there was an existing file, then we must get rid of it
54
- */
55
- if (existingFile) {
56
- existingFile.setOptions(options);
57
- modelInstance.$attachments.detached.push(existingFile);
58
- }
59
- /**
60
- * Also write the file to the disk right away
61
- */
62
- await attachmentManager.save(newFile);
63
- }
64
- }
65
- export async function preComputeUrl(modelInstance, attributeName) {
66
- const attachment = modelInstance.$attributes[attributeName];
67
- const options = getOptions(modelInstance, attributeName);
68
- attachment.setOptions(options);
69
- return attachmentManager.preComputeUrl(attachment);
70
- }
71
- /**
72
- * Launch converter by variant option
73
- */
74
- export async function generateVariants(modelInstance, attributeName) {
75
- attachmentManager.queue.push({
76
- name: `${modelInstance.constructor.name}-${attributeName}`,
77
- async run() {
78
- try {
79
- const converterManager = new ConverterManager({
80
- record: modelInstance,
81
- attributeName,
82
- });
83
- await converterManager.save();
84
- }
85
- catch (err) {
86
- throw new E_CANNOT_CREATE_VARIANT([err.message]);
87
- }
88
- },
89
- });
90
- }