Skip to content

Releases: ateeq1999/rok-sqlc

rok-sqlc 0.5.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 17:44

Breaking changes to the generated API

  • The exists pre-check: a create or update body's foreign key that names
    no row the caller can see answers 422 validation_failed, with
    details keyed by the column, code exists and the message
    <column> does not refer to an existing <singular>, instead of
    409 conflict naming 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 a 422 never 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's Tenant for a reference to a tenant-scoped
    table, so that create's or update's service method and handler gain
    user or tenant. null and absent fields are skipped, and every
    failure is listed in one answer. The generated create_in and
    update_in run the checks on their transaction before writing; the
    constraint's 409 stays 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 unique pre-check: a create or update body holding a value a unique
    key of the table already has answers 422 validation_failed, with
    details keyed by the column, code unique and the message
    <column> is already taken, instead of 409 conflict naming 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 the exists checks, so one 422 lists 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 on name),
    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 not null) are checked; a PATCH holding part of a
    composite key is left to the constraint, whose 409 also stays for a
    race. To opt out, override create_in or update_in in the feature's
    service.rs.

Added

  • shared/validation.rs has FieldFailures, the collector of a write's
    pre-check failures, answered as one 422 (missing, taken and
    taken_together add the exists and unique failures). An owned
    table's db/<table>/listing.rs has exists_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 is lower(<column>), or one of plain
    columns and exactly one lower(<column>) entry, such as
    (tenant_id, lower(name)) (any casing of lower, 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, or read::get_by_name within the tenant, compare
    that column with lower(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's users gains get_by_email (its skipped lookups keep
    the route out), and its update now documents 409. In the module corpus,
    the (organization_id, lower(name)) indexes of
    accounting.expense_categories, crm.customer_segments,
    customers.customer_groups and hr.leave_types give each a
    read::get_by_name within the tenant; core.users' index (its condition
    isn't only the soft-delete one) and marketplace.vendor_delivery_areas'
    (two lower() 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). CreateXDto and UpdateXDto check them (validator's
    email and ip, otherwise functions generated into dto.rs, calling
    helpers shared/validation.rs holds once for every feature), a failed
    rule answering 422 validation_failed with the rule's name as the detail
    code; trim, toLowerCase, toUpperCase and normalizeEmail apply
    while the body is read, so the transformed value is stored. url ports
    validator.js's isURL defaults, as VineJS checks: the scheme may be left
    out (example.com passes) but must be http, https or ftp, and the
    host needs a top-level domain unless it is an IP address. mobile takes
    locales, or none for any of the 16 it carries (narrower than VineJS's
    every locale). The OpenAPI document shows format (not uri for url),
    pattern and lengths. regex, mobile and postalCode add a
    regex = "1" hint. The actix template trims and lowercases users.email
    and trims display_name.
  • ADR 0017 records the validation rules, the 409 to 422 trade-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 check regex patterns.

rok-sqlc 0.4.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 12:50
e4d7f19

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:
    400 bad_request (was MALFORMED_BODY), 413 payload_too_large
    (was PAYLOAD_TOO_LARGE) and 422 validation_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 do...
Read more

rok-sqlc 0.3.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 22:40
2339c82

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) and CHARACTER(n) give
      length(max = n), counted in characters;
    • a single-column CHECK comparing char_length(col),
      character_length(col) or length(col) with an integer gives a
      length bound, combined with the declared length (the tighter bounds
      win, and equal when they meet);
    • a single-column CHECK comparing an integer or float column with a
      literal (>=, >, <=, <, either side), BETWEEN, or an AND of
      these gives range. A strict bound on an integer becomes inclusive
      (> 0 is min = 1); on a float it stays strict (exclusive_min), and
      one that can't be represented exactly is dropped;
    • on a NUMERIC column the range is a custom rule calling a generated
      <field>_in_range function, with the code range and range's
      min/max params, since validator's range can't compare Decimal;
    • a typed id over text implements validator::ValidateLength<u64>, and a
      typed id over an integer validator::ValidateRange, so a column's
      length or range rule applies through the id and through foreign keys
      using it. A typed id over NUMERIC gets no range rule.

    Patch fields are checked only when present. Other CHECKs, NOT NULL,
    enums and value lists are not rules. db/mod.rs names the validator
    dependency. validate without serde fails the run.

  • extractors = ["axum", "actix_web"] (the JSON payload's extractors, the
    repeatable -e/--extractor <axum|actix_web> flag, and
    Config::extractors with the new #[non_exhaustive] Extractor, part of
    the stable library API) generates db/http/, which needs validate:

    • db::http::axum::ValidatedJson<T> (axum 0.8) and
      db::http::actix_web::ValidatedJson<T> (actix-web 4), extractors for
      any T: 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
      DefaultBodyLimit and actix-web's JsonConfig (limit and
      Content-Type predicate) apply; both limits default to 2 MiB;
    • BodyRejection, answered in the Rok error envelope
      {"success": false, "error": {"message", "code", "details"?}}: 400
      MALFORMED_BODY for a missing or wrong Content-Type, bad JSON syntax,
      or a body that can't be read; 413 PAYLOAD_TOO_LARGE for a body over
      the framework's size limit, with the message
      "the request body is over the size limit" from both; 422
      VALIDATION_FAILED with serde's message and no details for JSON that
      doesn't fit the type; 422 VALIDATION_FAILED with validator's errors
      as details for 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, and validated(body), which checks a
      deserialized body against its rules.

    With actix-web, no JsonConfig error handler is needed. db/mod.rs names
    the axum, serde_path_to_error (whose error wraps serde's inside axum's
    rejection), actix-web and serde_json dependencies. extractors without
    validate fails the run, and an unknown framework lists the valid ones.
    With extractors, http is a reserved module name: a table whose module
    would be http fails with an error that says so (rename it with
    [modules]). Without extractors, such a table generates as before.

  • The schema model keeps each text column's declared length and the simple
    CHECK bounds above, through CREATE 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 crate docs/examples/validated-api.