@nestbolt/medialibrary
Collections
Define media collections on your entities to group files and enforce constraints like MIME types, file sizes, and collection limits.
Media collections let you group files into named categories on an entity and enforce upload constraints. For example, a User entity might have an "avatar" collection (single image, max 5 MB) and a "documents" collection (any file type, no limit).
Defining Collections
Use the @RegisterMediaCollections decorator on your entity class. The decorator receives a callback with an addCollection function that returns a MediaCollectionBuilder:
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import {
HasMedia,
HasMediaMixin,
RegisterMediaCollections,
} from "@nestbolt/medialibrary";
@Entity("users")
@HasMedia()
@RegisterMediaCollections((addCollection) => {
addCollection("avatar")
.acceptsMimeTypes(["image/jpeg", "image/png", "image/webp"])
.singleFile()
.maxFileSize(5 * 1024 * 1024)
.useFallbackUrl("/defaults/avatar.png");
addCollection("documents");
})
export class User extends HasMediaMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
name!: string;
}You can define as many collections as needed. Collections without any method calls (like "documents" above) accept all file types with no size or count restrictions beyond the global maxFileSize.
The @HasMedia Decorator
The @HasMedia() decorator marks an entity as media-capable and registers its model type name. It must be applied before @RegisterMediaCollections or @RegisterMediaConversions:
@HasMedia()
export class Post extends HasMediaMixin(BaseEntity) { ... }By default, the model type is the class name ("Post"). You can override it:
@HasMedia({ modelType: "BlogPost" })
export class Post extends HasMediaMixin(BaseEntity) { ... }The modelType is the string used in forModel() calls and stored in the database. Changing it after data has been created will break the association with existing media records.
MediaCollectionBuilder Methods
acceptsMimeTypes(mimeTypes)
Restricts the collection to accept only files with specific MIME types. Uploads with non-matching MIME types throw a FileUnacceptableException:
addCollection("images")
.acceptsMimeTypes(["image/jpeg", "image/png", "image/webp", "image/gif"]);Common MIME types for reference:
| Category | MIME Types |
|---|---|
| Images | image/jpeg, image/png, image/webp, image/gif, image/avif, image/svg+xml, image/tiff |
| Videos | video/mp4, video/webm, video/quicktime, video/mpeg |
| Documents | application/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document |
| Audio | audio/mpeg, audio/wav, audio/ogg, audio/webm |
singleFile()
Ensures only one file exists in the collection at any time. When a new file is uploaded to a single-file collection, the existing file (and its conversions) is automatically deleted:
addCollection("avatar")
.singleFile();This is ideal for profile pictures, logos, or any one-to-one file relationship.
onlyKeepLatest(max)
Limits the collection to the max most recent files. When a new upload would exceed the limit, the oldest files are removed:
addCollection("gallery")
.onlyKeepLatest(20);For example, if the collection has 20 images and a new one is uploaded, the oldest image is deleted to maintain the limit of 20.
maxFileSize(bytes)
Sets a maximum file size in bytes for this specific collection. This overrides the global maxFileSize setting from the module configuration:
addCollection("avatar")
.maxFileSize(2 * 1024 * 1024); // 2 MBaddCollection("videos")
.maxFileSize(100 * 1024 * 1024); // 100 MBUploads exceeding this limit throw a FileIsTooBigException.
useDisk(diskName)
Specifies which storage disk to use for files in this collection. Overrides the global defaultDisk:
addCollection("gallery")
.useDisk("s3");This is useful when you want certain types of files on different storage backends. For example, store avatars locally for fast access but put gallery images on S3.
storeConversionsOnDisk(diskName)
Specifies a separate disk for storing image conversions generated for files in this collection:
addCollection("images")
.useDisk("local")
.storeConversionsOnDisk("s3");This allows you to keep original files on local storage while serving converted images from a CDN-backed S3 bucket.
useFallbackUrl(url, conversionName?)
Provides a fallback URL returned when the collection is empty (no media has been uploaded yet). Useful for default avatars or placeholder images:
addCollection("avatar")
.singleFile()
.useFallbackUrl("/defaults/avatar.png");You can also set conversion-specific fallback URLs:
addCollection("avatar")
.singleFile()
.useFallbackUrl("/defaults/avatar.png")
.useFallbackUrl("/defaults/avatar-thumb.png", "thumbnail");When no conversionName is specified, the fallback URL is used as the default for all conversions (keyed as "*" internally).
useFallbackPath(path, conversionName?)
Similar to useFallbackUrl, but provides a fallback file path instead:
addCollection("avatar")
.useFallbackPath("/public/defaults/avatar.png");acceptsFile(predicate)
Provides a custom validation function that receives file information and returns true to accept or false to reject:
addCollection("uploads")
.acceptsFile((file) => {
// Reject files larger than 50 MB
if (file.size > 50 * 1024 * 1024) return false;
// Reject executable files
if (file.fileName.endsWith(".exe")) return false;
return true;
});The file parameter has the following shape:
interface FileInfo {
mimeType: string;
size: number;
fileName: string;
}This predicate is called after MIME type and file size checks, so you can use it for more complex validation logic.
registerMediaConversions(registrar)
Defines image conversions that are specific to this collection. This is an alternative to the @RegisterMediaConversions decorator when you want conversions scoped to a single collection:
addCollection("profile-photos")
.acceptsMimeTypes(["image/jpeg", "image/png"])
.singleFile()
.registerMediaConversions((addConversion) => {
addConversion("thumb")
.resize(100, 100, { fit: "cover" })
.format("webp");
});Complete Example
Here is an entity with multiple collections demonstrating various constraints:
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import {
HasMedia,
HasMediaMixin,
RegisterMediaCollections,
RegisterMediaConversions,
} from "@nestbolt/medialibrary";
@Entity("products")
@HasMedia()
@RegisterMediaCollections((addCollection) => {
// Single hero image, images only, max 10 MB
addCollection("hero")
.acceptsMimeTypes(["image/jpeg", "image/png", "image/webp"])
.singleFile()
.maxFileSize(10 * 1024 * 1024)
.useFallbackUrl("/defaults/product-placeholder.jpg");
// Gallery with up to 30 images, stored on S3
addCollection("gallery")
.acceptsMimeTypes(["image/jpeg", "image/png", "image/webp"])
.onlyKeepLatest(30)
.useDisk("s3");
// PDF spec sheets, max 25 MB each
addCollection("spec-sheets")
.acceptsMimeTypes(["application/pdf"])
.maxFileSize(25 * 1024 * 1024);
// Videos with custom validation
addCollection("videos")
.acceptsMimeTypes(["video/mp4", "video/webm"])
.maxFileSize(500 * 1024 * 1024)
.onlyKeepLatest(5)
.useDisk("s3")
.acceptsFile((file) => {
// Additional validation beyond MIME type
return file.size > 1024; // reject empty/corrupt files
});
// Unrestricted attachments
addCollection("attachments");
})
@RegisterMediaConversions((addConversion) => {
addConversion("thumbnail")
.resize(200, 200, { fit: "cover" })
.format("webp")
.quality(80)
.performOnCollections("hero", "gallery");
addConversion("large")
.resize(1200)
.format("webp")
.quality(90)
.performOnCollections("hero", "gallery");
})
export class Product extends HasMediaMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
name!: string;
@Column({ type: "decimal", precision: 10, scale: 2 })
price!: number;
}Uploading to Collections
When uploading, specify the target collection in .toMediaCollection(). The collection's constraints are automatically enforced:
// This works -- JPEG is accepted by the "hero" collection
await mediaService
.addMediaFromBuffer(jpegBuffer, "hero-image.jpg")
.forModel("Product", product.id)
.toMediaCollection("hero");
// This throws FileUnacceptableException -- PDF is not accepted by "hero"
await mediaService
.addMediaFromBuffer(pdfBuffer, "spec.pdf")
.forModel("Product", product.id)
.toMediaCollection("hero");
// This works -- PDF is accepted by "spec-sheets"
await mediaService
.addMediaFromBuffer(pdfBuffer, "spec.pdf")
.forModel("Product", product.id)
.toMediaCollection("spec-sheets");The Default Collection
If you upload without specifying a collection name, the file goes to the "default" collection:
await mediaService
.addMedia("/path/to/file.txt")
.forModel("Post", postId)
.toMediaCollection(); // goes to "default"The default collection has no constraints unless you explicitly define one:
addCollection("default")
.maxFileSize(5 * 1024 * 1024);How Collections Are Registered
The @RegisterMediaCollections decorator stores collection definitions in a global registry keyed by the model type name (from @HasMedia()). When a file is uploaded, MediaService looks up the target collection's configuration to apply validation rules and determine the storage disk.
This means collections are associated with entity classes, not with individual entity instances. All Post entities share the same collection definitions.