Primate 0.44: guided upgrades, earlier route checks and JSON dictionaries
Today we're announcing Primate 0.44. This release makes upgrades easier, catches route mistakes during builds, and adds more flexible schemas, store sequences and client navigation state.
See what to do before upgrading
Primate 0.44 adds a read-only command that tells you whether your application is up to date and, when a newer release is available, what to do next.
primate updateOn 0.44, before the next release ships, the command says that you're up to date. Once the next release ships, it shows the steps for upgrading to that release. Complete those steps, upgrade your dependencies, and run the command again to check for the next upgrade.
The command does not change your application or install anything. Because it is new in 0.44, use this post's upgrade notes when moving from 0.43 to 0.44. See Update guidance for the full command contract.
Catch invalid body routes during production builds
Production builds now catch routes that declare a body schema without its content type, so you can fix the mistake before it reaches a running application.
import p from "pema";
import route from "primate/route";
const CreatePost = p({
title: p.string,
body: p.string,
});
export default route({
post: route.with(
{ body: CreatePost, contentType: "application/json" },
async request => {
const post = await request.body.json();
return { id: 1, ...post };
},
),
});Route modules still run their top-level initialization during this check, so their imports and setup must work on the build host. See Build validation and request body validation.
Require a top-level object for JSON fields
Use p.json().dict() when a JSON field must contain an object while its nested
values may use any valid JSON shape.
import p from "pema";
const Preferences = p.json().dict();
Preferences.parse({
theme: "dark",
permissions: ["read", "write"],
dashboard: { compact: true },
});It rejects a top-level array or primitive but accepts arrays, objects and primitives inside the object.
Use p.dict() instead when every property must follow the same schema:
p.dict(); // Record<string, string>
p.dict(p.boolean); // Record<string, boolean>See JSON values for the complete comparison.
Allocate portable Store sequences
Unsigned Pema integer fields can now generate portable, persistent sequence numbers:
const Event = store({
table: "event",
db,
schema: {
id: store.key.primary(p.uuid),
number: p.u32.sequence(),
},
});
await Event.insert({}); // number: 1
await Event.insert({ number: 10 }); // number: 10
await Event.insert({}); // number: 11Omit the field to use the next number, or provide a higher value to advance the
sequence. Counters work across batches and concurrent writers and survive row
deletion. Gaps can occur after failed inserts, so do not use sequences where
gapless numbering is required. The API is available on u8, u16, u32,
u64 and u128. See integer sequences.
Carry private context with a history entry
Client navigation can now carry private, temporary context with a browser history entry without adding it to the URL.
import navigate from "primate/navigate";
await navigate(`/tasks/${task.id}`, {
state: { returnTo: "/tasks?status=open" },
});The destination reads the current value through navigate.state(). Completion
listeners also receive it as state alongside source and url.
const unsubscribe = navigate.after(({ source, state, url }) => {
console.log({ source, state, url });
});State follows the entry through Back and Forward navigation but is absent from copied URLs, new tabs and full refreshes. Use it for temporary context such as a return destination, not persistent preferences. See client navigation state.
Resolve application aliases in config and migration builds
Aliases from compilerOptions.paths now work when Primate loads configuration,
stores and migration inputs.
{
"compilerOptions": {
"paths": {
"@db": ["./config/db.ts"],
"@stores/*": ["./stores/*"]
}
}
}
import db from "@db";
export default db;This applies to code loaded by Primate. Scripts run directly by Node still need
relative imports or package imports beginning with #. See Database
configuration.
Run Go applications from installed packages
Installed @primate/go packages now load their bundled WebAssembly support
correctly, so importing @primate/go/env no longer fails before the application
starts. This fix is also available to Primate 0.43 applications in
@primate/go@0.12.1. See the Go backend guide.
Upgrade notes
Migrate from the retired HTML and Web Components frontends
Primate no longer includes the @primate/html and @primate/webc frontends.
Applications using .html or .webc views should migrate those views to any
other supported frontend, such as Marko, HTMX, Voby, React or Svelte. Remove the
retired frontend modules from the application configuration and package
manifest after migrating every view.
Add contentType beside body schemas
Search route declarations for route.with options that contain body but omit
contentType. Production builds now reject those declarations. Add the media
type that matches how the handler reads the body, commonly
application/json for request.body.json().
route.with(
{ body: Input, contentType: "application/json" },
handler,
);
Choose the intended dictionary contract
Existing p.json() schemas continue to accept every JSON-compatible top-level
shape. Add .dict() only when arrays and primitives should be rejected at the
outer boundary. Keep using p.dict(value) when every property value must pass
the same non-JSON-field schema.
Keep direct Node imports resolvable
Aliases configured in tsconfig.json work in the Primate config and migration
bundle, but not in a module launched directly by Node. Keep direct scripts on
relative or # package imports instead of relying on an application alias.
Fin
If you like Primate, consider joining our Discord server, visiting us in #primate on irc.libera.chat, or starring us on GitHub.