@stonepandastudio/cairn 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -23
- package/bin/cairn.js +13 -5
- package/lib/doctor/index.js +6 -1
- package/lib/init.js +15 -1
- package/lib/manifest.js +33 -0
- package/lib/render/cli.js +116 -0
- package/lib/render/engine.js +148 -0
- package/lib/render/index.js +220 -0
- package/lib/sync/cli.js +197 -0
- package/lib/sync/index.js +249 -0
- package/package.json +2 -1
- package/presets/EXTRACTION.md +210 -0
- package/presets/README.md +86 -0
- package/presets/angular/code-guidelines.md +197 -0
- package/presets/angular/slots/architect-discussion-topics.md +10 -0
- package/presets/angular/slots/architect-mandatory-docs.md +5 -0
- package/presets/angular/slots/architect-references.md +4 -0
- package/presets/angular/slots/implementation-reference.md +34 -0
- package/presets/angular/slots/key-patterns.md +11 -0
- package/presets/angular/slots/plan-step-ordering.md +11 -0
- package/presets/angular/slots/review-checklist.md +16 -0
- package/presets/angular/variants/i18n-external-service.md +11 -0
- package/presets/angular/variants/i18n-glossr.md +63 -0
- package/presets/core/AGENTS.md +49 -0
- package/presets/core/README.md +35 -0
- package/presets/core/WORKFLOW.md +56 -0
- package/presets/core/agents/architect.md +269 -0
- package/presets/core/agents/developer.md +145 -0
- package/presets/core/agents/reviewer.md +167 -0
- package/presets/core/commands/_stub.md +7 -0
- package/presets/core/workflow.json +45 -0
- package/presets/drizzle/code-guidelines.md +33 -0
- package/presets/drizzle/slots/architect-discussion-topics.md +4 -0
- package/presets/drizzle/slots/architect-mandatory-docs.md +4 -0
- package/presets/drizzle/slots/implementation-reference.md +17 -0
- package/presets/drizzle/slots/key-patterns.md +7 -0
- package/presets/drizzle/slots/review-checklist.md +10 -0
- package/presets/nestjs/code-guidelines.md +273 -0
- package/presets/nestjs/slots/architect-discussion-topics.md +4 -0
- package/presets/nestjs/slots/architect-mandatory-docs.md +5 -0
- package/presets/nestjs/slots/architect-references.md +5 -0
- package/presets/nestjs/slots/implementation-reference.md +45 -0
- package/presets/nestjs/slots/key-patterns.md +11 -0
- package/presets/nestjs/slots/plan-step-ordering.md +12 -0
- package/presets/nestjs/slots/review-checklist.md +12 -0
- package/presets/nestjs/variants/validation-class-validator.md +120 -0
- package/presets/nestjs/variants/validation-zod.md +194 -0
- package/presets/nextjs/code-guidelines.md +45 -0
- package/presets/nextjs/slots/architect-discussion-topics.md +5 -0
- package/presets/nextjs/slots/architect-mandatory-docs.md +3 -0
- package/presets/nextjs/slots/architect-references.md +6 -0
- package/presets/nextjs/slots/implementation-reference.md +24 -0
- package/presets/nextjs/slots/key-patterns.md +8 -0
- package/presets/nextjs/slots/plan-step-ordering.md +11 -0
- package/presets/nextjs/slots/review-checklist.md +11 -0
- package/presets/react/code-guidelines.md +46 -0
- package/presets/react/slots/architect-discussion-topics.md +5 -0
- package/presets/react/slots/architect-references.md +5 -0
- package/presets/react/slots/implementation-reference.md +26 -0
- package/presets/react/slots/key-patterns.md +8 -0
- package/presets/react/slots/plan-step-ordering.md +9 -0
- package/presets/react/slots/review-checklist.md +10 -0
- package/presets/tailwind/code-guidelines.md +28 -0
- package/presets/tailwind/slots/implementation-reference.md +8 -0
- package/presets/tailwind/slots/key-patterns.md +5 -0
- package/presets/tailwind/slots/review-checklist.md +8 -0
- package/presets/typeorm/code-guidelines.md +329 -0
- package/presets/typeorm/slots/architect-discussion-topics.md +4 -0
- package/presets/typeorm/slots/architect-mandatory-docs.md +3 -0
- package/presets/typeorm/slots/implementation-reference.md +19 -0
- package/presets/typeorm/slots/key-patterns.md +8 -0
- package/presets/typeorm/slots/review-checklist.md +8 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# TypeORM Guidelines
|
|
2
|
+
|
|
3
|
+
<!-- cairn preset: typeorm. Composes onto presets/nestjs. A backend on a different
|
|
4
|
+
ORM takes presets/nestjs without this file. -->
|
|
5
|
+
|
|
6
|
+
## TypeORM Entities
|
|
7
|
+
|
|
8
|
+
* Use snake_case for database column names: `@Column({ name: 'created_at' })`
|
|
9
|
+
* Use camelCase for entity property names: `createdAt`
|
|
10
|
+
* Always define primary key with `@PrimaryGeneratedColumn()`
|
|
11
|
+
* Use timestamp columns: `@CreateDateColumn()`, `@UpdateDateColumn()`
|
|
12
|
+
* Define relationships properly: `@OneToMany()`, `@ManyToOne()`, `@ManyToMany()`
|
|
13
|
+
* Add indices for frequently queried columns: `@Index()`
|
|
14
|
+
* Set cascade options carefully - avoid unintended deletions
|
|
15
|
+
* Use entity listeners when needed: `@BeforeInsert()`, `@BeforeUpdate()`
|
|
16
|
+
|
|
17
|
+
## Database Operations
|
|
18
|
+
|
|
19
|
+
**⚠️ MANDATORY RULE**: See the **Query Configuration** section below — this is NOT optional.
|
|
20
|
+
|
|
21
|
+
* Use repository pattern with `@InjectRepository(EntityName)`
|
|
22
|
+
* Use query builders for complex queries
|
|
23
|
+
* Optimize queries with proper joins and select statements
|
|
24
|
+
* Use transactions for multi-step operations: `queryRunner.startTransaction()`
|
|
25
|
+
* Handle concurrent updates with optimistic/pessimistic locking
|
|
26
|
+
* Be aware of N+1 query problems - use eager loading or joins
|
|
27
|
+
* Update database schema via migrations only
|
|
28
|
+
* Follow naming conventions for tables and columns in migrations
|
|
29
|
+
* Update `ai/infrastructure/DATABASE_SCHEMA.md` accordingly after schema changes
|
|
30
|
+
* Avoid loading unnecessary data - select only required fields
|
|
31
|
+
* Always decompose queries with one-to-many relationships
|
|
32
|
+
* Relationship one-to-one can be loaded in same query
|
|
33
|
+
* Store loaded entities in maps (id -> entity) to avoid duplicate loads
|
|
34
|
+
* Prefer to use TypeORM `.find` and `findOne` methods over query builder when possible
|
|
35
|
+
* Store IDs of related entities instead of full entities when possible to reduce memory usage
|
|
36
|
+
* **MUST extract query configurations to separate files in `queries/` folder** — see **Query Configuration** below
|
|
37
|
+
|
|
38
|
+
## Query Configuration
|
|
39
|
+
|
|
40
|
+
**⚠️ MANDATORY RULE**: Extract TypeORM query configurations into separate files in the `queries/` folder within feature modules. This is NOT optional.
|
|
41
|
+
|
|
42
|
+
**When to extract (ALWAYS)**:
|
|
43
|
+
* ✅ Any `FindManyOptions<Entity>` or `FindOneOptions<Entity>` with relations, select, or complex where clauses
|
|
44
|
+
* ✅ Any query configuration longer than 3-4 lines
|
|
45
|
+
* ✅ Any query that filters by multiple conditions
|
|
46
|
+
* ✅ Any query that loads related entities (one-to-one or joins)
|
|
47
|
+
* ✅ Any query that is or might be reused in multiple methods
|
|
48
|
+
|
|
49
|
+
**What NOT to extract**:
|
|
50
|
+
* ❌ Very simple queries like `{ where: { id, companyId } }` (can stay inline if truly minimal)
|
|
51
|
+
* ❌ One-off queries with no filtering (but prefer extraction for consistency)
|
|
52
|
+
|
|
53
|
+
**Benefits**:
|
|
54
|
+
* Reusable query configurations
|
|
55
|
+
* Easier to test and maintain
|
|
56
|
+
* Keeps service methods clean and focused
|
|
57
|
+
* Centralizes query optimization
|
|
58
|
+
* Makes query logic explicit and documented
|
|
59
|
+
|
|
60
|
+
**Naming Convention** (STRICT):
|
|
61
|
+
* File: `get-[entity]-by-[criteria].query.ts`
|
|
62
|
+
* Constant: `GET_[ENTITY]_BY_[CRITERIA]_QUERY`
|
|
63
|
+
* Example: `get-devices-by-projects.query.ts` exports `GET_DEVICES_BY_PROJECTS_QUERY`
|
|
64
|
+
|
|
65
|
+
**Structure**:
|
|
66
|
+
```typescript
|
|
67
|
+
// src/feature/siteDevices/queries/get-devices-by-projects.query.ts
|
|
68
|
+
import { FindManyOptions, In } from 'typeorm';
|
|
69
|
+
import { SiteDeviceEntity } from '@feature/siteDevices/entities/site-device.entity';
|
|
70
|
+
import { WithCompanyId } from '@common/types/with-company-id.interface';
|
|
71
|
+
|
|
72
|
+
interface GetDevicesByProjectsQueryProps extends WithCompanyId {
|
|
73
|
+
deviceIds?: string[];
|
|
74
|
+
city?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const GET_DEVICES_BY_PROJECTS_QUERY = ({
|
|
78
|
+
companyId,
|
|
79
|
+
deviceIds,
|
|
80
|
+
city,
|
|
81
|
+
}: GetDevicesByProjectsQueryProps): FindManyOptions<SiteDeviceEntity> => ({
|
|
82
|
+
where: {
|
|
83
|
+
companyId,
|
|
84
|
+
...(deviceIds && deviceIds.length > 0 ? { id: In(deviceIds) } : {}),
|
|
85
|
+
...(city ? { site: { city: { name: ILike(`%${city}%`) } } } : {}),
|
|
86
|
+
},
|
|
87
|
+
relations: {
|
|
88
|
+
site: { city: true },
|
|
89
|
+
equipmentType: { icon: true },
|
|
90
|
+
},
|
|
91
|
+
select: {
|
|
92
|
+
id: true,
|
|
93
|
+
description: true,
|
|
94
|
+
site: {
|
|
95
|
+
id: true,
|
|
96
|
+
name: true,
|
|
97
|
+
address: true,
|
|
98
|
+
city: { name: true },
|
|
99
|
+
},
|
|
100
|
+
equipmentType: {
|
|
101
|
+
name: true,
|
|
102
|
+
icon: { id: true, url: true },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Usage in Service**:
|
|
109
|
+
```typescript
|
|
110
|
+
// ✅ Correct - use extracted query
|
|
111
|
+
import { GET_DEVICES_BY_PROJECTS_QUERY } from './queries/get-devices-by-projects.query';
|
|
112
|
+
|
|
113
|
+
async getDevicesByProjects(companyId: string, filters: FilterDto) {
|
|
114
|
+
return this.repository.find(
|
|
115
|
+
GET_DEVICES_BY_PROJECTS_QUERY({
|
|
116
|
+
companyId,
|
|
117
|
+
deviceIds: filters.deviceIds,
|
|
118
|
+
city: filters.city,
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ❌ Incorrect - inline query configuration
|
|
124
|
+
async getDevicesByProjects(companyId: string, filters: FilterDto) {
|
|
125
|
+
return this.repository.find({
|
|
126
|
+
where: { companyId, ... }, // Don't define queries inline
|
|
127
|
+
relations: { ... },
|
|
128
|
+
select: { ... },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Common Mistakes to Avoid** (VIOLATIONS REQUIRE REWORK):
|
|
134
|
+
* ❌ DON'T inline any query with relations: `relations: { site: { city: true } }` in service code
|
|
135
|
+
* ❌ DON'T inline any query with select statements: `select: { id: true, name: true, ... }` in service code
|
|
136
|
+
* ❌ DON'T inline any query with complex where clauses in service code
|
|
137
|
+
* ❌ DON'T create long query configs (>3-4 lines) anywhere except in `queries/` files
|
|
138
|
+
* ✅ DO extract to `queries/` folder and import as a constant
|
|
139
|
+
* ✅ DO use extracted queries consistently across all service methods
|
|
140
|
+
* ✅ DO name extracted queries with the `GET_*_QUERY` pattern
|
|
141
|
+
|
|
142
|
+
**Pre-Implementation Checklist**:
|
|
143
|
+
1. Before writing a service method with database queries, plan the query extractions
|
|
144
|
+
2. Identify all `find()`, `findOne()` calls that need query configs
|
|
145
|
+
3. Create query files for each unique configuration
|
|
146
|
+
4. Import and use the extracted query constants
|
|
147
|
+
5. Never write inline queries with relations/select in service methods
|
|
148
|
+
|
|
149
|
+
## QueryBuilder Field Selection
|
|
150
|
+
|
|
151
|
+
**⚠️ MANDATORY**: When using `createQueryBuilder`, always select only the fields needed — never rely on the default `SELECT *` behavior.
|
|
152
|
+
|
|
153
|
+
### Why it matters
|
|
154
|
+
|
|
155
|
+
`leftJoinAndSelect('tk.namespace', 'namespace')` selects **every column** of the joined entity. On a namespace with 8 columns when you need only 3, you transfer 5 useless columns per row — multiplied by every key in the list. At scale this wastes network bandwidth, DB I/O, and memory.
|
|
156
|
+
|
|
157
|
+
### Rules
|
|
158
|
+
|
|
159
|
+
**Main entity** — replace the implicit `SELECT *` with an explicit `.select([...])`:
|
|
160
|
+
```typescript
|
|
161
|
+
// ❌ Fetches all 8 columns of translation_keys including deleted_at
|
|
162
|
+
.createQueryBuilder('tk')
|
|
163
|
+
|
|
164
|
+
// ✅ Fetches only what the serializer actually uses
|
|
165
|
+
.createQueryBuilder('tk')
|
|
166
|
+
.select(['tk.id', 'tk.projectId', 'tk.key', 'tk.description', 'tk.namespaceId', 'tk.createdAt'])
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Joined relations** — use `leftJoin` + `addSelect([...])` instead of `leftJoinAndSelect` when you don't need all columns:
|
|
170
|
+
```typescript
|
|
171
|
+
// ❌ Fetches all 8 namespace columns
|
|
172
|
+
.leftJoinAndSelect('tk.namespace', 'namespace')
|
|
173
|
+
|
|
174
|
+
// ✅ Fetches only the 3 columns used in the response
|
|
175
|
+
.leftJoin('tk.namespace', 'namespace')
|
|
176
|
+
.addSelect(['namespace.id', 'namespace.name', 'namespace.color'])
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Exception** — `leftJoinAndSelect` is acceptable only when the joined entity has ≤ 4 columns total and all are needed (e.g., a tag table with `id`, `translationKeyId`, `tag`).
|
|
180
|
+
|
|
181
|
+
### Full example
|
|
182
|
+
```typescript
|
|
183
|
+
const qb = this.translationKeyRepository
|
|
184
|
+
.createQueryBuilder('tk')
|
|
185
|
+
.select(['tk.id', 'tk.projectId', 'tk.key', 'tk.description', 'tk.namespaceId', 'tk.createdAt'])
|
|
186
|
+
.leftJoin('tk.namespace', 'namespace')
|
|
187
|
+
.addSelect(['namespace.id', 'namespace.name', 'namespace.color'])
|
|
188
|
+
.leftJoinAndSelect('tk.tags', 'tags') // tags has only 3 cols — acceptable
|
|
189
|
+
.where('tk.projectId = :projectId', { projectId })
|
|
190
|
+
.andWhere('tk.companyId = :companyId', { companyId });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Checklist before writing a QueryBuilder query
|
|
194
|
+
1. List every field that the serializer/response DTO actually reads
|
|
195
|
+
2. Add `.select([...])` with only those fields on the main entity
|
|
196
|
+
3. For each joined relation, count its columns — if > 4 or not all needed, use `leftJoin` + `addSelect`
|
|
197
|
+
4. Never use `leftJoinAndSelect` on entities with many columns (namespaces, projects, users, companies)
|
|
198
|
+
|
|
199
|
+
## Database Query Decomposition Pattern
|
|
200
|
+
|
|
201
|
+
**IMPORTANT**: Always decompose queries with one-to-many or many-to-many relationships into separate queries.
|
|
202
|
+
|
|
203
|
+
**Why**:
|
|
204
|
+
- Avoids Cartesian products from JOINs
|
|
205
|
+
- Reduces data duplication over the wire
|
|
206
|
+
- Better performance for large datasets
|
|
207
|
+
- More explicit control over loaded data
|
|
208
|
+
|
|
209
|
+
**Pattern**:
|
|
210
|
+
1. Load primary entities with only one-to-one relations
|
|
211
|
+
2. Extract unique IDs for related entities
|
|
212
|
+
3. Load related entities in batch queries (using `In()` operator)
|
|
213
|
+
4. Create Maps for O(1) lookup — see the map rule in **Database Operations**
|
|
214
|
+
5. Attach related entities to primary entities in memory
|
|
215
|
+
|
|
216
|
+
**Example**:
|
|
217
|
+
```typescript
|
|
218
|
+
// Step 1: Load devices with only image (one-to-one)
|
|
219
|
+
const devices = await this.deviceRepository.find({
|
|
220
|
+
where: { companyId },
|
|
221
|
+
relations: { image: true }, // ✅ One-to-one OK
|
|
222
|
+
select: { id: true, siteId: true, equipmentTypeId: true, ... }
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Step 2: Extract unique IDs
|
|
226
|
+
const siteIds = [...new Set(devices.map(d => d.siteId).filter(Boolean))];
|
|
227
|
+
const equipmentTypeIds = [...new Set(devices.map(d => d.equipmentTypeId).filter(Boolean))];
|
|
228
|
+
|
|
229
|
+
// Step 3: Load related entities in batch
|
|
230
|
+
const sites = await this.siteRepository.find({
|
|
231
|
+
where: { id: In(siteIds) },
|
|
232
|
+
select: { id: true, name: true, cityId: true }
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const equipmentTypes = await this.equipmentTypeRepository.find({
|
|
236
|
+
where: { id: In(equipmentTypeIds) },
|
|
237
|
+
relations: { icon: true }, // ✅ One-to-one OK
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Step 4: Create lookup maps (O(1) access)
|
|
241
|
+
const siteMap = new Map(sites.map(s => [s.id, s]));
|
|
242
|
+
const equipmentTypeMap = new Map(equipmentTypes.map(e => [e.id, e]));
|
|
243
|
+
|
|
244
|
+
// Step 5: Attach in memory
|
|
245
|
+
return devices.map(device => ({
|
|
246
|
+
...device,
|
|
247
|
+
site: device.siteId ? siteMap.get(device.siteId) : null,
|
|
248
|
+
equipmentType: device.equipmentTypeId ? equipmentTypeMap.get(device.equipmentTypeId) : null,
|
|
249
|
+
}));
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**When to use this pattern**:
|
|
253
|
+
- ✅ Loading entities with one-to-many relations (e.g., devices → sites)
|
|
254
|
+
- ✅ Loading entities with many-to-many relations (e.g., devices ↔ projects)
|
|
255
|
+
- ✅ When same related entity appears multiple times (sites, cities)
|
|
256
|
+
- ✅ When related entities need their own related entities (site → city → province)
|
|
257
|
+
|
|
258
|
+
**When NOT to use this pattern**:
|
|
259
|
+
- ❌ Simple one-to-one relations (use `relations` in single query)
|
|
260
|
+
- ❌ Loading single entity by ID
|
|
261
|
+
- ❌ Very small datasets (< 10 records)
|
|
262
|
+
|
|
263
|
+
## Migrations
|
|
264
|
+
|
|
265
|
+
> **⚠️ CRITICAL — read before touching any migration file.**
|
|
266
|
+
> TypeORM runs migrations in ascending timestamp order. A wrong timestamp silently runs a migration out of sequence, corrupting the schema. This has happened in production. Do not repeat it.
|
|
267
|
+
|
|
268
|
+
### File naming
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
{timestamp}-{PascalCaseName}.ts
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- **`{timestamp}`** — 13-digit Unix millisecond timestamp captured **at the exact moment the file is created**. Run `Date.now()` in a Node REPL or browser console and copy that value verbatim. Examples of acceptable timestamps: `1749481200000`, `1749523417382`.
|
|
275
|
+
- **`{PascalCaseName}`** — short, descriptive, PascalCase description of what the migration does.
|
|
276
|
+
|
|
277
|
+
**Anti-patterns that are strictly forbidden**:
|
|
278
|
+
- ❌ Round numbers with trailing zeros: `1780700000000`, `1780600000000` — these are fake timestamps, not real `Date.now()` values
|
|
279
|
+
- ❌ Hand-crafting a timestamp to slot between two existing ones (e.g. setting it to `existingTimestamp - 8` to force ordering)
|
|
280
|
+
- ❌ Copying a timestamp from a plan doc, a chat message, or another migration file
|
|
281
|
+
- ❌ Guessing or estimating the current time
|
|
282
|
+
|
|
283
|
+
**Correct examples**:
|
|
284
|
+
```
|
|
285
|
+
1749481200000-AddStripePriceId.ts
|
|
286
|
+
1749512863741-AddInvoicesTable.ts
|
|
287
|
+
1749523417382-SubscriptionModelRework.ts
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Ordering guarantee
|
|
291
|
+
|
|
292
|
+
If migration B depends on migration A (e.g. B drops a column A created), A's timestamp **must** be lower than B's. Because timestamps are generated at creation time, the natural order of development guarantees this — as long as you always use real `Date.now()` values. If you fabricate timestamps, you lose this guarantee.
|
|
293
|
+
|
|
294
|
+
### In plan / description documents
|
|
295
|
+
|
|
296
|
+
Never hardcode a specific timestamp in plan or description files. Write the placeholder instead:
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
{TIMESTAMP}-MigrationName.ts
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Include a note alongside: *"Replace `{TIMESTAMP}` with the output of `Date.now()` at the moment you create the file."*
|
|
303
|
+
|
|
304
|
+
This prevents developers from copying a stale or fabricated timestamp out of a plan doc.
|
|
305
|
+
|
|
306
|
+
### Generation
|
|
307
|
+
|
|
308
|
+
TypeORM CLI generates a real timestamp automatically:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npm run migration:generate -- src/database/migrations/MigrationName
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
For hand-written migrations: create the file manually, but get the timestamp from `Date.now()` at that moment — never invent one.
|
|
315
|
+
|
|
316
|
+
## Mappers
|
|
317
|
+
|
|
318
|
+
**When to use mappers**:
|
|
319
|
+
* Complex business logic transformations that go beyond simple field picking
|
|
320
|
+
* Data format conversions (e.g., string to Date, unit conversions)
|
|
321
|
+
* Aggregating data from multiple sources
|
|
322
|
+
* Complex nested structure transformations
|
|
323
|
+
|
|
324
|
+
**When NOT to use mappers**:
|
|
325
|
+
* Simple field selection — the response layer already does this
|
|
326
|
+
* Basic entity-to-DTO conversion — see **Response Transformation** in the nestjs preset
|
|
327
|
+
* Picking a subset of fields
|
|
328
|
+
|
|
329
|
+
Place mapping functions in `mappers/` folder only when truly needed for complex transformations beyond what the response layer provides.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!-- cairn preset: typeorm — appended to presets/nestjs/slots/architect-discussion-topics.md
|
|
2
|
+
on `--stack nestjs,typeorm`. -->
|
|
3
|
+
- **Schema / entity ambiguities** — unclear relationships, nullable vs. required fields, missing constraints, index needs
|
|
4
|
+
- **Migration scope** — what needs a migration vs. a code-only change; destructive changes flagged explicitly
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- cairn preset: typeorm — appended to presets/nestjs/slots/implementation-reference.md
|
|
2
|
+
on `--stack nestjs,typeorm`. -->
|
|
3
|
+
|
|
4
|
+
### Entities
|
|
5
|
+
- `@Entity()`, `@Column()`, `@PrimaryGeneratedColumn()`, `@CreateDateColumn()`, `@UpdateDateColumn()`
|
|
6
|
+
- snake_case column names (`@Column({ name: 'created_at' })`), camelCase properties
|
|
7
|
+
- Relationships: `@OneToMany()`, `@ManyToOne()`, `@ManyToMany()` with explicit `@JoinColumn()` / `@JoinTable()`
|
|
8
|
+
- `@Index()` where queries filter or sort by a column
|
|
9
|
+
- **Never mutate schema directly** — always generate and run a migration
|
|
10
|
+
|
|
11
|
+
### Repositories & transactions
|
|
12
|
+
- Inject repositories via `@InjectRepository(Entity)`
|
|
13
|
+
- Wrap multi-step DB operations in a transaction (`queryRunner.startTransaction()`)
|
|
14
|
+
- Complex reads go through a query builder in a `queries/` file, not inline in the service
|
|
15
|
+
|
|
16
|
+
### Migrations
|
|
17
|
+
- Generate: `npm run migration:generate -- --name=<MigrationName>`
|
|
18
|
+
- Verify the generated SQL before committing — review for accidental destructive changes
|
|
19
|
+
- The DB is the source of truth; never edit entities and skip the migration
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- cairn preset: typeorm — appended to presets/nestjs/slots/key-patterns.md
|
|
2
|
+
on `--stack nestjs,typeorm`. -->
|
|
3
|
+
|
|
4
|
+
Database schema and entity relationships: **`ai/infrastructure/DATABASE_SCHEMA.md`** —
|
|
5
|
+
keep it current with every migration.
|
|
6
|
+
|
|
7
|
+
TypeORM usage (repository pattern, query builders, transactions, N+1 avoidance):
|
|
8
|
+
**`ai/infrastructure/code-guidelines.md`** § TypeORM.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- cairn preset: typeorm — appended to presets/nestjs/slots/review-checklist.md
|
|
2
|
+
when the repo is `--stack nestjs,typeorm`. ORM rules only. -->
|
|
3
|
+
|
|
4
|
+
| No inline TypeORM queries with relations/select — must be in a `queries/` folder | Service files |
|
|
5
|
+
| One-to-many / many-to-many relationships decomposed into separate queries, not JOINs | Service files |
|
|
6
|
+
| Mappers only for complex transformations — simple field picking uses `@Expose()` in the DTO | Mapper files |
|
|
7
|
+
| Schema changes go through a migration, never a direct entity edit + `synchronize` | Entity + migration files |
|
|
8
|
+
| `ai/infrastructure/DATABASE_SCHEMA.md` updated to match a schema change | that file |
|