@molecule/api-resource-template
API resource · resource-template · API (Node) · v1.0.5 · Apache-2.0
Generic templates table keyed by (resource_type, snapshot).
npm install @molecule/api-resource-templateHow it works
@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/instantiateReference
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).
Auto-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.
Quick Start
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!' }
Type
resource
Installation
npm install @molecule/api-resource-template @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource zod
API
Interfaces
TemplateVariable
A 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
}
Template
A 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
}
TemplateQuery
Filters 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
}
PaginatedResult
A 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
}
CreateTemplateInput
Input 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
}
UpdateTemplateInput
Input 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
}
InstantiateResult
Result 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[]
}
Types
VariableValues
Map of variable name to substitution value.
type VariableValues = Record<string, string | number | boolean | null | undefined>
Functions
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, ornullfor anonymous viewers.
Returns: true when the template is visible.
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, ornull.
Returns: true when the user is the creator.
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.
del(req, res)
Deletes a template by ID.
function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req— Request withidpath param.res— Response object.
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 withidpath param and{ variables? }body.res— Response object.
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.
read(req, res)
Reads a single template by ID.
function read(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req— Request withidpath param.res— Response object.
update(req, res)
Updates a template's mutable fields. version is bumped automatically.
function update(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req— Request withidpath param and patch body.res— Response object.
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.
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.
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.
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.
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.
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.
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.
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=truerestricts to public rows only (drops the viewer's private).createdBy=<X>is INTERSECTED with the viewer-visible set: a non-ownercreatedBystill only surfaces that user's PUBLIC rows — a caller can never page another tenant's private rows viacreatedBy.- When
viewerIdis 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.
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.
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.
Constants
requestHandlerMap
Handler 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
}
routes
HTTP 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']
},
]
createTemplateSchema
Schema 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
>
updateTemplateSchema
Schema 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
>
instantiateSchema
Schema 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
>
Injection Notes
Requirements
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
Runtime Dependencies
-
@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 offresult.data(server). On the client,unwrapList(res)from@molecule/app-httpnormalizes this envelope (pass it the whole HttpResponse), so the rows come back; reading the response as a bare array — orres.dataalone (which is the envelope) — yields an EMPTY list. Session-auth prerequisite: every route — including reads — requires an authenticated session; handlers readres.locals.session.userIdand fail closed with 401, so mount behind your global auth middleware. Visibility is per-row: a template is readable whenisPublicis true or the caller is itscreatedBycreator (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.createdByis 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 on migrate (a CREATE TABLE IF NOT EXISTS, so a table the app's own migrations already create keeps only its own columns: ALTER in the ones it lacks); anywhere else run it once —
nothing at runtime creates them.