Skip to content

Build, test, and choose the next guide

View as Markdown

The tutorial now spans schema, generated records, access rules, an atomic workflow, a typed client, a custom dashboard, a route-based report, and scoped operator access. This page validates those pieces as one application. You will check migrations and builds, exercise the live API’s success and failure paths, confirm transaction and row-scope invariants, and inspect the browser at two viewports before choosing the guide for the next feature.

Run a release-style validation of the task app. Check migrations and builds, exercise the generated and app-owned APIs including their failure paths, confirm the dashboard and report in the browser, and summarize anything that is not ready.

Start with artifacts that can be checked offline

Section titled “Start with artifacts that can be checked offline”

The database schema, shared contracts, API handlers, and frontend imports form one dependency chain. Check them in that order:

Terminal window
pnpm --filter ./packages/api db:check
pnpm --filter ./packages/shared typecheck
pnpm --filter ./packages/api typecheck
pnpm build

db:check validates migration metadata. It does not apply pending migrations. The production build compiles the browser-safe shared package before the API and frontend consume it. A failure in packages/shared often means a wire contract or exported type changed; a frontend failure can reveal a typed client that no longer matches that contract.

Run the focused API tests from the workflow and report pages:

Terminal window
pnpm --filter ./packages/api exec vitest run \
app/complete-task.test.ts \
modules/reports/project-progress.test.ts

The complete-task suite should cover a successful transition, a repeated completion conflict, a missing or invisible task, and a forced event-insert failure. The last case proves rollback: the task remains open when the history row cannot be inserted. The report suite should parse the full result with gridDatasetSchema and fix today to a known Temporal.PlainDate when it asserts overdue totals.

import { Temporal } from "@sapporta/shared/temporal";
import { gridDatasetSchema } from "@sapporta/shared/grid-dataset";
const dataset = projectProgressDataset({
projects: [{ id: 1, name: "Website relaunch" }],
tasks: [
{
project_id: 1,
status: "open",
due_date: Temporal.PlainDate.from("2026-07-09"),
},
{
project_id: 1,
status: "completed",
due_date: Temporal.PlainDate.from("2026-07-10"),
},
],
today: Temporal.PlainDate.from("2026-07-10"),
});
gridDatasetSchema.parse(dataset);
expect(dataset.nodes[0]?.columns).toMatchObject({
total: 2,
completed: 1,
overdue: 1,
completion: 0.5,
});

The tutorial route and tests use the documented 2026-07-10 as-of date. A continuing application can expose that date as report input or derive it from Temporal.Now.plainDateISO(). All parsing, comparison, arithmetic, and formatting for dates and timestamps continues to use Temporal.

Start the built app or the development server. Then query the mounted surface:

Terminal window
pnpm dev
pnpm exec sapporta tables list
pnpm exec sapporta endpoints list
pnpm exec sapporta endpoints show "GET /api/tables/tasks"
pnpm exec sapporta endpoints show "PUT /api/tables/tasks/{id}"
pnpm exec sapporta endpoints show "POST /api/tasks/{id}/complete"
pnpm exec sapporta endpoints show "GET /api/reports/project-progress"

This step catches two deployment mistakes that TypeScript cannot: a TsRestApi sub-app that was not mounted with route(), and contract emitters that were not composed with extend(). Direct calls come after discovery and use the mounted /api paths.

Create one open task through the generated API. The request contains domain fields only; the server derives workspace ownership:

Terminal window
pnpm exec sapporta rows create tasks --values '{
"project_id": 1,
"title": "Publish release notes",
"status": "open",
"priority": "medium",
"due_date": "2026-08-01"
}'
pnpm exec sapporta rows list tasks \
--where '{"status":{"eq":"open"}}' \
--output json

A representative list response retains the generated envelope:

{
"data": [
{
"id": 7,
"project_id": 1,
"title": "Publish release notes",
"status": "open",
"priority": "medium",
"due_date": "2026-08-01"
}
],
"meta": { "total": 1, "page": 1, "limit": 50, "pages": 1 }
}

Use PUT /api/tables/tasks/7 for another ordinary row edit. Do not substitute PATCH. Use the app-owned endpoint for completion because that transition also creates immutable history:

Terminal window
pnpm exec sapporta api post /api/tasks/7/complete --body '{}'
pnpm exec sapporta api post /api/tasks/7/complete --body '{}'
pnpm exec sapporta rows list task_events \
--where '{"task_id":{"eq":7}}' \
--output json
pnpm exec sapporta api get /api/reports/project-progress

The first completion returns 200 with the completed task and event. The second returns the declared conflict body, and the event query still returns one completion event:

{
"error": "Task is already completed",
"code": "TASK_ALREADY_COMPLETED"
}

The report footer should agree with the generated task and event reads. Its exact total depends on the smoke-test rows created and completed in this pass. The generated list, history query, and report are three views of the same state, not independent fixtures.

Run the same read checks with a Workspace A token after seeding one visibly named task in Workspace B. The generated list and report must omit Workspace B. Call complete-task with that hidden task ID and with an unused ID. Both results must follow the same declared missing-row behavior.

Authentication, feature abilities, and row visibility are separate assertions:

  • no session or token receives the documented authentication error;
  • a signed-in user without the workflow ability receives the declared authorization error;
  • an authorized Workspace A caller receives only Workspace A rows; and
  • clients never submit workspace_id, owner, role, or scope columns.

The forced transaction failure belongs in an automated test because it needs a controlled database error. The SQLite transaction callback remains synchronous, uses insertValuesSync() for the history write, and contains no awaited external effect.

Sign in and open each generated table once, then exercise the custom routes:

  1. Open /tables/projects, create or edit a project, and follow its Tasks child relationship.
  2. Open /tables/tasks, filter and sort the worklist, and inspect one task’s Task history.
  3. Open /projects/progress, complete an open task, and observe the updated project total and action state.
  4. Open /reports/project-progress, run the full report, select a project filter, reload the URL, and follow project and status links back to generated records.

Check a narrow viewport and a desktop viewport. Loading, empty, ready, expected error, and refreshed states should remain explicit. Browser Network requests for the custom routes should use /api/tasks/:id/complete and /api/reports/project-progress; the React routes themselves do not include /api.

The finished app has three workspace-scoped tables, generated CRUD surfaces, an atomic multi-table transition, a shared contract and typed client, a protected workflow screen, a scoped report, and a revocable operating path. The important constraints remain visible in the final checks: server-authored scope, row-secured base reads, synchronous SQLite transactions, Temporal values, declared error bodies, explicit route composition, and frontend-owned report links.

Choose the next guide by the boundary that needs to change:

  • Model data for tables, relationships, validation, semantic values, and migrations.
  • Generated surfaces for ordinary record screens and table-aware Grid workflows.
  • Security for abilities, workspaces, ownership, and row visibility.
  • App-owned features for business transitions, transactions, uploads, external effects, and custom response shapes.
  • Reports for scoped datasets, filters, formatting, and drill-through links.
  • Discovery and operations for CLI, agents, OpenAPI, configuration, builds, and deployment.

The guide index explains those workflows. The reference index is the lookup path for exact contracts, types, routes, and options.