filipebraida/adonisjs-starter-kit/attachment
Claude Code and agent skill by filipebraida/adonisjs-starter-kit
- community
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/attachmentSKILL.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 asAttachment:
``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.tsas 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 knowsvariants: ['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(notHttpContext) 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, …). TheDRIVE_DISKenv var selects it.
Repo refs
@attachmentfield +preComputeUrlson 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
- Add a converter in
config/attachment.tsif the format/size you need doesn't exist yet:
``ts converters: { thumbnail: { options: { resize: 300, format: 'webp' } }, hero: { options: { resize: 1600, format: 'webp' } }, }, ``
- Decorate the model column:
``ts @attachment({ preComputeUrl: false, variants: ['hero'] }) declare cover: Attachment ``
- Migration: add a JSON column for the attachment metadata:
``ts table.json('cover').nullable() ``
- 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) } ``
- Validator:
cover: vine.file({ extnames: [...], size: 5 * 1024 * 1024 }).nullable(). - Action:
input.target.cover = await attachmentManager.createFromFile(input.cover). - Transformer: expose the URL via a helper (
coverUrl()) so consumers don't dig into the variant graph. - 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
preComputeUrlsbefore serialization — the Transformer returnsundefinedfor the variant URL and the frontend breaks. - ❌ Setting
preComputeUrl: trueon the decorator to "save code" — every model read now does filesystem I/O, even when the row isn't serialized. - ❌ Reading
user.avatar.urldirectly 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
sizelimit — DoS via huge uploads. - ❌ Bypassing
attachmentManager.createFromFile(...)and stuffing theMultipartFileinto 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
- SKILL.md in the repositoryCheckingpackages/skills/attachment/SKILL.md
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
[](https://verified-skill.com/skills/filipebraida/adonisjs-starter-kit/attachment)