@jrmc/adonis-attachment 3.2.2 → 3.3.0-beta.2
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 +1 -0
- package/build/index.js +1 -0
- package/build/src/attachment_manager.d.ts +11 -2
- package/build/src/attachment_manager.js +3 -0
- package/build/src/attachments/variant_attachment.d.ts +1 -0
- package/build/src/attachments/variant_attachment.js +3 -0
- package/build/src/converter_manager.d.ts +1 -1
- package/build/src/converter_manager.js +37 -21
- package/build/src/decorators/attachment.d.ts +6 -0
- package/build/src/decorators/attachment.js +74 -53
- package/build/src/mixins/attachmentable.d.ts +5 -5
- package/build/src/mixins/attachmentable.js +1 -2
- package/build/src/services/record_with_attachment.d.ts +28 -0
- package/build/src/services/record_with_attachment.js +223 -0
- package/build/src/types/attachment.d.ts +2 -0
- package/build/src/types/converter.d.ts +10 -2
- package/build/src/types/index.d.ts +1 -0
- package/build/src/types/index.js +1 -0
- package/build/src/types/mixin.d.ts +1 -1
- package/build/src/types/service.d.ts +17 -0
- package/build/src/types/service.js +1 -0
- package/build/src/utils/default_values.d.ts +1 -1
- package/build/src/utils/default_values.js +1 -1
- package/build/src/utils/helpers.d.ts +3 -6
- package/build/src/utils/helpers.js +20 -15
- package/build/src/utils/hooks.js +12 -68
- package/package.json +4 -2
- package/build/src/utils/actions.d.ts +0 -24
- package/build/src/utils/actions.js +0 -90
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
|
|
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 === '') {
|
|
@@ -11,6 +11,7 @@ import { AttachmentBase } from './attachment_base.js';
|
|
|
11
11
|
export declare class Variant extends AttachmentBase implements VariantInterface {
|
|
12
12
|
#private;
|
|
13
13
|
key: string;
|
|
14
|
+
blurhash?: string;
|
|
14
15
|
constructor(drive: DriveService, attributes: VariantAttributes, input?: Input);
|
|
15
16
|
/**
|
|
16
17
|
* Getters
|
|
@@ -8,10 +8,12 @@ import { AttachmentBase } from './attachment_base.js';
|
|
|
8
8
|
export class Variant extends AttachmentBase {
|
|
9
9
|
key;
|
|
10
10
|
#folder;
|
|
11
|
+
blurhash;
|
|
11
12
|
constructor(drive, attributes, input) {
|
|
12
13
|
super(drive, attributes, input);
|
|
13
14
|
this.key = attributes.key;
|
|
14
15
|
this.#folder = attributes.folder;
|
|
16
|
+
this.blurhash = attributes.blurhash;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Getters
|
|
@@ -27,6 +29,7 @@ export class Variant extends AttachmentBase {
|
|
|
27
29
|
key: this.key,
|
|
28
30
|
folder: this.folder,
|
|
29
31
|
name: this.name,
|
|
32
|
+
blurhash: this.blurhash,
|
|
30
33
|
...super.toObject(),
|
|
31
34
|
};
|
|
32
35
|
}
|
|
@@ -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
|
}
|
|
@@ -2,42 +2,58 @@ import string from '@adonisjs/core/helpers/string';
|
|
|
2
2
|
import db from '@adonisjs/lucid/services/db';
|
|
3
3
|
import attachmentManager from '../services/main.js';
|
|
4
4
|
import * as errors from './errors.js';
|
|
5
|
-
import {
|
|
5
|
+
import { encodeImageToBlurhash } from './utils/helpers.js';
|
|
6
6
|
export class ConverterManager {
|
|
7
7
|
#record;
|
|
8
8
|
#attributeName;
|
|
9
|
-
|
|
9
|
+
#options;
|
|
10
|
+
constructor({ record, attributeName, options }) {
|
|
10
11
|
this.#record = record;
|
|
11
12
|
this.#attributeName = attributeName;
|
|
13
|
+
this.#options = options;
|
|
12
14
|
}
|
|
13
15
|
async save() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const Model = this.#record.constructor;
|
|
18
|
-
const id = this.#record.$attributes['id'];
|
|
16
|
+
let attachments = this.#record.getAttachments({
|
|
17
|
+
attributeName: this.#attributeName,
|
|
18
|
+
});
|
|
19
|
+
const Model = this.#record.model.constructor;
|
|
20
|
+
const id = this.#record.model.$attributes['id'];
|
|
19
21
|
const data = {};
|
|
20
|
-
if (options.variants) {
|
|
21
|
-
for (const option of options.variants) {
|
|
22
|
+
if (this.#options.variants) {
|
|
23
|
+
for (const option of this.#options.variants) {
|
|
22
24
|
const converter = (await attachmentManager.getConverter(option));
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
input
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
if (attachments && converter) {
|
|
26
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
27
|
+
const input = attachments[i].input;
|
|
28
|
+
const output = await converter.handle({
|
|
29
|
+
input,
|
|
30
|
+
options: converter.options,
|
|
31
|
+
});
|
|
32
|
+
if (output === undefined) {
|
|
33
|
+
throw new errors.E_CANNOT_PATH_BY_CONVERTER();
|
|
34
|
+
}
|
|
35
|
+
const variant = await attachments[i].createVariant(option, output);
|
|
36
|
+
if (converter.options.blurhash) {
|
|
37
|
+
const options = typeof converter.options.blurhash !== 'boolean' ? converter.options.blurhash : undefined;
|
|
38
|
+
variant.blurhash = await encodeImageToBlurhash(variant.input, options);
|
|
39
|
+
}
|
|
40
|
+
await attachmentManager.save(variant);
|
|
30
41
|
}
|
|
31
|
-
const variant = await attachment.createVariant(option, output);
|
|
32
|
-
await attachmentManager.save(variant);
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
}
|
|
36
|
-
|
|
45
|
+
if (Array.isArray(this.#record.model.$original[this.#attributeName])) {
|
|
46
|
+
data[string.snakeCase(this.#attributeName)] = JSON.stringify(attachments.map((att) => att.toObject()));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
data[string.snakeCase(this.#attributeName)] = JSON.stringify(attachments[0].toObject());
|
|
50
|
+
}
|
|
37
51
|
const trx = await db.transaction();
|
|
38
52
|
trx.after('rollback', () => {
|
|
39
|
-
for (
|
|
40
|
-
|
|
53
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
54
|
+
for (const variant of attachments[i].variants) {
|
|
55
|
+
attachmentManager.delete(variant);
|
|
56
|
+
}
|
|
41
57
|
}
|
|
42
58
|
});
|
|
43
59
|
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
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 =
|
|
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) && dirtyValue.length);
|
|
218
|
+
const isOriginalAttachment = originalValue instanceof Attachment ||
|
|
219
|
+
(Array.isArray(originalValue) && originalValue.every((item) => item instanceof Attachment));
|
|
220
|
+
return isDirtyAttachment || isOriginalAttachment;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -41,6 +41,7 @@ export type Attachment = AttachmentBase & {
|
|
|
41
41
|
export type Variant = AttachmentBase & {
|
|
42
42
|
key: string;
|
|
43
43
|
folder: string;
|
|
44
|
+
blurhash?: string;
|
|
44
45
|
toObject(): VariantAttributes;
|
|
45
46
|
};
|
|
46
47
|
export type LucidOptions = {
|
|
@@ -67,4 +68,5 @@ export type AttachmentAttributes = AttachmentBaseAttributes & {
|
|
|
67
68
|
export type VariantAttributes = AttachmentBaseAttributes & {
|
|
68
69
|
key: string;
|
|
69
70
|
folder: string;
|
|
71
|
+
blurhash?: string;
|
|
70
72
|
};
|
|
@@ -4,22 +4,29 @@
|
|
|
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 {
|
|
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:
|
|
17
|
+
record: Record;
|
|
17
18
|
attributeName: string;
|
|
19
|
+
options: LucidOptions;
|
|
18
20
|
};
|
|
19
21
|
export type ConverterAttributes = {
|
|
20
22
|
input: Input;
|
|
21
23
|
options: ConverterOptions;
|
|
22
24
|
};
|
|
25
|
+
export type BlurhashOptions = {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
componentX: number;
|
|
28
|
+
componentY: number;
|
|
29
|
+
};
|
|
23
30
|
type jpeg = {
|
|
24
31
|
format: 'jpeg';
|
|
25
32
|
options: {
|
|
@@ -125,5 +132,6 @@ export type ConverterOptions = {
|
|
|
125
132
|
fastShrinkOnLoad?: Boolean;
|
|
126
133
|
};
|
|
127
134
|
format?: 'jpeg' | 'jpg' | 'png' | 'gif' | 'webp' | 'avif' | 'heif' | 'tiff' | 'raw' | jpeg | png | gif | webp | avif | heif;
|
|
135
|
+
blurhash?: boolean | BlurhashOptions;
|
|
128
136
|
};
|
|
129
137
|
export {};
|
package/build/src/types/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import type { Attachment } from './attachment.js';
|
|
|
9
9
|
export type AttributeOfModelWithAttachment = {
|
|
10
10
|
attached: Attachment[];
|
|
11
11
|
detached: Attachment[];
|
|
12
|
-
|
|
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 {};
|
|
@@ -4,15 +4,12 @@
|
|
|
4
4
|
* @license MIT
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
8
|
-
import type {
|
|
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;
|
|
7
|
+
import type { Input } from '../types/input.js';
|
|
8
|
+
import type { BlurhashOptions } from '../types/converter.js';
|
|
12
9
|
export declare function cleanObject(obj: any): any;
|
|
13
|
-
export declare function clone(object: Object): any;
|
|
14
10
|
export declare function use(module: string): Promise<any>;
|
|
15
11
|
export declare function bufferToTempFile(input: Buffer): Promise<string>;
|
|
16
12
|
export declare function streamToTempFile(input: NodeJS.ReadableStream): Promise<string>;
|
|
17
13
|
export declare function downloadToTempFile(input: URL): Promise<string>;
|
|
18
14
|
export declare function isBase64(str: string): boolean;
|
|
15
|
+
export declare function encodeImageToBlurhash(input: Input, options?: BlurhashOptions): Promise<string>;
|
|
@@ -11,20 +11,9 @@ 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 {
|
|
14
|
+
import { encode } from 'blurhash';
|
|
15
15
|
import * as errors from '../errors.js';
|
|
16
|
-
import { optionsSym } from './symbols.js';
|
|
17
16
|
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
17
|
export function cleanObject(obj) {
|
|
29
18
|
if (obj === null || typeof obj !== 'object') {
|
|
30
19
|
return obj;
|
|
@@ -43,9 +32,6 @@ export function cleanObject(obj) {
|
|
|
43
32
|
}
|
|
44
33
|
return cleanedObj;
|
|
45
34
|
}
|
|
46
|
-
export function clone(object) {
|
|
47
|
-
return JSON.parse(JSON.stringify(object));
|
|
48
|
-
}
|
|
49
35
|
export async function use(module) {
|
|
50
36
|
try {
|
|
51
37
|
const result = await import(module);
|
|
@@ -106,3 +92,22 @@ export function isBase64(str) {
|
|
|
106
92
|
return false;
|
|
107
93
|
}
|
|
108
94
|
}
|
|
95
|
+
export function encodeImageToBlurhash(input, options) {
|
|
96
|
+
const { componentX, componentY } = options || { componentX: 4, componentY: 4 };
|
|
97
|
+
return new Promise(async (resolve, reject) => {
|
|
98
|
+
try {
|
|
99
|
+
const sharp = await use('sharp');
|
|
100
|
+
// Convert input to pixels
|
|
101
|
+
const { data: pixels, info: metadata } = await sharp(input)
|
|
102
|
+
.raw()
|
|
103
|
+
.ensureAlpha()
|
|
104
|
+
.toBuffer({ resolveWithObject: true });
|
|
105
|
+
return resolve(encode(new Uint8ClampedArray(pixels), metadata.width, metadata.height, componentX, componentY));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.log('--------------');
|
|
109
|
+
console.log(error);
|
|
110
|
+
return reject(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
package/build/src/utils/hooks.js
CHANGED
|
@@ -4,16 +4,12 @@
|
|
|
4
4
|
* @license MIT
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
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
|
|
14
|
-
await
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
3
|
+
"version": "3.3.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Turn any field on your Lucid model to an attachment data type",
|
|
6
6
|
"engines": {
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@poppinss/defer": "^1.1.1",
|
|
68
|
+
"blurhash": "^2.0.5",
|
|
68
69
|
"exifreader": "^4.26.0",
|
|
69
70
|
"file-type": "^19.6.0",
|
|
70
71
|
"mime-types": "^2.1.35"
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
"flydrive": "^1.1.0",
|
|
95
96
|
"luxon": "^3.5.0",
|
|
96
97
|
"prettier": "^3.4.2",
|
|
98
|
+
"sharp": "^0.33.5",
|
|
97
99
|
"ts-node": "^10.9.2",
|
|
98
100
|
"typescript": "^5.7.3",
|
|
99
101
|
"vitepress": "^1.5.0"
|
|
@@ -115,7 +117,7 @@
|
|
|
115
117
|
},
|
|
116
118
|
"publishConfig": {
|
|
117
119
|
"access": "public",
|
|
118
|
-
"tag": "
|
|
120
|
+
"tag": "beta"
|
|
119
121
|
},
|
|
120
122
|
"volta": {
|
|
121
123
|
"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
|
-
}
|