Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@attachments() decorator #13

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import attachmentManager from './services/main.js'
export { configure } from './configure.js'
export { Attachment } from './src/attachments/attachment.js'
export { attachment } from './src/decorators/attachment.js'
export { attachments } from './src/decorators/attachment.js'
export { defineConfig } from './src/define_config.js'
export { Attachmentable } from './src/mixins/attachmentable.js'
export * as errors from './src/errors.js'
Expand Down
122 changes: 71 additions & 51 deletions src/decorators/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,72 +24,92 @@ import {
import { clone } from '../utils/helpers.js'
import { defaultStateAttributeMixin } from '../utils/default_values.js'

export const attachment = (options?: LucidOptions) => {
return function (target: any, attributeName: string) {
if (!target[optionsSym]) {
target[optionsSym] = {}
}

target[optionsSym][attributeName] = options

const Model = target.constructor as LucidModel & {
$attachments: AttributeOfModelWithAttachment
}
Model.boot()
export const bootModel = (model: LucidModel & {
$attachments: AttributeOfModelWithAttachment
}, options?: LucidOptions) => {
model.boot()

Model.$attachments = clone(defaultStateAttributeMixin)
model.$attachments = clone(defaultStateAttributeMixin)

/**
* Registering all hooks only once
*/

if (!Model.$hooks.has('find', afterFindHook)) {
Model.after('find', afterFindHook)
if (!model.$hooks.has('find', afterFindHook)) {
model.after('find', afterFindHook)
}
if (!Model.$hooks.has('fetch', afterFetchHook)) {
Model.after('fetch', afterFetchHook)
if (!model.$hooks.has('fetch', afterFetchHook)) {
model.after('fetch', afterFetchHook)
}
if (!Model.$hooks.has('paginate', afterFetchHook)) {
Model.after('paginate', afterFetchHook)
if (!model.$hooks.has('paginate', afterFetchHook)) {
model.after('paginate', afterFetchHook)
}
if (!Model.$hooks.has('save', beforeSaveHook)) {
Model.before('save', beforeSaveHook)
if (!model.$hooks.has('save', beforeSaveHook)) {
model.before('save', beforeSaveHook)
}
if (!Model.$hooks.has('save', afterSaveHook)) {
Model.after('save', afterSaveHook)
if (!model.$hooks.has('save', afterSaveHook)) {
model.after('save', afterSaveHook)
}
if (!Model.$hooks.has('delete', beforeDeleteHook)) {
Model.before('delete', beforeDeleteHook)
if (!model.$hooks.has('delete', beforeDeleteHook)) {
model.before('delete', beforeDeleteHook)
}
}

const makeColumnOptions = (options?: LucidOptions) => {
const { disk, folder, variants, meta, rename, ...columnOptions } = {
...defaultOptionsDecorator,
...options,
}

Model.$addColumn(attributeName, {
consume: (value) => {
if (value) {
const attachment = attachmentManager.createFromDbResponse(value)
attachment?.setOptions({ disk, folder, variants })
...defaultOptionsDecorator,
...options,
}
return ({
consume: (value: any) => {
if (value) {
const attachment = attachmentManager.createFromDbResponse(value)
attachment?.setOptions({ disk, folder, variants })

if (options && options?.meta !== undefined) {
attachment?.setOptions({ meta: options!.meta })
}
if (options && options?.rename !== undefined) {
attachment?.setOptions({ rename: options!.rename })
if (options && options?.meta !== undefined) {
attachment?.setOptions({ meta: options!.meta })
}
if (options && options?.rename !== undefined) {
attachment?.setOptions({ rename: options!.rename })
}
if (options && options?.preComputeUrl !== undefined) {
attachment?.setOptions({ preComputeUrl: options!.preComputeUrl })
}
return attachment
} else {
return null
}
if (options && options?.preComputeUrl !== undefined) {
attachment?.setOptions({ preComputeUrl: options!.preComputeUrl })
}
return attachment
} else {
return null
}
},
prepare: (value) => (value ? JSON.stringify(value.toObject()) : null),
serialize: (value) => (value ? value.toJSON() : null),
prepare: (value: any) => (value ? JSON.stringify(value.toObject()) : null),
serialize: (value: any) => (value ? value.toJSON() : null),
...columnOptions,
})
}
})
}


const makeAttachmentDecorator = (columnOptionsTransformer?: (columnOptions: any) => any) =>
(options?: LucidOptions) => {
return function (target: any, attributeName: string) {
if (!target[optionsSym]) {
target[optionsSym] = {}
}

target[optionsSym][attributeName] = options

const Model = target.constructor as LucidModel & {
$attachments: AttributeOfModelWithAttachment
}

bootModel(Model, options)

const columnOptions = makeColumnOptions(options)
const transformedColumnOptions = columnOptionsTransformer ? columnOptionsTransformer(columnOptions): columnOptions
Model.$addColumn(attributeName, transformedColumnOptions)
}
}


export const attachment = makeAttachmentDecorator()
export const attachments = makeAttachmentDecorator((columnOptions) => ({
consume: (value: any[]) => value.map(columnOptions.consume),
prepare: (value: any[]) => (value ? JSON.stringify(value.map(v => v.toObject())) : null),
}))
54 changes: 54 additions & 0 deletions tests/attachments.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @jrmc/adonis-attachment
*
* @license MIT
* @copyright Jeremy Chaufourier <[email protected]>
*/
import { readFile } from 'node:fs/promises'

import app from '@adonisjs/core/services/app'
import '@japa/assert'
import { test } from '@japa/runner'
import drive from '@adonisjs/drive/services/main'

import { UserFactory } from './fixtures/factories/user.js'

test.group('attachments', () => {
test('create', async ({ assert }) => {
const user = await UserFactory.create()

assert.isNotNull(user.weekendPics)
assert.equal(user.weekendPics?.length, 2)
for (const pic of user.weekendPics ?? []) {
assert.equal(pic.originalName, 'avatar.jpg')
assert.match(pic.name, /(.*).jpg$/)
assert.equal(pic.mimeType, 'image/jpeg')
assert.equal(pic.extname, 'jpg')
}
})

test('delete files after removing', async ({ assert, cleanup }) => {
const fakeDisk = drive.fake('fs')
cleanup(() => drive.restore('fs'))

const user = await UserFactory.create()
const paths = user.weekendPics?.map(p => p.path)
user.weekendPics = []
await user.save()

for (const path of paths ?? [])
fakeDisk.assertMissing(path!)
})

test('delete files after remove entity', async ({ assert, cleanup }) => {
const fakeDisk = drive.fake('fs')
cleanup(() => drive.restore('fs'))

const user = await UserFactory.create()
const paths = user.weekendPics?.map(p => p.path)
await user.delete()

for (const path of paths ?? [])
fakeDisk.assertMissing(path!)
})
})
15 changes: 10 additions & 5 deletions tests/fixtures/factories/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ import Factory from '@adonisjs/lucid/factories'
import app from '@adonisjs/core/services/app'

export const UserFactory = Factory.define(User, async ({ faker }) => {
const attachmentManager = await app.container.make('jrmc.attachment')

const buffer = await readFile(app.makePath('../fixtures/images/img.jpg'))
const avatar = await attachmentManager.createFromBuffer(buffer, 'avatar.jpg')
const makeAttachment = async () => {
const attachmentManager = await app.container.make('jrmc.attachment')
const buffer = await readFile(app.makePath('../fixtures/images/img.jpg'))
return await attachmentManager.createFromBuffer(buffer, 'avatar.jpg')
}

return {
name: faker.person.lastName(),
avatar,
avatar: await makeAttachment(),
weekendPics: [
await makeAttachment(),
await makeAttachment()
]
}
}).build()
1 change: 1 addition & 0 deletions tests/fixtures/migrations/create_users_table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default class extends BaseSchema {
table.string('name')
table.json('avatar')
table.json('avatar_2')
table.json('weekend_pics')
table.timestamp('created_at')
table.timestamp('updated_at')
})
Expand Down
5 changes: 4 additions & 1 deletion tests/fixtures/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { compose } from '@adonisjs/core/helpers'
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { attachment } from '../../../index.js'
import { attachment, attachments } from '../../../index.js'
import type { Attachment } from '../../../src/types/attachment.js'
import { DateTime } from 'luxon'

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

@attachments({ preComputeUrl: true })
declare weekendPics: Attachment[] | null

@column.dateTime({ autoCreate: true, serialize: (value: DateTime) => value.toUnixInteger() })
declare createdAt: DateTime

Expand Down