← All @molecule/* packages · App templates
@molecule/api-resource-templateAPI resource · resource-template · API (Node) · v1.0.1 · Apache-2.0
Generic templates table keyed by (resource_type, snapshot).
npm install @molecule/api-resource-template@molecule/api-resource-template is an API resource: the routes, validation and storage for resource-template, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { routes, requestHandlerMap } from '@molecule/api-resource-template'
// Wire routes into your Express app via `mlcl inject`:
// POST /resource-templates
// GET /resource-templates
// GET /resource-templates/:id
// PATCH /resource-templates/:id
// DELETE /resource-templates/:id
// POST /resource-templates/:id/instantiateAuto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Resource-template resource for molecule.dev.
Generic template registry: store reusable, versioned snapshots keyed by
(resourceType, slug) plus a pure-data instantiate helper that
resolves {{variable}} placeholders inside the snapshot to materialise
a concrete payload. Handler errors flow through t() with English
defaults — no companion locale bond is shipped (no user-visible UI text
lives in this package).
import { routes, requestHandlerMap } from '@molecule/api-resource-template'
// Wire routes into your Express app via `mlcl inject`:
// POST /resource-templates
// GET /resource-templates
// GET /resource-templates/:id
// PATCH /resource-templates/:id
// DELETE /resource-templates/:id
// POST /resource-templates/:id/instantiate
import { instantiateTemplate } from '@molecule/api-resource-template'
const result = instantiateTemplate(
{
snapshot: { title: 'Hello {{name}}', body: '{{greeting}}!' },
variables: [{ name: 'greeting', defaultValue: 'Welcome' }],
},
{ name: 'Ada' },
)
// result.payload === { title: 'Hello Ada', body: 'Welcome!' }
resource
npm install @molecule/api-resource-template @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource zod
CreateTemplateInputInput for creating a new template.
interface CreateTemplateInput {
/** Resource type the template materialises. */
resourceType: string
/** Unique slug within `resourceType`. */
slug: string
/** Display name. */
name: string
/** Optional long description. */
description?: string | null
/** Raw JSON snapshot. */
snapshot: unknown
/** Declared variables. */
variables?: TemplateVariable[]
/** Free-form tags. */
tags?: string[]
/** Whether to expose the template publicly. */
isPublic?: boolean
/** Creator user ID. */
createdBy?: string | null
}
InstantiateResultResult of an instantiation request.
interface InstantiateResult<T = unknown> {
/** Cloned snapshot with placeholders resolved. */
payload: T
/** Variable map that was actually applied (defaults merged in). */
resolvedVariables: VariableValues
/** Variable names that were left unresolved (no value supplied, no default). */
missingVariables: string[]
}
PaginatedResultA paginated result set.
interface PaginatedResult<T> {
/** Items on the current page. */
data: T[]
/** Total matching rows across all pages. */
total: number
/** Page size. */
limit: number
/** Rows skipped before this page. */
offset: number
}
TemplateA persisted template entry. The snapshot field is the raw JSON to
clone on instantiation; placeholders inside it are resolved against
the supplied variables map.
interface Template {
/** Unique template identifier. */
id: string
/** The type of resource this template materialises (e.g. 'document'). */
resourceType: string
/** Human-friendly URL slug, unique within `resourceType`. */
slug: string
/** Display name. */
name: string
/** Optional long description. */
description: string | null
/** Raw JSON snapshot — may contain `{{variable}}` placeholders. */
snapshot: unknown
/** Declared variables expected by the snapshot. */
variables: TemplateVariable[]
/** Free-form tags for filtering and search. */
tags: string[]
/** Monotonic version counter — bumped on every update. */
version: number
/** Whether the template is publicly listable. */
isPublic: boolean
/** ID of the creating user, if known. */
createdBy: string | null
/** ISO 8601 creation timestamp. */
createdAt: string
/** ISO 8601 last-update timestamp. */
updatedAt: string
}
TemplateQueryFilters for listing templates.
interface TemplateQuery {
/**
* ID of the authenticated caller. Scopes the result to the templates this
* viewer is allowed to see: public templates plus their OWN private ones.
* When omitted, only public templates are returned (fail closed).
*/
viewerId?: string
/** Limit results to a specific resource type. */
resourceType?: string
/** Restrict to public templates only. */
publicOnly?: boolean
/** Filter by creator user ID. */
createdBy?: string
/** Match any of the supplied tags. */
tags?: string[]
/** Maximum number of rows to return. */
limit?: number
/** Number of rows to skip. */
offset?: number
}
TemplateVariableA single declared variable a template expects when instantiated.
interface TemplateVariable {
/** Variable name, referenced as `{{name}}` in the snapshot. */
name: string
/** Optional default value when the caller does not supply one. */
defaultValue?: string | number | boolean | null
/** Whether the variable must be provided at instantiation time. */
required?: boolean
/** Free-form human-readable description of the variable. */
description?: string
}
UpdateTemplateInputInput for updating an existing template. Any field omitted is left as-is;
version is incremented automatically when any field changes.
interface UpdateTemplateInput {
/** New display name. */
name?: string
/** New description (`null` to clear). */
description?: string | null
/** Replace the snapshot wholesale. */
snapshot?: unknown
/** Replace the declared variables. */
variables?: TemplateVariable[]
/** Replace the tags. */
tags?: string[]
/** Toggle public visibility. */
isPublic?: boolean
}
VariableValuesMap of variable name to substitution value.
type VariableValues = Record<string, string | number | boolean | null | undefined>
canEditTemplate(template, userId)Returns true when the supplied user can edit the template (currently
limited to the creator). Hosts that need richer rules (org admin, share
grants, etc.) should layer their own check on top.
function canEditTemplate(template: Template, userId: string | null): boolean
template — Template under inspection.userId — The editing user's ID, or null.Returns: true when the user is the creator.
canViewTemplate(template, userId)Returns true when the supplied user can view the template — that is,
the template is public or the user is its creator.
function canViewTemplate(template: Template, userId: string | null): boolean
template — Template under inspection.userId — The viewing user's ID, or null for anonymous viewers.Returns: true when the template is visible.
create(req, res)Creates a new template entry. Returns 409 Conflict if a row already
exists for the same (resourceType, slug) pair.
function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — Request with creation body.res — Response object.createTemplate(input)Creates a new template entry. Throws when a row already exists for the
same (resourceType, slug) pair.
function createTemplate(input: CreateTemplateInput): Promise<Template>
input — Template creation payload.Returns: The persisted template.
del(req, res)Deletes a template by ID.
function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — Request with id path param.res — Response object.deleteTemplate(id)Deletes a template by ID. Idempotent — returns false when no row was
deleted (e.g. unknown ID).
function deleteTemplate(id: string): Promise<boolean>
id — The template ID.Returns: true when a row was deleted.
getTemplate(id)Returns a single template by ID, or null when not found.
function getTemplate(id: string): Promise<Template | null>
id — The template ID.Returns: The template or null.
getTemplateBySlug(resourceType, slug)Returns a single template by (resourceType, slug), or null when not
found.
function getTemplateBySlug(resourceType: string, slug: string): Promise<Template | null>
resourceType — The resource type.slug — The template slug, unique within the resource type.Returns: The template or null.
instantiate(req, res)Instantiates a template by ID. The response includes the resolved payload, the merged variable map, and any unresolved variable names.
function instantiate(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — Request with id path param and { variables? } body.res — Response object.instantiateById(id, variables)Loads a template and resolves its snapshot against the supplied variable
map. Returns null when the template does not exist.
function instantiateById(
id: string,
variables?: VariableValues,
): Promise<InstantiateResult<T> | null>
id — The template ID.variables — Caller-supplied variable values.Returns: The instantiation result, or null when not found.
instantiateTemplate(template, variables)Resolves a template's snapshot against a caller-supplied variable map. Declared defaults are merged in for any variable the caller omits.
function instantiateTemplate(
template: Pick<Template, 'snapshot' | 'variables'>,
variables?: VariableValues,
): InstantiateResult<T>
template — The template to instantiate.variables — Caller-supplied variable values.Returns: The resolved payload, the merged variable map, and any unresolved variable names.
list(req, res)Lists templates with optional filtering by resourceType, tags, and
publicOnly.
function list(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — Request with optional query string filters.res — Response object.listTemplates(query)Lists templates visible to the caller, with optional filtering and pagination.
Visibility is scoped to the caller (query.viewerId): a viewer sees public
templates PLUS their own private ones — never another user's private rows.
The DataStore's buildWhere joins clauses with AND only (there is no OR
group), so the visibility OR is realised as two queries (public rows + the
viewer's own rows) merged + de-duped in memory. Tag filtering is likewise
in-memory (no native JSONB ?| operator). The table is bounded
(admin/user-managed), so the in-memory merge, sort, and pagination are fine —
the same justification used for the in-memory tag filter.
Filters compose with the visibility scope:
publicOnly=true restricts to public rows only (drops the viewer's private).createdBy=<X> is INTERSECTED with the viewer-visible set: a non-owner
createdBy still only surfaces that user's PUBLIC rows — a caller can never
page another tenant's private rows via createdBy.viewerId is omitted, only public rows are returned (fail closed).function listTemplates(query?: TemplateQuery): Promise<PaginatedResult<Template>>
query — Filters, viewer scope, and pagination.Returns: Paginated list of viewer-visible templates.
mergeVariableValues(declared, supplied)Merges declared template defaults with caller-supplied overrides. Caller values take precedence; declared defaults fill in the rest.
function mergeVariableValues(
declared: TemplateVariable[],
supplied?: VariableValues,
): VariableValues
declared — Variable declarations from the template.supplied — Caller-supplied variable map.Returns: The merged variable map.
read(req, res)Reads a single template by ID.
function read(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — Request with id path param.res — Response object.substitute(value, variables, missing)Recursively walks a JSON-like value and substitutes placeholders inside any string leaves. Non-string scalars are returned unchanged.
function substitute(value: T, variables: VariableValues, missing?: string[]): T
value — Snapshot value to walk (object, array, scalar, or null).variables — Map of variable name to value.missing — Mutable array collecting unresolved variable names.Returns: A deep clone with placeholders resolved.
substituteString(input, variables, missing)Substitutes {{variable}} placeholders inside any string value. Numbers,
booleans, and null are coerced to their JSON-string form. Unknown
variables are pushed onto missing and left as the original placeholder
text.
function substituteString(input: string, variables: VariableValues, missing?: string[]): string
input — The template string.variables — Map of variable name to value.missing — Mutable array collecting unresolved variable names.Returns: The substituted string.
update(req, res)Updates a template's mutable fields. version is bumped automatically.
function update(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — Request with id path param and patch body.res — Response object.updateTemplate(id, patch)Updates an existing template by ID. Any provided field replaces the
stored value; version is incremented when at least one field changes.
function updateTemplate(id: string, patch: UpdateTemplateInput): Promise<Template | null>
id — The template ID.patch — Fields to update.Returns: The updated template, or null when the row does not exist.
createTemplateSchemaSchema for creating a new template.
const createTemplateSchema: z.ZodObject<
{
resourceType: z.ZodString
slug: z.ZodString
name: z.ZodString
description: z.ZodOptional<z.ZodNullable<z.ZodString>>
snapshot: z.ZodUnknown
variables: z.ZodOptional<
z.ZodArray<
z.ZodObject<
{
name: z.ZodString
defaultValue: z.ZodOptional<
z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>
>
required: z.ZodOptional<z.ZodBoolean>
description: z.ZodOptional<z.ZodString>
},
z.core.$strip
>
>
>
tags: z.ZodOptional<z.ZodArray<z.ZodString>>
isPublic: z.ZodOptional<z.ZodBoolean>
},
z.core.$strip
>
instantiateSchemaSchema for the instantiate endpoint body. The variable map accepts only primitive values — placeholders inside the snapshot are string-shaped.
const instantiateSchema: z.ZodObject<
{
variables: z.ZodOptional<
z.ZodRecord<
z.ZodString,
z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>
>
>
},
z.core.$strip
>
requestHandlerMapHandler map for resource-template routes.
const requestHandlerMap: {
readonly create: typeof create
readonly list: typeof list
readonly read: typeof read
readonly update: typeof update
readonly del: typeof del
readonly instantiate: typeof instantiate
}
routesHTTP routes for template CRUD and instantiation.
const routes: readonly [
{
readonly method: 'post'
readonly path: '/resource-templates'
readonly handler: 'create'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/resource-templates'
readonly handler: 'list'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/resource-templates/:id'
readonly handler: 'read'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'patch'
readonly path: '/resource-templates/:id'
readonly handler: 'update'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/resource-templates/:id'
readonly handler: 'del'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'post'
readonly path: '/resource-templates/:id/instantiate'
readonly handler: 'instantiate'
readonly middlewares: readonly ['authenticate']
},
]
updateTemplateSchemaSchema for updating an existing template. Every field is optional;
version is bumped automatically when at least one field changes.
const updateTemplateSchema: z.ZodObject<
{
name: z.ZodOptional<z.ZodString>
description: z.ZodOptional<z.ZodNullable<z.ZodString>>
snapshot: z.ZodOptional<z.ZodUnknown>
variables: z.ZodOptional<
z.ZodArray<
z.ZodObject<
{
name: z.ZodString
defaultValue: z.ZodOptional<
z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>
>
required: z.ZodOptional<z.ZodBoolean>
description: z.ZodOptional<z.ZodString>
},
z.core.$strip
>
>
>
tags: z.ZodOptional<z.ZodArray<z.ZodString>>
isPublic: z.ZodOptional<z.ZodBoolean>
},
z.core.$strip
>
Peer dependencies:
@molecule/api-database ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-logger ^1.0.1@molecule/api-resource ^1.0.1zod ^4.0.0@molecule/api-database
@molecule/api-i18n
@molecule/api-logger
@molecule/api-resource
zod
List endpoints return a PAGINATED envelope { data, total, limit, offset }, not a
bare array — read the rows off result.data (server). On the client, unwrapList(res)
from @molecule/app-http normalizes this envelope (pass it the whole HttpResponse), so
the rows come back; reading the response as a bare array — or res.data alone (which is
the envelope) — yields an EMPTY list.
Session-auth prerequisite: every route — including reads — requires an
authenticated session; handlers read res.locals.session.userId and fail
closed with 401, so mount behind your global auth middleware. Visibility is
per-row: a template is readable when isPublic is true or the caller is its
createdBy creator (canViewTemplate), and editable/deletable ONLY by its
creator (canEditTemplate) — a non-visible row returns 404 (existence is
not leaked); a public row edited by a non-owner returns 403. createdBy is
derived from the session, never from the request body.
(resourceType, slug) is UNIQUE — a duplicate create returns 409
(template.error.conflict).
Tables: src/__setup__/resource-templates.sql creates resource-templates
(note the hyphenated table name). An mlcl-scaffolded API replays
__setup__/*.sql automatically on migrate; anywhere else run it once —
nothing at runtime creates them.