Repository navigation
Releases: ateeq1999/rok-sqlc
Release list
rok-sqlc 0.5.0
Breaking changes to the generated API
- The
existspre-check: a create or update body's foreign key that names
no row the caller can see answers422 validation_failed, with
detailskeyed by the column, codeexistsand the message
<column> does not refer to an existing <singular>, instead of
409 conflictnaming the constraint. A row counts as missing when it
doesn't exist, is soft-deleted, is in another tenant, or belongs to
another caller, so a422never confirms a row the caller couldn't read.
The caller's tenant and id are used even when the referencing table
lacks the column: a feature whose routes need a caller passes it to the
check of a reference to an owned table, and a feature without a tenant
column takes the request'sTenantfor a reference to a tenant-scoped
table, so that create's or update's service method and handler gain
userortenant.nulland absent fields are skipped, and every
failure is listed in one answer. The generatedcreate_inand
update_inrun the checks on their transaction before writing; the
constraint's409stays for a row deleted in between. A reference from a
public route to an owned table (no caller: only existence, liveness and
tenant are checked), or to anything but the whole single-column key, is
left to the constraint (ADR 0017). - The
uniquepre-check: a create or update body holding a value a unique
key of the table already has answers422 validation_failed, with
detailskeyed by the column, codeuniqueand the message
<column> is already taken, instead of409 conflictnaming the
constraint. A composite key is reported against its last column, as
<col a> and <col b> are already taken together. The check runs on the
write's transaction with theexistschecks, so one422lists both
kinds. It stays in the caller's tenant, takes a key's owner column from
the caller ((owner_id, name)is checked per owner, reported onname),
never counts the row being updated, counts only live rows for a partial
index on<soft-delete column> IS NULL, and compares a key's
lower(<column>)entry ignoring case. Only keys whose columns the body
holds (present and notnull) are checked; aPATCHholding part of a
composite key is left to the constraint, whose409also stays for a
race. To opt out, overridecreate_inorupdate_inin the feature's
service.rs.
Added
shared/validation.rshasFieldFailures, the collector of a write's
pre-check failures, answered as one422(missing,takenand
taken_togetheradd theexistsanduniquefailures). An owned
table'sdb/<table>/listing.rshasexists_owned, the check of a
reference to one of its rows, and a table whose feature checks a unique
key has<columns>_taken(executor, [tenant], values.., [owner], except).- Unique indexes with a
lower(<column>)entry are case-insensitive
unique keys: one whose only entry islower(<column>), or one of plain
columns and exactly onelower(<column>)entry, such as
(tenant_id, lower(name))(any casing oflower, the column optionally
quoted). The usual partial-index rule applies (WHERE deleted_at IS NULL
counts under the soft-delete convention). Their lookups, such as
read::get_by_email, orread::get_by_namewithin the tenant, compare
that column withlower(col) = lower($n), and so does the API's
by-<col>route. Other expression indexes are still ignored, and
ON CONFLICT (<cols>)doesn't match such a key (PostgreSQL needs the
expression). Output changes only for schemas with such an index: the
actix template'susersgainsget_by_email(its skippedlookupskeep
the route out), and its update now documents409. In the module corpus,
the(organization_id, lower(name))indexes of
accounting.expense_categories,crm.customer_segments,
customers.customer_groupsandhr.leave_typesgive each a
read::get_by_namewithin the tenant;core.users' index (its condition
isn't only the soft-delete one) andmarketplace.vendor_delivery_areas'
(twolower()entries) still give none. [api.rules]: string rules and transforms by VineJS name, by table and
text column, in the config file and the JSON payload (ApiConfig::rules,
ApiRule).CreateXDtoandUpdateXDtocheck them (validator's
emailandip, otherwise functions generated intodto.rs, calling
helpersshared/validation.rsholds once for every feature), a failed
rule answering422 validation_failedwith the rule's name as the detail
code;trim,toLowerCase,toUpperCaseandnormalizeEmailapply
while the body is read, so the transformed value is stored.urlports
validator.js'sisURLdefaults, as VineJS checks: the scheme may be left
out (example.compasses) but must behttp,httpsorftp, and the
host needs a top-level domain unless it is an IP address.mobiletakes
locales, or none for any of the 16 it carries (narrower than VineJS's
every locale). The OpenAPI document showsformat(noturiforurl),
patternand lengths.regex,mobileandpostalCodeadd a
regex = "1"hint. The actix template trims and lowercasesusers.email
and trimsdisplay_name.- ADR 0017 records the validation rules, the
409to422trade-off and
what is still left to the constraints. The configuring guide has a
validation section, and the wire standard lists every detail code. - The tool depends on
regex-syntax, to checkregexpatterns.
rok-sqlc 0.4.0
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. Thesuccessfield is gone and the codes are snake_case:
400bad_request(wasMALFORMED_BODY),413payload_too_large
(wasPAYLOAD_TOO_LARGE) and422validation_failed(was
VALIDATION_FAILED). Indb::http,ErrorBodylosessuccess,
ErrorPayloadserializescodebeforemessage, the constant
MALFORMED_BODYis nowBAD_REQUEST, andErrorBody::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 inCreateXDtoand the response,
leaveUpdateXDtoand its OpenAPI schema, and aPATCHnaming one
answers422 validation_failedwith the detail codeimmutable. The
data layer'sUpdateXkeeps them. A key, owner, tenant or managed
column fails the run. The actix template makesusers.emailimmutable,
so its contract no longer lists the update body'semail.- The Release workflow publishes the crate to crates.io once every binary
builds, using theCARGO_REGISTRY_TOKENrepository 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 asConfig::api(ApiConfig,ApiAccess,ApiEndpoint), checked
against the schema: an owner on apublictable, 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
(PaginatedResponseand 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 followapi.db_path. - API features (ADR 0016): with
api.output, every table but join tables
andapi.excluded ones getsapi.output/<module>/withmod.rs,
routes.rs,handlers.rs,dto.rs(CreateXDto,UpdateXDto,
XResponse,ListXsQuery),model.rs,repo.rs,service_api.rs(the
XsServiceApitrait with a default per endpoint, each write split into
<write>_inon the caller's connection and<write>in a transaction)
and a scaffoldedservice.rs; plusgenerated.rs(register_services,
configure_all) andREADME.md. Endpoints:GET /(page,per_page,
q,sort,order),POST /(201withLocation), andGET,
PATCHandDELETE /{key};[api.skip]drops any. The data layer
gains<table>/listing.rs(XSort,list_matching,count_matching)
andsearch::push_contains_any;shared/gainsresponse::path_segment
andvalidation::{path_config, query_config}. Generated files under
api.outputandshared/are removed once stale; scaffolded ones never
are. The scaffolded service hasXsService::new(pool), which
register_servicescalls. - Access, owners and hidden columns in API features: handlers take
AuthUser([api.access]user, the default;401without a valid
token) orAdminUser(admin;403for a caller who isn't an admin),
or no caller (public). On an owned table ([api.owner]) the service
methods take theAuthUser, every read is scoped to the caller's id,
update_inandremove_inlock the caller's row before writing (another
caller's row is a404), andcreatesets the owner, which neither body
holds (CreateXDto::into_new(owner)).[api.hidden]columns stay in
both bodies but never appear inXResponse.listing.rsgains
get_ownedandlock_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})
withapi.search_endpoint, whose filter is the data layer's less hidden
columns (a422naming 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-manyPUT /{key}/<name>sync andPUT/DELETE /{key}/<name>/{other}), answering
through the related feature's response under its access and owner; and
on soft-delete tablesPOST /{key}/restoreandDELETE /{key}?permanent=true. Versioned tables answerETagand needIf-Match
onPATCH(428without,412when stale); tenant-scoped tables take
Tenant<T>.shared/gainsresponse::etagand
validation::{IfMatch, IdList};generated.rsre-exports the responses
relationship routes use, andlisting.rsgains 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.rsandAGENTS.md), each
file only when absent. It generates nothing. The application is a library
crate (lib.rsdeclares the modulespub,main.rsstarts it), so a
fresh project passescargo clippy --all-targets -- -D warnings.rok-sqlc initaftercargo newreplaces cargo's untouched default
src/main.rs(hello world) with the scaffolded one and says so; any other
src/main.rsis kept.rok-sqlc check-arch(a) checks a project against ADR 0016's
architecture, readingapi.outputandapi.db_pathfrom
rok-sqlc.toml(-c/--config): the feature layout (eight files, no
utils/helpers/common, no unknown top-level directory undersrc/;
warnings under-l/--lenient) and, parsing the source withsyn, rules
1, 2, 4, 5 and 6 and the cross-feature write rule. It prints each
violation aspath:line: rule: messageand exits 1 if there are any.
init'sAGENTS.mdstates 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;--checkreports a missing one.Report::scaffoldedlists
the ones written. primary_key_ids = "cuid2"(IdGeneration::Cuid2, ADR 0016) makes new
text primary keys withcuid::cuid2()(24 characters): the typed id gets
generate(), andcreatefills a key the input leaves out.UUIDkeys
are left alone.db/mod.rshintscuid = "2".- The API guide in
docs/guide/: the architecture guidelines, with the
rulescheck-archenforces, and which files are generated, scaffolded or
yours. The README gains a "Generated APIs" section. Their samples come
fromdocs/examples/api, a project started withinit, which the test
suite regenerates, compiles and runscheck-archon. - 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 derivesToSchema(query strings
IntoParams), as do the data layer's typed ids, enums, sort keys, search
conditions andErrorBody. 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 withErrorBody.generated.rsgains
GeneratedApiDocand theBearerAuthmodifier (HTTP bearer, JWT).
initscaffoldssrc/openapi.rs(ApiDocfor hand-written paths and
document(), merging the generated document in), and itsapp.rs
serves/api-docs/openapi.jsonand Swagger UI at/docs/. The
dependency hints addutoipa(with the features the columns need) and
utoipa-swagger-uiwithvendored, so builds need no network. Health's
handlers arelivenessandreadiness, the target document's
operation ids. The guide gains an OpenAPI page. - A list takes and documents
qonly when its table has[api.search]
columns: the others' query string is the new sharedPageQuery, which
has noq(an unknown parameter is ignored, as before). Thebearer_auth
scheme has a description, and no OpenAPI description shows rustdoc link
syntax. - With
[api] openapi = false,rok-sqlc gwarns while the scaffolded
src/openapi.rsstill usesGeneratedApiDoc(Report::warningsin the
library), and the OpenAPI guide lists the scaffolded lines to remove. - The OpenAPI document reads like the target:
PaginatedResponse.totalhas
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 isactix), each file only when absent and without its
generated files, and prints the next steps. An unknown name lists the
valid ones.- The
actixtemplate (templates/actix-api/): theusersand
audit-logpart of the axum starter's API, generated from one schema,
with a contract test that checks the do...
rok-sqlc 0.3.0
Validated bodies and HTTP extractors (ADR 0015). A run without the new
validate derive and extractors key generates byte-for-byte what 0.2.1
generates.
Added
-
derive = ["serde", "validate"](-d validate,Derive::Validate) adds
validator::Validate(validator 0.21) to every create input and update
patch, with rules taken from the schema and English messages naming the
column ("name must be at most 80 characters"):VARCHAR(n),CHARACTER VARYING(n),CHAR(n)andCHARACTER(n)give
length(max = n), counted in characters;- a single-column
CHECKcomparingchar_length(col),
character_length(col)orlength(col)with an integer gives a
lengthbound, combined with the declared length (the tighter bounds
win, andequalwhen they meet); - a single-column
CHECKcomparing an integer or float column with a
literal (>=,>,<=,<, either side),BETWEEN, or anANDof
these givesrange. A strict bound on an integer becomes inclusive
(> 0ismin = 1); on a float it stays strict (exclusive_min), and
one that can't be represented exactly is dropped; - on a
NUMERICcolumn the range is acustomrule calling a generated
<field>_in_rangefunction, with the coderangeandrange's
min/maxparams, since validator'srangecan't compareDecimal; - a typed id over text implements
validator::ValidateLength<u64>, and a
typed id over an integervalidator::ValidateRange, so a column's
lengthorrangerule applies through the id and through foreign keys
using it. A typed id overNUMERICgets no range rule.
Patch fields are checked only when present. Other
CHECKs,NOT NULL,
enums and value lists are not rules.db/mod.rsnames thevalidator
dependency.validatewithoutserdefails the run. -
extractors = ["axum", "actix_web"](the JSON payload'sextractors, the
repeatable-e/--extractor <axum|actix_web>flag, and
Config::extractorswith the new#[non_exhaustive]Extractor, part of
the stable library API) generatesdb/http/, which needsvalidate:db::http::axum::ValidatedJson<T>(axum 0.8) and
db::http::actix_web::ValidatedJson<T>(actix-web 4), extractors for
anyT: DeserializeOwned + Validate: a generated input or patch, or a
hand-written request type. Each reads the body through its framework's
own JSON extractor (axum::Json<T>,web::Json<T>), so axum's
DefaultBodyLimitand actix-web'sJsonConfig(limit and
Content-Typepredicate) apply; both limits default to 2 MiB;BodyRejection, answered in the Rok error envelope
{"success": false, "error": {"message", "code", "details"?}}:400
MALFORMED_BODYfor a missing or wrongContent-Type, bad JSON syntax,
or a body that can't be read;413PAYLOAD_TOO_LARGEfor a body over
the framework's size limit, with the message
"the request body is over the size limit"from both;422
VALIDATION_FAILEDwith serde's message and nodetailsfor JSON that
doesn't fit the type;422VALIDATION_FAILEDwith validator's errors
asdetailsfor a broken rule. Each framework's rejection is classified
in its module, with the message taken from serde's own error (never the
framework's text), so both answer the same body with the same status,
code and message;ErrorBody,ErrorPayload, the three codes,
BodyRejection::from_serde, andvalidated(body), which checks a
deserialized body against its rules.
With actix-web, no
JsonConfigerror handler is needed.db/mod.rsnames
theaxum,serde_path_to_error(whose error wraps serde's inside axum's
rejection),actix-webandserde_jsondependencies.extractorswithout
validatefails the run, and an unknown framework lists the valid ones.
Withextractors,httpis a reserved module name: a table whose module
would behttpfails with an error that says so (rename it with
[modules]). Withoutextractors, such a table generates as before. -
The schema model keeps each text column's declared length and the simple
CHECKbounds above, throughCREATE TABLE,ADD COLUMN,ALTER COLUMN … TYPE(which replaces or clears them),ADD CONSTRAINT, and renames. -
The README has a "Validated bodies and HTTP extractors" section, from the
new example cratedocs/examples/validated-api.