{"id":19528879,"url":"https://github.com/adonisjs/attachment-lite","last_synced_at":"2025-04-09T23:21:15.659Z","repository":{"id":39663232,"uuid":"406990653","full_name":"adonisjs/attachment-lite","owner":"adonisjs","description":"Turn any field on your Lucid models to an attachment data type","archived":false,"fork":false,"pushed_at":"2024-05-22T17:50:07.000Z","size":1561,"stargazers_count":71,"open_issues_count":5,"forks_count":20,"subscribers_count":7,"default_branch":"develop","last_synced_at":"2025-04-02T21:12:35.646Z","etag":null,"topics":["first-party-package"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adonisjs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"thetutlage"}},"created_at":"2021-09-16T02:27:06.000Z","updated_at":"2025-04-02T06:50:34.000Z","dependencies_parsed_at":"2024-06-18T16:57:20.373Z","dependency_job_id":"13ae77bf-9de5-4219-b5d5-4169f377e472","html_url":"https://github.com/adonisjs/attachment-lite","commit_stats":{"total_commits":33,"total_committers":5,"mean_commits":6.6,"dds":0.1515151515151515,"last_synced_commit":"aec052133d4f8d4bc84706f0ee448dab773a08e0"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adonisjs%2Fattachment-lite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adonisjs%2Fattachment-lite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adonisjs%2Fattachment-lite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adonisjs%2Fattachment-lite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adonisjs","download_url":"https://codeload.github.com/adonisjs/attachment-lite/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247547275,"owners_count":20956529,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["first-party-package"],"created_at":"2024-11-11T01:20:30.045Z","updated_at":"2025-04-09T23:21:15.638Z","avatar_url":"https://github.com/adonisjs.png","language":"TypeScript","funding_links":["https://github.com/sponsors/thetutlage"],"categories":[],"sub_categories":[],"readme":"# Attachment Lite\n\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"./assets/banner.jpeg\" /\u003e\n\u003c/div\u003e\n\n---\n\n[![github-actions-image]][github-actions-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]\n\nA simple, opinionated package to convert any column on your Lucid model to an attachment data type.\n\nAttachment lite allows you to store a reference of user uploaded files within the database. It does not require any additional database tables and stores the file metadata as JSON within the same column.\n\n## How it works?\nThe `attachment-lite` package is **an alternative to the media library approach**. I believe media libraries are great when creating a CMS that wants a central place to keep all the images/documents.\n\nHowever, many applications like a SAAS product or a community forum do not need media libraries.\n\nFor example, websites like Twitter or dev.to don't have a media library section where you upload and choose images from. Instead, images on these platforms are tightly coupled with the resource.\n\nWhen you update your profile image on Twitter, the old image disappears, and the new one appears. There is no central gallery of images to choose the profile picture from.\n\nA very long story to tell you that the `attachment-lite` package is an excellent solution for managing one-off file uploads in your application.\n\n## Features\n- Turn any column in your database to an attachment data type.\n- No additional database tables are required. The file metadata is stored as JSON within the same column.\n- Automatically removes the old file from the disk when a new file is assigned.\n- Handles failure cases gracefully. No files will be stored if the model fails to persist.\n- Similarly, no old files are removed if the model fails to persist during an update or the deletion fails.\n\n## Pre-requisites\nThe `attachment-lite` package requires `@adonisjs/lucid \u003e= v16.3.1` and `@adonisjs/core \u003e= 5.3.4`.\n\nAlso, it relies on [AdonisJS drive](https://docs.adonisjs.com/guides/drive) for writing files on the disk.\n\n## Setup\nInstall the package from the npm registry as follows.\n\n```sh\nnpm i @adonisjs/attachment-lite\n```\n\nNext, configure the package by running the following ace command.\n\n```sh\nnode ace configure @adonisjs/attachment-lite\n```\n\n## Usage\n\nOften times, the size of the image metadata could exceed the allowable length of an SQL `String` data type. So, it is recommended to create/modify the column which will hold the metadata to use a `JSON` data type. \n\nIf you are creating the column for the first time, make sure that you use the JSON data type. Example:\n\n```ts\n  // Within the migration file\n\n  protected tableName = 'users'\n  \n  public async up() {\n    this.schema.createTable(this.tableName, (table) =\u003e {\n      table.increments()\n      table.json('avatar') // \u003c-- Use a JSON data type\n    })\n  }\n```\n\nIf you already have a column for storing image paths/URLs, you need to create a new migration and alter the column definition to a JSON data type. Example:\n\n```bash\n# Create a new migration file\nnode ace make:migration change_avatar_column_to_json --table=users\n```\n\n```ts\n  // Within the migration file\n  \n  protected tableName = 'users'\n\n  public async up() {\n    this.schema.alterTable(this.tableName, (table) =\u003e {\n      table.json('avatar').alter() // \u003c-- Alter the column definition\n    })\n  }\n```\n\nNext, in the model, import the `attachment` decorator and the `AttachmentContract` interface from the package.\n\n\u003e Make sure NOT to use the `@column` decorator when using the `@attachment` decorator.\n\n```ts\nimport { BaseModel } from '@ioc:Adonis/Lucid/Orm'\nimport {\n  attachment,\n  AttachmentContract\n} from '@ioc:Adonis/Addons/AttachmentLite'\n\nclass User extends BaseModel {\n  @attachment()\n  public avatar: AttachmentContract\n}\n```\n\nNow you can create an attachment from the user uploaded file as follows.\n\n```ts\nimport { Attachment } from '@ioc:Adonis/Addons/AttachmentLite'\n\nclass UsersController {\n  public store({ request }: HttpContextContract) {\n    const avatar = request.file('avatar')!\n    const user = new User()\n\n    user.avatar = Attachment.fromFile(avatar)\n    await user.save()\n  }\n}\n```\n\nThe `Attachment.fromFile` creates an instance of the Attachment class from the user uploaded file. When you persist the model to the database, the attachment-lite will write the file to the disk.\n\n### Handling updates\nYou can update the property with a newly uploaded user file, and the package will take care of removing the old file and storing the new one.\n\n```ts\nimport { Attachment } from '@ioc:Adonis/Addons/AttachmentLite'\n\nclass UsersController {\n  public update({ request }: HttpContextContract) {\n    const user = await User.firstOrFail()\n    const avatar = request.file('avatar')!\n\n    user.avatar = Attachment.fromFile(avatar)\n\n    // Old file will be removed from the disk as well.\n    await user.save()\n  }\n}\n```\n\nSimilarly, assign `null` value to the model property to delete the file without assigning a new file. \n\nAlso, make sure you update the property type on the model to be `null` as well.\n\n```ts\nclass User extends BaseModel {\n  @attachment()\n  public avatar: AttachmentContract | null\n}\n```\n\n```ts\nconst user = await User.first()\nuser.avatar = null\n\n// Removes the file from the disk\nawait user.save()\n```\n\n### Handling deletes\nUpon deleting the model instance, all the related attachments will be removed from the disk.\n\n\u003e Do note: For attachment lite to delete files, you will have to use the `modelInstance.delete` method. Using `delete` on the query builder will not work.\n\n```ts\nconst user = await User.first()\n\n// Removes any attachments related to this user\nawait user.delete()\n```\n\n## Specifying disk\nBy default, all files are written/deleted from the default disk. However, you can specify a custom disk at the time of using the `attachment` decorator.\n\n\u003e The `disk` property value is never persisted to the database. It means, if you first define the disk as `s3`, upload a few files and then change the disk value to `gcs`, the package will look for files using the `gcs` disk.\n\n```ts\nclass User extends BaseModel {\n  @attachment({ disk: 's3' })\n  public avatar: AttachmentContract\n}\n```\n\n## Specifying subfolder\n\nYou can also store files inside the subfolder by defining the `folder` property as follows.\n\n```ts\nclass User extends BaseModel {\n  @attachment({ folder: 'avatars' })\n  public avatar: AttachmentContract\n}\n```\n\n## Generating URLs\n\nYou can generate a URL for a given attachment using the `getUrl` or `getSignedUrl` methods. They are identical to the [Drive methods](https://docs.adonisjs.com/guides/drive#generating-urls), just that you don't have to specify the file name.\n\n```ts\nawait user.avatar.getSignedUrl({ expiresIn: '30mins' })\n```\n\n## Generating URLs for the API response\n\nThe Drive API methods for generating URLs are asynchronous, whereas serializing a model to JSON is synchronous. Therefore, it is not to create URLs at the time of serializing a model.\n\n```ts\n// ❌ Does not work\n\nconst users = await User.all()\nusers.map((user) =\u003e {\n  user.avatar.url = await user.avatar.getSignedUrl()\n  return user\n})\n```\n\nTo address this use case, you can opt for pre-computing URLs\n\n### Pre compute URLs\n\nEnable the `preComputeUrl` flag to pre compute the URLs after SELECT queries. For example:\n\n```ts\nclass User extends BaseModel {\n  @attachment({ preComputeUrl: true })\n  public avatar: AttachmentContract\n}\n```\n\nFetch result\n\n```ts\nconst users = await User.all()\nusers[0].avatar.url // pre computed already \n```\n\nFind result\n\n```ts\nconst user = await User.findOrFail(1)\nuser.avatar.url // pre computed already \n```\n\nPagination result\n\n```ts\nconst users = await User.query.paginate(1)\nusers[0].avatar.url // pre computed already \n```\n\nThe `preComputeUrl` property will generate the URL and set it on the Attachment class instance. Also, a signed URL is generated when the disk is **private**, and a normal URL is generated when the disk is **public**.\n\n### Pre compute on demand\n\nWe recommend not enabling the `preComputeUrl` option when you need the URL for just one or two queries and not within the rest of your application.\n\nFor those couple of queries, you can manually compute the URLs within the controller. Here's a small helper method that you can drop on the model directly.\n\n```ts\nclass User extends BaseModel {\n  public static async preComputeUrls(models: User | User[]) {\n    if (Array.isArray(models)) {\n      await Promise.all(models.map((model) =\u003e this.preComputeUrls(model)))\n      return\n    }\n\n    await models.avatar?.computeUrl()\n    await models.coverImage?.computeUrl()\n  }\n}\n```\n\nAnd now use it as follows.\n\n```\nconst users = await User.all()\nawait User.preComputeUrls(users)\n\nreturn users\n```\n\nOr for a single user\n\n```ts\nconst user = await User.findOrFail(1)\nawait User.preComputeUrls(user)\n\nreturn user\n```\n\n## Using Attachment lite with model factories\nAttachment lite primarly uses the multipart request body to persist user upload files. However, you can also construct an instance of `Attachment` class manually and use the AdonisJS drive to persist the corresponding file.\n\nIn the following example, we create an instance of the Attachment class to represent the Post cover image.\n\n```ts\nimport Post from 'App/Models/Post'\nimport Drive from '@ioc:Adonis/Core/Drive'\nimport { file } from '@ioc:Adonis/Core/Helpers'\nimport Factory from '@ioc:Adonis/Lucid/Factory'\nimport { Attachment } from '@ioc:Adonis/Addons/AttachmentLite'\n\nexport default Factory.define(Post, async ({ faker }) =\u003e {\n  /**\n   * Step 1: Create an instance of attachment\n   */\n  const coverImage = new Attachment({\n    extname: 'png',\n    mimeType: 'image/png',\n    size: 10 * 1000,\n    name: `${faker.random.alphaNumeric(10)}.png`,\n  })\n\n  /**\n   * Step 2: Mark image as persisted, this will disable the\n   * functions of attachment lite that looks for multipart\n   * body and attempts to write the file from the stream\n   */\n  coverImage.isPersisted = true\n\n  /**\n   * Step 3: Persist the file using Drive.\n   */\n  await Drive.put(coverImage.name, (await file.generatePng('1mb')).contents)\n\n  return {\n    title: faker.lorem.words(5),\n    coverImage: coverImage,\n  }\n}).build()\n```\n \n[github-actions-image]: https://img.shields.io/github/workflow/status/adonisjs/attachment-lite/test?style=for-the-badge\n[github-actions-url]: https://github.com/adonisjs/attachment-lite/actions/workflows/test.yml \"github-actions\"\n\n[npm-image]: https://img.shields.io/npm/v/@adonisjs/attachment-lite.svg?style=for-the-badge\u0026logo=npm\n[npm-url]: https://npmjs.org/package/@adonisjs/attachment-lite \"npm\"\n\n[license-image]: https://img.shields.io/npm/l/@adonisjs/attachment-lite?color=blueviolet\u0026style=for-the-badge\n[license-url]: LICENSE.md \"license\"\n\n[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge\u0026logo=typescript\n[typescript-url]:  \"typescript\"\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadonisjs%2Fattachment-lite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadonisjs%2Fattachment-lite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadonisjs%2Fattachment-lite/lists"}