@dudousxd/nestjs-media
Version:
Filesystem + media-library for NestJS — one package
181 lines (145 loc) • 6.46 kB
Markdown
---
name: media-library-attachments
description: >-
Attach files to entities with /nestjs-media's media-library layer
(spatie-style). Use MediaService.library.attach({ ownerType, ownerId,
collection, fileName, mimeType, contents }), library.for(ownerType, id),
library.list, library.delete, and library.url(id, conversion?) where image
conversions are generated lazily on first url() and cached (or eagerly on
attach). Covers MediaCollectionConfig single-file collections, acceptsMimeTypes
validation (MimeNotAllowedError), customProperties, plus the column model
MediaService.attachments.createFromFile(...) returning an Attachment value
object with Attachment.fromJSON / toJSON. Explains the MediaLibrary-not-
configured, ImageProcessorMissingError, and ConversionNotDefinedError failures.
license: MIT
metadata:
type: core
library: "@dudousxd/nestjs-media"
library_version: 0.5.0
framework: nestjs
---
# Attaching files to entities
The media-library layer records files against an owning entity (the spatie
media-library model). It needs a `store` configured on `MediaModule` (see the
`media-module-setup` skill). Reach it through `MediaService.library`.
## Setup
A normal NestJS controller accepts the upload; Multer memory storage gives you a
`Buffer`:
```ts
import { Controller, Injectable, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { MediaService } from '@dudousxd/nestjs-media';
('posts/:id/photos')
export class PhotosController {
constructor(private readonly media: MediaService) {}
()
(FileInterceptor('file'))
upload(('id') postId: string, () file: Express.Multer.File) {
return this.media.library.attach({
ownerType: 'Post',
ownerId: postId,
collection: 'gallery',
fileName: file.originalname,
mimeType: file.mimetype,
contents: file.buffer,
});
}
}
```
`attach` writes the bytes to the collection's disk, saves a `MediaRecord`, runs
any `eager` conversions, and returns the saved record (including its `id`).
## Core patterns
### 1. Bind an owner, list, and serve URLs
`library.for(ownerType, id)` returns a binding so you don't repeat owner
type/id; the id is coerced to a string. Conversions are produced lazily and
cached on the first `url(id, name)` call:
```ts
()
export class PhotosService {
constructor(private readonly media: MediaService) {}
attach(postId: string, file: Express.Multer.File) {
return this.media.library
.for('Post', postId)
.attach({ collection: 'gallery', fileName: file.originalname, mimeType: file.mimetype, contents: file.buffer });
}
async list(postId: string) {
const records = await this.media.library.list('Post', postId, 'gallery');
return Promise.all(
records.map(async (m) => ({
id: m.id,
full: await this.media.library.url(m.id),
thumb: await this.media.library.url(m.id, 'thumb'), // generated + cached on first call
})),
);
}
remove(id: string) {
return this.media.library.delete(id); // deletes the original + every conversion
}
}
```
### 2. Single-file collections, MIME allow-lists, customProperties
Collection rules come from `MediaCollectionConfig` in the module config. A
`single: true` collection replaces whatever is already attached; `acceptsMimeTypes`
is an allow-list; arbitrary metadata rides along in `customProperties`:
```ts
await this.media.library.attach({
ownerType: 'User',
ownerId: userId,
collection: 'avatar', // configured { single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] }
fileName: file.originalname,
mimeType: file.mimetype,
contents: file.buffer,
customProperties: { uploadedBy: userId, alt: 'Profile photo' },
});
```
### 3. The column model — `Attachment` as a model field
When you'd rather store the file as a JSON column on your own row (adonis-attachment
style) than in the `media` table, use `media.attachments`. It is always available
(no `store` needed):
```ts
const attachment = await this.media.attachments.createFromFile(
{ fileName: file.originalname, mimeType: file.mimetype, contents: file.buffer },
{ disk: 's3', variants: [{ name: 'thumb', width: 200 }] },
);
user.avatar = attachment.toJSON(); // persist the JSON on your column
const url = await this.media.attachments.url(attachment, 'thumb');
// rehydrate a stored column value later:
import { Attachment } from '@dudousxd/nestjs-media/storage';
const restored = Attachment.fromJSON(user.avatar);
```
## Common mistakes
### Mistake 1 — calling `media.library` without a `store`
```ts
// Wrong — no `store` was passed to MediaModule, so this getter throws:
// "MediaLibrary is not configured. Pass a `store` to MediaModule.forRoot ..."
this.media.library.attach({ /* ... */ });
// Correct — either configure a store, or use the column model / raw storage.
const attachment = await this.media.attachments.createFromFile(
{ fileName, mimeType, contents },
);
```
`library`, `uploads`, and `directUploads` throw a named error when their feature
was not configured, instead of returning `undefined`. Source:
`packages/nestjs/src/media.service.ts`.
### Mistake 2 — requesting a conversion with no `imageProcessor`
```ts
// Wrong — collection declares `conversions` but the module has no imageProcessor,
// so the first url(id, 'thumb') throws ImageProcessorMissingError.
await this.media.library.url(id, 'thumb');
// Correct — pass an imageProcessor in the module config.
// imageProcessor: new SharpImageProcessor()
```
Conversions need an `ImageProcessor`; without one `ensureConversion` raises
`ImageProcessorMissingError`. Source: `packages/core/src/media-library.ts`.
### Mistake 3 — asking for a conversion the collection never declared
```ts
// Wrong — 'gallery' defines 'thumb' and 'og' only; 'banner' is undefined,
// so this throws ConversionNotDefinedError.
await this.media.library.url(id, 'banner');
// Correct — request a preset that exists, or add it to the collection's
// `conversions` array in the module config.
await this.media.library.url(id, 'thumb');
```
`ensureConversion` looks the preset up by name in the collection config and throws
`ConversionNotDefinedError` if it's absent — it never silently invents one.
Source: `packages/core/src/media-library.ts`.