@jrmc/adonis-attachment 3.3.0-beta.3 → 3.3.0-beta.5

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.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/build/index.d.ts +2 -0
  3. package/build/index.js +2 -0
  4. package/build/services/regenerate_service.d.ts +8 -0
  5. package/build/services/regenerate_service.js +29 -0
  6. package/build/src/attachment_manager.d.ts +3 -4
  7. package/build/src/attachment_manager.js +2 -5
  8. package/build/src/attachments/attachment.js +11 -5
  9. package/build/src/attachments/attachment_base.d.ts +4 -1
  10. package/build/src/attachments/attachment_base.js +38 -3
  11. package/build/src/attachments/variant_attachment.d.ts +2 -0
  12. package/build/src/attachments/variant_attachment.js +4 -0
  13. package/build/src/converter_manager.d.ts +2 -2
  14. package/build/src/converter_manager.js +88 -48
  15. package/build/src/converters/autodetect_converter.d.ts +2 -1
  16. package/build/src/converters/autodetect_converter.js +7 -9
  17. package/build/src/converters/converter.d.ts +3 -1
  18. package/build/src/converters/converter.js +3 -0
  19. package/build/src/converters/video_thumbnail_converter.d.ts +2 -2
  20. package/build/src/converters/video_thumbnail_converter.js +1 -1
  21. package/build/src/decorators/attachment.d.ts +2 -2
  22. package/build/src/define_config.d.ts +2 -2
  23. package/build/src/define_config.js +8 -4
  24. package/build/src/mixins/attachmentable.d.ts +4 -4
  25. package/build/src/mixins/attachmentable.js +3 -3
  26. package/build/src/services/record_with_attachment.d.ts +6 -3
  27. package/build/src/services/record_with_attachment.js +88 -42
  28. package/build/src/types/attachment.d.ts +7 -2
  29. package/build/src/types/config.d.ts +1 -1
  30. package/build/src/types/converter.d.ts +31 -28
  31. package/build/src/types/mixin.d.ts +3 -3
  32. package/build/src/types/regenerate.d.ts +5 -0
  33. package/build/src/types/regenerate.js +1 -0
  34. package/build/src/types/service.d.ts +3 -2
  35. package/build/src/utils/helpers.d.ts +1 -0
  36. package/build/src/utils/helpers.js +9 -0
  37. package/package.json +1 -1
package/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  This package is currently development and will replace [attachment-advanced](https://github.com/batosai/attachment-advanced) for AdonisJS 6.
4
4
 
5
- Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-kit)
6
-
7
5
  ## Links
8
6
 
9
7
  [View documentation](https://adonis-attachment.jrmc.dev/)
@@ -12,7 +10,7 @@ Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-
12
10
 
13
11
  [Discord](https://discord.gg/89eMn2vB)
14
12
 
15
- ⚠️ [Breaking change](https://adonis-attachment.jrmc.dev/changelog.html) version 2, include [@adonisjs/drive](https://docs.adonisjs.com/guides/digging-deeper/drive)
13
+ Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-kit)
16
14
 
17
15
  ## Roadmap
18
16
 
@@ -21,9 +19,11 @@ Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-
21
19
  - [x] attachment file by path
22
20
  - [x] attachment file by url
23
21
  - [x] attachment file by stream
22
+ - [x] attachment files
24
23
  - [x] save meta data
25
24
  - [x] variantes
26
25
  - [x] images
26
+ - [x] [blurhash](https://blurha.sh/)
27
27
  - [x] documents thumbnail
28
28
  - [x] videos thumbnail
29
29
  - [ ] command regenerate
package/build/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import attachmentManager from './services/main.js';
2
+ import RegenerateService from './services/regenerate_service.js';
2
3
  export { configure } from './configure.js';
3
4
  export { Attachment } from './src/attachments/attachment.js';
4
5
  export { attachment } from './src/decorators/attachment.js';
@@ -7,4 +8,5 @@ export { defineConfig } from './src/define_config.js';
7
8
  export { Attachmentable } from './src/mixins/attachmentable.js';
8
9
  export * as errors from './src/errors.js';
9
10
  export { attachmentManager };
11
+ export { RegenerateService };
10
12
  export { type AttachmentVariants } from './src/types/config.js';
package/build/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import attachmentManager from './services/main.js';
2
+ import RegenerateService from './services/regenerate_service.js';
2
3
  export { configure } from './configure.js';
3
4
  export { Attachment } from './src/attachments/attachment.js';
4
5
  export { attachment } from './src/decorators/attachment.js';
@@ -7,3 +8,4 @@ export { defineConfig } from './src/define_config.js';
7
8
  export { Attachmentable } from './src/mixins/attachmentable.js';
8
9
  export * as errors from './src/errors.js';
9
10
  export { attachmentManager };
11
+ export { RegenerateService };
@@ -0,0 +1,8 @@
1
+ import type { LucidRow, LucidModel } from '@adonisjs/lucid/types/model';
2
+ import type { RegenerateOptions } from '../src/types/regenerate.js';
3
+ export default class RegenerateService {
4
+ #private;
5
+ model(Model: LucidModel, options?: RegenerateOptions): this;
6
+ row(row: LucidRow, options?: RegenerateOptions): this;
7
+ run(): Promise<void | void[]>;
8
+ }
@@ -0,0 +1,29 @@
1
+ import Record from '../src/services/record_with_attachment.js';
2
+ export default class RegenerateService {
3
+ #Model;
4
+ #row;
5
+ #options;
6
+ model(Model, options = {}) {
7
+ this.#Model = Model;
8
+ this.#options = options;
9
+ return this;
10
+ }
11
+ row(row, options = {}) {
12
+ this.#row = row;
13
+ this.#options = options;
14
+ return this;
15
+ }
16
+ async run() {
17
+ if (this.#row) {
18
+ const record = new Record(this.#row);
19
+ return record.regenerateVariants(this.#options);
20
+ }
21
+ else if (this.#Model) {
22
+ const entities = await this.#Model.all();
23
+ return Promise.all(entities.map(async (entity) => {
24
+ const record = new Record(entity);
25
+ return record.regenerateVariants(this.#options);
26
+ }));
27
+ }
28
+ }
29
+ }
@@ -7,14 +7,13 @@
7
7
  import type { DriveService, SignedURLOptions } from '@adonisjs/drive/types';
8
8
  import type { MultipartFile } from '@adonisjs/core/bodyparser';
9
9
  import type { AttachmentAttributes, AttachmentBase, Attachment as AttachmentType } from './types/attachment.js';
10
+ import type { ResolvedAttachmentConfig } from './define_config.js';
10
11
  import { DeferQueue } from '@poppinss/defer';
11
12
  import Converter from './converters/converter.js';
12
- import { ResolvedAttachmentConfig } from './define_config.js';
13
13
  export declare class AttachmentManager<KnownConverters extends Record<string, Converter>> {
14
14
  #private;
15
15
  queue: DeferQueue;
16
16
  constructor(config: ResolvedAttachmentConfig<KnownConverters>, drive: DriveService);
17
- getConfig(): ResolvedAttachmentConfig<KnownConverters>;
18
17
  createFromDbResponse(response?: string | JSON): AttachmentType | null;
19
18
  createFromFile(input: MultipartFile): Promise<AttachmentType>;
20
19
  createFromFiles(inputs: MultipartFile[]): Promise<(AttachmentBase & {
@@ -34,6 +33,6 @@ export declare class AttachmentManager<KnownConverters extends Record<string, Co
34
33
  getConverter(key: string): Promise<void | Converter>;
35
34
  computeUrl(attachment: AttachmentType | AttachmentBase, signedUrlOptions?: SignedURLOptions): Promise<void>;
36
35
  preComputeUrl(attachment: AttachmentType): Promise<void>;
37
- save(attachment: AttachmentBase): Promise<void>;
38
- delete(attachment: AttachmentBase): Promise<void>;
36
+ write(attachment: AttachmentBase): Promise<void>;
37
+ remove(attachment: AttachmentBase): Promise<void>;
39
38
  }
@@ -23,9 +23,6 @@ export class AttachmentManager {
23
23
  const concurrency = this.#config.queue?.concurrency || 1;
24
24
  this.queue = new DeferQueue({ concurrency });
25
25
  }
26
- getConfig() {
27
- return this.#config;
28
- }
29
26
  createFromDbResponse(response) {
30
27
  if (response === null) {
31
28
  return null;
@@ -125,7 +122,7 @@ export class AttachmentManager {
125
122
  }
126
123
  }
127
124
  }
128
- async save(attachment) {
125
+ async write(attachment) {
129
126
  const destinationPath = attachment.path;
130
127
  if (attachment.options?.meta) {
131
128
  attachment.meta = await ExifAdapter.exif(attachment.input, this.#config);
@@ -140,7 +137,7 @@ export class AttachmentManager {
140
137
  await attachment.getDisk().copyFromFs(attachment.input, destinationPath);
141
138
  }
142
139
  }
143
- async delete(attachment) {
140
+ async remove(attachment) {
144
141
  if (attachment.path) {
145
142
  await attachment.getDisk().delete(attachment.path);
146
143
  if (attachment instanceof Attachment) {
@@ -45,7 +45,7 @@ export class Attachment extends AttachmentBase {
45
45
  const attributes = {
46
46
  ...meta,
47
47
  key,
48
- folder: path.join(this.options.folder, 'variants', this.name),
48
+ folder: path.join(this.folder, 'variants', this.name),
49
49
  };
50
50
  const variant = new Variant(this.drive, attributes, input);
51
51
  variant.setOptions(this.options);
@@ -58,17 +58,20 @@ export class Attachment extends AttachmentBase {
58
58
  getVariant(variantName) {
59
59
  return this.variants?.find((v) => v.key === variantName);
60
60
  }
61
- getUrl(variantName) {
61
+ async getUrl(variantName) {
62
62
  if (variantName) {
63
63
  const variant = this.getVariant(variantName);
64
64
  if (variant) {
65
65
  variant.setOptions(this.options);
66
- return variant.getUrl();
66
+ const url = await variant.getUrl();
67
+ if (url) {
68
+ return url;
69
+ }
67
70
  }
68
71
  }
69
72
  return super.getUrl();
70
73
  }
71
- getSignedUrl(variantNameOrOptions, signedUrlOptions) {
74
+ async getSignedUrl(variantNameOrOptions, signedUrlOptions) {
72
75
  let variantName;
73
76
  let options = signedUrlOptions;
74
77
  if (typeof variantNameOrOptions === 'string') {
@@ -81,7 +84,10 @@ export class Attachment extends AttachmentBase {
81
84
  const variant = this.getVariant(variantName);
82
85
  if (variant) {
83
86
  variant.setOptions(this.options);
84
- return variant.getSignedUrl(options);
87
+ const url = variant.getSignedUrl(options);
88
+ if (url) {
89
+ return url;
90
+ }
85
91
  }
86
92
  }
87
93
  return super.getSignedUrl(options);
@@ -4,6 +4,7 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
+ import type { LucidRow } from '@adonisjs/lucid/types/model';
7
8
  import type { DriveService, SignedURLOptions } from '@adonisjs/drive/types';
8
9
  import type { LucidOptions, AttachmentBaseAttributes, AttachmentBase as AttachmentBaseInterface } from '../types/attachment.js';
9
10
  import type { Exif, Input } from '../types/input.js';
@@ -17,7 +18,7 @@ export declare class AttachmentBase implements AttachmentBaseInterface {
17
18
  meta?: Exif;
18
19
  originalPath?: string;
19
20
  url?: string;
20
- options?: LucidOptions;
21
+ options: LucidOptions;
21
22
  constructor(drive: DriveService, attributes: AttachmentBaseAttributes, input?: Input);
22
23
  /**
23
24
  * Getters
@@ -29,9 +30,11 @@ export declare class AttachmentBase implements AttachmentBaseInterface {
29
30
  * Methods
30
31
  */
31
32
  getDisk(): import("flydrive").Disk;
33
+ getStream(): Promise<import("stream").Readable>;
32
34
  getUrl(): Promise<string>;
33
35
  getSignedUrl(signedUrlOptions?: SignedURLOptions): Promise<string>;
34
36
  setOptions(options: LucidOptions): this;
37
+ makeFolder(record?: LucidRow): this;
35
38
  /**
36
39
  *
37
40
  */
@@ -4,9 +4,11 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
+ import string from '@adonisjs/core/helpers/string';
7
8
  import path from 'node:path';
8
9
  import { cuid } from '@adonisjs/core/helpers';
9
10
  import { defaultOptionsDecorator } from '../utils/default_values.js';
11
+ import { extractPathParameters } from '../utils/helpers.js';
10
12
  export class AttachmentBase {
11
13
  drive;
12
14
  input;
@@ -27,6 +29,7 @@ export class AttachmentBase {
27
29
  this.mimeType = attributes.mimeType;
28
30
  this.originalPath = attributes.path;
29
31
  this.#folder = attributes.folder;
32
+ this.setOptions({ folder: attributes.folder });
30
33
  if (attributes.name) {
31
34
  this.#name = attributes.name;
32
35
  }
@@ -43,12 +46,20 @@ export class AttachmentBase {
43
46
  return this.#name;
44
47
  }
45
48
  get folder() {
46
- if (this.options) {
47
- return this.options?.folder;
49
+ if (this.#folder) {
50
+ return this.#folder;
51
+ }
52
+ if (typeof this.options.folder === 'string') {
53
+ const parameters = extractPathParameters(this.options.folder);
54
+ if (!parameters.length) {
55
+ return this.options.folder;
56
+ }
48
57
  }
49
- return this.#folder;
50
58
  }
51
59
  get path() {
60
+ if (!this.folder && this.originalPath) {
61
+ return this.originalPath;
62
+ }
52
63
  return path.join(this.folder, this.name);
53
64
  }
54
65
  /**
@@ -57,6 +68,9 @@ export class AttachmentBase {
57
68
  getDisk() {
58
69
  return this.drive.use(this.options?.disk);
59
70
  }
71
+ getStream() {
72
+ return this.getDisk().getStream(this.path);
73
+ }
60
74
  getUrl() {
61
75
  return this.getDisk().getUrl(this.path);
62
76
  }
@@ -70,6 +84,27 @@ export class AttachmentBase {
70
84
  };
71
85
  return this;
72
86
  }
87
+ makeFolder(record) {
88
+ if (typeof this.options.folder === 'function' && record) {
89
+ this.#folder = this.options.folder(record);
90
+ }
91
+ else if (this.options.folder) {
92
+ this.#folder = this.options.folder;
93
+ }
94
+ if (this.#folder && record) {
95
+ const parameters = extractPathParameters(this.#folder);
96
+ if (parameters) {
97
+ parameters.forEach((parameter) => {
98
+ const attribute = record.$attributes[parameter];
99
+ if (typeof attribute === 'string') {
100
+ const name = string.slug(string.noCase(string.escapeHTML(attribute.toLowerCase())));
101
+ this.#folder = this.#folder?.replace(`:${parameter}`, name);
102
+ }
103
+ });
104
+ }
105
+ }
106
+ return this;
107
+ }
73
108
  /**
74
109
  *
75
110
  */
@@ -8,11 +8,13 @@ import type { DriveService } from '@adonisjs/drive/types';
8
8
  import type { VariantAttributes, Variant as VariantInterface } from '../types/attachment.js';
9
9
  import type { Input } from '../types/input.js';
10
10
  import { AttachmentBase } from './attachment_base.js';
11
+ import { BlurhashOptions } from '../types/converter.js';
11
12
  export declare class Variant extends AttachmentBase implements VariantInterface {
12
13
  #private;
13
14
  key: string;
14
15
  blurhash?: string;
15
16
  constructor(drive: DriveService, attributes: VariantAttributes, input?: Input);
17
+ generateBlurhash(options?: BlurhashOptions): Promise<void>;
16
18
  /**
17
19
  * Getters
18
20
  */
@@ -5,6 +5,7 @@
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
7
  import { AttachmentBase } from './attachment_base.js';
8
+ import { imageToBlurhash } from '../utils/helpers.js';
8
9
  export class Variant extends AttachmentBase {
9
10
  key;
10
11
  #folder;
@@ -15,6 +16,9 @@ export class Variant extends AttachmentBase {
15
16
  this.#folder = attributes.folder;
16
17
  this.blurhash = attributes.blurhash;
17
18
  }
19
+ async generateBlurhash(options) {
20
+ this.blurhash = await imageToBlurhash(this.input, options);
21
+ }
18
22
  /**
19
23
  * Getters
20
24
  */
@@ -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, options }: ConverterInitializeAttributes);
5
- save(): Promise<void>;
4
+ constructor({ record, attributeName, options, filters }: ConverterInitializeAttributes);
5
+ run(): Promise<void>;
6
6
  }
@@ -3,71 +3,93 @@ import string from '@adonisjs/core/helpers/string';
3
3
  import db from '@adonisjs/lucid/services/db';
4
4
  import attachmentManager from '../services/main.js';
5
5
  import * as errors from './errors.js';
6
- import { imageToBlurhash } from './utils/helpers.js';
6
+ import { streamToTempFile } from './utils/helpers.js';
7
7
  export class ConverterManager {
8
8
  #record;
9
9
  #attributeName;
10
10
  #options;
11
- constructor({ record, attributeName, options }) {
11
+ #filters;
12
+ constructor({ record, attributeName, options, filters }) {
12
13
  this.#record = record;
13
14
  this.#attributeName = attributeName;
14
15
  this.#options = options;
16
+ this.#filters = filters;
15
17
  }
16
- async save() {
17
- let attachments = this.#record.getAttachments({
18
+ async run() {
19
+ const attachments = this.#record.getAttachments({
18
20
  attributeName: this.#attributeName,
19
21
  });
20
- const Model = this.#record.model.constructor;
21
- const id = this.#record.model.$attributes['id'];
22
- const data = {};
23
- if (this.#options.variants) {
24
- for await (const option of this.#options.variants) {
25
- const converter = (await attachmentManager.getConverter(option));
26
- if (attachments && converter) {
27
- for await (const attachment of attachments) {
28
- const input = attachment.input;
29
- const output = await converter.handle({
30
- input,
31
- options: converter.options,
32
- });
33
- if (output === undefined) {
34
- throw new errors.E_CANNOT_PATH_BY_CONVERTER();
35
- }
36
- const variant = await attachment.createVariant(option, output);
37
- if (converter.options.blurhash) {
38
- if ((typeof converter.options.blurhash !== 'boolean' &&
39
- converter.options.blurhash.enabled === true) ||
40
- converter.options.blurhash === true) {
41
- try {
42
- const options = typeof converter.options.blurhash !== 'boolean'
43
- ? converter.options.blurhash
44
- : undefined;
45
- variant.blurhash = await imageToBlurhash(variant.input, options);
46
- }
47
- catch (error) {
48
- logger.error(error.message);
49
- }
50
- }
51
- }
52
- await attachmentManager.save(variant);
53
- }
22
+ const variants = [];
23
+ await this.#purge(attachments);
24
+ if (!attachments || !this.#options.variants || !this.#options.variants.length) {
25
+ return;
26
+ }
27
+ for await (const key of this.#options.variants) {
28
+ if (this.#filters?.variants !== undefined && !this.#filters?.variants?.includes(key)) {
29
+ continue;
30
+ }
31
+ const converter = (await attachmentManager.getConverter(key));
32
+ if (converter) {
33
+ for await (const attachment of attachments) {
34
+ const variant = await this.#generate({
35
+ key,
36
+ attachment,
37
+ converter
38
+ });
39
+ variants.push(variant);
54
40
  }
55
41
  }
56
42
  }
57
- if (Array.isArray(this.#record.model.$original[this.#attributeName])) {
58
- data[string.snakeCase(this.#attributeName)] = JSON.stringify(attachments.map((att) => att.toObject()));
43
+ return this.#commit(attachments, () => {
44
+ for (let i = 0; i < variants.length; i++) {
45
+ attachmentManager.remove(variants[i]);
46
+ }
47
+ });
48
+ }
49
+ async #generate({ key, attachment, converter }) {
50
+ let input = attachment.input;
51
+ if (!input) {
52
+ input = await streamToTempFile(await attachment.getStream());
59
53
  }
60
- else {
61
- data[string.snakeCase(this.#attributeName)] = JSON.stringify(attachments[0].toObject());
54
+ const output = await converter.handle({
55
+ input,
56
+ options: converter.options,
57
+ });
58
+ if (output === undefined) {
59
+ throw new errors.E_CANNOT_PATH_BY_CONVERTER();
62
60
  }
63
- const trx = await db.transaction();
64
- trx.after('rollback', () => {
65
- for (let i = 0; i < attachments.length; i++) {
66
- for (const variant of attachments[i].variants) {
67
- attachmentManager.delete(variant);
61
+ const variant = await attachment.createVariant(key, output);
62
+ if (converter.options.blurhash) {
63
+ if ((typeof converter.options.blurhash !== 'boolean' &&
64
+ converter.options.blurhash.enabled === true) ||
65
+ converter.options.blurhash === true) {
66
+ try {
67
+ const options = typeof converter.options.blurhash !== 'boolean'
68
+ ? converter.options.blurhash
69
+ : undefined;
70
+ await variant.generateBlurhash(options);
71
+ }
72
+ catch (error) {
73
+ logger.error(error.message);
68
74
  }
69
75
  }
70
- });
76
+ }
77
+ await attachmentManager.write(variant);
78
+ return variant;
79
+ }
80
+ async #commit(attachments, rollback) {
81
+ const Model = this.#record.row.constructor;
82
+ const id = this.#record.row.$attributes['id'];
83
+ const data = {};
84
+ const index = string.snakeCase(this.#attributeName);
85
+ if (Array.isArray(this.#record.row.$original[this.#attributeName])) {
86
+ data[index] = JSON.stringify(attachments.map((att) => att.toObject()));
87
+ }
88
+ else {
89
+ data[index] = JSON.stringify(attachments[0].toObject());
90
+ }
91
+ const trx = await db.transaction();
92
+ trx.after('rollback', rollback);
71
93
  try {
72
94
  await trx.query().from(Model.table).where('id', id).update(data);
73
95
  return trx.commit();
@@ -76,4 +98,22 @@ export class ConverterManager {
76
98
  return trx.rollback();
77
99
  }
78
100
  }
101
+ async #purge(attachments) {
102
+ return Promise.all(attachments.map(async (attachment) => {
103
+ if (attachment.variants) {
104
+ await Promise.all(attachment.variants.map(async (variant) => {
105
+ if (this.#filters?.variants !== undefined && !this.#filters?.variants?.includes(variant.key)) {
106
+ return;
107
+ }
108
+ return attachmentManager.remove(variant);
109
+ }));
110
+ if (this.#filters?.variants !== undefined) {
111
+ attachment.variants = await attachment.variants.filter((variant) => !this.#filters?.variants?.includes(variant.key));
112
+ }
113
+ else {
114
+ attachment.variants = [];
115
+ }
116
+ }
117
+ }));
118
+ }
79
119
  }
@@ -5,7 +5,8 @@
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
7
  import type { ConverterAttributes } from '../types/converter.js';
8
+ import type { Input } from '../types/input.js';
8
9
  import Converter from './converter.js';
9
10
  export default class AutodetectConverter extends Converter {
10
- handle({ input, options }: ConverterAttributes): Promise<false | import("../types/input.js").Input | undefined>;
11
+ handle({ input, options }: ConverterAttributes): Promise<Input | undefined>;
11
12
  }
@@ -18,17 +18,15 @@ export default class AutodetectConverter extends Converter {
18
18
  else {
19
19
  fileType = await fileTypeFromFile(input);
20
20
  }
21
- if (fileType?.mime.includes('image')) {
22
- converter = new ImageConverter(options, this.binPaths);
23
- }
24
- else if (fileType?.mime.includes('video')) {
21
+ if (fileType?.mime.includes('video')) {
25
22
  converter = new VideoThumnailConverter(options, this.binPaths);
26
23
  }
27
- if (converter) {
28
- return await converter.handle({
29
- input,
30
- options,
31
- });
24
+ else {
25
+ converter = new ImageConverter(options, this.binPaths);
32
26
  }
27
+ return converter.handle({
28
+ input,
29
+ options,
30
+ });
33
31
  }
34
32
  }
@@ -5,9 +5,11 @@
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
7
  import type { BinPaths } from '../types/config.js';
8
- import type { Converter as ConverterInterface, ConverterOptions } from '../types/converter.js';
8
+ import type { Converter as ConverterInterface, ConverterOptions, ConverterAttributes } from '../types/converter.js';
9
+ import type { Input } from '../types/input.js';
9
10
  export default class Converter implements ConverterInterface {
10
11
  options?: ConverterOptions;
11
12
  binPaths?: BinPaths;
12
13
  constructor(options?: ConverterOptions, binPaths?: BinPaths);
14
+ handle({ input }: ConverterAttributes): Promise<Input | undefined>;
13
15
  }
@@ -11,4 +11,7 @@ export default class Converter {
11
11
  this.options = options;
12
12
  this.binPaths = binPaths;
13
13
  }
14
+ async handle({ input }) {
15
+ return input;
16
+ }
14
17
  }
@@ -8,6 +8,6 @@ import type { ConverterAttributes } from '../types/converter.js';
8
8
  import type { Input } from '../types/input.js';
9
9
  import Converter from './converter.js';
10
10
  export default class VideoThumbnailConvert extends Converter {
11
- handle({ input, options }: ConverterAttributes): Promise<false | Input>;
12
- videoToImage(ffmpeg: Function, input: Input): Promise<string | false>;
11
+ handle({ input, options }: ConverterAttributes): Promise<Input | undefined>;
12
+ videoToImage(ffmpeg: Function, input: Input): Promise<string | undefined>;
13
13
  }
@@ -16,7 +16,7 @@ export default class VideoThumbnailConvert extends Converter {
16
16
  const filePath = await this.videoToImage(ffmpeg, input);
17
17
  if (options && filePath) {
18
18
  const converter = new ImageConverter();
19
- return await converter.handle({
19
+ return converter.handle({
20
20
  input: filePath,
21
21
  options,
22
22
  });
@@ -6,9 +6,9 @@
6
6
  */
7
7
  import type { LucidModel } from '@adonisjs/lucid/types/model';
8
8
  import type { LucidOptions } from '../types/attachment.js';
9
- import type { AttributeOfModelWithAttachment } from '../types/mixin.js';
9
+ import type { AttributeOfRowWithAttachment } from '../types/mixin.js';
10
10
  export declare const bootModel: (model: LucidModel & {
11
- $attachments: AttributeOfModelWithAttachment;
11
+ $attachments: AttributeOfRowWithAttachment;
12
12
  }) => void;
13
13
  export declare const attachment: (options?: LucidOptions) => (target: any, attributeName: string) => void;
14
14
  export declare const attachments: (options?: LucidOptions) => (target: any, attributeName: string) => void;
@@ -16,8 +16,8 @@ export type ResolvedAttachmentConfig<KnownConverters extends Record<string, Conv
16
16
  rename?: boolean;
17
17
  preComputeUrl?: boolean;
18
18
  converters?: {
19
- [K in keyof KnownConverters]: KnownConverters[K];
19
+ [K in keyof KnownConverters]: Converter;
20
20
  };
21
21
  queue?: Queue;
22
22
  };
23
- export declare function defineConfig<KnownConverter extends Record<string, ConverterConfig>>(config: AttachmentConfig<KnownConverter>): ConfigProvider<ResolvedAttachmentConfig<KnownConverter>>;
23
+ export declare function defineConfig<KnownConverter extends Record<string, ConverterConfig>>(config: AttachmentConfig<KnownConverter>): ConfigProvider<ResolvedAttachmentConfig<Record<string, Converter>>>;
@@ -5,7 +5,6 @@
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
7
  import { configProvider } from '@adonisjs/core';
8
- // export function defineConfig<T extends AttachmentConfig>(config: T): T {
9
8
  export function defineConfig(config) {
10
9
  return configProvider.create(async (_app) => {
11
10
  const convertersList = Object.keys(config.converters || {});
@@ -14,9 +13,14 @@ export function defineConfig(config) {
14
13
  for (let converterName of convertersList) {
15
14
  const converter = config.converters[converterName];
16
15
  const binConfig = config.bin;
17
- const { default: value } = await converter.converter();
18
- const Converter = value;
19
- converters[converterName] = new Converter(converter.options, binConfig);
16
+ try {
17
+ const { default: value } = await converter.converter();
18
+ const Converter = value;
19
+ converters[converterName] = new Converter(converter.options, binConfig);
20
+ }
21
+ catch (error) {
22
+ console.error(`Failed to load converter ${converterName}:`, error);
23
+ }
20
24
  }
21
25
  }
22
26
  return {
@@ -6,11 +6,11 @@
6
6
  */
7
7
  import type { BaseModel } from '@adonisjs/lucid/orm';
8
8
  import type { NormalizeConstructor } from '@adonisjs/core/types/helpers';
9
- import type { AttributeOfModelWithAttachment } from '../types/mixin.js';
9
+ import type { AttributeOfRowWithAttachment } from '../types/mixin.js';
10
10
  type Constructor = NormalizeConstructor<typeof BaseModel>;
11
11
  export declare function Attachmentable<T extends Constructor>(superclass: T): {
12
12
  new (...args: any[]): {
13
- $attachments: AttributeOfModelWithAttachment;
13
+ $attachments: AttributeOfRowWithAttachment;
14
14
  $attributes: import("@adonisjs/lucid/types/model").ModelObject;
15
15
  $extras: import("@adonisjs/lucid/types/model").ModelObject;
16
16
  $original: import("@adonisjs/lucid/types/model").ModelObject;
@@ -43,10 +43,10 @@ export declare function Attachmentable<T extends Constructor>(superclass: T): {
43
43
  $consumeAdapterResult(adapterResult: import("@adonisjs/lucid/types/model").ModelObject, sideloadAttributes?: import("@adonisjs/lucid/types/model").ModelObject): void;
44
44
  $hydrateOriginals(): void;
45
45
  fill(value: Partial<{
46
- $attachments: AttributeOfModelWithAttachment;
46
+ $attachments: AttributeOfRowWithAttachment;
47
47
  }>, allowExtraProperties?: boolean): /*elided*/ any;
48
48
  merge(value: Partial<{
49
- $attachments: AttributeOfModelWithAttachment;
49
+ $attachments: AttributeOfRowWithAttachment;
50
50
  }>, allowExtraProperties?: boolean): /*elided*/ any;
51
51
  isDirty(fields?: "$attachments" | ("$attachments" | undefined)[] | undefined): boolean;
52
52
  enableForceUpdate(): /*elided*/ any;
@@ -14,7 +14,7 @@ import { beforeSave, beforeFind, beforeFetch, beforePaginate, beforeCreate, } fr
14
14
  import { defaultStateAttributeMixin } from '../utils/default_values.js';
15
15
  import logger from '@adonisjs/core/services/logger';
16
16
  export function Attachmentable(superclass) {
17
- class ModelWithAttachment extends superclass {
17
+ class RowWithAttachment extends superclass {
18
18
  $attachments = structuredClone(defaultStateAttributeMixin);
19
19
  static async warn() {
20
20
  logger.warn(`The "Attachmentable" mixin is deprecated and may be removed in a future version.`);
@@ -26,6 +26,6 @@ export function Attachmentable(superclass) {
26
26
  beforeFetch(),
27
27
  beforePaginate(),
28
28
  beforeSave()
29
- ], ModelWithAttachment, "warn", null);
30
- return ModelWithAttachment;
29
+ ], RowWithAttachment, "warn", null);
30
+ return RowWithAttachment;
31
31
  }
@@ -1,9 +1,10 @@
1
- import type { ModelWithAttachment } from '../types/mixin.js';
1
+ import type { RowWithAttachment } from '../types/mixin.js';
2
2
  import type { Attachment as AttachmentType } from '../types/attachment.js';
3
3
  import type { Record as RecordImplementation } from '../types/service.js';
4
+ import type { RegenerateOptions } from '../types/regenerate.js';
4
5
  export default class Record implements RecordImplementation {
5
6
  #private;
6
- constructor(model: ModelWithAttachment);
7
+ constructor(row: RowWithAttachment);
7
8
  /**
8
9
  * During commit, we should cleanup the old detached files
9
10
  */
@@ -18,11 +19,13 @@ export default class Record implements RecordImplementation {
18
19
  }): Promise<void>;
19
20
  preComputeUrl(): Promise<void>;
20
21
  generateVariants(): Promise<void>;
22
+ regenerateVariants(options?: RegenerateOptions): Promise<void>;
21
23
  detach(): Promise<PromiseSettledResult<void>[]>;
22
24
  detachAll(): Promise<PromiseSettledResult<void>[]>;
23
- get model(): ModelWithAttachment;
25
+ get row(): RowWithAttachment;
24
26
  getAttachments(options: {
25
27
  attributeName: string;
26
28
  requiredOriginal?: boolean;
29
+ requiredDirty?: boolean;
27
30
  }): AttachmentType[];
28
31
  }
@@ -5,32 +5,32 @@ import { optionsSym } from '../utils/symbols.js';
5
5
  import { ConverterManager } from '../converter_manager.js';
6
6
  import { E_CANNOT_CREATE_VARIANT } from '../errors.js';
7
7
  export default class Record {
8
- #model;
9
- constructor(model) {
10
- this.#model = model;
11
- if (!this.#model.$attachments) {
8
+ #row;
9
+ constructor(row) {
10
+ this.#row = row;
11
+ if (!this.#row.$attachments) {
12
12
  /**
13
13
  * Empty previous $attachments
14
14
  */
15
- this.#model.$attachments = structuredClone(defaultStateAttributeMixin);
15
+ this.#row.$attachments = structuredClone(defaultStateAttributeMixin);
16
16
  }
17
17
  }
18
18
  /**
19
19
  * During commit, we should cleanup the old detached files
20
20
  */
21
21
  async commit() {
22
- await Promise.allSettled(this.#model.$attachments.detached.map((attachment) => attachmentManager.delete(attachment)));
22
+ await Promise.allSettled(this.#row.$attachments.detached.map((attachment) => attachmentManager.remove(attachment)));
23
23
  }
24
24
  /**
25
25
  * During rollback we should remove the attached files.
26
26
  */
27
27
  async rollback() {
28
- await Promise.allSettled(this.#model.$attachments.attached.map((attachment) => attachmentManager.delete(attachment)));
28
+ await Promise.allSettled(this.#row.$attachments.attached.map((attachment) => attachmentManager.remove(attachment)));
29
29
  }
30
30
  async persist() {
31
31
  const attachmentAttributeNames = this.#getDirtyAttributeNamesOfAttachment();
32
32
  /**
33
- * Persist attachments before saving the model to the database. This
33
+ * Persist attachments before saving the row to the database. This
34
34
  * way if file saving fails we will not write anything to the
35
35
  * database
36
36
  */
@@ -47,7 +47,7 @@ export default class Record {
47
47
  /**
48
48
  * memorise attribute name for generate variants
49
49
  */
50
- this.#model.$attachments.dirtied.push(name);
50
+ this.#row.$attachments.dirtied.push(name);
51
51
  for (let i = 0; i < newAttachments.length; i++) {
52
52
  if (originalAttachments.includes(newAttachments[i])) {
53
53
  continue;
@@ -57,22 +57,22 @@ export default class Record {
57
57
  * file.
58
58
  */
59
59
  if (newAttachments[i]) {
60
- newAttachments[i].setOptions(options);
61
- this.#model.$attachments.attached.push(newAttachments[i]);
60
+ newAttachments[i].setOptions(options).makeFolder(this.#row);
61
+ this.#row.$attachments.attached.push(newAttachments[i]);
62
62
  /**
63
63
  * Also write the file to the disk right away
64
64
  */
65
- await attachmentManager.save(newAttachments[i]);
65
+ await attachmentManager.write(newAttachments[i]);
66
66
  }
67
67
  }
68
68
  }));
69
69
  }
70
70
  async transaction(options = { enabledRollback: true }) {
71
71
  try {
72
- if (this.#model.$trx) {
73
- this.#model.$trx.after('commit', () => this.commit());
72
+ if (this.#row.$trx) {
73
+ this.#row.$trx.after('commit', () => this.commit());
74
74
  if (options.enabledRollback) {
75
- this.#model.$trx.after('rollback', () => this.rollback());
75
+ this.#row.$trx.after('rollback', () => this.rollback());
76
76
  }
77
77
  }
78
78
  else {
@@ -90,7 +90,7 @@ export default class Record {
90
90
  const attachmentAttributeNames = this.#getAttributeNamesOfAttachment();
91
91
  await Promise.all(attachmentAttributeNames.map(async (name) => {
92
92
  const options = this.#getOptionsByAttributeName(name);
93
- if (this.#model.$attributes[name]) {
93
+ if (this.#row.$attributes[name]) {
94
94
  const attachments = this.#getAttachmentsByAttributeName(name);
95
95
  for (let i = 0; i < attachments.length; i++) {
96
96
  attachments[i].setOptions(options);
@@ -100,8 +100,8 @@ export default class Record {
100
100
  }));
101
101
  }
102
102
  async generateVariants() {
103
- /* this.#model.$dirty is not avalable in afterSave hooks */
104
- const attachmentAttributeNames = this.#model.$attachments.dirtied;
103
+ /* this.#row.$dirty is not avalable in afterSave hooks */
104
+ const attachmentAttributeNames = this.#row.$attachments.dirtied;
105
105
  /**
106
106
  * For all properties Attachment
107
107
  * Launch async generation variants
@@ -109,7 +109,7 @@ export default class Record {
109
109
  for await (const name of attachmentAttributeNames) {
110
110
  const record = this;
111
111
  attachmentManager.queue.push({
112
- name: `${this.#model.constructor.name}-${name}`,
112
+ name: `${this.#row.constructor.name}-${name}`,
113
113
  async run() {
114
114
  try {
115
115
  const converterManager = new ConverterManager({
@@ -117,7 +117,38 @@ export default class Record {
117
117
  attributeName: name,
118
118
  options: record.#getOptionsByAttributeName(name),
119
119
  });
120
- await converterManager.save();
120
+ await converterManager.run();
121
+ }
122
+ catch (err) {
123
+ throw new E_CANNOT_CREATE_VARIANT([err.message]);
124
+ }
125
+ },
126
+ });
127
+ }
128
+ }
129
+ async regenerateVariants(options = {}) {
130
+ let attachmentAttributeNames;
131
+ if (options.attributes?.length) {
132
+ attachmentAttributeNames = options.attributes;
133
+ }
134
+ else {
135
+ attachmentAttributeNames = this.#getAttributeNamesOfAttachment();
136
+ }
137
+ for await (const name of attachmentAttributeNames) {
138
+ const record = this;
139
+ attachmentManager.queue.push({
140
+ name: `${this.#row.constructor.name}-${name}`,
141
+ async run() {
142
+ try {
143
+ const converterManager = new ConverterManager({
144
+ record,
145
+ attributeName: name,
146
+ options: record.#getOptionsByAttributeName(name),
147
+ filters: {
148
+ variants: options.variants
149
+ }
150
+ });
151
+ await converterManager.run();
121
152
  }
122
153
  catch (err) {
123
154
  throw new E_CANNOT_CREATE_VARIANT([err.message]);
@@ -134,7 +165,7 @@ export default class Record {
134
165
  return Promise.allSettled(attachmentAttributeNames.map((name) => {
135
166
  let attachments = [];
136
167
  const options = this.#getOptionsByAttributeName(name);
137
- if (this.#model.$dirty[name] === null) {
168
+ if (this.#row.$dirty[name] === null) {
138
169
  attachments = this.#getOriginalAttachmentsByAttributeName(name);
139
170
  }
140
171
  else {
@@ -158,7 +189,7 @@ export default class Record {
158
189
  }
159
190
  for (let i = 0; i < attachments.length; i++) {
160
191
  attachments[i].setOptions(options);
161
- this.#model.$attachments.detached.push(attachments[i]);
192
+ this.#row.$attachments.detached.push(attachments[i]);
162
193
  }
163
194
  }));
164
195
  }
@@ -172,53 +203,68 @@ export default class Record {
172
203
  const attachments = this.#getAttachmentsByAttributeName(name);
173
204
  for (let i = 0; i < attachments.length; i++) {
174
205
  attachments[i].setOptions(options);
175
- this.#model.$attachments.detached.push(attachments[i]);
206
+ this.#row.$attachments.detached.push(attachments[i]);
176
207
  }
177
208
  }));
178
209
  }
179
- get model() {
180
- return this.#model;
210
+ get row() {
211
+ return this.#row;
181
212
  }
182
213
  getAttachments(options) {
214
+ let attachments;
183
215
  if (options.requiredOriginal) {
184
- return this.#getOriginalAttachmentsByAttributeName(options.attributeName);
216
+ attachments = this.#getOriginalAttachmentsByAttributeName(options.attributeName);
217
+ }
218
+ else if (options.requiredDirty) {
219
+ attachments = this.#getDirtyAttachmentsByAttributeName(options.attributeName);
185
220
  }
186
221
  else {
187
- return this.#getAttachmentsByAttributeName(options.attributeName);
222
+ attachments = this.#getAttachmentsByAttributeName(options.attributeName);
188
223
  }
224
+ const opts = this.#getOptionsByAttributeName(options.attributeName);
225
+ attachments.map((attachment) => attachment.setOptions(opts).makeFolder(this.#row));
226
+ return attachments;
189
227
  }
190
228
  #getAttachmentsByAttributeName(name) {
191
- if (Array.isArray(this.#model.$attributes[name])) {
192
- return this.#model.$attributes[name];
229
+ if (Array.isArray(this.#row.$attributes[name])) {
230
+ return this.#row.$attributes[name];
193
231
  }
194
- return [this.#model.$attributes[name]];
232
+ return [this.#row.$attributes[name]];
195
233
  }
196
234
  #getOriginalAttachmentsByAttributeName(name) {
197
- if (Array.isArray(this.#model.$original[name])) {
198
- return this.#model.$original[name];
235
+ if (Array.isArray(this.#row.$original[name])) {
236
+ return this.#row.$original[name];
237
+ }
238
+ return [this.#row.$original[name]];
239
+ }
240
+ #getDirtyAttachmentsByAttributeName(name) {
241
+ if (Array.isArray(this.#row.$dirty[name])) {
242
+ return this.#row.$dirty[name];
199
243
  }
200
- return [this.#model.$original[name]];
244
+ return [this.#row.$dirty[name]];
201
245
  }
202
246
  #getOptionsByAttributeName(name) {
203
- return this.#model.constructor.prototype[optionsSym]?.[name];
247
+ return this.#row.constructor.prototype[optionsSym]?.[name];
204
248
  }
205
249
  #getAttributeNamesOfAttachment() {
206
- return Object.keys(this.#model.$attributes).filter((name) => {
207
- const value = this.#model.$attributes[name];
250
+ return Object.keys(this.#row.$attributes).filter((name) => {
251
+ const value = this.#row.$attributes[name];
208
252
  return (value instanceof Attachment ||
209
253
  (Array.isArray(value) && value.every((item) => item instanceof Attachment)));
210
254
  });
211
255
  }
212
256
  #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
257
+ return Object.keys(this.#row.$dirty).filter((name) => {
258
+ const dirtyValue = this.#row.$dirty[name];
259
+ const originalValue = this.#row.$original[name]; // if dirtyValue is null, check original type
216
260
  const isDirtyAttachment = dirtyValue instanceof Attachment ||
217
261
  (Array.isArray(dirtyValue) &&
218
- dirtyValue.every((item) => item instanceof Attachment) &&
219
- dirtyValue.length);
262
+ dirtyValue.length &&
263
+ dirtyValue.every((item) => item instanceof Attachment));
220
264
  const isOriginalAttachment = originalValue instanceof Attachment ||
221
- (Array.isArray(originalValue) && originalValue.every((item) => item instanceof Attachment));
265
+ (Array.isArray(originalValue) &&
266
+ originalValue.length &&
267
+ originalValue.every((item) => item instanceof Attachment));
222
268
  return isDirtyAttachment || isOriginalAttachment;
223
269
  });
224
270
  }
@@ -4,11 +4,13 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
+ import type { LucidRow } from '@adonisjs/lucid/types/model';
7
8
  import type { DriveService } from '@adonisjs/drive/types';
8
9
  import type { Exif, Input } from './input.js';
9
10
  import type { Disk } from '@adonisjs/drive';
10
11
  import type { SignedURLOptions } from '@adonisjs/drive/types';
11
12
  import type { AttachmentVariants } from '@jrmc/adonis-attachment';
13
+ import { BlurhashOptions } from './converter.js';
12
14
  export type AttachmentBase = {
13
15
  drive: DriveService;
14
16
  input?: Input;
@@ -21,8 +23,10 @@ export type AttachmentBase = {
21
23
  meta?: Exif;
22
24
  originalPath?: string;
23
25
  url?: string;
24
- options?: LucidOptions;
26
+ options: LucidOptions;
27
+ makeFolder(record?: LucidRow): void;
25
28
  getDisk(): Disk;
29
+ getStream(): Promise<NodeJS.ReadableStream>;
26
30
  getUrl(): Promise<string>;
27
31
  getSignedUrl(signedUrlOptions?: SignedURLOptions): Promise<string>;
28
32
  setOptions(options: LucidOptions): AttachmentBase;
@@ -42,11 +46,12 @@ export type Variant = AttachmentBase & {
42
46
  key: string;
43
47
  folder: string;
44
48
  blurhash?: string;
49
+ generateBlurhash(options?: BlurhashOptions): void;
45
50
  toObject(): VariantAttributes;
46
51
  };
47
52
  export type LucidOptions = {
48
53
  disk?: string;
49
- folder?: string;
54
+ folder?: string | ((record?: LucidRow) => string);
50
55
  preComputeUrl?: boolean;
51
56
  variants?: (keyof AttachmentVariants)[];
52
57
  rename?: boolean;
@@ -4,8 +4,8 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
- import { ConfigProvider } from '@adonisjs/core/types';
8
7
  import type { Converter, ConverterOptions } from './converter.js';
8
+ import { ConfigProvider } from '@adonisjs/core/types';
9
9
  import { AttachmentManager } from '../attachment_manager.js';
10
10
  type ImportConverter = {
11
11
  default: unknown;
@@ -11,12 +11,15 @@ import type { LucidOptions } from './attachment.js';
11
11
  export type Converter = {
12
12
  options?: ConverterOptions;
13
13
  binPaths?: BinPaths;
14
- handle?: (attributes: ConverterAttributes) => Promise<Input | undefined>;
14
+ handle: (attributes: ConverterAttributes) => Promise<Input | undefined>;
15
15
  };
16
16
  export type ConverterInitializeAttributes = {
17
17
  record: Record;
18
18
  attributeName: string;
19
19
  options: LucidOptions;
20
+ filters?: {
21
+ variants?: string[];
22
+ };
20
23
  };
21
24
  export type ConverterAttributes = {
22
25
  input: Input;
@@ -31,40 +34,40 @@ type jpeg = {
31
34
  format: 'jpeg';
32
35
  options: {
33
36
  quality?: number;
34
- progressive?: Boolean;
37
+ progressive?: boolean;
35
38
  chromaSubsampling?: string;
36
- optimiseCoding?: Boolean;
37
- optimizeCoding?: Boolean;
38
- mozjpeg?: Boolean;
39
- trellisQuantisation?: Boolean;
40
- overshootDeringing?: Boolean;
41
- optimiseScans?: Boolean;
42
- optimizeScans?: Boolean;
39
+ optimiseCoding?: boolean;
40
+ optimizeCoding?: boolean;
41
+ mozjpeg?: boolean;
42
+ trellisQuantisation?: boolean;
43
+ overshootDeringing?: boolean;
44
+ optimiseScans?: boolean;
45
+ optimizeScans?: boolean;
43
46
  quantisationTable?: number;
44
47
  quantizationTable?: number;
45
- force?: Boolean;
48
+ force?: boolean;
46
49
  };
47
50
  };
48
51
  type png = {
49
52
  format: 'png';
50
53
  options: {
51
54
  quality?: number;
52
- progressive?: Boolean;
55
+ progressive?: boolean;
53
56
  compressionLevel?: number;
54
- adaptiveFiltering?: Boolean;
55
- palette?: Boolean;
57
+ adaptiveFiltering?: boolean;
58
+ palette?: boolean;
56
59
  effort?: number;
57
60
  colours?: number;
58
61
  colors?: number;
59
62
  dither?: number;
60
- force?: Boolean;
63
+ force?: boolean;
61
64
  };
62
65
  };
63
66
  type gif = {
64
67
  format: 'gif';
65
68
  options: {
66
- reuse?: Boolean;
67
- progressive?: Boolean;
69
+ reuse?: boolean;
70
+ progressive?: boolean;
68
71
  colours?: number;
69
72
  colors?: number;
70
73
  effort?: number;
@@ -73,7 +76,7 @@ type gif = {
73
76
  interPaletteMaxError?: number;
74
77
  loop?: number;
75
78
  delay?: number | number[];
76
- force?: Boolean;
79
+ force?: boolean;
77
80
  };
78
81
  };
79
82
  type webp = {
@@ -81,23 +84,23 @@ type webp = {
81
84
  options: {
82
85
  quality?: number;
83
86
  alphaQuality?: number;
84
- lossless?: Boolean;
85
- nearLossless?: Boolean;
86
- smartSubsample?: Boolean;
87
+ lossless?: boolean;
88
+ nearLossless?: boolean;
89
+ smartSubsample?: boolean;
87
90
  preset?: string;
88
91
  effort?: number;
89
92
  loop?: number;
90
93
  delay?: number | number[];
91
- minSize?: Boolean;
92
- mixed?: Boolean;
93
- force?: Boolean;
94
+ minSize?: boolean;
95
+ mixed?: boolean;
96
+ force?: boolean;
94
97
  };
95
98
  };
96
99
  type avif = {
97
100
  format: 'avif';
98
101
  options: {
99
102
  quality?: number;
100
- lossless?: Boolean;
103
+ lossless?: boolean;
101
104
  effort?: number;
102
105
  chromaSubsampling?: string;
103
106
  bitdepth?: number;
@@ -108,7 +111,7 @@ type heif = {
108
111
  options: {
109
112
  compression?: string;
110
113
  quality?: number;
111
- lossless?: Boolean;
114
+ lossless?: boolean;
112
115
  effort?: number;
113
116
  chromaSubsampling?: string;
114
117
  bitdepth?: number;
@@ -127,9 +130,9 @@ export type ConverterOptions = {
127
130
  alpha: number;
128
131
  };
129
132
  kernel?: string;
130
- withoutEnlargement?: Boolean;
131
- withoutReduction?: Boolean;
132
- fastShrinkOnLoad?: Boolean;
133
+ withoutEnlargement?: boolean;
134
+ withoutReduction?: boolean;
135
+ fastShrinkOnLoad?: boolean;
133
136
  };
134
137
  format?: 'jpeg' | 'jpg' | 'png' | 'gif' | 'webp' | 'avif' | 'heif' | 'tiff' | 'raw' | jpeg | png | gif | webp | avif | heif;
135
138
  blurhash?: boolean | BlurhashOptions;
@@ -6,11 +6,11 @@
6
6
  */
7
7
  import type { LucidRow } from '@adonisjs/lucid/types/model';
8
8
  import type { Attachment } from './attachment.js';
9
- export type AttributeOfModelWithAttachment = {
9
+ export type AttributeOfRowWithAttachment = {
10
10
  attached: Attachment[];
11
11
  detached: Attachment[];
12
12
  dirtied: string[];
13
13
  };
14
- export type ModelWithAttachment = LucidRow & {
15
- $attachments: AttributeOfModelWithAttachment;
14
+ export type RowWithAttachment = LucidRow & {
15
+ $attachments: AttributeOfRowWithAttachment;
16
16
  };
@@ -0,0 +1,5 @@
1
+ import type { AttachmentVariants } from '@jrmc/adonis-attachment';
2
+ export type RegenerateOptions = {
3
+ attributes?: string[];
4
+ variants?: (keyof AttachmentVariants)[];
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,7 @@
1
1
  import type { Attachment } from './attachment.js';
2
- import type { ModelWithAttachment } from './mixin.js';
2
+ import type { RowWithAttachment } from './mixin.js';
3
3
  export interface Record {
4
- model: ModelWithAttachment;
4
+ row: RowWithAttachment;
5
5
  commit(): Promise<void>;
6
6
  rollback(): Promise<void>;
7
7
  persist(): Promise<void>;
@@ -13,5 +13,6 @@ export interface Record {
13
13
  getAttachments(options: {
14
14
  attributeName: string;
15
15
  requiredOriginal?: boolean;
16
+ requiredDirty?: boolean;
16
17
  }): Attachment[];
17
18
  }
@@ -13,3 +13,4 @@ export declare function streamToTempFile(input: NodeJS.ReadableStream): Promise<
13
13
  export declare function downloadToTempFile(input: URL): Promise<string>;
14
14
  export declare function isBase64(str: string): boolean;
15
15
  export declare function imageToBlurhash(input: Input, options?: BlurhashOptions): Promise<string>;
16
+ export declare function extractPathParameters(path: string): string[];
@@ -110,3 +110,12 @@ export function imageToBlurhash(input, options) {
110
110
  }
111
111
  });
112
112
  }
113
+ export function extractPathParameters(path) {
114
+ const paramRegex = /:(\w+)/g;
115
+ const parameters = [];
116
+ let match;
117
+ while ((match = paramRegex.exec(path)) !== null) {
118
+ parameters.push(match[1]);
119
+ }
120
+ return parameters;
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrmc/adonis-attachment",
3
- "version": "3.3.0-beta.3",
3
+ "version": "3.3.0-beta.5",
4
4
  "type": "module",
5
5
  "description": "Turn any field on your Lucid model to an attachment data type",
6
6
  "engines": {