Contract helpers and wire types
Identity
Section titled “Identity”@sapporta/shared/contracts, @sapporta/rest-core, and project shared package
exports.
Contract
Section titled “Contract”- In a browser-safe project shared package, import
initContractfrom@sapporta/rest-core. Server-only modules may use the@sapporta/serverre-export, but that does not justify adding a server dependency to the shared leaf. initContract().router(...)groups query and mutation contracts. Each route declares its method, path, path parameters, query, headers, body, response schemas, summary, and metadata.- Contract paths are relative to the application API mount. A project contract
uses
/tasks/:id/complete; the parent server mount and browser base resolver supply/api. z.object({}).strict()represents an action with a required empty JSON body and rejects extra caller fields.errorBodySchemais the browser-safe generic HTTP error envelope, withErrorBodyas its type. Export a separate strict feature schema when exact codes drive typed client recovery.- The error envelope and
ApiErrorcome from the framework packages, at@sapporta/shared/contractsand@sapporta/shared/client. The project’s own shared package does not re-export them, so a contract that declares a non-2xx response importserrorBodySchemafrom@sapporta/shared/contractsdirectly. - Project contracts, schemas, and wire types are re-exported through
.jsESM barrels underpackages/shared/src/contracts/index.ts, then frompackages/shared/src/index.ts. - Table metadata wire types and auth/report contracts remain safe for browser imports.
- The shared package contains no Hono handlers, Drizzle queries, database connections, React components, or other I/O.
Generated table query contracts
Section titled “Generated table query contracts”@sapporta/shared/contracts owns the transport grammar before the server
resolves table-specific columns and Drizzle expressions:
exportRowsQuerySchemaandExportRowsQuerycover filters,q, and sort.listRowsQuerySchemaandListRowsQueryadd bounded page and limit values,fixed[col][op]conditions, and the optionaltreemode, typed bytreeMatchContextSchemaandTreeMatchContextas"ancestors"or"ancestors-and-descendants".lookupQuerySchemaandLookupQuerykeep ID recovery separate from search.countQuerySchemaandCountQuerycover filters and optional grouping.
Pagination and lookup numbers are strings at the URL and generated-client input.
The schemas coerce them at the boundary, so parsed ListRowsQuery and
LookupQuery values carry bounded numbers into server resolvers. Lookup ID mode
similarly turns the comma-separated ids string into a non-empty bounded string
array.
The accompanying constants make those defaults and bounds explicit:
DEFAULT_PAGE, DEFAULT_PAGE_SIZE, MAX_PAGE, MAX_PAGE_SIZE,
DEFAULT_LOOKUP_LIMIT, MAX_LOOKUP_LIMIT, and MAX_LOOKUP_IDS.
List and tree wire types
Section titled “List and tree wire types”paginatedRowsSchema and PaginatedRows describe the list response
{ data, meta }. listMetaSchema and ListMeta hold total, page, limit,
and pages, and an optional tree of type ListTreeMeta
(listTreeMetaSchema). A tree read with a filter or search sets it:
type ListTreeMeta = { matchCount: number; // rows that match the filter and search themselves contextIds: string[]; // ancestors present only because a descendant matched};TableSchema.tree is a TableTree (tableTreeSchema) with every field
resolved: parentColumn, column, defaultExpanded, and matchContext.
TableTreeInput is the declared shape, in which only parentColumn is
required. resolveTableTree(tree, rowLabelColumns) fills in the defaults: the
first row-label column shows the hierarchy, rows start expanded, and a filter or
search keeps each match’s ancestors and descendants.
Preserve repeated query keys
Section titled “Preserve repeated query keys”An ordinary Record<string, string> cannot represent the same filter key twice.
The main @sapporta/shared entry point therefore exports:
type QueryParamValue = string | readonly string[];type QueryParamRecord = Record<string, QueryParamValue>;appendQueryParam() keeps a singleton as a string and promotes it to an ordered
array only when a second value arrives. queryParamRecordToSearchParams()
converts that record back to repeated URL keys. Together they carry repeated
same-key filters through frontend builders, typed clients, and server adapters
without collapsing one predicate.
isQueryParamRecord() checks that an unknown object contains only string or
string-array values. hasRepeatedQueryParams() then reports whether a validated
record contains at least one repeated key.
Count result contracts
Section titled “Count result contracts”Generated count contracts export countQuerySchema, groupCountSchema,
countResultSchema, and countResponseSchema with their CountQuery,
GroupCount, CountResult, and CountResponse types. CountResult is a
discriminated union:
type CountResult = | { kind: "total"; count: number } | { kind: "grouped"; groups: Array<{ value: string | number | boolean | null; count: number; }>; };The group bound comes from MAX_COUNT_GROUPS in the main @sapporta/shared
entry point.
Feature contracts declare feature-owned responses. Shared infrastructure
authentication responses such as 401 and 403 are documented once at their
auth boundary rather than copied into every feature response map.