|
| 1 | +# Teable v2 (DDD) agent guide |
| 2 | + |
| 3 | +This repo is introducing a new `packages/v2/*` architecture. Keep `v2` strict and boring: **domain first**, **interfaces first**, **Result-only errors**, **specifications for querying**, **builders/factories for creation**. |
| 4 | + |
| 5 | +## v2 layering (strict) |
| 6 | + |
| 7 | +`packages/v2/core` is the domain/core. |
| 8 | + |
| 9 | +- **Allowed dependencies (inside `v2/core`)** |
| 10 | + - `neverthrow` for `Result` |
| 11 | + - `zod` for validation (`safeParse` only) |
| 12 | + - `nanoid` for ID generation |
| 13 | + - `@teable/v2-di` is allowed only in `src/commands/**` (application wiring), not in domain |
| 14 | + - Pure TS/JS standard library |
| 15 | +- **Forbidden inside `v2/core`** |
| 16 | + - No NestJS, Prisma, HTTP, queues, DB clients, file system, env access |
| 17 | + - No direct infrastructure code |
| 18 | + - No `throw` / exceptions for control flow |
| 19 | + |
| 20 | +Future adapters live in their own workspace packages under `packages/v2/*` and depend on `@teable/v2-core` (never the other way around). |
| 21 | + |
| 22 | +## v2 API contracts (HTTP) |
| 23 | + |
| 24 | +For HTTP-ish integrations, keep framework-independent contracts/mappers in `packages/v2/contract-http`: |
| 25 | + |
| 26 | +- Define API paths (e.g. `/tables`) as constants. |
| 27 | +- Re-export command input schemas (zod) for route-level validation if needed. |
| 28 | +- Keep DTO types + domain-to-DTO mappers here. |
| 29 | +- Router packages (e.g. `@teable/v2-contract-http-express`, `@teable/v2-contract-http-fastify`) should be thin adapters that only: |
| 30 | + - parse JSON/body |
| 31 | + - create a container |
| 32 | + - resolve handlers |
| 33 | + - call the endpoint executor/mappers from `@teable/v2-contract-http` |
| 34 | +- OpenAPI is generated from the ts-rest contract via `@teable/v2-contract-http-openapi`. |
| 35 | + |
| 36 | +## Dependency injection (DI) |
| 37 | + |
| 38 | +- Do not import `tsyringe` / `reflect-metadata` directly anywhere; use `@teable/v2-di`. |
| 39 | +- Do not use DI inside `v2/core/src/domain/**`; DI is only for application wiring (e.g. `v2/core/src/commands/**`). |
| 40 | +- Prefer constructor injection with explicit tokens for ports (interfaces). |
| 41 | +- Provide environment-level composition roots as separate packages (e.g. `@teable/v2-container-node`, `@teable/v2-container-browser`) that register all port implementations. |
| 42 | + |
| 43 | +## Build tooling (v2) |
| 44 | + |
| 45 | +- v2 packages build with `tsdown` (not `tsc` emit). `tsc` is used only for `typecheck` (`--noEmit`). |
| 46 | +- Each v2 package has a local `tsdown.config.ts` that extends the shared base config from `@teable/v2-tsdown-config`. |
| 47 | +- Outputs are written to `dist/` (CommonJS `.js` + `.d.ts`), and workspace deps (`@teable/v2-*`) are kept external (no bundling across packages). |
| 48 | + |
| 49 | +## Error handling (non-negotiable) |
| 50 | + |
| 51 | +- **Never throw in `v2/core`.** |
| 52 | +- Use `neverthrow` `Result` everywhere. |
| 53 | +- If something isn’t implemented yet: return `err('Not implemented')` (or a typed error string). |
| 54 | +- Use `zod.safeParse(...)` and convert failures into `err(...)` (no `parse()`). |
| 55 | + |
| 56 | +## Type system rules (non-negotiable) |
| 57 | + |
| 58 | +Inside `v2/core` domain APIs: |
| 59 | + |
| 60 | +- Do not use raw primitives (`string`, `number`, `boolean`) as domain parameters/returns for domain concepts. |
| 61 | +- Use **Value Objects** / **branded types** for IDs, names, and key concepts. |
| 62 | +- IDs are **nominal** (not structurally compatible): `FieldId` must not be assignable to `ViewId`. |
| 63 | +- Raw primitives are allowed only at the **outer boundary** (DTOs) and must be immediately validated and converted via factories/builders. |
| 64 | + |
| 65 | +Practical exceptions that are required by the architecture: |
| 66 | +- `neverthrow` error side uses strings (e.g. `Result<T, string>`). |
| 67 | +- The Specification interface requires `isSatisfiedBy(...): boolean`. |
| 68 | + - Value Objects may expose `toString()` / `toDate()` / `toNumber()` for adapter/serialization boundaries (avoid using these in domain logic). |
| 69 | + |
| 70 | +## Builders/factories (non-negotiable) |
| 71 | + |
| 72 | +- Do not `new Table()` / `new Field()` / `new View()` outside factories/builders. |
| 73 | +- Table creation must go through the **TableBuilder** (the public creation API). |
| 74 | +- Value Objects are created via static factory methods that validate with `zod`. |
| 75 | +- Builder configuration methods should be fluent (return the builder) and must not throw; validation/creation errors are surfaced via `build(): Result<...>`. |
| 76 | + |
| 77 | +## Specification pattern (required) |
| 78 | + |
| 79 | +Repositories query via specifications, not ad-hoc filters. |
| 80 | + |
| 81 | +- Implement `ISpecification` exactly as defined in `v2/core`. |
| 82 | +- Provide composable specs (`AndSpec`, `OrSpec`, `NotSpec`). |
| 83 | +- `accept(visitor)` is wired for future translation into persistence queries. |
| 84 | +- Build specs via entity spec builders (e.g. `Table.specs(baseId)`); do not `new` spec classes directly. |
| 85 | +- Each spec targets a single attribute (e.g. `TableByNameSpec` only checks name). `BaseId` is its own spec and is composed via `and/or/not`. |
| 86 | +- `and` and `or` must be separated by nesting (use `andGroup`/`orGroup`); never mix them at the same level. BaseId specs are auto-included by the builder unless explicitly disabled. |
| 87 | +- Spec visitors rely on `visit(spec)` + type narrowing inside the visitor; avoid per-spec visitor interfaces or `isWith*` guards. |
| 88 | + |
| 89 | +## Folder conventions (recommended) |
| 90 | + |
| 91 | +Inside `packages/v2/core/src`: |
| 92 | + |
| 93 | +- `domain/` — aggregates, entities, value objects, domain events |
| 94 | +- `specification/` — spec framework + visitors |
| 95 | +- `ports/` — interfaces/ports (repositories, event bus/publisher, mappers) |
| 96 | +- `commands/` — commands + handlers (application use-cases over domain) |
| 97 | + |
| 98 | +## Naming conventions |
| 99 | + |
| 100 | +- Value Objects: `*Id`, `*Name` (e.g. `TableId`, `FieldName`) |
| 101 | +- Commands: `*Command` |
| 102 | +- Handlers/use-cases: `*Handler` |
| 103 | +- Domain events: past tense (e.g. `TableCreated`) |
| 104 | +- Specifications: `*Spec` (e.g. `TableByIdSpec`) |
| 105 | + |
| 106 | +## Adding a new field type |
| 107 | + |
| 108 | +1. Add a new field subtype under `domain/table/fields/types/`. |
| 109 | +2. Add any new value objects/config under the same subtree. |
| 110 | +3. Extend the table builder with a new field child-builder: |
| 111 | + - add `TableFieldBuilder.<newType>()` (in `domain/table/TableBuilder.ts`) |
| 112 | + - implement a `<NewType>FieldBuilder` with fluent `with...()` methods and `done(): TableBuilder` |
| 113 | +4. Update `IFieldVisitor` (and any visitors like `NoopFieldVisitor`) to support the new field subtype. |
| 114 | +5. Update `CreateTableCommand` input validation to allow the new type. |
| 115 | + |
| 116 | +## Adding a repository adapter later |
| 117 | + |
| 118 | +1. Keep the port in `v2/core/src/ports/TableRepository.ts`. |
| 119 | +2. Implement the adapter in a separate package (e.g. `packages/v2/adapter-postgres-state`). |
| 120 | +3. Translate Specifications via a visitor (start with the stub visitor in `v2/core`). |
| 121 | +4. Map persistence DTOs <-> domain using mapper interfaces from `v2/core/src/ports/mappers`. |
| 122 | + |
| 123 | +## Testing expectations (minimal) |
| 124 | + |
| 125 | +## Testing strategy (domain → e2e) |
| 126 | + |
| 127 | +v2 uses a layered test strategy. The same behavior should usually be asserted **once** at the most appropriate layer (avoid duplicating identical assertions across many layers). |
| 128 | + |
| 129 | +### 1) Domain unit tests (`v2/core` domain) |
| 130 | + |
| 131 | +**Where** |
| 132 | +- `packages/v2/core/src/domain/**/*.spec.ts` |
| 133 | + |
| 134 | +**Focus** |
| 135 | +- Value Object validation (`.create(...)` + `zod.safeParse`) |
| 136 | +- Aggregate/entity behavior and invariants |
| 137 | +- Builder behavior (`Table.builder()...build()`), including default view behavior |
| 138 | +- Domain event creation/recording (e.g. `TableCreated`) |
| 139 | +- Specification correctness for in-memory satisfaction (`isSatisfiedBy`) |
| 140 | + |
| 141 | +**Must NOT do** |
| 142 | +- No DI/container, no repositories/ports, no DB, no HTTP, no filesystem, no timeouts |
| 143 | +- No infrastructure DTOs (HTTP/persistence) and no framework code |
| 144 | + |
| 145 | +**What to assert** |
| 146 | +- `Result` is `ok/err` (never exceptions) |
| 147 | +- Invariants on returned domain objects (counts, names, IDs are nominal types, etc.) |
| 148 | +- Domain events are produced and contain essential info (do not snapshot the entire object) |
| 149 | + |
| 150 | +### 2) Application/use-case tests (`v2/core` commands + DI) |
| 151 | + |
| 152 | +**Where** |
| 153 | +- Prefer `packages/v2/test-node/src/**/*.spec.ts` (a dedicated test package) |
| 154 | + |
| 155 | +**Focus** |
| 156 | +- Handler orchestration (build aggregate, call repository, publish events) |
| 157 | +- Correct `Result` behavior for ok/err paths |
| 158 | +- Command-level validation (invalid input → `err(...)`) |
| 159 | +- Correct wiring via DI (handlers resolved from container; do not `new Handler(...)` in tests) |
| 160 | + |
| 161 | +**Allowed** |
| 162 | +- Fakes/in-memory ports (recommended) OR the node-test container (pglite-backed) when you want a slightly higher-confidence integration without HTTP. |
| 163 | + |
| 164 | +**What to assert** |
| 165 | +- Handler returns expected status (`ok/err`) and minimal returned data (e.g. created table name) |
| 166 | +- Domain events were published (e.g. contains `TableCreated`) |
| 167 | +- Repository side-effect happened (either “save called” via fake, or “can be queried back” via `findOne(spec)`) |
| 168 | + |
| 169 | +### 3) Adapter integration tests (persistence/infra adapters) |
| 170 | + |
| 171 | +**Where** |
| 172 | +- `packages/v2/adapter-*/src/**/*.spec.ts` |
| 173 | + |
| 174 | +**Focus** |
| 175 | +- Spec → query translation via Spec Visitors (no ad-hoc where parsing) |
| 176 | +- Mapper correctness (persistence DTO ↔︎ domain) |
| 177 | +- Repository behavior against a real DB driver |
| 178 | + |
| 179 | +**Allowed** |
| 180 | +- `pglite` for tests (fast, hermetic) |
| 181 | + |
| 182 | +**What to assert** |
| 183 | +- Round-trips: save → query by spec → domain object matches essentials |
| 184 | +- Visitor builds the expected query constraints (at least for supported specs) |
| 185 | + |
| 186 | +### 4) Contract tests (`contract-http`) |
| 187 | + |
| 188 | +**Where** |
| 189 | +- `packages/v2/contract-http/src/**/*.spec.ts` (optional but recommended for mapping-heavy endpoints) |
| 190 | + |
| 191 | +**Focus** |
| 192 | +- DTO mappers and endpoint executors |
| 193 | +- Contract response shapes and status codes |
| 194 | + |
| 195 | +**What to assert** |
| 196 | +- `execute*Endpoint(...)` returns only the status codes declared in the contract |
| 197 | +- Response DTO structure matches schema intent (avoid deep snapshots) |
| 198 | + |
| 199 | +### 5) Router adapter tests (Express/Fastify) |
| 200 | + |
| 201 | +**Where** |
| 202 | +- `packages/v2/contract-http-express/src/**/*.spec.ts` |
| 203 | +- `packages/v2/contract-http-fastify/src/**/*.spec.ts` |
| 204 | + |
| 205 | +**Focus** |
| 206 | +- Framework glue: request parsing, ts-rest integration, error mapping |
| 207 | +- Container creation is correct and lazy (don’t eagerly connect to PG when a custom container is injected) |
| 208 | + |
| 209 | +**What to assert** |
| 210 | +- Valid request → expected status/result |
| 211 | +- Invalid request → 400 (schema validation) |
| 212 | + |
| 213 | +### 6) E2E tests (`v2/e2e`) |
| 214 | + |
| 215 | +**Where** |
| 216 | +- `packages/v2/e2e/src/**/*.e2e.spec.ts` |
| 217 | + |
| 218 | +**Focus** |
| 219 | +- “Over-the-wire” HTTP behavior using the generated ts-rest client |
| 220 | +- Cross-package integration: router + contract + container + repository adapter |
| 221 | + |
| 222 | +**Allowed** |
| 223 | +- Start an in-process server on an ephemeral port (no fixed ports) |
| 224 | +- Use the node-test container with `pglite` and ensure proper cleanup (`dispose`) |
| 225 | + |
| 226 | +**What to assert** |
| 227 | +- HTTP status codes and response DTOs (validate shape, not internal domain objects) |
| 228 | +- Minimal business outcome (e.g. table created, includes `TableCreated` event) |
0 commit comments