You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Breaking changes to the generated API
extractors: db/http/ answers a rejected body with the wire standard
of ADR 0016, {"error": {"code", "message", "details"?}}, on both axum
and actix-web. The success field is gone and the codes are snake_case: 400bad_request (was MALFORMED_BODY), 413payload_too_large
(was PAYLOAD_TOO_LARGE) and 422validation_failed (was VALIDATION_FAILED). In db::http, ErrorBody loses success, ErrorPayload serializes code before message, the constant MALFORMED_BODY is now BAD_REQUEST, and ErrorBody::new(code, message)
builds a body for any other error answer.
Added
[api.immutable]: columns set on create and never changed by the
generated update, by table, in the config file and the JSON payload
(ApiConfig::immutable). They stay in CreateXDto and the response,
leave UpdateXDto and its OpenAPI schema, and a PATCH naming one
answers 422 validation_failed with the detail code immutable. The
data layer's UpdateX keeps them. A key, owner, tenant or managed
column fails the run. The actix template makes users.email immutable,
so its contract no longer lists the update body's email.
The Release workflow publishes the crate to crates.io once every binary
builds, using the CARGO_REGISTRY_TOKEN repository secret. A version
crates.io already has is skipped, so running a tag again doesn't fail.
The [api] configuration for generated API features (ADR 0016): output (-a/--api-output), db_path, prefix, search_endpoint, exclude, and the table-keyed [api.skip], [api.routes], [api.access], [api.owner], [api.hidden], [api.search] and [api.sort], in the config file and the JSON payload. The library takes
it as Config::api (ApiConfig, ApiAccess, ApiEndpoint), checked
against the schema: an owner on a public table, or a search or sort
column that is hidden, fails the run.
With api.output, a run generates the infrastructure of ADR 0016: db/pool.rs (PoolSettings, init_pool, check_health) and, in shared/ beside the features directory, response.rs
(PaginatedResponse and the error body), pagination.rs (ListQuery, Pagination, SortOrder), validation.rs, tenant.rs (Tenant<T>) and health.rs (/health/live, /health/ready). Paths into the data
layer follow api.db_path.
API features (ADR 0016): with api.output, every table but join tables
and api.excluded ones gets api.output/<module>/ with mod.rs, routes.rs, handlers.rs, dto.rs (CreateXDto, UpdateXDto, XResponse, ListXsQuery), model.rs, repo.rs, service_api.rs (the XsServiceApi trait with a default per endpoint, each write split into <write>_in on the caller's connection and <write> in a transaction)
and a scaffolded service.rs; plus generated.rs (register_services, configure_all) and README.md. Endpoints: GET / (page, per_page, q, sort, order), POST / (201 with Location), and GET, PATCH and DELETE /{key}; [api.skip] drops any. The data layer
gains <table>/listing.rs (XSort, list_matching, count_matching)
and search::push_contains_any; shared/ gains response::path_segment
and validation::{path_config, query_config}. Generated files under api.output and shared/ are removed once stale; scaffolded ones never
are. The scaffolded service has XsService::new(pool), which register_services calls.
Access, owners and hidden columns in API features: handlers take AuthUser ([api.access]user, the default; 401 without a valid
token) or AdminUser (admin; 403 for a caller who isn't an admin),
or no caller (public). On an owned table ([api.owner]) the service
methods take the AuthUser, every read is scoped to the caller's id, update_in and remove_in lock the caller's row before writing (another
caller's row is a 404), and create sets the owner, which neither body
holds (CreateXDto::into_new(owner)). [api.hidden] columns stay in
both bodies but never appear in XResponse. listing.rs gains get_owned and lock_owned.
API features serve the rest of ADR 0016's routes, each skippable in [api.skip]: GET /by-<column>/{value} per single-column unique key that
isn't hidden; POST /search ({filter, sort, order, page, per_page})
with api.search_endpoint, whose filter is the data layer's less hidden
columns (a 422 naming the field) and relationship filters; relationship
routes to tables with a feature (GET /{key}?include=<name> for a to-one, GET /{key}/<name> for a to-many, answering a bare JSON array, and for
a many-to-many PUT /{key}/<name> sync and PUT/DELETE /{key}/<name>/{other}), answering
through the related feature's response under its access and owner; and
on soft-delete tables POST /{key}/restore and DELETE /{key}?permanent=true. Versioned tables answer ETag and need If-Match
on PATCH (428 without, 412 when stale); tenant-scoped tables take Tenant<T>. shared/ gains response::etag and validation::{IfMatch, IdList}; generated.rs re-exports the responses
relationship routes use, and listing.rs gains the search reads, owned_by/owned/retain_owned, and the join tables' writes.
rok-sqlc init [DIR] scaffolds the actix-web application skeleton of
ADR 0016 (rok-sqlc.toml, migrations/, src/{lib,main,app,config,error}.rs, src/shared/{mod,auth}.rs, src/features/mod.rs and AGENTS.md), each
file only when absent. It generates nothing. The application is a library
crate (lib.rs declares the modules pub, main.rs starts it), so a
fresh project passes cargo clippy --all-targets -- -D warnings.
rok-sqlc init after cargo new replaces cargo's untouched default src/main.rs (hello world) with the scaffolded one and says so; any other src/main.rs is kept.
rok-sqlc check-arch (a) checks a project against ADR 0016's
architecture, reading api.output and api.db_path from rok-sqlc.toml (-c/--config): the feature layout (eight files, no utils/helpers/common, no unknown top-level directory under src/;
warnings under -l/--lenient) and, parsing the source with syn, rules
1, 2, 4, 5 and 6 and the cross-feature write rule. It prints each
violation as path:line: rule: message and exits 1 if there are any. init's AGENTS.md states exactly the rules it enforces.
Scaffolded files: a planned file starting with the scaffold header is
written only when absent, never rewritten, never reported as changed and
never stale; --check reports a missing one. Report::scaffolded lists
the ones written.
primary_key_ids = "cuid2" (IdGeneration::Cuid2, ADR 0016) makes new
text primary keys with cuid::cuid2() (24 characters): the typed id gets generate(), and create fills a key the input leaves out. UUID keys
are left alone. db/mod.rs hints cuid = "2".
The API guide in docs/guide/: the architecture guidelines, with the
rules check-arch enforces, and which files are generated, scaffolded or
yours. The README gains a "Generated APIs" section. Their samples come
from docs/examples/api, a project started with init, which the test
suite regenerates, compiles and runs check-arch on.
The OpenAPI document of generated API features (ADR 0016), with utoipa 5
and utoipa-swagger-ui 9, on by default and off with [api] openapi = false (ApiConfig::openapi), which leaves output with no utoipa at all.
Every type a feature exposes derives ToSchema (query strings IntoParams), as do the data layer's typed ids, enums, sort keys, search
conditions and ErrorBody. Every generated handler carries #[utoipa::path]: its absolute path, a tag per feature (the route base),
the handler's name as operation id, its params and body, security(("bearer_auth" = [])) when it needs a caller, and exactly the
statuses it answers, each error with ErrorBody. generated.rs gains GeneratedApiDoc and the BearerAuth modifier (HTTP bearer, JWT). init scaffolds src/openapi.rs (ApiDoc for hand-written paths and document(), merging the generated document in), and its app.rs
serves /api-docs/openapi.json and Swagger UI at /docs/. The
dependency hints add utoipa (with the features the columns need) and utoipa-swagger-ui with vendored, so builds need no network. Health's
handlers are liveness and readiness, the target document's
operation ids. The guide gains an OpenAPI page.
A list takes and documents q only when its table has [api.search]
columns: the others' query string is the new shared PageQuery, which
has no q (an unknown parameter is ignored, as before). The bearer_auth
scheme has a description, and no OpenAPI description shows rustdoc link
syntax.
With [api] openapi = false, rok-sqlc g warns while the scaffolded src/openapi.rs still uses GeneratedApiDoc (Report::warnings in the
library), and the OpenAPI guide lists the scaffolded lines to remove.
The OpenAPI document reads like the target: PaginatedResponse.total has minimum: 0, and path parameters are described as " id"
("User id") for a single key and " " for a
composite key's columns and a unique lookup's value; a many-to-many
route's {other} is the related row's id.
rok-sqlc init --template <NAME> (-t) copies a ready-made project from
the templates built into the binary (templates/ in the repository; the
only one is actix), each file only when absent and without its
generated files, and prints the next steps. An unknown name lists the
valid ones.
The actix template (templates/actix-api/): the users and audit-log part of the axum starter's API, generated from one schema,
with a contract test that checks the document it serves against the
starter's (tests/contract.rs), API tests against PostgreSQL, and a
README. CI regenerates it and fails on drift, and runs its lint gate, check-arch and tests.
The rest of the API guide: getting started, configuring API features,
customizing the generated API, the wire standard, testing, deploying,
after a schema change, and troubleshooting, linked from the README. docs/examples/api gains a hand-written feature (stats, with its own
OpenAPI path) and service and API tests with #[sqlx::test], which the
test suite runs when ROK_SQLC_TEST_DATABASE_URL is set.