Repository navigation
docs: show where uniqueness-style rules belong - #150
Conversation
Add a how-to separating a state invariant, a command precondition and a database constraint, with a runnable check-then-create example in billing-persistence. Two concurrent creates both pass the preflight lookup; the store's write-time unique check rejects one, and that violation is qualified into a typed SlugTaken while an unavailable store stays a defect. Closes #39 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 21 minutes. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (6)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
It adds only low-risk documentation and a well-tested example (core package untouched), with the sole finding being a minor non-compiling doc snippet.
Review effort: Balanced
Findings: 1
What changed in this PR
This PR answers issue #39 ("where do uniqueness-style rules belong?") with documentation and a runnable example rather than any library change. It establishes a three-layer separation — a closed-world state invariant (in invariants), an open-world command precondition (preflight lookup, feedback only), and a database constraint (the unique index, the only layer that holds under concurrency) — and shows how to map a known unique-violation back to a typed SlugTaken error while keeping infrastructure failures in the Defect channel. The packages/entity core is deliberately untouched (no async predicates, no repository in construction), so no changeset is included.
Changes:
- Adds a runnable
uniqueness.tsexample (registerOrganization+ in-memory store standing in for a unique index) with a deterministic concurrency spec asserting[201, 409]. - Adds a new how-to guide (
enforce-uniqueness.md) plus README and example-doc sections, and wires the how-to into the VitePress sidebar.
| File | Description |
|---|---|
| examples/billing-persistence/src/uniqueness.ts | New example: typed SlugTaken, in-memory store, check-then-create use case routing unexpected rejections to Defect. |
| examples/billing-persistence/src/uniqueness.spec.ts | Spec covering preflight duplicate, concurrent-race enforcement, invariant short-circuit, and store-down Defect. |
| examples/billing-persistence/README.md | Adds a "Uniqueness" section summarizing the example and linking the how-to. |
| docs/how-to/enforce-uniqueness.md | New how-to guide separating state invariant, precondition, and DB constraint. |
| docs/examples/billing-persistence.md | Adds a "Uniqueness" section cross-linking the how-to. |
| docs/.vitepress/config.ts | Adds the new how-to to the shared guide sidebar. |
The example's unthrown API usage (fromPromise/fromSafePromise/.toAsync/.ensure), the Entity.CreateInput/Entity.InvalidEntity namespace types, all doc cross-links, and the concurrency test's determinism were verified. One minor documentation snippet uses a zod schema value in type position; see the inline comment.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The store handed raw promises to the use case, which pulled the violation back out with `instanceof`. The port now returns AsyncResult: an adapter triages the driver rejection once, at its own `fromPromise` boundary, into a `UniqueViolation` TaggedError or a defect, and the use case maps the violation with `mapErrCases`. Also type `SlugTaken`'s payload as `Organization["slug"]` in the how-to, where `Slug` named a value in type position. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Closes #39
Summary
docs/how-to/enforce-uniqueness.md, added to the how-to sidebar). It separates a state invariant (closed-world, ininvariants), a command precondition (preflight lookup in the use case, feedback only), and a database constraint (the unique index, the only one that holds under concurrency). It links Ecto's validations/constraints as prior art and Let an invariant carry a domain error code, not just a message #70 for stable error codes.examples/billing-persistence/src/uniqueness.ts:registerOrganizationbuilds anOrganization, runs a preflightslugIsTaken, then inserts. An in-memory store that checks and writes in one synchronous step stands in for a DB unique index. The docs say plainly that a real unique index plays that role.SlugTaken(TaggedError) whether the preflight or the constraint caught it. It is deliberately not anInvalidEntity. Only aUniqueViolationon the named constraint is qualified that way. Every other store rejection, and any failed lookup, stays a Defect.docs/examples/billing-persistence.md.packages/entityis unchanged: no async predicates and no repository in construction, so there is no changeset.Test plan
uniqueness.spec.ts: two concurrent creates are held until both lookups answer. Both see the slug free, exactly one write lands, and the outcome is[201, 409].pnpm format --check && pnpm lint && pnpm typecheck && pnpm test && pnpm knip && pnpm build.🤖 Generated with Claude Code