Fields & representations¶
Layer: Backend · data shapes (the fields inside a resource)
Fields are a resource’s columns. A representation is a named field-set — the shape an op returns or accepts. You write the field lists in jsonnet; codegen emits the pydantic schemas, the TypeScript types, and a scaffolded field catalog you fill in with how each column renders.
How it works¶
Each representation becomes a pydantic schema and a TypeScript type. On the frontend, codegen scaffolds a field catalog once — you fill in how each column renders.
The config you write¶
You write — be/config/inventory_resources/asset.jsonnet (field types + representations):
local fields = import "be/fields.libsonnet";
{
representations: [
{
name: "default", // lean cross-resource shape: refs, autocomplete, saved views
exclude_actions: true, // omit the per-row `actions` envelope
fields: [
fields.id(), // { name: "id", type: "uuid" }
{ name: "name", type: "str" },
{ name: "serial_number", type: "str" },
{ name: "status", type: "enum", enum: "inventory.models.AssetStatus" }, // see enums.md
],
},
{
name: "resource", // GET /assets/{id}
fields: [
fields.id(),
{ name: "name", type: "str" },
{ name: "location", type: "str", nullable: true }, // nullable -> `T | None`
{ name: "documentation_url", type: "str", nullable: true },
{ name: "status", type: "enum", enum: "inventory.models.AssetStatus" },
{ name: "category_id", type: "uuid" },
// Many-to-one dump; joined = one SQL JOIN (right pick for scalars).
// selectin is the default; pass many=true for collections.
fields.nested("category", "inventory.models.Category", [
fields.id(),
{ name: "name", type: "str" },
], load="joined"),
],
},
{ name: "list_item", fields: [/* rows for POST /assets/search */] },
],
// Map a representation to a role. `default` = the lean shape above;
// `opa` = the projection shipped to OPA on every check (see permissions.md).
representation_roles: { default: "default", opa: "authz" },
}
Field-list helpers, from be/fields.libsonnet — usable anywhere a fields: list appears:
fields.id(), // uuid PK; fields.id("int") for integer PKs
fields.timestamps(), // splices created_at + updated_at datetimes
// Exact fixed-point. Renders as fsh_lib.numeric.DecimalString: exact in Python,
// crosses the wire as a precision-safe *string* (TS string). precision/scale
// bound it like NUMERIC(p, s); scale requires precision.
fields.decimal("unit_price", precision=12, scale=2, nullable=false),
// Frozen-dataclass column exposed to the API as a typed pydantic schema.
fields.composite("location", {
schema_module: "fsh_lib.geo", schema_class: "CoordinateSchema",
column_value_module: "codegen_database.types", column_value_class: "Coordinate",
}, nullable=true),
// Related-model dump. many=true for collections, load="joined"|"selectin".
fields.nested("tasks", "fsh.models.Task", [fields.id(), { name: "title", type: "str" }], many=true),
Primitive types seen across the example app: str bool int float uuid
datetime enum json decimal nested composite. enum needs a dotted
enum: path (enums); a rep tagged opa/comms_summary feeds
permissions / emails and templates.
The frontend codegen emits¶
Each representation becomes a TypeScript type — regenerated every run:
Generated — fe/src/_generated/types/inventory/asset.gen.ts (do not edit):
export type ListItemAssetResource = {
type?: "asset";
id: string;
name: string;
serial_number: string;
status: AssetStatus; // the enum union, from the enum field
category: ListItemAssetResourceCategoryNested; // the nested dump
actions: Array<AssetObjectPermission | AssetCollectionPermission>;
};
export type ResourceAssetResource = { // the `resource` rep: adds location, documentation_url, ...
/* ...scalars... */
category: ResourceAssetResourceCategoryNested;
actions: Array<AssetObjectPermission | AssetCollectionPermission>;
};
Codegen then scaffolds a field catalog once (if_exists=skip); you own it
after. This is where the render utilities live — Badge + an enum
display’s badgeProps, formatDate, formatNumber, internal/external to
links, and detail-only fields typed off the detail rep:
Scaffolded, safe to edit — fe/src/fields/inventory/asset.tsx:
import { Badge, defineFields, formatDate, formatNumber } from "@fsh/components-library";
import { assetStatus } from "../../enums/assetStatus";
import { urls } from "../../routes";
import type {
ListItemAssetResource,
ResourceAssetResource,
} from "../../_generated/types/inventory/asset.gen";
// Cells default to the list row (ListItemAssetResource) — most fields need no
// per-cell type. A detail-only field annotates its own read off the detail rep.
export const assetFields = defineFields<ListItemAssetResource>()(
(field) => [
field("name", {
header: "Name",
cell: (row) => row.name,
table: { rowHeader: true, sortable: true, resizable: true, width: "lg" },
}),
field("status", {
header: "Status",
// Badge colour + label come from the enum display, not hand-written here.
cell: (row) => <Badge {...assetStatus.badgeProps(row.status)} />,
table: { resizable: true, width: "sm" },
}),
field("category", {
header: "Category",
cell: (row) => row.category.name, // flatten the nested dump
to: (row) => urls.categories.detail.href({ id: row.category.id }), // internal link
table: { resizable: true, width: "md" },
}),
field("documentation_url", {
header: "Documentation",
// Detail-only: pick the field off the detail rep (not on the list row).
cell: (row: Pick<ResourceAssetResource, "documentation_url">) =>
row.documentation_url ? "Product manual" : null,
to: (row: Pick<ResourceAssetResource, "documentation_url">) => row.documentation_url,
external: true, // external link
}),
field("acquired_sample", {
header: "Acquired",
cell: () => formatDate("2026-06-11T12:00:00", "localeDate"), // date formatting util
}),
field("replacement_value_sample", {
header: "Replacement value",
cell: () => formatNumber(1250.5, { currency: "USD" }), // number/currency util
}),
],
// Command-palette / card summary: which fields form the title + subtitle
// (the status entry reuses its Badge cell above).
(f) => ({ title: f.name, subtitle: [f.serial_number, f.status] }),
);
A computed cell can run its own query — e.g. user.tsx resolves role ids to
names with useListRoles and renders <TextBits items={...} /> (non-suspense
so a cell never suspends). How the catalog feeds columns, sorting, and gating is
table configuration.