@jrmc/adonis-attachment 3.0.0 → 3.2.0

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.
@@ -10,101 +10,23 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
10
10
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
11
11
  return c > 3 && r && Object.defineProperty(target, key, r), r;
12
12
  };
13
- import { beforeSave, afterSave, beforeDelete, afterFind, afterFetch, afterPaginate, } from '@adonisjs/lucid/orm';
14
- import { persistAttachment, commit, rollback, generateVariants, preComputeUrl, } from '../utils/actions.js';
15
- import { clone, getAttachmentAttributeNames, getDirtyAttachmentAttributeNames, } from '../utils/helpers.js';
13
+ import { beforeSave, beforeFind, beforeFetch, beforePaginate, beforeCreate, } from '@adonisjs/lucid/orm';
14
+ import { clone } from '../utils/helpers.js';
16
15
  import { defaultStateAttributeMixin } from '../utils/default_values.js';
17
- export const Attachmentable = (superclass) => {
16
+ import logger from '@adonisjs/core/services/logger';
17
+ export function Attachmentable(superclass) {
18
18
  class ModelWithAttachment extends superclass {
19
19
  $attachments = clone(defaultStateAttributeMixin);
20
- static async afterFindHook(modelInstance) {
21
- const attachmentAttributeNames = getAttachmentAttributeNames(modelInstance);
22
- await Promise.all(attachmentAttributeNames.map((attributeName) => {
23
- return preComputeUrl(modelInstance, attributeName);
24
- }));
25
- }
26
- static async afterFetchHook(modelInstances) {
27
- await Promise.all(modelInstances.map((row) => this.afterFindHook(row)));
28
- }
29
- static async beforeSaveHook(modelInstance) {
30
- const attachmentAttributeNames = getDirtyAttachmentAttributeNames(modelInstance);
31
- /**
32
- * Empty previous $attachments
33
- */
34
- modelInstance.$attachments = clone(defaultStateAttributeMixin);
35
- /**
36
- * Set attributes Attachment type modified
37
- */
38
- attachmentAttributeNames.forEach((attributeName) => modelInstance.$attachments.attributesModified.push(attributeName));
39
- /**
40
- * Persist attachments before saving the model to the database. This
41
- * way if file saving fails we will not write anything to the
42
- * database
43
- */
44
- await Promise.all(attachmentAttributeNames.map((attributeName) => persistAttachment(modelInstance, attributeName)));
45
- try {
46
- if (modelInstance.$trx) {
47
- modelInstance.$trx.after('commit', () => commit(modelInstance));
48
- modelInstance.$trx.after('rollback', () => rollback(modelInstance));
49
- }
50
- else {
51
- await commit(modelInstance);
52
- }
53
- }
54
- catch (error) {
55
- await rollback(modelInstance);
56
- throw error;
57
- }
58
- }
59
- static async afterSaveHook(modelInstance) {
60
- const attachmentAttributeNames = getAttachmentAttributeNames(modelInstance);
61
- /**
62
- * For all properties Attachment
63
- * Launch async generation variants
64
- */
65
- await Promise.all(attachmentAttributeNames.map((attributeName) => {
66
- if (modelInstance.$attachments.attributesModified.includes(attributeName)) {
67
- return generateVariants(modelInstance, attributeName);
68
- }
69
- }));
70
- }
71
- static async beforeDeleteHook(modelInstance) {
72
- const attachmentAttributeNames = getAttachmentAttributeNames(modelInstance);
73
- /**
74
- * Mark all attachments for deletion
75
- */
76
- attachmentAttributeNames.map((attributeName) => {
77
- if (modelInstance.$attributes[attributeName]) {
78
- modelInstance.$attachments.detached.push(modelInstance.$attributes[attributeName]);
79
- }
80
- });
81
- /**
82
- * If model is using transaction, then wait for the transaction
83
- * to settle
84
- */
85
- if (modelInstance.$trx) {
86
- modelInstance.$trx.after('commit', () => commit(modelInstance));
87
- }
88
- else {
89
- await commit(modelInstance);
90
- }
20
+ static async warn() {
21
+ logger.warn(`The "Attachmentable" mixin is deprecated and may be removed in a future version.`);
91
22
  }
92
23
  }
93
24
  __decorate([
94
- afterFind()
95
- ], ModelWithAttachment, "afterFindHook", null);
96
- __decorate([
97
- afterFetch(),
98
- afterPaginate()
99
- ], ModelWithAttachment, "afterFetchHook", null);
100
- __decorate([
25
+ beforeCreate(),
26
+ beforeFind(),
27
+ beforeFetch(),
28
+ beforePaginate(),
101
29
  beforeSave()
102
- ], ModelWithAttachment, "beforeSaveHook", null);
103
- __decorate([
104
- afterSave()
105
- ], ModelWithAttachment, "afterSaveHook", null);
106
- __decorate([
107
- beforeDelete()
108
- ], ModelWithAttachment, "beforeDeleteHook", null);
30
+ ], ModelWithAttachment, "warn", null);
109
31
  return ModelWithAttachment;
110
- };
32
+ }
@@ -86,6 +86,27 @@ type webp = {
86
86
  force?: Boolean;
87
87
  };
88
88
  };
89
+ type avif = {
90
+ format: 'avif';
91
+ options: {
92
+ quality?: number;
93
+ lossless?: Boolean;
94
+ effort?: number;
95
+ chromaSubsampling?: string;
96
+ bitdepth?: number;
97
+ };
98
+ };
99
+ type heif = {
100
+ format: 'heif';
101
+ options: {
102
+ compression?: string;
103
+ quality?: number;
104
+ lossless?: Boolean;
105
+ effort?: number;
106
+ chromaSubsampling?: string;
107
+ bitdepth?: number;
108
+ };
109
+ };
89
110
  export type ConverterOptions = {
90
111
  resize?: number | {
91
112
  width?: number;
@@ -103,6 +124,6 @@ export type ConverterOptions = {
103
124
  withoutReduction?: Boolean;
104
125
  fastShrinkOnLoad?: Boolean;
105
126
  };
106
- format?: string | jpeg | png | gif | webp;
127
+ format?: 'jpeg' | 'jpg' | 'png' | 'gif' | 'webp' | 'avif' | 'heif' | 'tiff' | 'raw' | jpeg | png | gif | webp | avif | heif;
107
128
  };
108
129
  export {};
@@ -21,4 +21,9 @@ export type Exif = {
21
21
  height: number;
22
22
  };
23
23
  };
24
+ export type Meta = {
25
+ extname: string;
26
+ mimeType: string;
27
+ size: number;
28
+ };
24
29
  export type Input = Buffer | string;
@@ -5,19 +5,14 @@
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
7
  import type { LucidOptions } from '../types/attachment.js';
8
- import type { Input } from '../types/input.js';
9
8
  import type { ModelWithAttachment } from '../types/mixin.js';
10
9
  export declare function getAttachmentAttributeNames(modelInstance: ModelWithAttachment): string[];
11
10
  export declare function getDirtyAttachmentAttributeNames(modelInstance: ModelWithAttachment): string[];
12
11
  export declare function getOptions(modelInstance: ModelWithAttachment, attributeName: string): LucidOptions;
13
- export declare function createAttachmentAttributes(input: Input, name?: string): Promise<{
14
- originalName: string;
15
- extname: import("file-type").FileExtension;
16
- mimeType: import("file-type").MimeType;
17
- size: number;
18
- }>;
19
12
  export declare function cleanObject(obj: any): any;
20
13
  export declare function clone(object: Object): any;
21
14
  export declare function use(module: string): Promise<any>;
22
15
  export declare function bufferToTempFile(input: Buffer): Promise<string>;
16
+ export declare function streamToTempFile(input: NodeJS.ReadableStream): Promise<string>;
17
+ export declare function downloadToTempFile(input: URL): Promise<string>;
23
18
  export declare function isBase64(str: string): boolean;
@@ -6,13 +6,15 @@
6
6
  */
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
- import fs from 'fs/promises';
10
- import { cuid } from '@adonisjs/core/helpers';
11
- import string from '@adonisjs/core/helpers/string';
12
- import { fileTypeFromBuffer, fileTypeFromFile } from 'file-type';
9
+ import https from 'node:https';
10
+ import fs from 'node:fs/promises';
11
+ import { pipeline } from 'node:stream';
12
+ import { promisify } from 'node:util';
13
+ import { createWriteStream } from 'node:fs';
13
14
  import { Attachment } from '../attachments/attachment.js';
14
15
  import * as errors from '../errors.js';
15
16
  import { optionsSym } from './symbols.js';
17
+ const streamPipeline = promisify(pipeline);
16
18
  export function getAttachmentAttributeNames(modelInstance) {
17
19
  return Object.keys(modelInstance.$attributes).filter((attr) => modelInstance.$attributes[attr] instanceof Attachment);
18
20
  }
@@ -23,27 +25,6 @@ export function getDirtyAttachmentAttributeNames(modelInstance) {
23
25
  export function getOptions(modelInstance, attributeName) {
24
26
  return modelInstance.constructor.prototype[optionsSym]?.[attributeName];
25
27
  }
26
- export async function createAttachmentAttributes(input, name) {
27
- let fileType;
28
- if (Buffer.isBuffer(input)) {
29
- fileType = await fileTypeFromBuffer(input);
30
- }
31
- else {
32
- fileType = await fileTypeFromFile(input);
33
- }
34
- if (name) {
35
- name = string.slug(name);
36
- }
37
- else {
38
- name = `${cuid()}.${fileType.ext}`;
39
- }
40
- return {
41
- originalName: name,
42
- extname: fileType.ext,
43
- mimeType: fileType.mime,
44
- size: input.length,
45
- };
46
- }
47
28
  export function cleanObject(obj) {
48
29
  if (obj === null || typeof obj !== 'object') {
49
30
  return obj;
@@ -83,6 +64,37 @@ export async function bufferToTempFile(input) {
83
64
  await fs.writeFile(tempFilePath, input);
84
65
  return tempFilePath;
85
66
  }
67
+ // TODO
68
+ // gestion des erreurs
69
+ export async function streamToTempFile(input) {
70
+ const folder = os.tmpdir();
71
+ const tempFilePath = path.join(folder, `tempfile-${Date.now()}.tmp`);
72
+ const writeStream = createWriteStream(tempFilePath);
73
+ try {
74
+ await streamPipeline(input, writeStream);
75
+ return tempFilePath;
76
+ }
77
+ catch (err) {
78
+ throw new errors.E_CANNOT_GENERATE_TEMP_FILE([err.message]);
79
+ }
80
+ }
81
+ export async function downloadToTempFile(input) {
82
+ return await new Promise((resolve) => {
83
+ https
84
+ .get(input, (response) => {
85
+ if (response.statusCode === 200) {
86
+ resolve(streamToTempFile(response));
87
+ }
88
+ else {
89
+ // reject(`${response.statusCode}`)
90
+ throw new errors.E_CANNOT_GENERATE_TEMP_FILE(['']);
91
+ }
92
+ })
93
+ .on('error', (err) => {
94
+ throw new errors.E_CANNOT_GENERATE_TEMP_FILE([err.message]);
95
+ });
96
+ });
97
+ }
86
98
  export function isBase64(str) {
87
99
  const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
88
100
  if (!base64Regex.test(str)) {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @jrmc/adonis-attachment
3
+ *
4
+ * @license MIT
5
+ * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
+ */
7
+ export declare const afterFindHook: (instance: unknown) => Promise<void>;
8
+ export declare const afterFetchHook: (instance: unknown) => Promise<void>;
9
+ export declare const beforeSaveHook: (instance: unknown) => Promise<void>;
10
+ export declare const afterSaveHook: (instance: unknown) => Promise<void>;
11
+ export declare const beforeDeleteHook: (instance: unknown) => Promise<void>;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @jrmc/adonis-attachment
3
+ *
4
+ * @license MIT
5
+ * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
+ */
7
+ import { persistAttachment, commit, rollback, generateVariants, preComputeUrl, } from '../utils/actions.js';
8
+ import { clone, getAttachmentAttributeNames, getDirtyAttachmentAttributeNames, } from '../utils/helpers.js';
9
+ import { defaultStateAttributeMixin } from '../utils/default_values.js';
10
+ // @afterFind()
11
+ export const afterFindHook = async (instance) => {
12
+ const modelInstance = instance;
13
+ const attachmentAttributeNames = getAttachmentAttributeNames(modelInstance);
14
+ await Promise.all(attachmentAttributeNames.map((attributeName) => {
15
+ return preComputeUrl(modelInstance, attributeName);
16
+ }));
17
+ };
18
+ // @afterFetch()
19
+ // @afterPaginate()
20
+ export const afterFetchHook = async (instance) => {
21
+ const modelInstances = instance;
22
+ await Promise.all(modelInstances.map((row) => afterFindHook(row)));
23
+ };
24
+ // @beforeSave()
25
+ export const beforeSaveHook = async (instance) => {
26
+ const modelInstance = instance;
27
+ const attachmentAttributeNames = getDirtyAttachmentAttributeNames(modelInstance);
28
+ /**
29
+ * Empty previous $attachments
30
+ */
31
+ modelInstance.$attachments = clone(defaultStateAttributeMixin);
32
+ /**
33
+ * Set attributes Attachment type modified
34
+ */
35
+ attachmentAttributeNames.forEach((attributeName) => modelInstance.$attachments.attributesModified.push(attributeName));
36
+ /**
37
+ * Persist attachments before saving the model to the database. This
38
+ * way if file saving fails we will not write anything to the
39
+ * database
40
+ */
41
+ await Promise.all(attachmentAttributeNames.map((attributeName) => persistAttachment(modelInstance, attributeName)));
42
+ try {
43
+ if (modelInstance.$trx) {
44
+ modelInstance.$trx.after('commit', () => commit(modelInstance));
45
+ modelInstance.$trx.after('rollback', () => rollback(modelInstance));
46
+ }
47
+ else {
48
+ await commit(modelInstance);
49
+ }
50
+ }
51
+ catch (error) {
52
+ await rollback(modelInstance);
53
+ throw error;
54
+ }
55
+ };
56
+ // @afterSave()
57
+ export const afterSaveHook = async (instance) => {
58
+ const modelInstance = instance;
59
+ const attachmentAttributeNames = getAttachmentAttributeNames(modelInstance);
60
+ /**
61
+ * For all properties Attachment
62
+ * Launch async generation variants
63
+ */
64
+ await Promise.all(attachmentAttributeNames.map((attributeName) => {
65
+ if (modelInstance.$attachments.attributesModified.includes(attributeName)) {
66
+ return generateVariants(modelInstance, attributeName);
67
+ }
68
+ }));
69
+ };
70
+ // @beforeDelete()
71
+ export const beforeDeleteHook = async (instance) => {
72
+ const modelInstance = instance;
73
+ const attachmentAttributeNames = getAttachmentAttributeNames(modelInstance);
74
+ /**
75
+ * Mark all attachments for deletion
76
+ */
77
+ attachmentAttributeNames.map((attributeName) => {
78
+ if (modelInstance.$attributes[attributeName]) {
79
+ modelInstance.$attachments.detached.push(modelInstance.$attributes[attributeName]);
80
+ }
81
+ });
82
+ /**
83
+ * If model is using transaction, then wait for the transaction
84
+ * to settle
85
+ */
86
+ if (modelInstance.$trx) {
87
+ modelInstance.$trx.after('commit', () => commit(modelInstance));
88
+ }
89
+ else {
90
+ await commit(modelInstance);
91
+ }
92
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrmc/adonis-attachment",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "description": "Turn any field on your Lucid model to an attachment data type",
6
6
  "engines": {
@@ -46,7 +46,9 @@
46
46
  "format": "prettier --write .",
47
47
  "docs:dev": "vitepress dev docs",
48
48
  "docs:build": "vitepress build docs",
49
- "docs:preview": "vitepress preview docs"
49
+ "docs:preview": "vitepress preview docs",
50
+ "test": "c8 npm run quick:test",
51
+ "quick:test": "node --import=./tsnode.esm.js --enable-source-maps bin/test.ts"
50
52
  },
51
53
  "exports": {
52
54
  ".": "./build/index.js",
@@ -62,23 +64,39 @@
62
64
  "./attachment_provider": "./build/providers/attachment_provider.js"
63
65
  },
64
66
  "dependencies": {
65
- "@poppinss/defer": "^1.1.0",
66
- "exifreader": "^4.23.3",
67
- "file-type": "^19.4.0"
67
+ "@poppinss/defer": "^1.1.1",
68
+ "exifreader": "^4.26.0",
69
+ "file-type": "^19.6.0",
70
+ "mime-types": "^2.1.35"
68
71
  },
69
72
  "devDependencies": {
70
- "@adonisjs/assembler": "^7.7.0",
71
- "@adonisjs/core": "^6.12.1",
73
+ "@adonisjs/assembler": "^7.8.2",
74
+ "@adonisjs/core": "^6.17.1",
72
75
  "@adonisjs/drive": "^3.2.0",
73
- "@adonisjs/lucid": "^20.6.0",
74
- "@adonisjs/prettier-config": "^1.3.0",
75
- "@adonisjs/tsconfig": "^1.3.0",
76
- "@types/node": "^22.2.0",
76
+ "@adonisjs/lucid": "^21.6.0",
77
+ "@adonisjs/prettier-config": "^1.4.0",
78
+ "@adonisjs/tsconfig": "^1.4.0",
79
+ "@japa/assert": "^4.0.1",
80
+ "@japa/expect-type": "^2.0.3",
81
+ "@japa/file-system": "^2.3.2",
82
+ "@japa/plugin-adonisjs": "^4.0.0",
83
+ "@japa/runner": "^4.1.0",
84
+ "@poppinss/utils": "^6.9.2",
85
+ "@swc/core": "^1.10.7",
86
+ "@types/luxon": "^3.4.2",
87
+ "@types/mime-types": "^2.1.4",
88
+ "@types/node": "^22.10.7",
89
+ "@types/sinon": "^17.0.3",
90
+ "better-sqlite3": "^11.8.0",
91
+ "c8": "^10.1.3",
77
92
  "copyfiles": "^2.4.1",
78
- "del-cli": "^5.1.0",
79
- "prettier": "^3.3.3",
80
- "typescript": "^5.5.4",
81
- "vitepress": "^1.3.3"
93
+ "del-cli": "^6.0.0",
94
+ "flydrive": "^1.1.0",
95
+ "luxon": "^3.5.0",
96
+ "prettier": "^3.4.2",
97
+ "ts-node": "^10.9.2",
98
+ "typescript": "^5.7.3",
99
+ "vitepress": "^1.5.0"
82
100
  },
83
101
  "peerDependencies": {
84
102
  "@adonisjs/core": "^6.12.1",
@@ -86,6 +104,15 @@
86
104
  "@adonisjs/lucid": "^20.6.0 || ^21.0.0"
87
105
  },
88
106
  "prettier": "@adonisjs/prettier-config",
107
+ "c8": {
108
+ "reporter": [
109
+ "text",
110
+ "html"
111
+ ],
112
+ "exclude": [
113
+ "tests/**"
114
+ ]
115
+ },
89
116
  "publishConfig": {
90
117
  "access": "public",
91
118
  "tag": "latest"