filipebraida/adonisjs-starter-kit/attachment

Claude Code and agent skill by filipebraida/adonisjs-starter-kit

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

File uploads with derived variants in an AdonisJS app via `@jrmc/adonis-attachment`. `@attachment()` decorator on a Lucid model + converters config drives image resize/reformat (e.g. webp thumbnails). URLs are pre-computed on read via a model helper. Use when adding an upload field, changing a converter, testing attachment code, or debugging avatar URLs. Trigger on: "file upload", "avatar", "attachment", "thumbnail", "@attachment", "adonis-attachment".

Install

npx vskill@latest install filipebraida/adonisjs-starter-kit/attachment

SKILL.md

Attachment

File uploads are managed by `@jrmc/adonis-attachment`. A Lucid model declares an attachment field with @attachment({...}); uploads produce a stored file plus derived variants (thumbnails, previews, formats) driven by named converters in config/attachment.ts. Actions accept a MultipartFile from a validated form and call attachmentManager.createFromFile(file) to assign it. Storage is Drive (fs locally, S3-family in prod). URLs are pre-computed on read via a static model helper — always call it before serializing to Inertia so the frontend gets a real URL, not undefined.

Rules

  • Field on model: decorate with @attachment({ preComputeUrl: false, variants: [...] }) and type as Attachment:

``ts @attachment({ preComputeUrl: false, variants: ['thumbnail'] }) declare avatar: Attachment ` preComputeUrl: false` keeps model reads cheap — URLs are computed on demand by the static helper.

  • Converters live in config/attachment.ts as a keyed record. Each converter declares options (resize, format, etc.). The variant names in @attachment({ variants: [...] }) reference those keys.
  • Declaration merge happens in the config file's declare module '@jrmc/adonis-attachment' block — that's how TypeScript knows variants: ['thumbnail'] is a valid key.
  • Validate uploads with VineJS vine.file({...}):

``ts avatar: vine.file({ extnames: ['png', 'jpg', 'jpeg', 'gif'], size: 1 * 1024 * 1024 }).nullable() ``

  • Persist inside an action — the action takes a MultipartFile (not HttpContext) and delegates to the manager:

``ts if (input.avatar) { input.target.avatar = await attachmentManager.createFromFile(input.avatar) } `` See [[actions-events]] for the "no HttpContext in actions" rule.

  • Pre-compute URLs before serialization. Call the model's static helper on the loaded row (or list) before running the Transformer:

``ts await User.preComputeUrls(users) return inertia.render('users/index', { users: UserTransformer.paginate(users, meta).useVariant('forList'), }) `` The Transformer variant reads the pre-computed URL from the loaded variant.

  • Fallback URL: Transformers usually resolve variant.url ?? this.resource.<remoteUrl> so an externally-hosted URL (social-auth avatar, S3 direct link) works when no local upload exists.
  • Drive config picks the disk (fs, s3, gcs, …). The DRIVE_DISK env var selects it.

Repo refs

  • @attachment field + preComputeUrls on read: app/users/models/user.ts (avatar).
  • Converters (resize / webp): config/attachment.ts.

Doc refs

  • Package README — https://github.com/batosai/adonis-attachment
  • AdonisJS Drive — https://docs.adonisjs.com/guides/digging-deeper/drive
  • VineJS file validation — https://vinejs.dev/docs/types/file

Workflow

Add an attachment field to a model
  1. Add a converter in config/attachment.ts if the format/size you need doesn't exist yet:

``ts converters: { thumbnail: { options: { resize: 300, format: 'webp' } }, hero: { options: { resize: 1600, format: 'webp' } }, }, ``

  1. Decorate the model column:

``ts @attachment({ preComputeUrl: false, variants: ['hero'] }) declare cover: Attachment ``

  1. Migration: add a JSON column for the attachment metadata:

``ts table.json('cover').nullable() ``

  1. Static `preComputeUrls` on the model — mirror the pattern used elsewhere:

``ts static async preComputeUrls(rows: Post | Post[]) { if (Array.isArray(rows)) { await Promise.all(rows.map((r) => this.preComputeUrls(r))) return } const hero = rows.cover?.getVariant('hero') if (hero) await attachmentManager.computeUrl(hero) } ``

  1. Validator: cover: vine.file({ extnames: [...], size: 5 * 1024 * 1024 }).nullable().
  2. Action: input.target.cover = await attachmentManager.createFromFile(input.cover).
  3. Transformer: expose the URL via a helper (coverUrl()) so consumers don't dig into the variant graph.
  4. Controller: call Model.preComputeUrls(...) on the loaded rows before running the Transformer.
Change a converter

Edit config/attachment.ts. Existing uploads keep their old variant unless you re-process — there's no automatic re-render on config change.

Testing
  • Use a real file fixture (small PNG) with the API client's .file(...) method.
  • Fake Drive: drive.fake() in group setup — see [[testing]].
  • Assert the DB row has the attachment JSON populated; assert file existence via disk.assertExists(...).

Anti-patterns

  • ❌ Skipping preComputeUrls before serialization — the Transformer returns undefined for the variant URL and the frontend breaks.
  • ❌ Setting preComputeUrl: true on the decorator to "save code" — every model read now does filesystem I/O, even when the row isn't serialized.
  • ❌ Reading user.avatar.url directly in a React component — always through a Transformer helper. Otherwise the shape drifts.
  • ❌ Storing the raw uploaded filename or path outside @attachment — the package owns storage and filenames.
  • ❌ Validator without a size limit — DoS via huge uploads.
  • ❌ Bypassing attachmentManager.createFromFile(...) and stuffing the MultipartFile into the model field directly.

Related skills

[[crud]] · [[actions-events]] · [[testing]] · [[inertia]]

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
94
Repository forks
12
7-day trend score
23

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/filipebraida/adonisjs-starter-kit/attachment/badge)](https://verified-skill.com/skills/filipebraida/adonisjs-starter-kit/attachment)

Something wrong with this skill or its page? Tell us.