@grest-ts/schema-file
Unified file abstraction for the Grest framework. Works across HTTP (multipart upload/download), WebSocket (base64 JSON), and tests (in-memory buffers) with a single API.
In-memory files — when to use
GGFile holds file content in Node.js process memory. The entire file is buffered as a Uint8Array (or lazily read from a browser File/Blob). This is simple and works great for prototyping, internal tools, and smaller applications where files are modest in size.
For production systems handling large files or high throughput, pass files through your server to an object store (S3, GCS, R2, etc.) and work with URLs/keys instead. A common pattern is to have your upload endpoint stream directly to S3 and return a URL, rather than buffering the whole file in memory. This keeps memory usage predictable and avoids OOM under load.
Features
- Single
GGFiletype used everywhere — contracts, services, clients, and tests - Schema validation with
IsFile— MIME type filtering and size limits - Single-consumption model prevents accidental double-reads
- Works in Node.js and browsers (wraps native
File/Bloblazily) - Testkit with ready-made files (PNG, JPEG, PDF, CSV, JSON, random bytes)
Quick Example
Defining a contract with file fields
import { IsFile } from "@grest-ts/schema-file"
import { IsObject, IsString, IsArray } from "@grest-ts/schema"
// Accept any file
const IsUploadRequest = IsObject({
file: IsFile,
description: IsString.orUndefined
})
// Accept images only, max 5 MB
const IsAvatarRequest = IsObject({
image: IsFile.accept('image/*').maxSize(5 * 1024 * 1024)
})
// Multiple files
const IsBatchRequest = IsObject({
files: IsArray(IsFile),
metadata: IsObject({ tags: IsArray(IsString) })
})
// File as output (for downloads)
const contract = {
downloadReport: {
input: IsObject({ reportId: IsString }),
success: IsFile,
errors: [SERVER_ERROR]
}
}Reading file content in a service
import { GGFile } from "@grest-ts/schema-file"
async function handleUpload(file: GGFile): Promise<void> {
console.log(file.name) // "report.pdf"
console.log(file.mimeType) // "application/pdf"
console.log(file.size) // 1048576
// Read the content (can only be called once)
const bytes = await file.buffer()
// or: const text = await file.text()
// or: const stream = file.stream()
}Returning a file from a service
async function downloadReport(request: { reportId: string }): Promise<GGFile> {
const content = await generateReport(request.reportId)
return GGFile.fromString(content, "report.csv", "text/csv")
}GGFile
Abstract base class representing a file. All file operations flow through this type.
Properties
| Property | Type | Description |
|---|---|---|
name | string | File name (e.g. "photo.jpg") |
mimeType | string | MIME type (e.g. "image/jpeg") |
size | number | Size in bytes |
consumed | boolean | Whether the content has been read |
Reading content
Each file can only be read once. After calling any of these methods, the file is marked as consumed and further reads throw an error. Use clone() if you need multiple reads.
// Read as Uint8Array
const bytes = await file.buffer()
// Read as UTF-8 string
const text = await file.text()
// Read as ReadableStream
const stream = file.stream()
// Create an independent copy (before consuming)
const copy = file.clone()
const bytes1 = await copy.buffer()
const bytes2 = await file.buffer() // still worksFactory methods
// From raw bytes
GGFile.fromBuffer(data: Uint8Array, name: string, mimeType?: string): GGFile
// From a string (UTF-8 encoded)
GGFile.fromString(content: string, name: string, mimeType?: string): GGFile
// From base64 (used internally for WebSocket transport)
GGFile.fromBase64(base64: string, name: string, mimeType?: string): GGFile
// From a browser <input type="file"> (lazy — reads only when consumed)
GGFile.fromBrowserFile(file: File): GGFileSerialization
Files are automatically serialized for transport — multipart FormData over HTTP, base64 JSON over WebSocket. You don't need to call these directly, but they're available:
// Convert to base64 string (consumes the file)
const b64 = await GGFile.toBase64(file)
// Serialize to JSON (consumes the file)
const json = await GGFile.toJSON(file)
// { __ggfile: true, name: "...", mimeType: "...", size: 123, data: "base64..." }
// Deserialize from JSON
const restored = GGFile.fromJSON(json)
// Check if a value is serialized GGFile JSON
GGFile.isJSON(value) // booleanIsFile (Schema)
IsFile is a GGSchema for validating files in contracts. It supports MIME type filtering and size limits.
import { IsFile } from "@grest-ts/schema-file"
// Accept any file
IsFile
// Constrain by MIME type
IsFile.accept('image/*') // any image
IsFile.accept('image/png', 'image/jpeg') // specific types
IsFile.accept('.pdf') // by extension
IsFile.accept('application/pdf') // by MIME type
// Constrain by size
IsFile.maxSize(10 * 1024 * 1024) // 10 MB
// Combine constraints
IsFile.accept('image/*').maxSize(5 * 1024 * 1024)
// Optional
IsFile.orUndefined
IsFile.orNullStatic shortcuts
FileSchema.image() // image/*
FileSchema.image({ maxSize: 5_000_000 })
FileSchema.pdf() // application/pdf
FileSchema.video() // video/*
FileSchema.audio() // audio/*
FileSchema.any() // no constraints
FileSchema.any({ maxSize: 50_000_000 })Validation errors
| Error code | Description |
|---|---|
file.type | Value is not a file |
file.mimeType | File type not in the accept list |
file.maxSize | File exceeds the maxSize limit |
Testkit
Import @grest-ts/schema-file/testkit for test utilities that create GGFile instances without real I/O.
import { GGTestFile } from "@grest-ts/schema-file/testkit"Generators
// From raw data
GGTestFile.fromBuffer(new Uint8Array([1, 2, 3]), 'data.bin')
GGTestFile.fromString('Hello', 'hello.txt')
GGTestFile.fromBase64('SGVsbG8=', 'hello.txt')
// Random bytes (useful for size-limit tests)
GGTestFile.random(1024, 'random.bin')
GGTestFile.random(6 * 1024 * 1024, 'too-large.png', 'image/png')
// Valid minimal files
GGTestFile.png1x1() // 1x1 transparent PNG
GGTestFile.jpeg1x1() // 1x1 red JPEG
GGTestFile.pdf() // empty-page PDF
// Structured data
GGTestFile.json({ key: 'value' }, 'config.json')
GGTestFile.csv([
['Name', 'Age'],
['Alice', '30']
], 'users.csv')Test example
import { callOn, GGTest } from "@grest-ts/testkit"
import { GGTestFile } from "@grest-ts/schema-file/testkit"
import { VALIDATION_ERROR } from "@grest-ts/schema"
const api = callOn(FileUploadTestApi)
test('upload text file', async () => {
const file = GGTestFile.fromString('Hello', 'hello.txt')
await api
.uploadFile({ file, description: 'greeting' })
.toMatchObject({ fileName: 'hello.txt', size: 5 })
})
test('reject non-image uploads', async () => {
const textFile = GGTestFile.fromString('not an image', 'doc.txt', 'text/plain')
await api
.uploadImage({ image: textFile })
.toBeError(VALIDATION_ERROR)
})
test('reject oversized files', async () => {
const large = GGTestFile.random(6 * 1024 * 1024, 'big.png', 'image/png')
await api
.uploadImage({ image: large })
.toBeError(VALIDATION_ERROR)
})