sonamu 0.10.13 → 0.11.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.
Files changed (230) hide show
  1. package/dist/ai/agents/agent.d.ts.map +1 -1
  2. package/dist/ai/providers/rtzr/error.d.ts.map +1 -1
  3. package/dist/ai/providers/rtzr/model.d.ts.map +1 -1
  4. package/dist/ai/providers/rtzr/utils.d.ts.map +1 -1
  5. package/dist/api/base-frame.d.ts.map +1 -1
  6. package/dist/api/decorators.d.ts.map +1 -1
  7. package/dist/api/sonamu.d.ts.map +1 -1
  8. package/dist/api/sonamu.js +3 -2
  9. package/dist/api/validator.d.ts.map +1 -1
  10. package/dist/auth/audit-log/events.d.ts.map +1 -1
  11. package/dist/auth/index.d.ts +1 -1
  12. package/dist/auth/index.d.ts.map +1 -1
  13. package/dist/auth/index.js +3 -3
  14. package/dist/auth/knex-adapter.d.ts.map +1 -1
  15. package/dist/auth/plugins/wrappers/admin.d.ts +4 -4
  16. package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
  17. package/dist/auth/plugins/wrappers/anonymous.d.ts.map +1 -1
  18. package/dist/auth/plugins/wrappers/api-key.d.ts.map +1 -1
  19. package/dist/auth/plugins/wrappers/jwt.d.ts.map +1 -1
  20. package/dist/auth/plugins/wrappers/organization.d.ts.map +1 -1
  21. package/dist/auth/plugins/wrappers/passkey.d.ts.map +1 -1
  22. package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
  23. package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -1
  24. package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
  25. package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
  26. package/dist/bin/build-config.d.ts +10 -0
  27. package/dist/bin/build-config.d.ts.map +1 -1
  28. package/dist/bin/build-config.js +130 -6
  29. package/dist/bin/fixture.d.ts +7 -4
  30. package/dist/bin/fixture.d.ts.map +1 -1
  31. package/dist/bin/fixture.js +161 -13
  32. package/dist/bin/test-command.d.ts +7 -0
  33. package/dist/bin/test-command.d.ts.map +1 -1
  34. package/dist/bin/test-command.js +18 -4
  35. package/dist/bin/typecheck-web.d.ts +2 -0
  36. package/dist/bin/typecheck-web.d.ts.map +1 -0
  37. package/dist/bin/typecheck-web.js +36 -0
  38. package/dist/cache/decorator.d.ts.map +1 -1
  39. package/dist/cache/drivers.d.ts.map +1 -1
  40. package/dist/cache-control/cache-control.d.ts.map +1 -1
  41. package/dist/compress/compress.d.ts.map +1 -1
  42. package/dist/cone/cone-generator.js +1 -1
  43. package/dist/database/base-model.d.ts.map +1 -1
  44. package/dist/database/base-model.types.d.ts +0 -1
  45. package/dist/database/base-model.types.d.ts.map +1 -1
  46. package/dist/database/db.d.ts.map +1 -1
  47. package/dist/database/puri-subset.types.d.ts +0 -1
  48. package/dist/database/puri-subset.types.d.ts.map +1 -1
  49. package/dist/database/puri-wrapper.d.ts +0 -1
  50. package/dist/database/puri-wrapper.d.ts.map +1 -1
  51. package/dist/database/puri.d.ts +0 -1
  52. package/dist/database/puri.d.ts.map +1 -1
  53. package/dist/database/puri.types.d.ts +0 -1
  54. package/dist/database/puri.types.d.ts.map +1 -1
  55. package/dist/database/transaction-context.d.ts.map +1 -1
  56. package/dist/database/upsert-builder.d.ts.map +1 -1
  57. package/dist/dict/en.d.ts +58 -58
  58. package/dist/dict/en.d.ts.map +1 -1
  59. package/dist/dict/ko.d.ts +58 -58
  60. package/dist/dict/ko.d.ts.map +1 -1
  61. package/dist/dict/rc-keys.d.ts.map +1 -1
  62. package/dist/dict/sonamu-dictionary.d.ts.map +1 -1
  63. package/dist/dict/sonamu-dictionary.js +2 -2
  64. package/dist/dict/utils.d.ts.map +1 -1
  65. package/dist/entity/entity-manager.d.ts +115 -113
  66. package/dist/entity/entity-manager.d.ts.map +1 -1
  67. package/dist/entity/entity-manager.js +25 -19
  68. package/dist/entity/entity-template-cone.d.ts +4 -4
  69. package/dist/entity/entity.d.ts +6 -1
  70. package/dist/entity/entity.d.ts.map +1 -1
  71. package/dist/entity/entity.js +8 -3
  72. package/dist/env.d.ts.map +1 -1
  73. package/dist/exceptions/so-exceptions.d.ts.map +1 -1
  74. package/dist/filter/types.d.ts.map +1 -1
  75. package/dist/index.js +6 -6
  76. package/dist/logger/configure.d.ts +10 -1
  77. package/dist/logger/configure.d.ts.map +1 -1
  78. package/dist/logger/configure.js +30 -5
  79. package/dist/migration/migrator.d.ts +12 -1
  80. package/dist/migration/migrator.d.ts.map +1 -1
  81. package/dist/migration/migrator.js +77 -30
  82. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  83. package/dist/migration/slack-confirm.d.ts.map +1 -1
  84. package/dist/naite/messaging-types.d.ts +0 -1
  85. package/dist/naite/messaging-types.d.ts.map +1 -1
  86. package/dist/naite/naite-reporter.d.ts +0 -1
  87. package/dist/naite/naite-reporter.d.ts.map +1 -1
  88. package/dist/naite/naite.d.ts.map +1 -1
  89. package/dist/storage/base-file.d.ts.map +1 -1
  90. package/dist/storage/buffered-file.d.ts.map +1 -1
  91. package/dist/storage/drivers.d.ts.map +1 -1
  92. package/dist/storage/s3-driver.d.ts.map +1 -1
  93. package/dist/storage/storage-manager.d.ts.map +1 -1
  94. package/dist/storage/uploaded-file.d.ts.map +1 -1
  95. package/dist/stream/ws-audience-resolver.d.ts.map +1 -1
  96. package/dist/stream/ws-audience.d.ts.map +1 -1
  97. package/dist/stream/ws-cluster-bus.d.ts.map +1 -1
  98. package/dist/stream/ws-delivery.d.ts.map +1 -1
  99. package/dist/stream/ws-local-connection-store.d.ts.map +1 -1
  100. package/dist/stream/ws-presence-store.d.ts.map +1 -1
  101. package/dist/stream/ws-registry.d.ts.map +1 -1
  102. package/dist/stream/ws-telemetry-memory.d.ts.map +1 -1
  103. package/dist/stream/ws-telemetry.d.ts.map +1 -1
  104. package/dist/stream/ws.d.ts.map +1 -1
  105. package/dist/syncer/api-parser.js +2 -2
  106. package/dist/syncer/file-patterns.d.ts +17 -17
  107. package/dist/syncer/filesystem-dependencies.js +1 -1
  108. package/dist/syncer/syncer.d.ts.map +1 -1
  109. package/dist/tasks/step-wrapper.d.ts.map +1 -1
  110. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  111. package/dist/template/implementations/entity.template.d.ts +2 -2
  112. package/dist/template/implementations/entity.template.d.ts.map +1 -1
  113. package/dist/template/implementations/entry-server.template.d.ts +2 -2
  114. package/dist/template/implementations/entry-server.template.d.ts.map +1 -1
  115. package/dist/template/implementations/generated.template.d.ts +2 -2
  116. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  117. package/dist/template/implementations/generated_http.template.d.ts +2 -2
  118. package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
  119. package/dist/template/implementations/generated_sso.template.d.ts +2 -2
  120. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  121. package/dist/template/implementations/http_validators.template.d.ts +2 -2
  122. package/dist/template/implementations/http_validators.template.d.ts.map +1 -1
  123. package/dist/template/implementations/init_types.template.d.ts +2 -2
  124. package/dist/template/implementations/init_types.template.d.ts.map +1 -1
  125. package/dist/template/implementations/model.template.d.ts +2 -2
  126. package/dist/template/implementations/model.template.d.ts.map +1 -1
  127. package/dist/template/implementations/model_test.template.d.ts +2 -2
  128. package/dist/template/implementations/model_test.template.d.ts.map +1 -1
  129. package/dist/template/implementations/queries.template.d.ts +2 -2
  130. package/dist/template/implementations/queries.template.d.ts.map +1 -1
  131. package/dist/template/implementations/sd.template.d.ts +2 -2
  132. package/dist/template/implementations/sd.template.d.ts.map +1 -1
  133. package/dist/template/implementations/sd.template.js +1 -1
  134. package/dist/template/implementations/services.template.d.ts +2 -2
  135. package/dist/template/implementations/services.template.d.ts.map +1 -1
  136. package/dist/template/implementations/view_form.template.d.ts +3 -3
  137. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  138. package/dist/template/implementations/view_id_all_select.template.d.ts +2 -2
  139. package/dist/template/implementations/view_id_all_select.template.d.ts.map +1 -1
  140. package/dist/template/implementations/view_list.template.d.ts +2 -2
  141. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  142. package/dist/template/implementations/view_search_input.template.d.ts +2 -2
  143. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
  144. package/dist/template/template-manager.d.ts.map +1 -1
  145. package/dist/template/template.d.ts.map +1 -1
  146. package/dist/template/zod-converter.d.ts.map +1 -1
  147. package/dist/testing/_relation-graph.d.ts.map +1 -1
  148. package/dist/testing/data-explorer.d.ts +2 -2
  149. package/dist/testing/data-explorer.d.ts.map +1 -1
  150. package/dist/testing/dev-vitest-manager.d.ts.map +1 -1
  151. package/dist/testing/faker-mappings.d.ts.map +1 -1
  152. package/dist/testing/fixture-generator.d.ts.map +1 -1
  153. package/dist/testing/fixture-generator.js +1 -1
  154. package/dist/testing/fixture-loader.d.ts.map +1 -1
  155. package/dist/testing/fixture-manager.d.ts +4 -0
  156. package/dist/testing/fixture-manager.d.ts.map +1 -1
  157. package/dist/testing/fixture-manager.js +99 -25
  158. package/dist/testing/naite-vitest-reporter.d.ts.map +1 -1
  159. package/dist/testing/parallel-db-manager.d.ts.map +1 -1
  160. package/dist/tooling/__tests__/helpers/sonamu-test-root.d.ts +10 -0
  161. package/dist/tooling/__tests__/helpers/sonamu-test-root.d.ts.map +1 -0
  162. package/dist/tooling/__tests__/helpers/sonamu-test-root.js +42 -0
  163. package/dist/tooling/cdd-service.d.ts +87 -0
  164. package/dist/tooling/cdd-service.d.ts.map +1 -0
  165. package/dist/tooling/cdd-service.js +215 -0
  166. package/dist/tooling/cli-tooling.d.ts +11 -0
  167. package/dist/tooling/cli-tooling.d.ts.map +1 -0
  168. package/dist/tooling/cli-tooling.js +901 -0
  169. package/dist/tooling/fixture-service.d.ts +29 -0
  170. package/dist/tooling/fixture-service.d.ts.map +1 -0
  171. package/dist/tooling/fixture-service.js +53 -0
  172. package/dist/tooling/index.d.ts +2 -0
  173. package/dist/tooling/index.d.ts.map +1 -0
  174. package/dist/tooling/index.js +2 -0
  175. package/dist/tooling/test-run-client.d.ts +27 -0
  176. package/dist/tooling/test-run-client.d.ts.map +1 -0
  177. package/dist/tooling/test-run-client.js +120 -0
  178. package/dist/tsdown.api.config.js +48 -0
  179. package/dist/types/types.d.ts +527 -527
  180. package/dist/types/types.d.ts.map +1 -1
  181. package/dist/ui/ai-client.d.ts.map +1 -1
  182. package/dist/utils/formatter.d.ts +6 -0
  183. package/dist/utils/formatter.d.ts.map +1 -1
  184. package/dist/utils/formatter.js +42 -11
  185. package/dist/utils/model.d.ts +1 -1
  186. package/dist/vector/chunking.d.ts.map +1 -1
  187. package/dist/vector/embedding.d.ts.map +1 -1
  188. package/package.json +13 -12
  189. package/src/api/sonamu.ts +2 -1
  190. package/src/auth/index.ts +1 -1
  191. package/src/bin/__tests__/zod-compiler-build.test.ts +67 -0
  192. package/src/bin/build-config.ts +151 -5
  193. package/src/bin/fixture.ts +204 -21
  194. package/src/bin/test-command.ts +27 -4
  195. package/src/bin/typecheck-web.ts +56 -0
  196. package/src/dict/sonamu-dictionary.ts +1 -1
  197. package/src/entity/__tests__/entity-manager-autoload.contract.test.ts +55 -0
  198. package/src/entity/__tests__/entity-manager-explicit-root-registration.contract.test.ts +107 -0
  199. package/src/entity/entity-manager.ts +31 -19
  200. package/src/entity/entity.ts +7 -2
  201. package/src/logger/__tests__/configure.test.ts +33 -0
  202. package/src/logger/configure.ts +45 -3
  203. package/src/migration/__tests__/migrator.test.ts +51 -0
  204. package/src/migration/migrator.ts +103 -34
  205. package/src/syncer/api-parser.ts +1 -1
  206. package/src/testing/fixture-manager.ts +124 -32
  207. package/src/tooling/__tests__/cdd-rule-bootstrap.contract.test.ts +118 -0
  208. package/src/tooling/__tests__/cli-tooling-auth.contract.test.ts +72 -0
  209. package/src/tooling/__tests__/cli-tooling-cone-locale.contract.test.ts +150 -0
  210. package/src/tooling/__tests__/cli-tooling-entity-patch.contract.test.ts +131 -0
  211. package/src/tooling/__tests__/cli-tooling-fixture-transfer.contract.test.ts +255 -0
  212. package/src/tooling/__tests__/cli-tooling-i18n-update.contract.test.ts +104 -0
  213. package/src/tooling/__tests__/cli-tooling-scaffold.contract.test.ts +86 -0
  214. package/src/tooling/__tests__/cli-tooling-stub-practice.contract.test.ts +147 -0
  215. package/src/tooling/__tests__/default-cdd-adapter.contract.test.ts +73 -0
  216. package/src/tooling/__tests__/helpers/sonamu-test-root.ts +42 -0
  217. package/src/tooling/__tests__/test-run-client-config.contract.test.ts +154 -0
  218. package/src/tooling/cdd-service.ts +256 -0
  219. package/src/tooling/cli-tooling.ts +937 -0
  220. package/src/tooling/fixture-service.ts +72 -0
  221. package/src/tooling/index.ts +1 -0
  222. package/src/tooling/test-run-client.ts +196 -0
  223. package/src/utils/__tests__/formatter-bin.test.ts +99 -0
  224. package/src/utils/formatter.ts +74 -10
  225. package/tsdown.api.config.ts +8 -10
  226. package/bin/cli.js +0 -75
  227. package/dist/bin/cli.d.ts +0 -2
  228. package/dist/bin/cli.d.ts.map +0 -1
  229. package/dist/bin/cli.js +0 -956
  230. package/src/bin/cli.ts +0 -1166
@@ -1,7 +1,7 @@
1
- import assert from "assert";
2
- import { execSync, type ExecSyncOptions } from "child_process";
3
- import { readFileSync, writeFileSync } from "fs";
4
- import { inspect } from "util";
1
+ import assert from "node:assert";
2
+ import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { inspect } from "node:util";
5
5
 
6
6
  import chalk from "chalk";
7
7
  import inflection from "inflection";
@@ -61,10 +61,51 @@ type UpsertRow = {
61
61
  [columnName: string]: UpsertRowValue;
62
62
  };
63
63
 
64
- function executeFixtureSyncCommand(command: string, options: ExecSyncOptions): void {
64
+ function waitForFixtureCommand(child: ChildProcess, executable: string): Promise<void> {
65
+ return new Promise((resolve, reject) => {
66
+ child.once("error", reject);
67
+ child.once("exit", (code, signal) => {
68
+ if (code === 0) resolve();
69
+ else
70
+ reject(
71
+ new Error(`${executable} 명령이 ${code ?? signal ?? "unknown"} 상태로 실패했습니다.`),
72
+ );
73
+ });
74
+ });
75
+ }
76
+
77
+ function executeFixtureCommand(
78
+ executable: string,
79
+ args: string[],
80
+ options: Pick<SpawnOptions, "env" | "stdio">,
81
+ ): Promise<void> {
82
+ return waitForFixtureCommand(spawn(executable, args, options), executable);
83
+ }
84
+
85
+ async function pipeFixtureCommands(
86
+ source: { executable: string; args: string[]; env: NodeJS.ProcessEnv },
87
+ destination: { executable: string; args: string[]; env: NodeJS.ProcessEnv },
88
+ ): Promise<void> {
89
+ const sourceProcess = spawn(source.executable, source.args, {
90
+ env: source.env,
91
+ stdio: ["ignore", "pipe", "inherit"],
92
+ });
93
+ const destinationProcess = spawn(destination.executable, destination.args, {
94
+ env: destination.env,
95
+ stdio: ["pipe", "inherit", "inherit"],
96
+ });
97
+ assert(sourceProcess.stdout);
98
+ assert(destinationProcess.stdin);
99
+ sourceProcess.stdout.pipe(destinationProcess.stdin);
100
+
65
101
  try {
66
- execSync(command, options);
102
+ await Promise.all([
103
+ waitForFixtureCommand(sourceProcess, source.executable),
104
+ waitForFixtureCommand(destinationProcess, destination.executable),
105
+ ]);
67
106
  } catch (error) {
107
+ sourceProcess.kill();
108
+ destinationProcess.kill();
68
109
  throw new Error("Fixture database synchronization command failed", { cause: error });
69
110
  }
70
111
  }
@@ -74,6 +115,8 @@ export interface DuplicateCheckOptions {
74
115
  columns?: {
75
116
  [entityId: string]: string[];
76
117
  };
118
+ includeRelations?: boolean;
119
+ maxDepth?: number;
77
120
  }
78
121
 
79
122
  export class FixtureManagerClass {
@@ -137,40 +180,76 @@ export class FixtureManagerClass {
137
180
 
138
181
  // 1. 로컬 test DB 연결 종료 및 재생성
139
182
  const testPgEnv = { PGPASSWORD: testConn.password || "" };
140
- executeFixtureSyncCommand(
141
- `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "
183
+ const testPsqlArgs = [
184
+ "-h",
185
+ testConn.host ?? "localhost",
186
+ "-p",
187
+ String(testConn.port ?? 5432),
188
+ "-U",
189
+ testConn.user ?? "",
190
+ "-d",
191
+ "postgres",
192
+ "-c",
193
+ ];
194
+ await executeFixtureCommand(
195
+ "psql",
196
+ [
197
+ ...testPsqlArgs,
198
+ `
142
199
  SELECT pg_terminate_backend(pg_stat_activity.pid)
143
200
  FROM pg_stat_activity
144
201
  WHERE datname = '${testConn.database}'
145
202
  AND pid <> pg_backend_pid();
146
- "`,
203
+ `,
204
+ ],
147
205
  { stdio: "inherit", env: { ...process.env, ...testPgEnv } },
148
206
  );
149
-
150
- executeFixtureSyncCommand(
151
- `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "DROP DATABASE IF EXISTS \\"${testConn.database}\\""`,
207
+ await executeFixtureCommand(
208
+ "psql",
209
+ [...testPsqlArgs, `DROP DATABASE IF EXISTS "${testConn.database}"`],
152
210
  { stdio: "inherit", env: { ...process.env, ...testPgEnv } },
153
211
  );
154
-
155
- executeFixtureSyncCommand(
156
- `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "CREATE DATABASE \\"${testConn.database}\\""`,
212
+ await executeFixtureCommand(
213
+ "psql",
214
+ [...testPsqlArgs, `CREATE DATABASE "${testConn.database}"`],
157
215
  { stdio: "inherit", env: { ...process.env, ...testPgEnv } },
158
216
  );
159
217
 
160
218
  // 2. 원격 fixture DB → 로컬 test DB로 복사 (pg_dump | pg_restore)
161
219
  const fixturePgEnv = { PGPASSWORD: fixtureConn.password || "" };
162
- const dumpCmd = `pg_dump -h ${fixtureConn.host} -p ${fixtureConn.port ?? 5432} -U ${fixtureConn.user} -d ${fixtureConn.database} -Fc`;
163
- const restoreCmd = `pg_restore -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d ${testConn.database} --no-owner --no-acl`;
164
-
165
- executeFixtureSyncCommand(
166
- `${dumpCmd} | PGPASSWORD="${testConn.password || ""}" ${restoreCmd}`,
220
+ await pipeFixtureCommands(
167
221
  {
168
- stdio: "inherit",
222
+ executable: "pg_dump",
223
+ args: [
224
+ "-h",
225
+ fixtureConn.host ?? "localhost",
226
+ "-p",
227
+ String(fixtureConn.port ?? 5432),
228
+ "-U",
229
+ fixtureConn.user ?? "",
230
+ "-d",
231
+ fixtureConn.database,
232
+ "-Fc",
233
+ ],
169
234
  env: { ...process.env, ...fixturePgEnv },
170
- shell: "/bin/bash",
235
+ },
236
+ {
237
+ executable: "pg_restore",
238
+ args: [
239
+ "-h",
240
+ testConn.host ?? "localhost",
241
+ "-p",
242
+ String(testConn.port ?? 5432),
243
+ "-U",
244
+ testConn.user ?? "",
245
+ "-d",
246
+ testConn.database,
247
+ "--no-owner",
248
+ "--no-acl",
249
+ ],
250
+ env: { ...process.env, ...testPgEnv },
171
251
  },
172
252
  );
173
-
174
253
  // 3. 시퀀스 리셋 (데이터 복사 후 시퀀스를 MAX(id)로 정렬)
175
254
  await this.resetSequences(Sonamu.dbConfig.test);
176
255
  }
@@ -364,6 +443,8 @@ export class FixtureManagerClass {
364
443
  const initialRecordsLength = fixtures.length;
365
444
  const newRecords = await this.createFixtureRecord(entity, row, {
366
445
  db: sourceDB,
446
+ includeRelations: duplicateCheck?.includeRelations,
447
+ maxDepth: duplicateCheck?.maxDepth,
367
448
  });
368
449
  fixtures.push(...newRecords);
369
450
  const currentFixtureRecord = fixtures.find((r) => r.fixtureId === `${entityId}#${row.id}`);
@@ -421,12 +502,14 @@ export class FixtureManagerClass {
421
502
  options?: {
422
503
  singleRecord?: boolean;
423
504
  db?: Knex;
505
+ includeRelations?: boolean;
506
+ maxDepth?: number;
424
507
  },
425
508
  ): Promise<FixtureRecord[]> {
426
509
  const records: FixtureRecord[] = [];
427
510
  const visitedEntities = new Set<string>();
428
511
 
429
- const create = async (entity: Entity, row: FixtureSourceRecord) => {
512
+ const create = async (entity: Entity, row: FixtureSourceRecord, depth: number) => {
430
513
  const fixtureId = `${entity.id}#${row.id}`;
431
514
  if (visitedEntities.has(fixtureId)) {
432
515
  return;
@@ -488,14 +571,23 @@ export class FixtureManagerClass {
488
571
  if (relatedId) {
489
572
  record.belongsRecords.push(`${prop.with}#${relatedId}`);
490
573
  }
491
- if (!options?.singleRecord && relatedId) {
574
+ if (
575
+ !options?.singleRecord &&
576
+ options?.includeRelations !== false &&
577
+ depth < (options?.maxDepth ?? Number.POSITIVE_INFINITY) &&
578
+ relatedId
579
+ ) {
492
580
  const relatedEntity = EntityManager.get(prop.with);
493
581
  const relatedRow = await db(relatedEntity.table).where("id", relatedId).first();
494
582
  if (relatedRow) {
495
- await create(relatedEntity, {
496
- ...dataExplorerRecordSchema.parse(relatedRow),
497
- id: z.union([z.number(), z.string()]).parse(relatedRow.id),
498
- });
583
+ await create(
584
+ relatedEntity,
585
+ {
586
+ ...dataExplorerRecordSchema.parse(relatedRow),
587
+ id: z.union([z.number(), z.string()]).parse(relatedRow.id),
588
+ },
589
+ depth + 1,
590
+ );
499
591
  }
500
592
  }
501
593
  }
@@ -504,7 +596,7 @@ export class FixtureManagerClass {
504
596
  records.push(record);
505
597
  };
506
598
 
507
- await create(rootEntity, rootRow);
599
+ await create(rootEntity, rootRow, 0);
508
600
 
509
601
  return records;
510
602
  }
@@ -1120,7 +1212,7 @@ export class FixtureManagerClass {
1120
1212
 
1121
1213
  async addFixtureLoader(code: string) {
1122
1214
  const path = `${Sonamu.apiRootPath}/src/testing/fixture.ts`;
1123
- const content = readFileSync(path).toString();
1215
+ const content = await readFile(path, "utf8");
1124
1216
 
1125
1217
  const fixtureLoaderStart = content.indexOf("const fixtureLoader = {");
1126
1218
  const fixtureLoaderEnd = content.indexOf("};", fixtureLoaderStart);
@@ -1128,7 +1220,7 @@ export class FixtureManagerClass {
1128
1220
  if (fixtureLoaderStart !== -1 && fixtureLoaderEnd !== -1) {
1129
1221
  const newContent = `${content.slice(0, fixtureLoaderEnd)} ${code}\n${content.slice(fixtureLoaderEnd)}`;
1130
1222
 
1131
- writeFileSync(path, newContent);
1223
+ await writeFile(path, newContent);
1132
1224
  } else {
1133
1225
  throw new Error("Failed to find fixtureLoader in fixture.ts");
1134
1226
  }
@@ -0,0 +1,118 @@
1
+ import * as fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+
7
+ import { createDefaultCddToolingAdapter } from "../cdd-service";
8
+
9
+ const temporaryDirectories: string[] = [];
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(
13
+ temporaryDirectories.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })),
14
+ );
15
+ });
16
+
17
+ async function createContractRoot() {
18
+ const workspace = await fs.mkdtemp(path.join(os.tmpdir(), "sonamu-cdd-rule-"));
19
+ temporaryDirectories.push(workspace);
20
+ const contractRoot = path.join(workspace, "contract");
21
+ await fs.mkdir(contractRoot);
22
+ return contractRoot;
23
+ }
24
+
25
+ const RULE_INPUT = {
26
+ ruleKey: "api",
27
+ id: "api-1",
28
+ when: "API 응답을 정의할 때",
29
+ text: "모든 목록 응답은 페이지네이션을 포함한다",
30
+ examples: ["GET /posts"],
31
+ dryRun: false,
32
+ };
33
+
34
+ describe("CDD 규칙 최초 등록", () => {
35
+ it("rules 디렉터리가 없어도 첫 규칙 파일을 만들어 저장한다", async () => {
36
+ const contractRoot = await createContractRoot();
37
+ const adapter = createDefaultCddToolingAdapter({ contractRoot, fs });
38
+
39
+ await expect(adapter.addRule(RULE_INPUT)).resolves.toMatchObject({
40
+ key: "api",
41
+ path: "rules/api.rules.json",
42
+ rules: [
43
+ {
44
+ id: "api-1",
45
+ when: "API 응답을 정의할 때",
46
+ instruction: "모든 목록 응답은 페이지네이션을 포함한다",
47
+ examples: ["GET /posts"],
48
+ },
49
+ ],
50
+ });
51
+
52
+ const saved = JSON.parse(
53
+ await fs.readFile(path.join(contractRoot, "rules", "api.rules.json"), "utf8"),
54
+ );
55
+ expect(saved.rules).toHaveLength(1);
56
+ expect(saved.rules[0].id).toBe("api-1");
57
+ });
58
+
59
+ it("rules 디렉터리는 있지만 규칙 키 파일이 없으면 새로 만든다", async () => {
60
+ const contractRoot = await createContractRoot();
61
+ await fs.mkdir(path.join(contractRoot, "rules"));
62
+ const adapter = createDefaultCddToolingAdapter({ contractRoot, fs });
63
+
64
+ await expect(adapter.addRule(RULE_INPUT)).resolves.toMatchObject({
65
+ key: "api",
66
+ rules: [{ id: "api-1" }],
67
+ });
68
+
69
+ await expect(adapter.rules()).resolves.toMatchObject({
70
+ rules: [{ key: "api", path: "rules/api.rules.json", ruleCount: 1 }],
71
+ });
72
+ });
73
+
74
+ it("기존 규칙 문서에는 규칙을 이어붙이고 description을 보존한다", async () => {
75
+ const contractRoot = await createContractRoot();
76
+ await fs.mkdir(path.join(contractRoot, "rules"));
77
+ await fs.writeFile(
78
+ path.join(contractRoot, "rules", "api.rules.json"),
79
+ JSON.stringify({
80
+ description: "API 규칙",
81
+ rules: [{ id: "api-0", when: "언제나", instruction: "기존 규칙" }],
82
+ }),
83
+ "utf8",
84
+ );
85
+ const adapter = createDefaultCddToolingAdapter({ contractRoot, fs });
86
+
87
+ await expect(adapter.addRule(RULE_INPUT)).resolves.toMatchObject({
88
+ description: "API 규칙",
89
+ rules: [{ id: "api-0" }, { id: "api-1" }],
90
+ });
91
+ });
92
+
93
+ it("dryRun이면 규칙 파일을 만들지 않는다", async () => {
94
+ const contractRoot = await createContractRoot();
95
+ const adapter = createDefaultCddToolingAdapter({ contractRoot, fs });
96
+
97
+ await expect(adapter.addRule({ ...RULE_INPUT, dryRun: true })).resolves.toMatchObject({
98
+ dryRun: true,
99
+ });
100
+
101
+ await expect(fs.access(path.join(contractRoot, "rules"))).rejects.toMatchObject({
102
+ code: "ENOENT",
103
+ });
104
+ });
105
+
106
+ it("깨진 JSON 규칙 문서는 빈 문서로 덮어쓰지 않고 실패한다", async () => {
107
+ const contractRoot = await createContractRoot();
108
+ const ruleFile = path.join(contractRoot, "rules", "api.rules.json");
109
+ await fs.mkdir(path.join(contractRoot, "rules"));
110
+ await fs.writeFile(ruleFile, "{ broken", "utf8");
111
+ const adapter = createDefaultCddToolingAdapter({ contractRoot, fs });
112
+
113
+ await expect(adapter.addRule(RULE_INPUT)).rejects.toMatchObject({
114
+ code: "INVALID_CDD_RULES",
115
+ });
116
+ await expect(fs.readFile(ruleFile, "utf8")).resolves.toBe("{ broken");
117
+ });
118
+ });
@@ -0,0 +1,72 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import * as authModule from "../../auth";
4
+ import { tooling } from "../cli-tooling";
5
+
6
+ function stubGenerator() {
7
+ return vi.spyOn(authModule, "generateBetterAuthEntities").mockResolvedValue(undefined);
8
+ }
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ describe("auth.generate tooling 연산", () => {
15
+ it("plugins 옵션이 없으면 빈 플러그인 목록으로 core 엔티티를 생성한다", async () => {
16
+ const generate = stubGenerator();
17
+
18
+ await expect(tooling.auth.generate({})).resolves.toBeUndefined();
19
+
20
+ expect(generate).toHaveBeenCalledTimes(1);
21
+ expect(generate).toHaveBeenCalledWith({ plugins: [] });
22
+ });
23
+
24
+ it("plugins 값이 undefined로 전달되어도 빈 플러그인 목록으로 처리한다", async () => {
25
+ const generate = stubGenerator();
26
+
27
+ await expect(tooling.auth.generate({ plugins: undefined })).resolves.toBeUndefined();
28
+
29
+ expect(generate).toHaveBeenCalledWith({ plugins: [] });
30
+ });
31
+
32
+ it.each([
33
+ ["배열", ["admin", "jwt"]],
34
+ ["숫자", 1],
35
+ ["객체", { admin: true }],
36
+ ["불리언", true],
37
+ ])("plugins가 문자열이 아닌 %s이면 인자 오류로 거절한다", async (_label, plugins) => {
38
+ const generate = stubGenerator();
39
+
40
+ // 문자열이 아닌 값을 조용히 무시하면 잘못된 입력이 core 전용 생성으로 둔갑합니다.
41
+ await expect(tooling.auth.generate({ plugins })).rejects.toMatchObject({
42
+ code: "INVALID_ARGUMENT",
43
+ exitCode: 2,
44
+ });
45
+
46
+ expect(generate).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it("plugins가 빈 문자열이면 빈 플러그인 목록으로 처리한다", async () => {
50
+ const generate = stubGenerator();
51
+
52
+ await expect(tooling.auth.generate({ plugins: "" })).resolves.toBeUndefined();
53
+
54
+ expect(generate).toHaveBeenCalledWith({ plugins: [] });
55
+ });
56
+
57
+ it("쉼표로 구분한 plugins 목록은 그대로 전달한다", async () => {
58
+ const generate = stubGenerator();
59
+
60
+ await tooling.auth.generate({ plugins: "admin, jwt" });
61
+
62
+ expect(generate).toHaveBeenCalledWith({ plugins: ["admin", "jwt"] });
63
+ });
64
+
65
+ it("지원하지 않는 plugin은 거절하고 엔티티 생성을 시도하지 않는다", async () => {
66
+ const generate = stubGenerator();
67
+
68
+ await expect(tooling.auth.generate({ plugins: "bogus" })).rejects.toThrow();
69
+
70
+ expect(generate).not.toHaveBeenCalled();
71
+ });
72
+ });
@@ -0,0 +1,150 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { loadConfig } from "../../api/config";
7
+ import { Sonamu } from "../../api/sonamu";
8
+ import { type ConeGenerationResult } from "../../cone/cone-generator";
9
+ import { EntityManager } from "../../entity/entity-manager";
10
+ import { Syncer } from "../../syncer/syncer";
11
+ import { type EntityJson } from "../../types/types";
12
+ import { tooling } from "../cli-tooling";
13
+ import { attachSonamuTestRoot, detachSonamuTestRoot } from "./helpers/sonamu-test-root";
14
+
15
+ const CONE_RESULT: ConeGenerationResult = {
16
+ propCones: {},
17
+ subsetCones: {},
18
+ enumCones: {},
19
+ tokensUsed: 100,
20
+ };
21
+
22
+ function entityJson(id: string): EntityJson {
23
+ return {
24
+ id,
25
+ table: `${id.toLowerCase()}s`,
26
+ title: id,
27
+ props: [{ name: "id", type: "integer" }],
28
+ indexes: [],
29
+ subsets: {},
30
+ enums: {},
31
+ };
32
+ }
33
+
34
+ /**
35
+ * 임시 프로젝트를 만들고 sonamu.config.ts를 실제로 로딩해 Sonamu에 주입합니다.
36
+ *
37
+ * @param i18nSource - config에 넣을 i18n 블록 소스. null이면 i18n 자체를 생략합니다.
38
+ */
39
+ async function attachProject(i18nSource: string | null): Promise<void> {
40
+ const apiRootPath = await attachSonamuTestRoot();
41
+ await writeFile(path.join(apiRootPath, ".env"), "SONAMU_CONE_LOCALE_TEST=1\n");
42
+ await writeFile(
43
+ path.join(apiRootPath, "src", "sonamu.config.ts"),
44
+ `export default { ${i18nSource === null ? "" : `i18n: ${i18nSource},`} };\n`,
45
+ );
46
+ Sonamu.config = await loadConfig(apiRootPath);
47
+ Sonamu.syncer = new Syncer();
48
+
49
+ await EntityManager.register(entityJson("Post"));
50
+ await EntityManager.register(entityJson("Comment"));
51
+ }
52
+
53
+ function spyOnCones(entityId: string) {
54
+ return vi.spyOn(EntityManager.get(entityId), "generateCones").mockResolvedValue(CONE_RESULT);
55
+ }
56
+
57
+ afterEach(async () => {
58
+ vi.restoreAllMocks();
59
+ await detachSonamuTestRoot();
60
+ });
61
+
62
+ describe("cone 생성의 locale 기본값", () => {
63
+ it("cone gen에 locale이 없으면 설정된 defaultLocale을 사용한다", async () => {
64
+ await attachProject(`{ defaultLocale: "en", supportedLocales: ["en", "ko"] }`);
65
+ const generateCones = spyOnCones("Post");
66
+
67
+ await tooling.entity.cones({ entityId: "Post" });
68
+
69
+ expect(generateCones).toHaveBeenCalledWith(expect.objectContaining({ locale: "en" }));
70
+ });
71
+
72
+ it("i18n 설정이 없으면 ko로 폴백한다", async () => {
73
+ await attachProject(null);
74
+ const generateCones = spyOnCones("Post");
75
+
76
+ await tooling.entity.cones({ entityId: "Post" });
77
+
78
+ expect(generateCones).toHaveBeenCalledWith(expect.objectContaining({ locale: "ko" }));
79
+ });
80
+
81
+ it("지원하지 않는 defaultLocale이면 ko로 폴백한다", async () => {
82
+ await attachProject(`{ defaultLocale: "fr", supportedLocales: ["fr"] }`);
83
+ const generateCones = spyOnCones("Post");
84
+
85
+ await tooling.entity.cones({ entityId: "Post" });
86
+
87
+ expect(generateCones).toHaveBeenCalledWith(expect.objectContaining({ locale: "ko" }));
88
+ });
89
+
90
+ it("명시한 locale이 설정된 defaultLocale보다 우선한다", async () => {
91
+ await attachProject(`{ defaultLocale: "en", supportedLocales: ["en", "ja"] }`);
92
+ const generateCones = spyOnCones("Post");
93
+
94
+ await tooling.entity.cones({ entityId: "Post", locale: "ja" });
95
+
96
+ expect(generateCones).toHaveBeenCalledWith(expect.objectContaining({ locale: "ja" }));
97
+ });
98
+
99
+ it("stub entity --ai도 설정된 defaultLocale을 사용한다", async () => {
100
+ await attachProject(`{ defaultLocale: "en", supportedLocales: ["en", "ko"] }`);
101
+ const createEntity = vi.spyOn(Sonamu.syncer, "createEntity").mockResolvedValue(undefined);
102
+ const generateCones = spyOnCones("Post");
103
+
104
+ await tooling.entity.create({ name: "Post", ai: true });
105
+
106
+ expect(createEntity).toHaveBeenCalledWith({ entityId: "Post", title: "Post" });
107
+ expect(generateCones).toHaveBeenCalledWith(expect.objectContaining({ locale: "en" }));
108
+ });
109
+ });
110
+
111
+ describe("cone gen all의 엔티티별 결과 보존", () => {
112
+ it("모두 성공하면 엔티티별 결과를 배열로 반환한다", async () => {
113
+ await attachProject(null);
114
+ spyOnCones("Post");
115
+ spyOnCones("Comment");
116
+
117
+ await expect(tooling.entity.cones({ entityId: "all" })).resolves.toEqual([
118
+ { entityId: "Post", ...CONE_RESULT },
119
+ { entityId: "Comment", ...CONE_RESULT },
120
+ ]);
121
+ });
122
+
123
+ it("일부 엔티티가 실패해도 성공한 엔티티 결과를 함께 보고한다", async () => {
124
+ await attachProject(null);
125
+ spyOnCones("Post");
126
+ vi.spyOn(EntityManager.get("Comment"), "generateCones").mockRejectedValue(
127
+ new Error("LLM rate limit"),
128
+ );
129
+
130
+ await expect(tooling.entity.cones({ entityId: "all" })).rejects.toMatchObject({
131
+ code: "CONE_GENERATION_FAILED",
132
+ details: {
133
+ results: [{ entityId: "Post", tokensUsed: 100 }],
134
+ failures: [{ entityId: "Comment", message: expect.stringContaining("LLM rate limit") }],
135
+ },
136
+ });
137
+ });
138
+
139
+ it("한 엔티티가 실패해도 다른 엔티티의 cone 생성을 건너뛰지 않는다", async () => {
140
+ await attachProject(null);
141
+ const post = spyOnCones("Post");
142
+ vi.spyOn(EntityManager.get("Comment"), "generateCones").mockRejectedValue(
143
+ new Error("LLM rate limit"),
144
+ );
145
+
146
+ await expect(tooling.entity.cones({ entityId: "all" })).rejects.toThrow();
147
+
148
+ expect(post).toHaveBeenCalledTimes(1);
149
+ });
150
+ });