@nestjs-dash/media
v1.1.0
Published
Polymorphic media library plugin for NestJS Dash (Spatie-style media collections on top of @nestjs-dash/storage)
Readme
@nestjs-dash/media
A polymorphic media library plugin for NestJS Dash — Spatie-laravel-medialibrary-style media collections (attach many files to any model, grouped into named collections like avatar or gallery) built on top of @nestjs-dash/storage's disks.
Docs: Media library.
When you need this
Only if you turn on media.enabled — most apps can use @nestjs-dash/storage's upload fields directly without this plugin. Reach for @nestjs-dash/media when a record needs multiple attached files organized into named collections, rather than a single upload field per column.
Install
pnpm add @nestjs-dash/mediaRequires @nestjs-dash/storage to be configured (this package stores files through it), a TypeORM dataSource, and typeorm (media records persist to a TypeORM table). List MediaEntity in your TypeORM entities: [...] explicitly.
Usage
media (and storage) are top-level AdminModule options — siblings of panel:
import { MediaEntity } from '@nestjs-dash/media';
import { AdminModule } from '@nestjs-dash/nestjs';
import { DataSource } from 'typeorm';
TypeOrmModule.forRootAsync({
useFactory: () => ({
entities: [, /* your entities */ MediaEntity],
}),
});
AdminModule.forRootAsync({
inject: [DataSource],
useFactory: (dataSource) => ({
dataSource,
media: { enabled: true },
storage: {
defaultDisk: 'local',
disks: { local: { driver: 'local', root: './storage' } },
},
panel: {
// ...
},
}),
});import { MediaService } from '@nestjs-dash/media';
import { StorageService } from '@nestjs-dash/storage';
@Injectable()
class ProductPhotosService {
constructor(
private readonly media: MediaService,
private readonly storage: StorageService,
) {}
async attachPhoto(product: ProductEntity, file: Express.Multer.File) {
const stored = await this.storage.put(file.buffer, {
directory: 'gallery',
fileName: file.originalname,
contentType: file.mimetype,
});
await this.media.addFromUpload({ modelType: 'Product', modelId: product.id }, 'gallery', {
path: stored.path,
disk: stored.disk,
fileName: file.originalname,
mimeType: file.mimetype,
size: stored.size,
});
}
}Field helpers: .collection('gallery').multiple() on ImageUpload / FileUpload (vs plain .disk() for a single column path).
Configuration
| Option | Where | Description |
| --- | --- | --- |
| media.enabled | AdminModule options | Must be true; otherwise reads return [] and remove() is a no-op. |
| dataSource | AdminModule options | TypeORM DataSource with MediaEntity registered (table nestjs_dash_media). |
| storage.disks / storage.defaultDisk | AdminModule options | Disks media may be stored on. A media disk must name one of these. |
API (MediaService)
| Method | Notes |
| --- | --- |
| getMedia(owner, collection) / getFirst(owner, collection) | Rows ordered by orderColumn, then id. |
| addFromUpload(owner, collection, upload) | Appends at the end of the collection. Throws BadRequestException if upload.disk is not a configured disk or upload.path is unsafe (see below). |
| remove(mediaId, owner?) | Returns true if deleted. When owner ({ modelType, modelId, collection? }) is given, the row must belong to it or nothing happens (false). Always pass it for request-driven deletes. |
| clearCollection(owner, collection) | Deletes every row (and file) in the collection. |
| reorder(owner, collection, ids) | Listed ids first, in that order; unlisted items keep their relative order after them. Ids from other owners are ignored. |
| syncCollection(owner, collection, items, defaults?) | Validates all new items first, saves kept/new items, then deletes rows missing from items. Ids from other owners are ignored. |
| toClientItems(owner, collection) | Serialised items with URLs; an item whose disk can no longer be resolved gets url: ''. |
| assertSafeMediaPath(path) | Exported helper; throws BadRequestException for unsafe keys. |
Deleting a row removes the database row first and then the stored file (best-effort, logged on failure). The file is kept if another media row still references the same disk + path.
Security considerations
- Ownership checks. Always derive
ownerfrom the route (resource slug + record id) after authorizing access to that record, and pass it toremove(mediaId, owner). Callingremove(mediaId)without an owner deletes any media row by id and must only be used from trusted server code. - Client-supplied paths.
addFromUpload/syncCollectionreject unconfigured disks and paths that are empty, absolute (/x,C:/x), contain backslashes, NUL bytes, or./..segments. This prevents traversal but does not prove the caller uploaded the file: a client could still reference another existing key on the same disk. Prefer passing thepathreturned by your ownStorageService.put()call, or restrict accepted keys to a per-upload directory/token on the server. - Accept lists are server-side.
accept/MIME hints on upload fields only filter the browser's file picker. Validate MIME type and extension on the server (in the upload endpoint) before storing; never trustmimeTypesent by the client. - Size limits. This package does not enforce file sizes; the
sizevalue it stores is informational. Enforce limits in the upload endpoint (for example multerlimits.fileSize) and at your reverse proxy. - Serve private disks through signed URLs (
visibility: 'private') rather than the public file route.
License
MIT
