sonamu 0.8.13 → 0.8.14
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/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +2 -3
- package/dist/auth/auth-generator.d.ts +8 -0
- package/dist/auth/auth-generator.d.ts.map +1 -1
- package/dist/auth/auth-generator.js +33 -1
- package/dist/auth/better-auth-entities.d.ts.map +1 -1
- package/dist/auth/better-auth-entities.js +12 -2
- package/dist/bin/cli.js +18 -3
- package/dist/cone/cone-generator.js +10 -4
- package/dist/database/knex.d.ts.map +1 -1
- package/dist/database/knex.js +64 -2
- package/dist/database/puri.d.ts +9 -1
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +42 -1
- package/dist/database/puri.types.d.ts +2 -0
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +6 -2
- package/dist/entity/entity-manager.d.ts +149 -1
- package/dist/entity/entity-manager.d.ts.map +1 -1
- package/dist/entity/entity-manager.js +68 -4
- package/dist/migration/__tests__/code-generation.search-text.test.js +435 -0
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +696 -32
- package/dist/migration/migration-set.js +3 -1
- package/dist/migration/postgresql-schema-reader.d.ts +16 -2
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +281 -7
- package/dist/stream/sse.js +5 -3
- package/dist/template/__tests__/generated.template.search-text.test.js +99 -0
- package/dist/template/generated.template.test-d.js +24 -0
- package/dist/template/implementations/generated.template.d.ts.map +1 -1
- package/dist/template/implementations/generated.template.js +2 -2
- package/dist/template/implementations/init_types.template.d.ts.map +1 -1
- package/dist/template/implementations/init_types.template.js +11 -3
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +6 -2
- package/dist/testing/dev-test-routes.d.ts.map +1 -1
- package/dist/testing/dev-test-routes.js +5 -3
- package/dist/testing/fixture-generator.d.ts +13 -0
- package/dist/testing/fixture-generator.d.ts.map +1 -1
- package/dist/testing/fixture-generator.js +105 -8
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +19 -2
- package/dist/types/__tests__/entity-json-schema-search-text.test.js +256 -0
- package/dist/types/types.d.ts +494 -1
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +117 -13
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +14 -2
- package/dist/ui/cdd-service.d.ts +16 -14
- package/dist/ui/cdd-service.d.ts.map +1 -1
- package/dist/ui/cdd-service.js +145 -37
- package/dist/ui/cdd-types.d.ts +60 -0
- package/dist/ui/cdd-types.d.ts.map +1 -0
- package/dist/ui/cdd-types.js +3 -0
- package/dist/ui-web/assets/index-D4XFBV-f.css +1 -0
- package/dist/ui-web/assets/{index-CQ_S40bD.js → index-D_19-Pi4.js} +87 -87
- package/dist/ui-web/index.html +2 -2
- package/package.json +7 -3
- package/src/api/sonamu.ts +1 -2
- package/src/auth/auth-generator.ts +38 -0
- package/src/auth/better-auth-entities.ts +18 -1
- package/src/bin/cli.ts +15 -1
- package/src/cone/cone-generator.ts +9 -3
- package/src/database/knex.ts +62 -4
- package/src/database/puri.ts +71 -0
- package/src/database/puri.types.ts +2 -0
- package/src/entity/entity-manager.ts +95 -3
- package/src/migration/__tests__/code-generation.search-text.test.ts +390 -0
- package/src/migration/code-generation.ts +848 -34
- package/src/migration/migration-set.ts +2 -0
- package/src/migration/postgresql-schema-reader.ts +366 -9
- package/src/skills/sonamu/auth-migration.md +80 -0
- package/src/skills/sonamu/cdd.md +148 -28
- package/src/skills/sonamu/cone.md +16 -0
- package/src/skills/sonamu/entity-relations.md +1 -1
- package/src/skills/sonamu/fixture-cli.md +4 -0
- package/src/skills/sonamu/frontend.md +65 -0
- package/src/skills/sonamu/migration.md +3 -1
- package/src/skills/sonamu/model.md +28 -0
- package/src/skills/sonamu/workflow.md +12 -5
- package/src/stream/sse.ts +4 -2
- package/src/template/__tests__/generated.template.search-text.test.ts +89 -0
- package/src/template/generated.template.test-d.ts +46 -0
- package/src/template/implementations/generated.template.ts +4 -1
- package/src/template/implementations/init_types.template.ts +20 -5
- package/src/template/zod-converter.ts +5 -0
- package/src/testing/dev-test-routes.ts +4 -2
- package/src/testing/fixture-generator.ts +157 -9
- package/src/testing/fixture-manager.ts +15 -1
- package/src/types/__tests__/entity-json-schema-search-text.test.ts +179 -0
- package/src/types/types.ts +168 -12
- package/src/ui/api.ts +24 -1
- package/src/ui/cdd-service.ts +195 -55
- package/src/ui/cdd-types.ts +73 -0
- package/dist/ui-web/assets/index-egkMxKos.css +0 -1
package/src/skills/sonamu/cdd.md
CHANGED
|
@@ -22,7 +22,10 @@ This project follows Contract-Driven Development (CDD). All development work mus
|
|
|
22
22
|
## Project Structure
|
|
23
23
|
|
|
24
24
|
```text
|
|
25
|
-
|
|
25
|
+
contract/
|
|
26
|
+
|- schemas/
|
|
27
|
+
| |- default-contract.schema.json # contract schema definition
|
|
28
|
+
| \- default-spec.schema.json # spec schema definition
|
|
26
29
|
|- main.contract.json # project root contract
|
|
27
30
|
|- {domain}/
|
|
28
31
|
| |- main.contract.json # domain representative contract
|
|
@@ -36,7 +39,7 @@ packages/api/contract/
|
|
|
36
39
|
Example:
|
|
37
40
|
|
|
38
41
|
```text
|
|
39
|
-
|
|
42
|
+
contract/
|
|
40
43
|
|- main.contract.json
|
|
41
44
|
|- auth/
|
|
42
45
|
| |- main.contract.json
|
|
@@ -58,22 +61,59 @@ packages/api/contract/
|
|
|
58
61
|
|
|
59
62
|
## Document Model
|
|
60
63
|
|
|
64
|
+
### Schema (`.schema.json`)
|
|
65
|
+
|
|
66
|
+
Contract/Spec 문서의 포맷을 정의하는 파일. `contract/schemas/` 디렉터리에 위치한다.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"id": "default-spec",
|
|
71
|
+
"type": "spec",
|
|
72
|
+
"fields": [
|
|
73
|
+
{
|
|
74
|
+
"name": "modules",
|
|
75
|
+
"type": "Record<string, string>",
|
|
76
|
+
"renderer": "label-grid",
|
|
77
|
+
"required": true,
|
|
78
|
+
"description": "구현에 사용되는 모듈 정의"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Field | Description |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `id` | 스키마 식별자 (Contract/Spec의 `schema` 필드에서 참조) |
|
|
87
|
+
| `type` | `contract` \| `spec` |
|
|
88
|
+
| `fields` | 문서에 포함될 커스텀 필드 목록 |
|
|
89
|
+
| `fields[].name` | 필드명 |
|
|
90
|
+
| `fields[].type` | `string`, `string[]`, `Record<string, string>`, `Record<string, object>` |
|
|
91
|
+
| `fields[].renderer` | 렌더링 컴포넌트 (생략 시 디폴트 사용) |
|
|
92
|
+
| `fields[].required` | 필수 여부 |
|
|
93
|
+
| `fields[].description` | `cdd advance` Layer 2 검증 기준으로 사용됨 |
|
|
94
|
+
|
|
61
95
|
### Contract (`.contract.json`)
|
|
62
96
|
|
|
63
97
|
A business-logic document that non-developers can read. AI must treat this file as **read-only**. If an update is needed, AI should only propose the change to the user.
|
|
64
98
|
|
|
65
99
|
```json
|
|
66
100
|
{
|
|
101
|
+
"schema": "default-contract",
|
|
67
102
|
"lastModified": "YYYY-MM-DD",
|
|
68
|
-
"
|
|
103
|
+
"features": {
|
|
104
|
+
"login": "이메일/비밀번호 로그인 및 세션 발급"
|
|
105
|
+
},
|
|
106
|
+
"overview": [...],
|
|
107
|
+
"domainGlossary": [...],
|
|
108
|
+
"userRoles": [...],
|
|
109
|
+
"businessRules": [...],
|
|
110
|
+
"edgeCases": [...]
|
|
69
111
|
}
|
|
70
112
|
```
|
|
71
113
|
|
|
72
|
-
|
|
114
|
+
필수 필드: `schema`, `lastModified`, `features`. 나머지 필드는 적용된 schema의 `fields`에 따라 결정된다.
|
|
73
115
|
|
|
74
|
-
`
|
|
75
|
-
|
|
76
|
-
`Overview -> Domain Glossary -> Features/Capabilities -> User Roles/Actors -> Business Rules/Constraints -> Edge Cases`
|
|
116
|
+
`features` 맵의 각 키는 Spec 파일명(feature key)과 1:1 매칭된다.
|
|
77
117
|
|
|
78
118
|
### Spec (`.spec.json`)
|
|
79
119
|
|
|
@@ -81,19 +121,33 @@ A feature-level technical document derived from Contract. Each file represents e
|
|
|
81
121
|
|
|
82
122
|
```json
|
|
83
123
|
{
|
|
84
|
-
"
|
|
124
|
+
"schema": "default-spec",
|
|
125
|
+
"schemaVersion": 2,
|
|
85
126
|
"summary": "Login processing and session issuance",
|
|
86
127
|
"description": [
|
|
87
128
|
"Validates user credentials and issues JWT-based sessions.",
|
|
88
129
|
"Includes password retry limit and account lockout policy."
|
|
89
130
|
],
|
|
90
131
|
"acceptanceCriteria": [
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
132
|
+
{
|
|
133
|
+
"id": "ac-login-1",
|
|
134
|
+
"condition": "유효한 이메일/비밀번호 로그인 시 세션이 생성된다",
|
|
135
|
+
"testRef": {
|
|
136
|
+
"target": "packages/api/src/application/auth/login.test.ts",
|
|
137
|
+
"pattern": "ac-login-1"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "ac-login-2",
|
|
142
|
+
"condition": "잘못된 비밀번호 입력 시 인증 실패 응답이 반환된다",
|
|
143
|
+
"testRef": {
|
|
144
|
+
"target": "",
|
|
145
|
+
"pattern": ""
|
|
146
|
+
}
|
|
147
|
+
}
|
|
94
148
|
],
|
|
95
149
|
"lastModified": "YYYY-MM-DD",
|
|
96
|
-
"status": "draft |
|
|
150
|
+
"status": "draft | specifying | implementing | validating | done",
|
|
97
151
|
"sources": ["packages/api/src/application/auth/login.ts", "packages/api/src/application/auth/login.test.ts"],
|
|
98
152
|
"contracts": ["./main.contract.json"],
|
|
99
153
|
"dependsOnSpecs": ["./session.spec.json"],
|
|
@@ -122,12 +176,13 @@ A feature-level technical document derived from Contract. Each file represents e
|
|
|
122
176
|
|
|
123
177
|
| Field | Type | Required | Description |
|
|
124
178
|
|---|---|---|---|
|
|
179
|
+
| `schema` | `string` | Y | 적용된 schema id |
|
|
125
180
|
| `schemaVersion` | `number` | Y | Schema version |
|
|
126
181
|
| `summary` | `string` | Y | One-line feature summary |
|
|
127
182
|
| `description` | `string[]` | Y | Detailed feature description |
|
|
128
|
-
| `acceptanceCriteria` | `
|
|
183
|
+
| `acceptanceCriteria` | `object[]` | Y | Completion criteria. 각 항목은 `id`, `condition`, `testRef` 포함 |
|
|
129
184
|
| `lastModified` | `string` | Y | Last modified date (YYYY-MM-DD) |
|
|
130
|
-
| `status` | `string` | Y | `"draft"` / `"
|
|
185
|
+
| `status` | `string` | Y | `"draft"` / `"specifying"` / `"implementing"` / `"validating"` / `"done"` |
|
|
131
186
|
| `sources` | `string[]` | Y | Implementation/test files (relative to project root) |
|
|
132
187
|
| `contracts` | `string[]` | Y | Referenced Contract files (relative to Spec file) |
|
|
133
188
|
| `dependsOnSpecs` | `string[]` | N | Dependent Spec files (relative to Spec file) |
|
|
@@ -136,9 +191,24 @@ A feature-level technical document derived from Contract. Each file represents e
|
|
|
136
191
|
| `dataFlow` | `string[]` | Y | Inter-module data flow |
|
|
137
192
|
| `errorHandling` | `Record<string, string>` | Y | Error handling (key: error name, value: trigger condition) |
|
|
138
193
|
| `constraints` | `string[]` | Y | Technical constraints |
|
|
194
|
+
| `guards` | `string[]` | N | Access control guards (e.g. `["admin"]`, `["admin", "sot"]`) |
|
|
195
|
+
| `testCases` | `string[]` | N | Key test scenarios derived from errorHandling/acceptanceCriteria (used as fixture/test design hints) |
|
|
196
|
+
| `fixtureStrategy` | `string` | N | Notes on fixture generation order or dependencies (e.g. "User → Department → Lab 순으로 생성") |
|
|
139
197
|
|
|
140
198
|
**Empty section notation**: `string[]` -> `[]`, `Record<string, string>` -> `{}`
|
|
141
199
|
|
|
200
|
+
### 엔티티 레벨 권장 추가 항목 (constraints에 명시)
|
|
201
|
+
|
|
202
|
+
다음 항목들은 Entity 설계에 영향을 주므로 `constraints` 또는 별도 필드에 명시해 두면 Entity 설계 시 혼선을 방지할 수 있다.
|
|
203
|
+
|
|
204
|
+
| 항목 | 예시 |
|
|
205
|
+
|---|---|
|
|
206
|
+
| PK 타입 전략 | `"User.id는 string (better-auth). 나머지는 number auto-increment"` |
|
|
207
|
+
| i18n 대상 여부 | `"name 필드는 ko/en 다국어 지원 (naite 패턴 사용)"` |
|
|
208
|
+
| 파일 업로드 여부 | `"thumbnail: SonamuFile (eager upload)"` |
|
|
209
|
+
| ManyToMany FK 타입 일치 | `"user__roles: user_id(string FK), role_id(number FK) 혼합"` |
|
|
210
|
+
| Read-only 엔티티 | `"Log 엔티티는 insert only. save/del 불필요"` |
|
|
211
|
+
|
|
142
212
|
**Spec is higher authority than code.** Code must always follow the confirmed Spec. If Spec and code conflict, code is wrong.
|
|
143
213
|
|
|
144
214
|
### Contract-Spec linking
|
|
@@ -155,23 +225,44 @@ Contract is not extended as a structural source of feature keys. **Spec referenc
|
|
|
155
225
|
|
|
156
226
|
| Value | Meaning | Transition condition |
|
|
157
227
|
|---|---|---|
|
|
158
|
-
| `draft` | Spec
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
228
|
+
| `draft` | Spec 초안 작성 중, 미확정 | 초기 상태 |
|
|
229
|
+
| `specifying` | 명세 세분화 중 | contracts 유효 참조, required 필드 충족, summary/description/AC 비어있지 않음 |
|
|
230
|
+
| `implementing` | Spec 확정, 구현 진행 중 | 명세 전체 확정 후 |
|
|
231
|
+
| `validating` | 구현 완료, AC 매칭 검증 중 | sources 파일 존재, AC testRef 지정 및 존재 |
|
|
232
|
+
| `done` | 전체 AC 만족, 일관성 검증 통과 | testRef.pattern 매칭, 빌드/테스트 통과 |
|
|
161
233
|
|
|
162
|
-
**Regression**: When `sources`, `contracts`, `dependsOnSpecs`, or `acceptanceCriteria` change on a `done` Spec, `status` reverts to `
|
|
234
|
+
**Regression**: When `sources`, `contracts`, `dependsOnSpecs`, or `acceptanceCriteria` change on a `done` Spec, `status` reverts to `implementing`.
|
|
163
235
|
|
|
164
236
|
### `acceptanceCriteria` field
|
|
165
237
|
|
|
166
|
-
Conditions that must be met for this Spec's implementation to be considered "done".
|
|
238
|
+
Conditions that must be met for this Spec's implementation to be considered "done". Each item is an object:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"id": "ac-login-1",
|
|
243
|
+
"condition": "유효한 이메일/비밀번호 로그인 시 세션이 생성된다",
|
|
244
|
+
"testRef": {
|
|
245
|
+
"target": "packages/api/src/application/auth/login.test.ts",
|
|
246
|
+
"pattern": "ac-login-1"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
| Field | Description |
|
|
252
|
+
|---|---|
|
|
253
|
+
| `id` | AC 식별자. `ac-{feature}-{n}` 형식 |
|
|
254
|
+
| `condition` | 검증 가능한 구체적 조건 |
|
|
255
|
+
| `testRef.target` | AC를 검증하는 테스트 파일 경로 |
|
|
256
|
+
| `testRef.pattern` | 테스트 파일 내 항목 식별 패턴 (`pnpm cdd test <target> -p <pattern>`) |
|
|
167
257
|
|
|
168
258
|
**Authoring rules**:
|
|
169
|
-
-
|
|
259
|
+
- `condition`은 검증 가능한 구체적 조건이어야 한다. 모호한 표현 불가.
|
|
170
260
|
- Include conditions derived from Contract's business rules and Edge Cases.
|
|
171
261
|
- Conditions derived from `constraints` and `errorHandling` may also be included.
|
|
172
|
-
- Recommended format: "
|
|
262
|
+
- Recommended format: "조건 X이면 Y이다" (input-result).
|
|
263
|
+
- `testRef`는 `implementing` 단계에서 비워두고, `validating` 단계에서 채운다.
|
|
173
264
|
|
|
174
|
-
**Validation usage**:
|
|
265
|
+
**Validation usage**: `cdd advance <spec>` 실행 시 Layer 1/Layer 2 검증에 사용된다. `testRef.pattern` 매칭은 `validating → done` 전이 시 확인된다.
|
|
175
266
|
|
|
176
267
|
### Spec detail level
|
|
177
268
|
|
|
@@ -193,8 +284,8 @@ Whenever any Spec field changes, update `lastModified` to today's date.
|
|
|
193
284
|
Spec files do not store history internally. Git handles it.
|
|
194
285
|
|
|
195
286
|
```bash
|
|
196
|
-
git log --
|
|
197
|
-
git log --follow --
|
|
287
|
+
git log -- contract/auth/login.spec.json
|
|
288
|
+
git log --follow -- contract/auth/login.spec.json # track renames
|
|
198
289
|
```
|
|
199
290
|
|
|
200
291
|
---
|
|
@@ -222,7 +313,7 @@ Contract review -> Spec authoring/fix -> Code implementation -> Test authoring/e
|
|
|
222
313
|
- Fill all structured fields (`modules`, `interfaces`, `dataFlow`, `errorHandling`, `constraints`).
|
|
223
314
|
- Define `acceptanceCriteria` with verifiable completion conditions.
|
|
224
315
|
- Add planned implementation file paths to `sources`.
|
|
225
|
-
- **All fields must be confirmed in this step.** After confirmation, set `status` to `"
|
|
316
|
+
- **All fields must be confirmed in this step.** After confirmation, set `status` to `"implementing"` and continue.
|
|
226
317
|
|
|
227
318
|
**Step 3: Code implementation**
|
|
228
319
|
- Implement exactly following the confirmed module structure and interfaces defined in Spec.
|
|
@@ -263,7 +354,7 @@ Impact analysis -> Contract/Spec review -> Spec update/fix -> Code update -> Tes
|
|
|
263
354
|
- If Spec update is required, update and confirm Spec first.
|
|
264
355
|
- If files are added/removed, update `sources`.
|
|
265
356
|
- Update `acceptanceCriteria` if completion conditions have changed.
|
|
266
|
-
- Set `status` to `"
|
|
357
|
+
- Set `status` to `"implementing"`.
|
|
267
358
|
- **Continue only after Spec is confirmed.**
|
|
268
359
|
|
|
269
360
|
**Step 4: Code update**
|
|
@@ -300,7 +391,7 @@ Bug analysis -> Related Spec/Contract review -> Spec update/fix (if needed) -> C
|
|
|
300
391
|
- If the bug is a missing technical case in Spec `errorHandling` or `constraints`, update those fields first.
|
|
301
392
|
- If the bug is a missing business case in Contract `Edge Cases`, propose Contract update to user. After Contract update, update Spec.
|
|
302
393
|
- Add missing conditions to `acceptanceCriteria` if applicable.
|
|
303
|
-
- Set `status` to `"
|
|
394
|
+
- Set `status` to `"implementing"`.
|
|
304
395
|
- **Continue only after Spec is confirmed.**
|
|
305
396
|
|
|
306
397
|
**Step 4: Code fix**
|
|
@@ -317,6 +408,33 @@ Bug analysis -> Related Spec/Contract review -> Spec update/fix (if needed) -> C
|
|
|
317
408
|
- **If mismatch exists, fix code.**
|
|
318
409
|
- After all validations pass, set `status` to `"done"` and update `lastModified` to today.
|
|
319
410
|
|
|
411
|
+
### 4. Spec-Code Audit (정합성 감사)
|
|
412
|
+
|
|
413
|
+
구현이 어느 정도 진행된 시점에서 전체 Spec과 코드의 정합성을 일괄 점검하는 패턴. 특히 여러 세션에 걸쳐 개발이 진행된 경우, 또는 구현 완료 후 리뷰 전에 실행한다.
|
|
414
|
+
|
|
415
|
+
```text
|
|
416
|
+
전체 Spec 스캔 -> 누락/불일치 목록 작성 -> 우선순위 정렬 -> 순차 수정
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Step 1: 전체 Spec 스캔**
|
|
420
|
+
- `pnpm cdd spec list --status implementing` 또는 `pnpm cdd tree`로 전체 Spec 현황 파악
|
|
421
|
+
- 각 Spec의 `acceptanceCriteria`와 `interfaces`를 기준으로 코드 구현 여부 확인
|
|
422
|
+
- 누락된 API, 잘못된 guard, 미구현 에러 처리 등을 목록으로 정리
|
|
423
|
+
|
|
424
|
+
**Step 2: 감사 결과 파일로 기록**
|
|
425
|
+
- `contract/spec-vs-code-audit.md` 또는 `.claude/skills/project/spec-audit.md`에 기록
|
|
426
|
+
- 형식: `[domain/feature] 항목 — 상태 (구현됨 / 누락 / 불일치)`
|
|
427
|
+
- 이 파일을 새 세션에서 컨텍스트 복원용으로 활용 가능
|
|
428
|
+
|
|
429
|
+
**Step 3: 우선순위 기반 수정**
|
|
430
|
+
- 가장 쉽고 확실한 것부터 순서 번호를 붙여 처리
|
|
431
|
+
- 각 항목 완료 시 감사 파일에 완료 표시
|
|
432
|
+
|
|
433
|
+
**활용 시점**:
|
|
434
|
+
- 긴 세션 종료 후 새 세션 시작 시 — 현황 파악 비용 최소화
|
|
435
|
+
- Claude Desktop에서 전체 Spec 스캔 → Claude Code에서 불일치 수정하는 분업 패턴에서 유용
|
|
436
|
+
- 도메인 단위 구현 완료 후 PR 전 최종 점검
|
|
437
|
+
|
|
320
438
|
---
|
|
321
439
|
|
|
322
440
|
## Contract/Spec Authoring Guide
|
|
@@ -338,7 +456,7 @@ Bug analysis -> Related Spec/Contract review -> Spec update/fix (if needed) -> C
|
|
|
338
456
|
- In `interfaces`, include only function/API names and short descriptions (no signatures or implementation logic).
|
|
339
457
|
- `dataFlow` and `constraints` use `string[]` format.
|
|
340
458
|
- `errorHandling` uses `Record<string, string>` format (key: error name, value: trigger condition).
|
|
341
|
-
- `acceptanceCriteria` must
|
|
459
|
+
- `acceptanceCriteria` is an `object[]`. Each item must have `id`, `condition`, `testRef`. `condition` must be a verifiable, specific condition.
|
|
342
460
|
- `sources` must list all related implementation and test files.
|
|
343
461
|
- `contracts` must list relative paths to base Contract files.
|
|
344
462
|
- Empty sections: `[]` for `string[]` fields, `{}` for `Record<string, string>` fields.
|
|
@@ -354,6 +472,7 @@ The `cdd` CLI tool automates CDD workflow tasks. Run via `pnpm cdd <command>`.
|
|
|
354
472
|
| Command | Description |
|
|
355
473
|
|---|---|
|
|
356
474
|
| `cdd init [dir]` | Initialize a CDD project (creates `contract/`, `main.contract.json`, `cdd.md`) |
|
|
475
|
+
| `cdd advance <spec> [--commit]` | Gate 검증 + delegate (Layer 1/2). `--commit`: Layer 2 통과 선언 후 즉시 상태 전이 |
|
|
357
476
|
| `cdd tree` | Display Contract/Spec tree grouped by domain with status colors |
|
|
358
477
|
| `cdd status` | Show project dashboard (Contract/Spec counts, status breakdown) |
|
|
359
478
|
| `cdd status <file>` | Spec/Contract status with relationship info (contracts, deps, dependents) |
|
|
@@ -361,6 +480,7 @@ The `cdd` CLI tool automates CDD workflow tasks. Run via `pnpm cdd <command>`.
|
|
|
361
480
|
| `cdd impact <file>` | Analyze source file change impact (direct Specs, chain Contracts, indirect Specs) |
|
|
362
481
|
| `cdd check` | Verify Code-Spec-Contract consistency + `acceptanceCriteria` fulfillment |
|
|
363
482
|
| `cdd spec create <n>` | Create a Spec template. Requires `--domain <n>` or `--contract <path>` |
|
|
483
|
+
| `cdd contract create [name]` | Contract 템플릿 생성. `name` 미지정 시 `main` |
|
|
364
484
|
| `cdd spec set-status <spec> <status>` | Change Spec status |
|
|
365
485
|
| `cdd spec list` | List Specs. Filters: `--status`, `--domain`, `--contract` |
|
|
366
486
|
| `cdd spec get <spec>` | Show full Spec or a specific field (`--field`) |
|
|
@@ -199,6 +199,22 @@ pnpm sonamu stub entity Post --no-cones
|
|
|
199
199
|
}
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
### better-auth 엔티티 PK (Account / Session / Verification)
|
|
203
|
+
|
|
204
|
+
better-auth가 관리하는 엔티티의 id는 `crypto.randomUUID()`로 생성되는 UUID다.
|
|
205
|
+
`fixtureStrategy: "sequence"`를 사용하면 fixture sync 시 `MAX(id::bigint)` 오류가 발생하므로 반드시 `fixtureGenerator: "faker.string.uuid()"`를 사용한다.
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"name": "id",
|
|
210
|
+
"type": "string",
|
|
211
|
+
"cone": {
|
|
212
|
+
"note": "better-auth가 crypto.randomUUID()로 생성하는 UUID 형식의 식별자",
|
|
213
|
+
"fixtureGenerator": "faker.string.uuid()"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
202
218
|
### enum 필드
|
|
203
219
|
|
|
204
220
|
```json
|
|
@@ -636,7 +636,7 @@ async save(spa: QuestionCollectionSaveParams[]): Promise<number[]> {
|
|
|
636
636
|
|
|
637
637
|
const categoryIdsList: (number[] | undefined)[] = [];
|
|
638
638
|
spa.forEach((sp) => {
|
|
639
|
-
const { category_ids, ...collectionData } = sp as
|
|
639
|
+
const { category_ids, ...collectionData } = sp as QuestionCollectionSaveParams;
|
|
640
640
|
categoryIdsList.push(category_ids);
|
|
641
641
|
wdb.ubRegister("question_collections", collectionData);
|
|
642
642
|
});
|
|
@@ -49,6 +49,10 @@ production/development master (실제 DB)
|
|
|
49
49
|
|
|
50
50
|
faker 기반으로 새로운 테스트 데이터를 생성합니다.
|
|
51
51
|
|
|
52
|
+
**CRITICAL: `--use-llm` 옵션은 실제 프로젝트에서 항상 사용해야 한다.** `--use-llm` 없이 생성하면 cone.note 기반 도메인 맥락이 반영되지 않아 faker 기본값만 사용되므로, 의미 없는 데이터가 생성될 수 있다. LLM이 `requirements.md`, `business-logic.md`를 참조해 맥락에 맞는 데이터를 생성하려면 이 옵션이 필수이다.
|
|
53
|
+
|
|
54
|
+
**CRITICAL: fixture gen을 실행하기 전에 `cone.note`가 주요 prop에 존재하는지 확인한다.** cone.note가 없으면 LLM이 맥락을 파악할 수 없어 의미 있는 데이터 생성이 불가능하다. cone.note가 부족하면 `pnpm sonamu cone generate --use-llm`으로 cone을 재생성한다.
|
|
55
|
+
|
|
52
56
|
#### 기본 사용법
|
|
53
57
|
|
|
54
58
|
```bash
|
|
@@ -490,6 +490,71 @@ export function UserIdAsyncSelect<T extends UserSubsetKey>({
|
|
|
490
490
|
- `value`: 현재 선택된 값 (PK 타입에 따라 number 또는 string)
|
|
491
491
|
- `onValueChange`: 값 변경 핸들러
|
|
492
492
|
|
|
493
|
+
### Cascade Dropdown 패턴 (계층 선택)
|
|
494
|
+
|
|
495
|
+
부서 → 과소 → 연구실처럼 상위 선택에 따라 하위 목록이 변해야 하는 경우, `baseListParams`를 동적으로 전달하면 된다.
|
|
496
|
+
|
|
497
|
+
**핵심 동작**: `baseListParams` prop이 변경되면 `IdAsyncSelect` 내부 React Query가 새 파라미터로 자동 재조회한다. (v0.2.5+에서 수정된 버그 - 이전 버전은 초기값만 사용하고 변경을 반영하지 않았음)
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
// 예시: 부서 → 과소 → 연구실 3단계 cascade
|
|
501
|
+
function UserForm() {
|
|
502
|
+
const { form, register, setForm } = useTypeForm(UserSaveParams, {
|
|
503
|
+
dept_id: null,
|
|
504
|
+
division_id: null,
|
|
505
|
+
lab_id: null,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<form>
|
|
510
|
+
{/* 1단계: 부서 선택 (전체 목록 → preload 또는 기본 IdAsyncSelect) */}
|
|
511
|
+
<DepartmentIdAsyncSelect
|
|
512
|
+
subset="A"
|
|
513
|
+
{...register("dept_id")}
|
|
514
|
+
onValueChange={(v) => {
|
|
515
|
+
// 부서 변경 시 하위 값 초기화
|
|
516
|
+
setForm((prev) => ({ ...prev, dept_id: v ?? null, division_id: null, lab_id: null }));
|
|
517
|
+
}}
|
|
518
|
+
/>
|
|
519
|
+
|
|
520
|
+
{/* 2단계: 과소 선택 (선택된 부서의 과소만 조회) */}
|
|
521
|
+
<DivisionIdAsyncSelect
|
|
522
|
+
subset="A"
|
|
523
|
+
baseListParams={form.dept_id ? { department_id: form.dept_id } : undefined}
|
|
524
|
+
disabled={!form.dept_id}
|
|
525
|
+
{...register("division_id")}
|
|
526
|
+
onValueChange={(v) => {
|
|
527
|
+
// 과소 변경 시 연구실 초기화
|
|
528
|
+
setForm((prev) => ({ ...prev, division_id: v ?? null, lab_id: null }));
|
|
529
|
+
}}
|
|
530
|
+
/>
|
|
531
|
+
|
|
532
|
+
{/* 3단계: 연구실 선택 (선택된 과소의 연구실만 조회) */}
|
|
533
|
+
<LabIdAsyncSelect
|
|
534
|
+
subset="A"
|
|
535
|
+
baseListParams={form.division_id ? { division_id: form.division_id } : undefined}
|
|
536
|
+
disabled={!form.division_id}
|
|
537
|
+
{...register("lab_id")}
|
|
538
|
+
/>
|
|
539
|
+
</form>
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**주의사항**:
|
|
545
|
+
- 상위가 변경될 때 하위 값을 명시적으로 `null`로 초기화해야 한다. IdAsyncSelect는 자동으로 초기화하지 않는다.
|
|
546
|
+
- `disabled` prop으로 상위가 선택되지 않은 경우 하위를 비활성화하는 것이 UX에 좋다.
|
|
547
|
+
- `baseListParams`가 `undefined`이면 IdAsyncSelect는 enabled=false 상태로 조회하지 않는다.
|
|
548
|
+
|
|
549
|
+
**Spec에 명시할 항목** (cascade가 있는 경우 spec.json의 acceptanceCriteria에 추가 권장):
|
|
550
|
+
```json
|
|
551
|
+
"acceptanceCriteria": [
|
|
552
|
+
"부서 선택 시 해당 부서의 과소만 드롭다운으로 조회된다",
|
|
553
|
+
"과소 선택 시 해당 과소의 연구실만 드롭다운으로 조회된다",
|
|
554
|
+
"부서 변경 시 하위 과소/연구실 선택이 초기화된다"
|
|
555
|
+
]
|
|
556
|
+
```
|
|
557
|
+
|
|
493
558
|
### IMPORTANT: String Primary Key Support
|
|
494
559
|
|
|
495
560
|
대부분 Entity는 Number PK (`IdAsyncSelect<number>`)이지만, better-auth 관련 Entity는 String PK를 사용합니다.
|
|
@@ -33,6 +33,8 @@ pnpm sonamu migrate run # 실제 DB에 적용
|
|
|
33
33
|
|
|
34
34
|
**예외:** PK 타입 변경 등 Sonamu가 자동 처리할 수 없는 특수 케이스만 raw SQL 허용 (아래 "PK 타입 변경" 섹션 참조)
|
|
35
35
|
|
|
36
|
+
**CRITICAL: Cross-table 연쇄 변경은 단일 파일로 처리한다.** FK drop → 타입 변경 → FK restore처럼 여러 테이블에 걸친 연쇄 변경은 반드시 하나의 migration 파일 안에서 순서대로 실행해야 한다. 파일을 나누면 중간 상태에서 constraint 위반이 발생한다. (상세: 아래 "PK 타입 변경" 섹션 참조)
|
|
37
|
+
|
|
36
38
|
## 기본 구조
|
|
37
39
|
|
|
38
40
|
```typescript
|
|
@@ -176,7 +178,7 @@ grep -r "with.*User" --include="*.entity.json"
|
|
|
176
178
|
|
|
177
179
|
### 흔한 실수
|
|
178
180
|
|
|
179
|
-
1. **여러 migration으로 분리**:
|
|
181
|
+
1. **여러 migration으로 분리**: FK drop, 타입 변경, FK restore를 별도 파일로 나누면 첫 번째 파일 apply 직후 constraint 위반. 관련 변경은 항상 단일 파일로 통합할 것.
|
|
180
182
|
|
|
181
183
|
2. **constraint 제거 없이 타입 변경**: `cannot alter type of a column used by a foreign key` 에러 발생
|
|
182
184
|
|
|
@@ -684,3 +684,31 @@ if (params.search === "id") {
|
|
|
684
684
|
- [ ] dry-run으로 변경 내용 검증
|
|
685
685
|
- [ ] pnpm typecheck로 타입 오류 확인
|
|
686
686
|
- [ ] pnpm test로 동작 검증
|
|
687
|
+
- [ ] `any` 타입 사용 여부 (사용 금지)
|
|
688
|
+
|
|
689
|
+
### any 타입 금지
|
|
690
|
+
|
|
691
|
+
`any` 타입은 TypeScript의 타입 안전성을 무력화하므로 **절대 사용하지 않는다.**
|
|
692
|
+
|
|
693
|
+
**BAD: any 사용**
|
|
694
|
+
```typescript
|
|
695
|
+
const { category_ids, ...data } = sp as any;
|
|
696
|
+
function process(input: any) { ... }
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**GOOD: 정확한 타입 또는 unknown 사용**
|
|
700
|
+
```typescript
|
|
701
|
+
// 정확한 타입으로 구조 분해
|
|
702
|
+
const { category_ids, ...data } = sp as QuestionCollectionSaveParams;
|
|
703
|
+
|
|
704
|
+
// 타입을 알 수 없을 때는 unknown (any 대신)
|
|
705
|
+
function process(input: unknown) {
|
|
706
|
+
if (typeof input === "string") { ... }
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**규칙:**
|
|
711
|
+
- `any`는 사용 금지
|
|
712
|
+
- 타입을 모를 때는 `unknown` 사용 후 타입 가드로 좁힌다
|
|
713
|
+
- 구조 분해 시 타입 단언이 필요하면 정확한 타입명을 명시한다 (`as ConcreteType`)
|
|
714
|
+
- `eslint-disable @typescript-eslint/no-explicit-any` 같은 억제 주석도 사용 금지
|
|
@@ -243,15 +243,22 @@ description: Sonamu 전체 개발 워크플로우. 프로젝트 생성부터 Fro
|
|
|
243
243
|
- cone.note가 비어있는 prop이 있으면 사용자에게 보고하고 `pnpm sonamu cone generate --use-llm`으로 cone을 재생성할지 확인
|
|
244
244
|
- cone.note가 있어야 LLM이 맥락에 맞는 fixture 데이터를 생성할 수 있다
|
|
245
245
|
44. 생성할 데이터의 최소 row 수 확인 (최소 10 ~ 최대 100)
|
|
246
|
-
45.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
45. **better-auth 엔티티 먼저 생성** (의존성 순서 필수):
|
|
247
|
+
- User → Account → Session 순으로 생성
|
|
248
|
+
- `pnpm sonamu fixture gen --include User,Account,Session --count 10 --use-llm`
|
|
249
|
+
- **CRITICAL**: User.id string PK를 위한 `users_id_seq`가 생성되어 있어야 함 (PHASE 0 Step 18-19에서 설정)
|
|
250
|
+
- 상세 내용은 `auth-migration.md` "Better-auth 엔티티 Fixture 생성" 섹션 참조
|
|
251
|
+
46. 승인하면 테스트에서 batch로 나눈 대로 fixture 생성 (LLM 사용 필수)
|
|
252
|
+
- `--use-llm` 옵션은 반드시 사용 (cone.note 기반 도메인 맥락 반영 필수)
|
|
253
|
+
47. 실제 DB에 생성되었는지 사용자에게 확인 요청
|
|
254
|
+
48. **`pnpm sonamu test`로 전체 테스트 재실행**
|
|
255
|
+
49. `pnpm dump`으로 DB 덤프 파일 생성
|
|
250
256
|
|
|
251
257
|
**완료 기준:**
|
|
252
258
|
|
|
253
259
|
- [ ] cone.note 존재 여부 체크 완료
|
|
254
|
-
- [ ] fixture
|
|
260
|
+
- [ ] better-auth 엔티티 (User, Account, Session) fixture 먼저 생성 완료
|
|
261
|
+
- [ ] fixture 데이터 생성 완료 (사용자 승인 시, `--use-llm` 사용)
|
|
255
262
|
- [ ] DB에 데이터 존재 확인
|
|
256
263
|
- [ ] 전체 테스트 통과
|
|
257
264
|
- [ ] `pnpm dump` 실행 완료
|
package/src/stream/sse.ts
CHANGED
|
@@ -29,9 +29,11 @@ class SSEConnectionImpl<T extends z.ZodObject> implements SSEConnection<T> {
|
|
|
29
29
|
private readonly socket: FastifyRequest["socket"],
|
|
30
30
|
private readonly reply: FastifyReply,
|
|
31
31
|
) {
|
|
32
|
-
|
|
32
|
+
const markClosed = () => {
|
|
33
33
|
this._closed = true;
|
|
34
|
-
}
|
|
34
|
+
};
|
|
35
|
+
this.socket.on("close", markClosed);
|
|
36
|
+
this.socket.on("error", markClosed);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
publish<K extends keyof z.infer<T>>(event: K, data: z.infer<T>[K]): void {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { Sonamu } from "../../api";
|
|
3
|
+
import { EntityManager } from "../../entity/entity-manager";
|
|
4
|
+
import { Template__generated } from "../implementations/generated.template";
|
|
5
|
+
import { Template__init_types } from "../implementations/init_types.template";
|
|
6
|
+
import { propToZodType, propToZodTypeDef } from "../zod-converter";
|
|
7
|
+
|
|
8
|
+
const TEST_API_ROOT = "/Users/Nebuleto/Workspace/sonamu/modules/sonamu";
|
|
9
|
+
|
|
10
|
+
Sonamu.apiRootPath = TEST_API_ROOT;
|
|
11
|
+
|
|
12
|
+
let entitySeq = 0;
|
|
13
|
+
|
|
14
|
+
async function registerEntity() {
|
|
15
|
+
entitySeq += 1;
|
|
16
|
+
|
|
17
|
+
const entity = {
|
|
18
|
+
id: `GeneratedTemplateSearchText${entitySeq}`,
|
|
19
|
+
title: `GeneratedTemplateSearchText${entitySeq}`,
|
|
20
|
+
table: `generated_template_search_text_${entitySeq}`,
|
|
21
|
+
props: [
|
|
22
|
+
{ name: "id", type: "integer" as const },
|
|
23
|
+
{ name: "title", type: "string" as const },
|
|
24
|
+
{
|
|
25
|
+
name: "slug",
|
|
26
|
+
type: "string" as const,
|
|
27
|
+
generated: {
|
|
28
|
+
type: "STORED" as const,
|
|
29
|
+
expression: "title",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "search_text",
|
|
34
|
+
type: "searchText" as const,
|
|
35
|
+
sourceColumns: [{ name: "title", caseInsensitive: true }],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
indexes: [],
|
|
39
|
+
subsets: {
|
|
40
|
+
A: ["id", "title", "slug", "search_text"],
|
|
41
|
+
},
|
|
42
|
+
enums: {},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
await EntityManager.register(entity, {
|
|
46
|
+
deferSearchTextJsonSourceValidation: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return EntityManager.get(entity.id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("Template__generated searchText", () => {
|
|
53
|
+
test("searchText를 문자열 스키마로 변환해야 한다", async () => {
|
|
54
|
+
const prop = {
|
|
55
|
+
name: "search_text",
|
|
56
|
+
type: "searchText" as const,
|
|
57
|
+
sourceColumns: [{ name: "title", caseInsensitive: true }],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const schema = await propToZodType(prop);
|
|
61
|
+
|
|
62
|
+
expect(propToZodTypeDef(prop, [])).toBe("search_text: z.string(),");
|
|
63
|
+
expect(schema.safeParse("fuzzy query").success).toBe(true);
|
|
64
|
+
expect(schema.safeParse(123).success).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("searchText를 __generated__ 메타데이터에 포함하면서 기존 generated 키를 유지해야 한다", async () => {
|
|
68
|
+
const entity = await registerEntity();
|
|
69
|
+
const template = new Template__generated();
|
|
70
|
+
|
|
71
|
+
const source = template.getBaseSchemaSourceCode(entity).lines.join("\n");
|
|
72
|
+
|
|
73
|
+
expect(source).toContain("search_text: z.string(),");
|
|
74
|
+
expect(source).toContain('readonly __generated__: readonly ["slug", "search_text"],');
|
|
75
|
+
expect(source).toContain('readonly __hasDefault__: readonly ["id"],');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("SaveParams write schema에서 generated/searchText 컬럼을 제외해야 한다", async () => {
|
|
79
|
+
const entity = await registerEntity();
|
|
80
|
+
const template = new Template__init_types();
|
|
81
|
+
template.getTargetAndPath = () => ({ target: "", path: "" });
|
|
82
|
+
|
|
83
|
+
const { body } = template.render({ entityId: entity.id });
|
|
84
|
+
|
|
85
|
+
expect(body).toContain(
|
|
86
|
+
`export const ${entity.id}SaveParams = ${entity.id}BaseSchema.omit({ slug: true, search_text: true }).partial({ id: true });`,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { InsertData } from "../database/puri.types";
|
|
4
|
+
|
|
5
|
+
type GeneratedTemplateRow = {
|
|
6
|
+
id: number;
|
|
7
|
+
title: string;
|
|
8
|
+
code: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
search_text: string;
|
|
11
|
+
__hasDefault__: readonly ["id"];
|
|
12
|
+
__generated__: readonly ["slug", "search_text"];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("generated template __generated__ metadata", () => {
|
|
16
|
+
it("searchText와 기존 generated 컬럼을 write payload에서 제외해야 한다", () => {
|
|
17
|
+
type Result = InsertData<GeneratedTemplateRow>;
|
|
18
|
+
|
|
19
|
+
expectTypeOf<Result>().toEqualTypeOf<{
|
|
20
|
+
id?: number;
|
|
21
|
+
title: string;
|
|
22
|
+
code: string;
|
|
23
|
+
}>();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("SaveParams와 동등한 write schema 추론도 generated/searchText 컬럼을 제외해야 한다", () => {
|
|
27
|
+
const GeneratedTemplateSaveParams = z
|
|
28
|
+
.object({
|
|
29
|
+
id: z.number(),
|
|
30
|
+
title: z.string(),
|
|
31
|
+
code: z.string(),
|
|
32
|
+
slug: z.string(),
|
|
33
|
+
search_text: z.string(),
|
|
34
|
+
})
|
|
35
|
+
.omit({ slug: true, search_text: true })
|
|
36
|
+
.partial({ id: true });
|
|
37
|
+
|
|
38
|
+
type Result = z.infer<typeof GeneratedTemplateSaveParams>;
|
|
39
|
+
|
|
40
|
+
expectTypeOf<Result>().toEqualTypeOf<{
|
|
41
|
+
id?: number;
|
|
42
|
+
title: string;
|
|
43
|
+
code: string;
|
|
44
|
+
}>();
|
|
45
|
+
});
|
|
46
|
+
});
|