ResolversLast updated on
Last updated on
Load file-based layers and templates for rendering
Resolvers provide a unified interface for loading file-based content (templates, assets) when rendering forms. They abstract away the underlying storage mechanism, whether in-memory, filesystem, or remote.
The @paradoc/resolvers package provides filesystem, HTTP, and memory resolvers. Install it separately from the SDK with npm install @paradoc/resolvers, and import the entrypoint for your environment.
Resolver Interface
All resolvers implement this simple interface:
interface Resolver {
read(path: string): Promise<Uint8Array>
}A resolver is bound once, when an artifact is constructed — p.form(schema, { resolver }), a builder's .build({ resolver }), or p.load(content, { resolver }). Every instance you derive from it afterward — every fill, every mutator, every render call — carries that same resolver, so you never pass it again.
ArtifactInstanceOptions
The options object every construction entry point accepts:
interface ArtifactInstanceOptions {
resolver?: Resolver
}It's accepted by:
p.form(input, options?),form.from(input, options?),form.safeFrom(input, options?), and a form builder's.build(options?)— and the same four entry points fordocumentandchecklist.load,loadFromObject,safeLoad, andsafeLoadFromObject, when reconstructing a form, document, or checklist. (Loading a bundle takes noresolver: a bundle has no file-backed layers of its own, only content entries that carry their own.)runtimeFormFromJSON,runtimeDocumentFromJSON, andruntimeChecklistFromJSON, when rebuilding a runtime (filled) instance from its JSON.
resolver is the only field, and it's optional: pass it only when the artifact has file-backed layers. It's bound once, at construction, and every instance you derive afterward — every fill, mutator, and clone — carries it. A render call never takes one.
Render a file-backed layer with no resolver bound, and Paradoc throws
UnboundResolverError, naming the layer, the path it wanted, and where to
bind one.
Memory Resolver (Browser-Compatible)
The createMemoryResolver function is exported from @paradoc/resolvers/memory and works in any environment, including browsers.
import { p } from '@paradoc/core'
import { createMemoryResolver } from '@paradoc/resolvers/memory'
const resolver = createMemoryResolver({
contents: {
'/templates/receipt.html': '<h1>Receipt for {{fields.customer}}</h1>',
'/templates/contract.docx': docxTemplateBytes,
'/assets/logo.png': logoBytes,
}
})
// Bind the resolver when the form is built — the form has file-based layers
const form = p.form(formSchema, { resolver })
const filled = form.fill({
fields: { customer: 'Acme Corp' }
})
const output = await filled.render()createMemoryResolver
function createMemoryResolver(options: MemoryResolverOptions): ResolverMemoryResolverOptions
Binary content, including a Node.js Buffer, is copied when the resolver is created, and every read returns a new copy. A path with no entry rejects with ERR_RESOLVER_NOT_FOUND. Missing contents, or a value that is not a string or Uint8Array, throws ERR_RESOLVER_INVALID_OPTIONS.
Filesystem Resolver (Node.js Only)
The @paradoc/resolvers/fs entrypoint requires Node.js.
import { createFsResolver } from '@paradoc/resolvers/fs'
const resolver = createFsResolver({
root: process.cwd()
})
// Reads files relative to the root directory
const bytes = await resolver.read('/templates/form.docx')createFsResolver
function createFsResolver(options: FsResolverOptions): ResolverFsResolverOptions
The filesystem resolver validates that paths and symlinks stay within the root directory. Policy failures expose ERR_RESOLVER_OUTSIDE_ROOT or ERR_RESOLVER_INVALID_PATH; missing files retain native Node.js error codes. An empty or non-string root throws ERR_RESOLVER_INVALID_OPTIONS. The root is checked on each read until it is found, so a root created after a failed read becomes readable.
HTTP Resolver
The createHttpResolver function is exported from @paradoc/resolvers/http and works in any environment with the Fetch API. It reads each path beneath a base URL.
import { createHttpResolver } from '@paradoc/resolvers/http'
const resolver = createHttpResolver({
baseUrl: 'https://example.com/forms/w-9/'
})
// Fetches https://example.com/forms/w-9/templates/form.pdf
const bytes = await resolver.read('/templates/form.pdf')createHttpResolver
function createHttpResolver(options: HttpResolverOptions): ResolverHttpResolverOptions
Paths follow the filesystem resolver's rules: forward slashes only, and a single leading slash means the base URL, not the origin root. Each path segment is percent-encoded. A path that resolves outside the base URL rejects with ERR_RESOLVER_OUTSIDE_ROOT, and one that names the base URL itself with ERR_RESOLVER_INVALID_PATH, before any request. Redirects follow the fetch function's own policy. A 404 rejects with ERR_RESOLVER_NOT_FOUND, and any other failed status rejects with ERR_RESOLVER_FETCH_FAILED. An error thrown by a supplied fetch passes through unchanged.
Usage with File-Based Layers
Resolvers are needed when forms define file-based layers:
import { p } from '@paradoc/core'
import { createFsResolver } from '@paradoc/resolvers/fs'
const resolver = createFsResolver({ root: process.cwd() })
const form = p.form({
name: 'w9',
fields: {
name: { type: 'text' },
taxId: { type: 'text' }
},
layers: {
pdf: {
kind: 'file', // File-based layer
mimeType: 'application/pdf',
path: '/templates/w9-form.pdf', // Resolved via resolver
bindings: { /* ... */ }
}
},
defaultLayer: 'pdf'
}, { resolver })
const filled = form.fill({
fields: { name: 'John Smith', taxId: '12-3456789' }
})
const output = await filled.render()Render a file layer with no resolver bound and Paradoc throws
UnboundResolverError, naming the layer, the path it wanted, and where to
bind one.
Inline vs File Layers
- Inline layers (
kind: 'inline') embed content directly in the form definition. No resolver needed. - File layers (
kind: 'file') reference external files by path. A resolver must be bound when the artifact is constructed.
// Inline layer - no resolver needed
layers: {
html: {
kind: 'inline',
mimeType: 'text/html',
text: '<h1>{{fields.title}}</h1>'
}
}
// File layer - resolver required
layers: {
pdf: {
kind: 'file',
mimeType: 'application/pdf',
path: '/templates/form.pdf'
}
}Custom Resolvers
Implement the Resolver interface for custom storage backends:
import type { Resolver } from '@paradoc/types'
// Example: S3 resolver
function createS3Resolver(bucket: string, s3Client: S3Client): Resolver {
return {
async read(path: string): Promise<Uint8Array> {
const response = await s3Client.getObject({ Bucket: bucket, Key: path })
return new Uint8Array(await response.Body.transformToByteArray())
}
}
}Related
- Renderers - How renderers use resolvers
- Form Layers - Layer configuration