Skip to content

Commit 045cfeb

Browse files
committed
refactor: refactoring core and make create table work
1 parent 482ecbf commit 045cfeb

File tree

287 files changed

+13006
-67
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

287 files changed

+13006
-67
lines changed

.vscode/settings.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,15 @@
5656
},
5757
{
5858
"pattern": "./packages/*/"
59+
},
60+
{
61+
"pattern": "./packages/v2/*/"
5962
}
6063
],
61-
"vitest.maximumConfigs": 10
64+
"vitest.maximumConfigs": 50,
65+
"vitest.nodeEnv": {
66+
"DOCKER_HOST": "unix:///Users/nichenqin/.colima/default/docker.sock",
67+
"TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE": "/var/run/docker.sock",
68+
"TESTCONTAINERS_HOST_OVERRIDE": "127.0.0.1"
69+
}
6270
}

agents.md

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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)

apps/nestjs-backend/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
"@nestjs/terminus": "10.2.3",
151151
"@nestjs/websockets": "10.3.5",
152152
"@openrouter/ai-sdk-provider": "1.2.3",
153+
"@orpc/nest": "1.13.0",
153154
"@opentelemetry/api": "1.9.0",
154155
"@opentelemetry/exporter-logs-otlp-http": "0.201.1",
155156
"@opentelemetry/exporter-metrics-otlp-http": "0.201.1",
@@ -171,6 +172,13 @@
171172
"@teable/core": "workspace:^",
172173
"@teable/db-main-prisma": "workspace:^",
173174
"@teable/openapi": "workspace:^",
175+
"@teable/v2-container-node": "workspace:*",
176+
"@teable/v2-contract-http": "workspace:*",
177+
"@teable/v2-contract-http-openapi": "workspace:*",
178+
"@teable/v2-contract-http-implementation": "workspace:*",
179+
"@teable/v2-core": "workspace:*",
180+
"@teable/v2-db-postgres": "workspace:*",
181+
"@teable/v2-di": "workspace:*",
174182
"@teamwork/websocket-json-stream": "2.0.0",
175183
"@valibot/to-json-schema": "1.3.0",
176184
"ai": "6.0.0-beta.137",

apps/nestjs-backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { TemplateOpenApiModule } from './features/template/template-open-api.mod
4444
import { TrashModule } from './features/trash/trash.module';
4545
import { UndoRedoModule } from './features/undo-redo/open-api/undo-redo.module';
4646
import { UserModule } from './features/user/user.module';
47+
import { V2Module } from './features/v2/v2.module';
4748
import { GlobalModule } from './global/global.module';
4849
import { InitBootstrapProvider } from './global/init-bootstrap.provider';
4950
import { LoggerModule } from './logger/logger.module';
@@ -94,6 +95,7 @@ export const appModules = {
9495
PluginChartModule,
9596
ObservabilityModule,
9697
BuiltinAssetsInitModule,
98+
V2Module,
9799
],
98100
providers: [InitBootstrapProvider],
99101
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { OnModuleDestroy } from '@nestjs/common';
2+
import { Injectable } from '@nestjs/common';
3+
import { ConfigService } from '@nestjs/config';
4+
import { createV2NodePgContainer } from '@teable/v2-container-node';
5+
import { v2PostgresDbTokens } from '@teable/v2-db-postgres';
6+
import type { DependencyContainer } from '@teable/v2-di';
7+
8+
@Injectable()
9+
export class V2ContainerService implements OnModuleDestroy {
10+
private containerPromise?: Promise<DependencyContainer>;
11+
12+
constructor(private readonly configService: ConfigService) {}
13+
14+
async getContainer(): Promise<DependencyContainer> {
15+
if (!this.containerPromise) {
16+
const connectionString = this.configService.getOrThrow<string>('PRISMA_DATABASE_URL');
17+
this.containerPromise = createV2NodePgContainer({ connectionString });
18+
}
19+
20+
return this.containerPromise;
21+
}
22+
23+
async onModuleDestroy(): Promise<void> {
24+
if (!this.containerPromise) return;
25+
26+
const container = await this.containerPromise;
27+
const db = container.resolve<{ destroy(): Promise<void> }>(v2PostgresDbTokens.db);
28+
await db.destroy();
29+
}
30+
}

0 commit comments

Comments
 (0)