Skip to content

Commit bc7b0e4

Browse files
committed
@attachments() decorator
1 parent a9ee81d commit bc7b0e4

File tree

6 files changed

+141
-57
lines changed

6 files changed

+141
-57
lines changed

index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import attachmentManager from './services/main.js'
33
export { configure } from './configure.js'
44
export { Attachment } from './src/attachments/attachment.js'
55
export { attachment } from './src/decorators/attachment.js'
6+
export { attachments } from './src/decorators/attachment.js'
67
export { defineConfig } from './src/define_config.js'
78
export { Attachmentable } from './src/mixins/attachmentable.js'
89
export * as errors from './src/errors.js'

src/decorators/attachment.ts

+71-51
Original file line numberDiff line numberDiff line change
@@ -24,72 +24,92 @@ import {
2424
import { clone } from '../utils/helpers.js'
2525
import { defaultStateAttributeMixin } from '../utils/default_values.js'
2626

27-
export const attachment = (options?: LucidOptions) => {
28-
return function (target: any, attributeName: string) {
29-
if (!target[optionsSym]) {
30-
target[optionsSym] = {}
31-
}
32-
33-
target[optionsSym][attributeName] = options
34-
35-
const Model = target.constructor as LucidModel & {
36-
$attachments: AttributeOfModelWithAttachment
37-
}
38-
Model.boot()
27+
export const bootModel = (model: LucidModel & {
28+
$attachments: AttributeOfModelWithAttachment
29+
}, options?: LucidOptions) => {
30+
model.boot()
3931

40-
Model.$attachments = clone(defaultStateAttributeMixin)
32+
model.$attachments = clone(defaultStateAttributeMixin)
4133

4234
/**
4335
* Registering all hooks only once
4436
*/
45-
46-
if (!Model.$hooks.has('find', afterFindHook)) {
47-
Model.after('find', afterFindHook)
37+
if (!model.$hooks.has('find', afterFindHook)) {
38+
model.after('find', afterFindHook)
4839
}
49-
if (!Model.$hooks.has('fetch', afterFetchHook)) {
50-
Model.after('fetch', afterFetchHook)
40+
if (!model.$hooks.has('fetch', afterFetchHook)) {
41+
model.after('fetch', afterFetchHook)
5142
}
52-
if (!Model.$hooks.has('paginate', afterFetchHook)) {
53-
Model.after('paginate', afterFetchHook)
43+
if (!model.$hooks.has('paginate', afterFetchHook)) {
44+
model.after('paginate', afterFetchHook)
5445
}
55-
if (!Model.$hooks.has('save', beforeSaveHook)) {
56-
Model.before('save', beforeSaveHook)
46+
if (!model.$hooks.has('save', beforeSaveHook)) {
47+
model.before('save', beforeSaveHook)
5748
}
58-
if (!Model.$hooks.has('save', afterSaveHook)) {
59-
Model.after('save', afterSaveHook)
49+
if (!model.$hooks.has('save', afterSaveHook)) {
50+
model.after('save', afterSaveHook)
6051
}
61-
if (!Model.$hooks.has('delete', beforeDeleteHook)) {
62-
Model.before('delete', beforeDeleteHook)
52+
if (!model.$hooks.has('delete', beforeDeleteHook)) {
53+
model.before('delete', beforeDeleteHook)
6354
}
55+
}
6456

57+
const makeColumnOptions = (options?: LucidOptions) => {
6558
const { disk, folder, variants, meta, rename, ...columnOptions } = {
66-
...defaultOptionsDecorator,
67-
...options,
68-
}
69-
70-
Model.$addColumn(attributeName, {
71-
consume: (value) => {
72-
if (value) {
73-
const attachment = attachmentManager.createFromDbResponse(value)
74-
attachment?.setOptions({ disk, folder, variants })
59+
...defaultOptionsDecorator,
60+
...options,
61+
}
62+
return ({
63+
consume: (value: any) => {
64+
if (value) {
65+
const attachment = attachmentManager.createFromDbResponse(value)
66+
attachment?.setOptions({ disk, folder, variants })
7567

76-
if (options && options?.meta !== undefined) {
77-
attachment?.setOptions({ meta: options!.meta })
78-
}
79-
if (options && options?.rename !== undefined) {
80-
attachment?.setOptions({ rename: options!.rename })
68+
if (options && options?.meta !== undefined) {
69+
attachment?.setOptions({ meta: options!.meta })
70+
}
71+
if (options && options?.rename !== undefined) {
72+
attachment?.setOptions({ rename: options!.rename })
73+
}
74+
if (options && options?.preComputeUrl !== undefined) {
75+
attachment?.setOptions({ preComputeUrl: options!.preComputeUrl })
76+
}
77+
return attachment
78+
} else {
79+
return null
8180
}
82-
if (options && options?.preComputeUrl !== undefined) {
83-
attachment?.setOptions({ preComputeUrl: options!.preComputeUrl })
84-
}
85-
return attachment
86-
} else {
87-
return null
88-
}
8981
},
90-
prepare: (value) => (value ? JSON.stringify(value.toObject()) : null),
91-
serialize: (value) => (value ? value.toJSON() : null),
82+
prepare: (value: any) => (value ? JSON.stringify(value.toObject()) : null),
83+
serialize: (value: any) => (value ? value.toJSON() : null),
9284
...columnOptions,
93-
})
94-
}
85+
})
9586
}
87+
88+
89+
const makeAttachmentDecorator = (columnOptionsTransformer?: (columnOptions: any) => any) =>
90+
(options?: LucidOptions) => {
91+
return function (target: any, attributeName: string) {
92+
if (!target[optionsSym]) {
93+
target[optionsSym] = {}
94+
}
95+
96+
target[optionsSym][attributeName] = options
97+
98+
const Model = target.constructor as LucidModel & {
99+
$attachments: AttributeOfModelWithAttachment
100+
}
101+
102+
bootModel(Model, options)
103+
104+
const columnOptions = makeColumnOptions(options)
105+
const transformedColumnOptions = columnOptionsTransformer ? columnOptionsTransformer(columnOptions): columnOptions
106+
Model.$addColumn(attributeName, transformedColumnOptions)
107+
}
108+
}
109+
110+
111+
export const attachment = makeAttachmentDecorator()
112+
export const attachments = makeAttachmentDecorator((columnOptions) => ({
113+
consume: (value: any[]) => value.map(columnOptions.consume),
114+
prepare: (value: any[]) => (value ? JSON.stringify(value.map(v => v.toObject())) : null),
115+
}))

tests/attachments.spec.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @jrmc/adonis-attachment
3+
*
4+
* @license MIT
5+
* @copyright Jeremy Chaufourier <[email protected]>
6+
*/
7+
import { readFile } from 'node:fs/promises'
8+
9+
import app from '@adonisjs/core/services/app'
10+
import '@japa/assert'
11+
import { test } from '@japa/runner'
12+
import drive from '@adonisjs/drive/services/main'
13+
14+
import { UserFactory } from './fixtures/factories/user.js'
15+
16+
test.group('attachments', () => {
17+
test('create', async ({ assert }) => {
18+
const user = await UserFactory.create()
19+
20+
assert.isNotNull(user.weekendPics)
21+
assert.equal(user.weekendPics?.length, 2)
22+
for (const pic of user.weekendPics ?? []) {
23+
assert.equal(pic.originalName, 'avatar.jpg')
24+
assert.match(pic.name, /(.*).jpg$/)
25+
assert.equal(pic.mimeType, 'image/jpeg')
26+
assert.equal(pic.extname, 'jpg')
27+
}
28+
})
29+
30+
test('delete files after removing', async ({ assert, cleanup }) => {
31+
const fakeDisk = drive.fake('fs')
32+
cleanup(() => drive.restore('fs'))
33+
34+
const user = await UserFactory.create()
35+
const paths = user.weekendPics?.map(p => p.path)
36+
user.weekendPics = []
37+
await user.save()
38+
39+
for (const path of paths ?? [])
40+
fakeDisk.assertMissing(path!)
41+
})
42+
43+
test('delete files after remove entity', async ({ assert, cleanup }) => {
44+
const fakeDisk = drive.fake('fs')
45+
cleanup(() => drive.restore('fs'))
46+
47+
const user = await UserFactory.create()
48+
const paths = user.weekendPics?.map(p => p.path)
49+
await user.delete()
50+
51+
for (const path of paths ?? [])
52+
fakeDisk.assertMissing(path!)
53+
})
54+
})

tests/fixtures/factories/user.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ import Factory from '@adonisjs/lucid/factories'
1111
import app from '@adonisjs/core/services/app'
1212

1313
export const UserFactory = Factory.define(User, async ({ faker }) => {
14-
const attachmentManager = await app.container.make('jrmc.attachment')
15-
16-
const buffer = await readFile(app.makePath('../fixtures/images/img.jpg'))
17-
const avatar = await attachmentManager.createFromBuffer(buffer, 'avatar.jpg')
14+
const makeAttachment = async () => {
15+
const attachmentManager = await app.container.make('jrmc.attachment')
16+
const buffer = await readFile(app.makePath('../fixtures/images/img.jpg'))
17+
return await attachmentManager.createFromBuffer(buffer, 'avatar.jpg')
18+
}
1819

1920
return {
2021
name: faker.person.lastName(),
21-
avatar,
22+
avatar: await makeAttachment(),
23+
weekendPics: [
24+
await makeAttachment(),
25+
await makeAttachment()
26+
]
2227
}
2328
}).build()

tests/fixtures/migrations/create_users_table.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default class extends BaseSchema {
1313
table.string('name')
1414
table.json('avatar')
1515
table.json('avatar_2')
16+
table.json('weekend_pics')
1617
table.timestamp('created_at')
1718
table.timestamp('updated_at')
1819
})

tests/fixtures/models/user.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { compose } from '@adonisjs/core/helpers'
99
import { BaseModel, column } from '@adonisjs/lucid/orm'
10-
import { attachment } from '../../../index.js'
10+
import { attachment, attachments } from '../../../index.js'
1111
import type { Attachment } from '../../../src/types/attachment.js'
1212
import { DateTime } from 'luxon'
1313

@@ -24,6 +24,9 @@ export default class User extends BaseModel {
2424
@attachment({ disk: 's3', folder: 'avatar', preComputeUrl: true, meta: false, rename: false })
2525
declare avatar2: Attachment | null
2626

27+
@attachments({ preComputeUrl: true })
28+
declare weekendPics: Attachment[] | null
29+
2730
@column.dateTime({ autoCreate: true, serialize: (value: DateTime) => value.toUnixInteger() })
2831
declare createdAt: DateTime
2932

0 commit comments

Comments
 (0)