{ "module": "crud_template", "content": { "path": "crud_template", "docstring": "# CRUD Template\n\n## Summary\n\nEvery resource follows the same four-file slice. Repetition is intentional: it\nmakes the codebase predictable and generatable.\n\n---\n\n## The slice\n\n```\ndomain//\n models.py # pure Pydantic model (no DB fields)\n ports/\n db.py # document model + repository factory\n service.py # service class + factory\n converters.py # (optional) transforms\n```\n\n## The id rule (model vs. document)\n\nTwo different objects, two different jobs:\n\n| Object | Lives in | Has `id`? | Purpose |\n|---|---|---|---|\n| **Domain model** | `domain//models.py` | **No** | business shape, framework-free |\n| **Document** | `domain//ports/db.py` | **Yes** (from `BaseDocument`) | Mongo persistence shape |\n\n`BaseDocument` (mongo-ops) supplies `id: Optional[PyObjectId]` (aliased to\n`_id`) plus `created_at` / `updated_at`. The domain model must **not** declare\n`id`; Mongo owns it.\n\n> **Trap — generated wire models.** `openapi-first models` produces the HTTP\n> *contract* (see the [Build Recipe](01_build_recipe.md), step 3). Response\n> schemas frequently declare a required `id: str`, so a generated class is\n> **not** a domain model. If you write\n> `class ItemDocument(Item, BaseDocument)` and `Item` re-exports a required\n> `id: str`, that field shadows `BaseDocument.id` and no Mongo `_id` can ever be\n> set. Fix it one of three ways:\n>\n> 1. **Preferred:** keep the domain model id-free (hand-author it from the CRUD\n> template), and keep the required `id` only on the wire model.\n> 2. Order the bases so the document wins:\n> `class ItemDocument(BaseDocument, Item)`.\n> 3. Keep a separate document type and translate with `ports/converters.py`.\n\n## models.py\n\nA framework-free Pydantic model describing the business shape (no `id`).\n\n```python\nclass Item(BaseModel):\n name: str\n quantity: int = 0\n```\n\n## ports/db.py\n\nThe persistence model extends the business model and adds database concerns.\nThe repository factory selects the right repository flavour.\n\n```python\n# Item is id-free; BaseDocument adds id/created_at/updated_at.\nclass ItemDocument(Item, BaseDocument):\n pass\n\ndef create_item_repo(*, owner_repo=None) -> ItemRepository:\n return repo_factory(\n collection=\"items\",\n document=ItemDocument,\n populate_rules=[PopulateRule(field=\"owner\", collection=\"users\")],\n cache=None, # or a CachedBaseRepository config\n )\n```\n\n## ports/service.py\n\nThe service is built through the DI library, injecting the repository. The\ngeneric CRUD base provides `create/read/update/patch/delete/search/...`.\n\n```python\nclass ItemService(CrudsServiceAdaptor[Item, ItemDocument]):\n model = Item\n schema = ItemDocument\n\ndef get_item_service(repo) -> ItemService:\n return build(ItemService, instances={\"repo\": repo})\n```\n\n## Naming conventions\n\n| Concept | Convention |\n|---|---|\n| collection | plural snake case (`items`) |\n| document | `Document` |\n| repository factory | `create__repo` |\n| service | `Service` |\n| service factory | `get__service` |\n| resource key | `_svc` |\n| operationId | `list_items`, `create_item`, ... |\n\n## Optional files\n\n- `converters.py` — pure functions mapping between domain types.\n- `ports/enrichment.py` — adapts a pipeline into an async callable the service\n can invoke (used when a resource is populated by a pipeline).\n", "objects": {} } }