Contract helpers and wire types
View as MarkdownIdentity
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.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.
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.