You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .agents/rules/data-access.md
+36-15Lines changed: 36 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,26 +2,45 @@
2
2
3
3
## Principle
4
4
5
-
All database access**MUST** go through a **service** (`packages/business/`) or **repository** (`packages/database/src/repositories/`). No app-layer code (`apps/builder`, `apps/worker`) may import `db` from `@chatbotx.io/database/client` and execute queries directly.
5
+
The chain is:**action / API handler → service (`packages/business/`) → repository (`packages/database/src/repositories/`) → DB**. No app-layer code (`apps/builder`, `apps/worker`, `integrations/`) may import `db` from `@chatbotx.io/database/client` and execute queries directly. The one exception is a **pure read with zero business logic** — see the carve-out below.
6
6
7
7
## Why
8
8
9
9
-**Centralized logic:** Business rules, cache invalidation, and event emission stay in one place instead of being scattered across actions, queries, and workers.
10
10
-**Testability:** Services and repositories can be mocked at a clear boundary.
11
11
-**Sharding readiness:** The message table is already sharded; future tables may follow. Services and repositories abstract the routing logic away from callers.
12
-
-**Consistency:** Multiple consumers (builder actions, worker handlers, oRPC endpoints) reuse the same data logic instead of duplicating it.
12
+
-**Consistency:** Multiple consumers (builder actions, worker handlers, oRPC endpoints, public API tokens) reuse the same data logic instead of duplicating it.
13
+
-**Public API / MCP surfaces need the same guarantees as the UI.** A workspace-token caller and a signed-in member hitting the same resource must run the same validation, cache invalidation, and event emission — which only happens if both call the same service method.
13
14
14
-
## Allowed layers
15
+
## Per-layer responsibilities
15
16
16
-
| Layer | May import `db` directly? | Role |
17
-
|-------|---------------------------|------|
18
-
|`packages/database/src/repositories/*`| Yes | Raw query logic, shard routing |
19
-
|`packages/business/src/*`| Yes | Business logic, cache, events, orchestrates repositories |
20
-
|`apps/builder/src/features/*/actions/`|**No**| Call a service from `@chatbotx.io/business`|
21
-
|`apps/builder/src/features/*/queries/`|**No**| Call a service or repository |
22
-
|`apps/builder/src/features/*/api/`|**No**| Call a service or repository |
23
-
|`apps/worker/src/**`|**No**| Call a service or repository |
24
-
|`integrations/**`|**No**| Call a service or repository |
17
+
| Layer | May import `db`? | Owns |
18
+
|-------|---|------|
19
+
|`packages/database/src/repositories/*`| Yes | Raw where-builders, joins, pagination, shard routing. **Never** cache invalidation, event emission, or validation. |
20
+
|`packages/business/src/*`| Yes | Validation, orchestration across repositories, cache invalidation, events, audit, quota checks, optional `tx?: DatabaseClient` passthrough. **Never** imports from `apps/` or `integrations/`. |
21
+
|`apps/builder/src/features/*/actions/`|**No**| Parse input → call a service method → map the result/error for the client. |
22
+
|`apps/builder/src/features/*/queries/`|**No**| See the `.query.ts` contract below. |
23
+
|`apps/builder/src/features/*/api/`|**No**| Resolve session context into plain params, call the same service method the private path uses. |
24
+
|`apps/worker/src/**`|**No**| Call a service or repository. |
25
+
|`integrations/**`|**No**| Call a service or repository. |
26
+
27
+
**Repository-from-app-layer exception:** a pure read with zero business logic (no cache, no validation, no shape mapping beyond selecting columns) may call a repository directly from the app layer. This is the exception, not the default — reach for a service first, and only fall back to a bare repository call when there's genuinely nothing for a service to add.
28
+
29
+
## The `.query.ts` file contract
30
+
31
+
A file under `apps/builder/src/features/*/queries/` (`get-x.query.ts`, `list-x.queries.ts`) is a thin request adapter over one or more services. It:
32
+
33
+
-**MAY** read session context (current user, member permissions) and turn it into plain params (`accessScope`, `canViewEmailAndPhone`, `restrictToAssignedUserId`, …) passed into a service call.
34
+
-**MAY** map a service result onto the builder's response/UI shape.
35
+
-**MAY** compose several services for one screen.
36
+
-**MUST NOT** hold where-builders, joins, pagination logic, count/caching strategy, or anything a worker or the public API would also need — that belongs in the service (orchestration) or repository (raw query), not duplicated per caller.
37
+
-**MUST NOT** import `db` — call a service (or, for the pure-read exception above, a repository).
38
+
39
+
**A session-free read is called straight from the handler; it needs no query file at all.** Only add a `.query.ts` file when there is real builder-side session-context work (permission scope resolution, response shaping) to adapt.
40
+
41
+
## Public API and private paths share one service method
42
+
43
+
The public API (workspace-token) handler and the private action/query adapter for the same operation **must call the same service method**. Only the app layer resolves the caller's permission scope (member permissions vs. an unscoped token) and passes it into the service as plain data (`scope`/`accessScope`) — the service itself never knows whether the caller was a signed-in member or a token. Do not write a second, parallel implementation of the same logic for the public path "because it's simpler" — that is exactly the duplication this layering exists to prevent.
25
44
26
45
## How to add new data access
27
46
@@ -30,7 +49,7 @@ All database access **MUST** go through a **service** (`packages/business/`) or
30
49
-`packages/business/src/<domain>/service.ts` — class extending `BaseService`
31
50
-`packages/business/src/<domain>/index.ts` — re-export the singleton
32
51
- Add the export to `packages/business/src/index.ts`
33
-
3.**For pure query helpers** that don't carry business logic (e.g., shard-routed reads), a repository in `packages/database/src/repositories/<domain>/` is acceptable.
52
+
3.**For pure query helpers** that don't carry business logic (e.g., shard-routed reads, a where-builder shared across callers), a repository in `packages/database/src/repositories/<domain>/` is acceptable — but the service is still the thing the app layer calls; the app layer reaches the repository directly only under the pure-read exception above.
34
53
4.**Services accept an optional `tx?: DatabaseClient`** parameter so callers can pass a transaction handle.
35
54
36
55
## Existing exceptions
@@ -41,7 +60,9 @@ Many older features still import `db` directly in actions and queries. These are
41
60
42
61
Before marking a task done:
43
62
44
-
-[ ] No new `import { db } from "@chatbotx.io/database/client"` in `apps/` or `integrations/`
63
+
-[ ] No new `import { db } from "@chatbotx.io/database/client"` in `apps/` or `integrations/` (outside the pure-read repository exception)
45
64
-[ ] No new `import ... from "@chatbotx.io/database/schema"` with direct query execution in `apps/` or `integrations/`
46
65
-[ ] All DB mutations go through a service method
47
-
-[ ] All DB reads go through a service or repository method
66
+
-[ ] All DB reads go through a service (or, for a pure read with zero business logic, a repository)
67
+
-[ ] A public API handler and its private-path equivalent call the same service method, with only the caller's scope differing
68
+
-[ ]`.query.ts` files hold no where-builders, pagination, or count logic — that lives in the service/repository
0 commit comments