diff --git a/crates/jerrycan/embedded/ai/00-designing.md b/crates/jerrycan/embedded/ai/00-designing.md index 95737b3..969e410 100644 --- a/crates/jerrycan/embedded/ai/00-designing.md +++ b/crates/jerrycan/embedded/ai/00-designing.md @@ -324,11 +324,12 @@ Every entity has an `id` primary key; you usually do NOT declare it: entity RESPONSE shape is unchanged. Three drop reasons — a body can hit several at once: 1. **Identity fk (guarded):** the body `belongs_to` the auth identity entity — - which MUST be named literally `User`, so the derived fk is `user_id` (see - 10-auth.md: an identity named anything else gets NO owner-scoping and its fk - stays client-writable) — AND the endpoint is guarded → `user_id` is omitted; the - handler injects the session user's id. An unguarded endpoint keeps it (no session - to inject). + named by `auth.identity` (default `"User"`, so the derived fk is `user_id`; + set e.g. `auth.identity: "Account"` and the fk becomes `account_id`) — AND the + endpoint is guarded → the identity fk is omitted; the handler injects the + session principal's id. A non-`User` identity gets the SAME owner-scoping and + server-injected fk, derived from the name (see 10-auth.md). An unguarded + endpoint keeps it (no session to inject). 2. **`default` field:** any field with a `default` is omitted; the server applies the declared value. Works on unguarded/public creates too — this is what lets `POST /subscribers { "email": … }` succeed while `confirmed` and `status` default diff --git a/crates/jerrycan/embedded/ai/05-errors.md b/crates/jerrycan/embedded/ai/05-errors.md index 29e773a..fc1a23a 100644 --- a/crates/jerrycan/embedded/ai/05-errors.md +++ b/crates/jerrycan/embedded/ai/05-errors.md @@ -96,7 +96,7 @@ take no argument. `job_failed` carries code `JC0521` but responds with HTTP 500. | JC0409 | 409 | `Error::conflict` — unique-key violation (a re-POSTed id), version conflict | | JC0413 | 413 | Body over the limit (default 1 MiB) | | JC0415 | 415 | `Error::unsupported_media_type` — wrong content type (e.g. multipart without a boundary) | -| JC0422 | 422 | JSON body failed to parse, or `Valid` found violations (structured `details` array) | +| JC0422 | 422 | JSON body failed to parse, `Valid` found violations (structured `details` array), or a foreign-key violation — a client-supplied fk referencing a nonexistent record (jerrycan-db) | | JC0429 | 429 | `Error::too_many_requests` — rate limit exceeded (jerrycan::ratelimit) | | JC0500 | 500 | `Error::internal` / response serialization failure | | JC0503 | 503 | Handler exceeded its time budget (default 30s), or `Error::handler_timeout` | diff --git a/crates/jerrycan/embedded/ai/08-database.md b/crates/jerrycan/embedded/ai/08-database.md index 9eb708c..405dfed 100644 --- a/crates/jerrycan/embedded/ai/08-database.md +++ b/crates/jerrycan/embedded/ai/08-database.md @@ -357,9 +357,11 @@ that surface. ## Errors you'll hit - A unique-key violation surfaces as `409 JC0409` (a re-POSTed id is the - client's fault); every other database failure is `500 JC0510`. Neither leaks - internals in the body — the real SeaORM/sqlx error goes to stderr for the - operator. Always `.map_err(db_error)?` so both codes happen for free. + client's fault); a foreign-key violation — a client-supplied fk pointing at a + row that does not exist — is `422 JC0422` (the request is well-formed but + references a nonexistent record); every other database failure is `500 JC0510`. + None leaks internals in the body — the real SeaORM/sqlx error goes to stderr for + the operator. Always `.map_err(db_error)?` so all three codes happen for free. - A failing migration stops the run and is NOT recorded — fix it and rerun. ## Anti-patterns diff --git a/crates/jerrycan/embedded/ai/09-validation.md b/crates/jerrycan/embedded/ai/09-validation.md index a51b1eb..67fafea 100644 --- a/crates/jerrycan/embedded/ai/09-validation.md +++ b/crates/jerrycan/embedded/ai/09-validation.md @@ -59,8 +59,19 @@ assert_eq!(body["details"][0]["field"], "text"); - `OpenApi::new(include_str!("../../../openapi.json"))` as an extension serves the platform-generated document at `GET /openapi.json` (generated apps wire this when the design lists `"validate"`). +- **Declarative field constraints** cover the common range/length rules with no + `Validate` impl at all: `min`/`max` (inclusive integer bounds, integer fields) + and `min_len`/`max_len` (inclusive string length in Unicode code points, string + fields) are first-class design fields (feature #80 / `JC0552`, since 0.6.5). An + out-of-range value is rejected at the request boundary as `422 JC0422` + automatically, and the same bound emits into OpenAPI + (`minimum`/`maximum`/`minLength`/`maxLength`), a DDL `CHECK`, and out-of-range + testgen reject probes — so the constraint is enforced and documented from one + declaration. Hand-write the `Validate` trait only for the custom rules the + declarative constraints can't express (cross-field checks, the non-empty-after- + trim example above). - Rule-attribute `derive(Validate)` is a contract-v1 candidate — today you - implement the trait by hand (the design schema has no field constraints yet). + implement the trait by hand for those custom rules. ## Errors you'll hit - `422 JC0422` with `details: [{field, message}]` — exactly what `Valid` diff --git a/crates/jerrycan/embedded/ai/13-error-codes.md b/crates/jerrycan/embedded/ai/13-error-codes.md index f76e169..c061b57 100644 --- a/crates/jerrycan/embedded/ai/13-error-codes.md +++ b/crates/jerrycan/embedded/ai/13-error-codes.md @@ -15,7 +15,7 @@ the cause + fix for any of them. | JC0409 | Conflict — the write violates a unique key (jerrycan-db) | | JC0413 | Payload too large — body over the route limit (default 1 MiB), or a multipart part over the per-part cap (8 MiB), >256 parts, or part headers over 8 KiB | | JC0415 | Unsupported media type — content type is not what the endpoint consumes (e.g. `Multipart` needs `multipart/form-data` with a boundary; a storage bucket upload must match the bucket's `allowed_mime` allowlist) | -| JC0422 | Unprocessable — bad JSON or validation violations | +| JC0422 | Unprocessable — bad JSON, validation violations, or a foreign-key violation (a client-supplied fk referencing a nonexistent record; jerrycan-db) | | JC0429 | Too many requests — the client exceeded its rate limit for the current window (the rate-limit extension); the response carries a `Retry-After` header | | JC0500 | Internal error (or handler panic) | | JC0503 | Handler timeout (default 30s) | diff --git a/crates/jerrycan/embedded/ai/15-jobs.md b/crates/jerrycan/embedded/ai/15-jobs.md index b462576..ed063ab 100644 --- a/crates/jerrycan/embedded/ai/15-jobs.md +++ b/crates/jerrycan/embedded/ai/15-jobs.md @@ -127,7 +127,7 @@ assert!(send_email(t.task_context()).await.is_ok()); ## Variations - **Wiring**: the generated `crates/jobs/src/lib.rs` builds the extension — - `Jobs::postgres(db).queue("email", 4).register("send_email", f).cron("expire_trials", "0 * * * *", "billing")` + `Jobs::postgres(db).queue("email", 2).register("send_email", f).cron("expire_trials", "0 * * * *", "billing")` — and the app installs it with `app.extend(jobs(db))`. `cron(job, expr, queue)` takes the job name, the 5-field cron expression, and the queue it enqueues onto (it is a builder taking `self` and PANICS at build time on an invalid `expr`). diff --git a/docs/ai/00-designing.md b/docs/ai/00-designing.md index 95737b3..969e410 100644 --- a/docs/ai/00-designing.md +++ b/docs/ai/00-designing.md @@ -324,11 +324,12 @@ Every entity has an `id` primary key; you usually do NOT declare it: entity RESPONSE shape is unchanged. Three drop reasons — a body can hit several at once: 1. **Identity fk (guarded):** the body `belongs_to` the auth identity entity — - which MUST be named literally `User`, so the derived fk is `user_id` (see - 10-auth.md: an identity named anything else gets NO owner-scoping and its fk - stays client-writable) — AND the endpoint is guarded → `user_id` is omitted; the - handler injects the session user's id. An unguarded endpoint keeps it (no session - to inject). + named by `auth.identity` (default `"User"`, so the derived fk is `user_id`; + set e.g. `auth.identity: "Account"` and the fk becomes `account_id`) — AND the + endpoint is guarded → the identity fk is omitted; the handler injects the + session principal's id. A non-`User` identity gets the SAME owner-scoping and + server-injected fk, derived from the name (see 10-auth.md). An unguarded + endpoint keeps it (no session to inject). 2. **`default` field:** any field with a `default` is omitted; the server applies the declared value. Works on unguarded/public creates too — this is what lets `POST /subscribers { "email": … }` succeed while `confirmed` and `status` default diff --git a/docs/ai/05-errors.md b/docs/ai/05-errors.md index 29e773a..fc1a23a 100644 --- a/docs/ai/05-errors.md +++ b/docs/ai/05-errors.md @@ -96,7 +96,7 @@ take no argument. `job_failed` carries code `JC0521` but responds with HTTP 500. | JC0409 | 409 | `Error::conflict` — unique-key violation (a re-POSTed id), version conflict | | JC0413 | 413 | Body over the limit (default 1 MiB) | | JC0415 | 415 | `Error::unsupported_media_type` — wrong content type (e.g. multipart without a boundary) | -| JC0422 | 422 | JSON body failed to parse, or `Valid` found violations (structured `details` array) | +| JC0422 | 422 | JSON body failed to parse, `Valid` found violations (structured `details` array), or a foreign-key violation — a client-supplied fk referencing a nonexistent record (jerrycan-db) | | JC0429 | 429 | `Error::too_many_requests` — rate limit exceeded (jerrycan::ratelimit) | | JC0500 | 500 | `Error::internal` / response serialization failure | | JC0503 | 503 | Handler exceeded its time budget (default 30s), or `Error::handler_timeout` | diff --git a/docs/ai/08-database.md b/docs/ai/08-database.md index 9eb708c..405dfed 100644 --- a/docs/ai/08-database.md +++ b/docs/ai/08-database.md @@ -357,9 +357,11 @@ that surface. ## Errors you'll hit - A unique-key violation surfaces as `409 JC0409` (a re-POSTed id is the - client's fault); every other database failure is `500 JC0510`. Neither leaks - internals in the body — the real SeaORM/sqlx error goes to stderr for the - operator. Always `.map_err(db_error)?` so both codes happen for free. + client's fault); a foreign-key violation — a client-supplied fk pointing at a + row that does not exist — is `422 JC0422` (the request is well-formed but + references a nonexistent record); every other database failure is `500 JC0510`. + None leaks internals in the body — the real SeaORM/sqlx error goes to stderr for + the operator. Always `.map_err(db_error)?` so all three codes happen for free. - A failing migration stops the run and is NOT recorded — fix it and rerun. ## Anti-patterns diff --git a/docs/ai/09-validation.md b/docs/ai/09-validation.md index a51b1eb..67fafea 100644 --- a/docs/ai/09-validation.md +++ b/docs/ai/09-validation.md @@ -59,8 +59,19 @@ assert_eq!(body["details"][0]["field"], "text"); - `OpenApi::new(include_str!("../../../openapi.json"))` as an extension serves the platform-generated document at `GET /openapi.json` (generated apps wire this when the design lists `"validate"`). +- **Declarative field constraints** cover the common range/length rules with no + `Validate` impl at all: `min`/`max` (inclusive integer bounds, integer fields) + and `min_len`/`max_len` (inclusive string length in Unicode code points, string + fields) are first-class design fields (feature #80 / `JC0552`, since 0.6.5). An + out-of-range value is rejected at the request boundary as `422 JC0422` + automatically, and the same bound emits into OpenAPI + (`minimum`/`maximum`/`minLength`/`maxLength`), a DDL `CHECK`, and out-of-range + testgen reject probes — so the constraint is enforced and documented from one + declaration. Hand-write the `Validate` trait only for the custom rules the + declarative constraints can't express (cross-field checks, the non-empty-after- + trim example above). - Rule-attribute `derive(Validate)` is a contract-v1 candidate — today you - implement the trait by hand (the design schema has no field constraints yet). + implement the trait by hand for those custom rules. ## Errors you'll hit - `422 JC0422` with `details: [{field, message}]` — exactly what `Valid` diff --git a/docs/ai/13-error-codes.md b/docs/ai/13-error-codes.md index f76e169..c061b57 100644 --- a/docs/ai/13-error-codes.md +++ b/docs/ai/13-error-codes.md @@ -15,7 +15,7 @@ the cause + fix for any of them. | JC0409 | Conflict — the write violates a unique key (jerrycan-db) | | JC0413 | Payload too large — body over the route limit (default 1 MiB), or a multipart part over the per-part cap (8 MiB), >256 parts, or part headers over 8 KiB | | JC0415 | Unsupported media type — content type is not what the endpoint consumes (e.g. `Multipart` needs `multipart/form-data` with a boundary; a storage bucket upload must match the bucket's `allowed_mime` allowlist) | -| JC0422 | Unprocessable — bad JSON or validation violations | +| JC0422 | Unprocessable — bad JSON, validation violations, or a foreign-key violation (a client-supplied fk referencing a nonexistent record; jerrycan-db) | | JC0429 | Too many requests — the client exceeded its rate limit for the current window (the rate-limit extension); the response carries a `Retry-After` header | | JC0500 | Internal error (or handler panic) | | JC0503 | Handler timeout (default 30s) | diff --git a/docs/ai/15-jobs.md b/docs/ai/15-jobs.md index b462576..ed063ab 100644 --- a/docs/ai/15-jobs.md +++ b/docs/ai/15-jobs.md @@ -127,7 +127,7 @@ assert!(send_email(t.task_context()).await.is_ok()); ## Variations - **Wiring**: the generated `crates/jobs/src/lib.rs` builds the extension — - `Jobs::postgres(db).queue("email", 4).register("send_email", f).cron("expire_trials", "0 * * * *", "billing")` + `Jobs::postgres(db).queue("email", 2).register("send_email", f).cron("expire_trials", "0 * * * *", "billing")` — and the app installs it with `app.extend(jobs(db))`. `cron(job, expr, queue)` takes the job name, the 5-field cron expression, and the queue it enqueues onto (it is a builder taking `self` and PANICS at build time on an invalid `expr`).