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.
- package/dist/ai/agents/agent.d.ts.map +1 -1
- package/dist/ai/providers/rtzr/error.d.ts.map +1 -1
- package/dist/ai/providers/rtzr/model.d.ts.map +1 -1
- package/dist/ai/providers/rtzr/utils.d.ts.map +1 -1
- package/dist/api/base-frame.d.ts.map +1 -1
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +3 -2
- package/dist/api/validator.d.ts.map +1 -1
- package/dist/auth/audit-log/events.d.ts.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -3
- package/dist/auth/knex-adapter.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/admin.d.ts +4 -4
- package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/anonymous.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/api-key.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/jwt.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/organization.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/passkey.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
- package/dist/bin/build-config.d.ts +10 -0
- package/dist/bin/build-config.d.ts.map +1 -1
- package/dist/bin/build-config.js +130 -6
- package/dist/bin/fixture.d.ts +7 -4
- package/dist/bin/fixture.d.ts.map +1 -1
- package/dist/bin/fixture.js +161 -13
- package/dist/bin/test-command.d.ts +7 -0
- package/dist/bin/test-command.d.ts.map +1 -1
- package/dist/bin/test-command.js +18 -4
- package/dist/bin/typecheck-web.d.ts +2 -0
- package/dist/bin/typecheck-web.d.ts.map +1 -0
- package/dist/bin/typecheck-web.js +36 -0
- package/dist/cache/decorator.d.ts.map +1 -1
- package/dist/cache/drivers.d.ts.map +1 -1
- package/dist/cache-control/cache-control.d.ts.map +1 -1
- package/dist/compress/compress.d.ts.map +1 -1
- package/dist/cone/cone-generator.js +1 -1
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.types.d.ts +0 -1
- package/dist/database/base-model.types.d.ts.map +1 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/puri-subset.types.d.ts +0 -1
- package/dist/database/puri-subset.types.d.ts.map +1 -1
- package/dist/database/puri-wrapper.d.ts +0 -1
- package/dist/database/puri-wrapper.d.ts.map +1 -1
- package/dist/database/puri.d.ts +0 -1
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.types.d.ts +0 -1
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/transaction-context.d.ts.map +1 -1
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/dict/en.d.ts +58 -58
- package/dist/dict/en.d.ts.map +1 -1
- package/dist/dict/ko.d.ts +58 -58
- package/dist/dict/ko.d.ts.map +1 -1
- package/dist/dict/rc-keys.d.ts.map +1 -1
- package/dist/dict/sonamu-dictionary.d.ts.map +1 -1
- package/dist/dict/sonamu-dictionary.js +2 -2
- package/dist/dict/utils.d.ts.map +1 -1
- package/dist/entity/entity-manager.d.ts +115 -113
- package/dist/entity/entity-manager.d.ts.map +1 -1
- package/dist/entity/entity-manager.js +25 -19
- package/dist/entity/entity-template-cone.d.ts +4 -4
- package/dist/entity/entity.d.ts +6 -1
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +8 -3
- package/dist/env.d.ts.map +1 -1
- package/dist/exceptions/so-exceptions.d.ts.map +1 -1
- package/dist/filter/types.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/logger/configure.d.ts +10 -1
- package/dist/logger/configure.d.ts.map +1 -1
- package/dist/logger/configure.js +30 -5
- package/dist/migration/migrator.d.ts +12 -1
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +77 -30
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/slack-confirm.d.ts.map +1 -1
- package/dist/naite/messaging-types.d.ts +0 -1
- package/dist/naite/messaging-types.d.ts.map +1 -1
- package/dist/naite/naite-reporter.d.ts +0 -1
- package/dist/naite/naite-reporter.d.ts.map +1 -1
- package/dist/naite/naite.d.ts.map +1 -1
- package/dist/storage/base-file.d.ts.map +1 -1
- package/dist/storage/buffered-file.d.ts.map +1 -1
- package/dist/storage/drivers.d.ts.map +1 -1
- package/dist/storage/s3-driver.d.ts.map +1 -1
- package/dist/storage/storage-manager.d.ts.map +1 -1
- package/dist/storage/uploaded-file.d.ts.map +1 -1
- package/dist/stream/ws-audience-resolver.d.ts.map +1 -1
- package/dist/stream/ws-audience.d.ts.map +1 -1
- package/dist/stream/ws-cluster-bus.d.ts.map +1 -1
- package/dist/stream/ws-delivery.d.ts.map +1 -1
- package/dist/stream/ws-local-connection-store.d.ts.map +1 -1
- package/dist/stream/ws-presence-store.d.ts.map +1 -1
- package/dist/stream/ws-registry.d.ts.map +1 -1
- package/dist/stream/ws-telemetry-memory.d.ts.map +1 -1
- package/dist/stream/ws-telemetry.d.ts.map +1 -1
- package/dist/stream/ws.d.ts.map +1 -1
- package/dist/syncer/api-parser.js +2 -2
- package/dist/syncer/file-patterns.d.ts +17 -17
- package/dist/syncer/filesystem-dependencies.js +1 -1
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/tasks/step-wrapper.d.ts.map +1 -1
- package/dist/tasks/workflow-manager.d.ts.map +1 -1
- package/dist/template/implementations/entity.template.d.ts +2 -2
- package/dist/template/implementations/entity.template.d.ts.map +1 -1
- package/dist/template/implementations/entry-server.template.d.ts +2 -2
- package/dist/template/implementations/entry-server.template.d.ts.map +1 -1
- package/dist/template/implementations/generated.template.d.ts +2 -2
- package/dist/template/implementations/generated.template.d.ts.map +1 -1
- package/dist/template/implementations/generated_http.template.d.ts +2 -2
- package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
- package/dist/template/implementations/generated_sso.template.d.ts +2 -2
- package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
- package/dist/template/implementations/http_validators.template.d.ts +2 -2
- package/dist/template/implementations/http_validators.template.d.ts.map +1 -1
- package/dist/template/implementations/init_types.template.d.ts +2 -2
- package/dist/template/implementations/init_types.template.d.ts.map +1 -1
- package/dist/template/implementations/model.template.d.ts +2 -2
- package/dist/template/implementations/model.template.d.ts.map +1 -1
- package/dist/template/implementations/model_test.template.d.ts +2 -2
- package/dist/template/implementations/model_test.template.d.ts.map +1 -1
- package/dist/template/implementations/queries.template.d.ts +2 -2
- package/dist/template/implementations/queries.template.d.ts.map +1 -1
- package/dist/template/implementations/sd.template.d.ts +2 -2
- package/dist/template/implementations/sd.template.d.ts.map +1 -1
- package/dist/template/implementations/sd.template.js +1 -1
- package/dist/template/implementations/services.template.d.ts +2 -2
- package/dist/template/implementations/services.template.d.ts.map +1 -1
- package/dist/template/implementations/view_form.template.d.ts +3 -3
- package/dist/template/implementations/view_form.template.d.ts.map +1 -1
- package/dist/template/implementations/view_id_all_select.template.d.ts +2 -2
- package/dist/template/implementations/view_id_all_select.template.d.ts.map +1 -1
- package/dist/template/implementations/view_list.template.d.ts +2 -2
- package/dist/template/implementations/view_list.template.d.ts.map +1 -1
- package/dist/template/implementations/view_search_input.template.d.ts +2 -2
- package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
- package/dist/template/template-manager.d.ts.map +1 -1
- package/dist/template/template.d.ts.map +1 -1
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/testing/_relation-graph.d.ts.map +1 -1
- package/dist/testing/data-explorer.d.ts +2 -2
- package/dist/testing/data-explorer.d.ts.map +1 -1
- package/dist/testing/dev-vitest-manager.d.ts.map +1 -1
- package/dist/testing/faker-mappings.d.ts.map +1 -1
- package/dist/testing/fixture-generator.d.ts.map +1 -1
- package/dist/testing/fixture-generator.js +1 -1
- package/dist/testing/fixture-loader.d.ts.map +1 -1
- package/dist/testing/fixture-manager.d.ts +4 -0
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +99 -25
- package/dist/testing/naite-vitest-reporter.d.ts.map +1 -1
- package/dist/testing/parallel-db-manager.d.ts.map +1 -1
- package/dist/tooling/__tests__/helpers/sonamu-test-root.d.ts +10 -0
- package/dist/tooling/__tests__/helpers/sonamu-test-root.d.ts.map +1 -0
- package/dist/tooling/__tests__/helpers/sonamu-test-root.js +42 -0
- package/dist/tooling/cdd-service.d.ts +87 -0
- package/dist/tooling/cdd-service.d.ts.map +1 -0
- package/dist/tooling/cdd-service.js +215 -0
- package/dist/tooling/cli-tooling.d.ts +11 -0
- package/dist/tooling/cli-tooling.d.ts.map +1 -0
- package/dist/tooling/cli-tooling.js +901 -0
- package/dist/tooling/fixture-service.d.ts +29 -0
- package/dist/tooling/fixture-service.d.ts.map +1 -0
- package/dist/tooling/fixture-service.js +53 -0
- package/dist/tooling/index.d.ts +2 -0
- package/dist/tooling/index.d.ts.map +1 -0
- package/dist/tooling/index.js +2 -0
- package/dist/tooling/test-run-client.d.ts +27 -0
- package/dist/tooling/test-run-client.d.ts.map +1 -0
- package/dist/tooling/test-run-client.js +120 -0
- package/dist/tsdown.api.config.js +48 -0
- package/dist/types/types.d.ts +527 -527
- package/dist/types/types.d.ts.map +1 -1
- package/dist/ui/ai-client.d.ts.map +1 -1
- package/dist/utils/formatter.d.ts +6 -0
- package/dist/utils/formatter.d.ts.map +1 -1
- package/dist/utils/formatter.js +42 -11
- package/dist/utils/model.d.ts +1 -1
- package/dist/vector/chunking.d.ts.map +1 -1
- package/dist/vector/embedding.d.ts.map +1 -1
- package/package.json +13 -12
- package/src/api/sonamu.ts +2 -1
- package/src/auth/index.ts +1 -1
- package/src/bin/__tests__/zod-compiler-build.test.ts +67 -0
- package/src/bin/build-config.ts +151 -5
- package/src/bin/fixture.ts +204 -21
- package/src/bin/test-command.ts +27 -4
- package/src/bin/typecheck-web.ts +56 -0
- package/src/dict/sonamu-dictionary.ts +1 -1
- package/src/entity/__tests__/entity-manager-autoload.contract.test.ts +55 -0
- package/src/entity/__tests__/entity-manager-explicit-root-registration.contract.test.ts +107 -0
- package/src/entity/entity-manager.ts +31 -19
- package/src/entity/entity.ts +7 -2
- package/src/logger/__tests__/configure.test.ts +33 -0
- package/src/logger/configure.ts +45 -3
- package/src/migration/__tests__/migrator.test.ts +51 -0
- package/src/migration/migrator.ts +103 -34
- package/src/syncer/api-parser.ts +1 -1
- package/src/testing/fixture-manager.ts +124 -32
- package/src/tooling/__tests__/cdd-rule-bootstrap.contract.test.ts +118 -0
- package/src/tooling/__tests__/cli-tooling-auth.contract.test.ts +72 -0
- package/src/tooling/__tests__/cli-tooling-cone-locale.contract.test.ts +150 -0
- package/src/tooling/__tests__/cli-tooling-entity-patch.contract.test.ts +131 -0
- package/src/tooling/__tests__/cli-tooling-fixture-transfer.contract.test.ts +255 -0
- package/src/tooling/__tests__/cli-tooling-i18n-update.contract.test.ts +104 -0
- package/src/tooling/__tests__/cli-tooling-scaffold.contract.test.ts +86 -0
- package/src/tooling/__tests__/cli-tooling-stub-practice.contract.test.ts +147 -0
- package/src/tooling/__tests__/default-cdd-adapter.contract.test.ts +73 -0
- package/src/tooling/__tests__/helpers/sonamu-test-root.ts +42 -0
- package/src/tooling/__tests__/test-run-client-config.contract.test.ts +154 -0
- package/src/tooling/cdd-service.ts +256 -0
- package/src/tooling/cli-tooling.ts +937 -0
- package/src/tooling/fixture-service.ts +72 -0
- package/src/tooling/index.ts +1 -0
- package/src/tooling/test-run-client.ts +196 -0
- package/src/utils/__tests__/formatter-bin.test.ts +99 -0
- package/src/utils/formatter.ts +74 -10
- package/tsdown.api.config.ts +8 -10
- package/bin/cli.js +0 -75
- package/dist/bin/cli.d.ts +0 -2
- package/dist/bin/cli.d.ts.map +0 -1
- package/dist/bin/cli.js +0 -956
- package/src/bin/cli.ts +0 -1166
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import assert from "assert";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
151
|
-
`
|
|
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
|
-
|
|
156
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
496
|
-
|
|
497
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
+
});
|