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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { EntityManager } from "../../entity/entity-manager";
|
|
7
|
+
import { type EntityJson, type EntityProp } from "../../types/types";
|
|
8
|
+
import { tooling } from "../cli-tooling";
|
|
9
|
+
import { attachSonamuTestRoot, detachSonamuTestRoot } from "./helpers/sonamu-test-root";
|
|
10
|
+
|
|
11
|
+
const BASE_PROPS: EntityProp[] = [{ name: "id", type: "integer" }];
|
|
12
|
+
|
|
13
|
+
const POST_JSON: EntityJson = {
|
|
14
|
+
id: "Post",
|
|
15
|
+
table: "posts",
|
|
16
|
+
title: "게시글",
|
|
17
|
+
props: BASE_PROPS,
|
|
18
|
+
indexes: [],
|
|
19
|
+
subsets: {},
|
|
20
|
+
enums: {},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let apiRootPath = "";
|
|
24
|
+
let entityJsonPath = "";
|
|
25
|
+
let patchFilePath = "";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* prop 추가 patch 파일을 만듭니다.
|
|
29
|
+
*
|
|
30
|
+
* 스키마를 위반하는 입력도 그대로 기록해야 하므로 raw JSON 문자열을 받습니다.
|
|
31
|
+
*/
|
|
32
|
+
async function writePatchFile(valueJson: string): Promise<string> {
|
|
33
|
+
await writeFile(
|
|
34
|
+
patchFilePath,
|
|
35
|
+
`{"entityId":"Post","operations":[{"op":"add","path":"/props/-","value":${valueJson}}]}`,
|
|
36
|
+
"utf8",
|
|
37
|
+
);
|
|
38
|
+
return patchFilePath;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readEntityFile(): Promise<string> {
|
|
42
|
+
return readFile(entityJsonPath, "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
apiRootPath = await attachSonamuTestRoot();
|
|
47
|
+
const entityDirectory = path.join(apiRootPath, "src", "application", "post");
|
|
48
|
+
await mkdir(entityDirectory, { recursive: true });
|
|
49
|
+
entityJsonPath = path.join(entityDirectory, "post.entity.json");
|
|
50
|
+
patchFilePath = path.join(apiRootPath, "patch.json");
|
|
51
|
+
await writeFile(entityJsonPath, JSON.stringify(POST_JSON, null, 2), "utf8");
|
|
52
|
+
await EntityManager.register(POST_JSON);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(async () => {
|
|
56
|
+
await detachSonamuTestRoot();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("entity apply의 prop 검증", () => {
|
|
60
|
+
it("존재하지 않는 prop type은 파일을 바꾸기 전에 거절한다", async () => {
|
|
61
|
+
const before = await readEntityFile();
|
|
62
|
+
const file = await writePatchFile(`{"name":"status","type":"bogus"}`);
|
|
63
|
+
|
|
64
|
+
await expect(tooling.entity.applyPatch({ file, execute: true })).rejects.toMatchObject({
|
|
65
|
+
code: "INVALID_ENTITY_PATCH",
|
|
66
|
+
exitCode: 2,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await expect(readEntityFile()).resolves.toBe(before);
|
|
70
|
+
expect(EntityManager.get("Post").props).toEqual(BASE_PROPS);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("타입에 맞지 않는 부가 필드도 파일을 바꾸기 전에 거절한다", async () => {
|
|
74
|
+
// integer prop은 length 부가 필드를 허용하지 않습니다.
|
|
75
|
+
const before = await readEntityFile();
|
|
76
|
+
const file = await writePatchFile(`{"name":"count","type":"integer","length":10}`);
|
|
77
|
+
|
|
78
|
+
await expect(tooling.entity.applyPatch({ file, execute: true })).rejects.toMatchObject({
|
|
79
|
+
code: "INVALID_ENTITY_PATCH",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await expect(readEntityFile()).resolves.toBe(before);
|
|
83
|
+
expect(EntityManager.get("Post").props).toEqual(BASE_PROPS);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("유효한 prop은 entity 파일에 반영한다", async () => {
|
|
87
|
+
const file = await writePatchFile(`{"name":"title","type":"string","length":128}`);
|
|
88
|
+
|
|
89
|
+
await expect(tooling.entity.applyPatch({ file, execute: true })).resolves.toMatchObject({
|
|
90
|
+
changed: true,
|
|
91
|
+
after: {
|
|
92
|
+
props: [
|
|
93
|
+
{ name: "id", type: "integer" },
|
|
94
|
+
{ name: "title", type: "string", length: 128 },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await expect(readEntityFile()).resolves.toContain(`"title"`);
|
|
100
|
+
expect(EntityManager.get("Post").props).toEqual([
|
|
101
|
+
{ name: "id", type: "integer" },
|
|
102
|
+
{ name: "title", type: "string", length: 128 },
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("dry run은 유효한 patch도 파일에 쓰지 않는다", async () => {
|
|
107
|
+
const before = await readEntityFile();
|
|
108
|
+
const file = await writePatchFile(`{"name":"title","type":"string","length":128}`);
|
|
109
|
+
|
|
110
|
+
await expect(tooling.entity.applyPatch({ file, dryRun: true })).resolves.toMatchObject({
|
|
111
|
+
changed: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await expect(readEntityFile()).resolves.toBe(before);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("entity show의 미존재 처리", () => {
|
|
119
|
+
it("존재하지 않는 entityId는 ENTITY_NOT_FOUND로 실패한다", async () => {
|
|
120
|
+
await expect(tooling.entity.show({ entityId: "UNKNOWN_ID" })).rejects.toMatchObject({
|
|
121
|
+
code: "ENTITY_NOT_FOUND",
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("존재하는 entityId는 entity JSON을 반환한다", async () => {
|
|
126
|
+
await expect(tooling.entity.show({ entityId: "Post" })).resolves.toMatchObject({
|
|
127
|
+
id: "Post",
|
|
128
|
+
table: "posts",
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { type Knex } from "knex";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { Sonamu } from "../../api/sonamu";
|
|
8
|
+
import { type SonamuDBConfig } from "../../database/db";
|
|
9
|
+
import { EntityManager } from "../../entity/entity-manager";
|
|
10
|
+
import { FixtureManager, type FixtureSourceRecord } from "../../testing/fixture-manager";
|
|
11
|
+
import { type FixtureRecord } from "../../types/types";
|
|
12
|
+
import { tooling } from "../cli-tooling";
|
|
13
|
+
import { attachSonamuTestRoot, detachSonamuTestRoot } from "./helpers/sonamu-test-root";
|
|
14
|
+
|
|
15
|
+
// 실제 커넥션 없이 preset 존재 여부만 검증되도록 빈 Knex 설정을 사용합니다.
|
|
16
|
+
const DB_CONFIG: SonamuDBConfig = {
|
|
17
|
+
development: {},
|
|
18
|
+
staging: {},
|
|
19
|
+
production: {},
|
|
20
|
+
test: {},
|
|
21
|
+
fixture: {},
|
|
22
|
+
development_readonly: {},
|
|
23
|
+
staging_readonly: {},
|
|
24
|
+
production_readonly: {},
|
|
25
|
+
test_readonly: {},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const FIXTURE_RECORD: FixtureRecord = {
|
|
29
|
+
fixtureId: "Post#1",
|
|
30
|
+
entityId: "Post",
|
|
31
|
+
id: 1,
|
|
32
|
+
columns: { id: { prop: { name: "id", type: "integer" }, value: 1 } },
|
|
33
|
+
fetchedRecords: [],
|
|
34
|
+
belongsRecords: [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type TransferOverrides = {
|
|
38
|
+
depth?: number;
|
|
39
|
+
target?: string;
|
|
40
|
+
relations?: string;
|
|
41
|
+
dryRun?: boolean;
|
|
42
|
+
execute?: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// fixture transfer 선택자는 depth를 제외하면 모든 케이스에서 동일합니다.
|
|
46
|
+
function transferInput(overrides: TransferOverrides) {
|
|
47
|
+
return {
|
|
48
|
+
entityId: "Post",
|
|
49
|
+
source: "development",
|
|
50
|
+
target: "fixture",
|
|
51
|
+
field: "id",
|
|
52
|
+
values: ["1"],
|
|
53
|
+
relations: "none",
|
|
54
|
+
...overrides,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stubFixtureManager() {
|
|
59
|
+
return {
|
|
60
|
+
getFixtures: vi.spyOn(FixtureManager, "getFixtures").mockResolvedValue([FIXTURE_RECORD]),
|
|
61
|
+
insertFixtures: vi.spyOn(FixtureManager, "insertFixtures").mockResolvedValue([]),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let apiRoot = "";
|
|
66
|
+
|
|
67
|
+
const POST_ENTITY_JSON = {
|
|
68
|
+
id: "Post",
|
|
69
|
+
table: "posts",
|
|
70
|
+
title: "게시글",
|
|
71
|
+
props: [
|
|
72
|
+
{ name: "id", type: "integer" },
|
|
73
|
+
{ name: "title", type: "string", length: 128 },
|
|
74
|
+
{ name: "author", type: "relation", relationType: "BelongsToOne", with: "User" },
|
|
75
|
+
],
|
|
76
|
+
indexes: [],
|
|
77
|
+
subsets: {},
|
|
78
|
+
enums: {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const USER_ENTITY_JSON = {
|
|
82
|
+
id: "User",
|
|
83
|
+
table: "users",
|
|
84
|
+
title: "사용자",
|
|
85
|
+
props: [
|
|
86
|
+
{ name: "id", type: "integer" },
|
|
87
|
+
{ name: "name", type: "string", length: 64 },
|
|
88
|
+
],
|
|
89
|
+
indexes: [],
|
|
90
|
+
subsets: {},
|
|
91
|
+
enums: {},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** 임시 api root에 Post -> User BelongsToOne 관계를 가진 엔티티를 등록합니다. */
|
|
95
|
+
async function registerRelatedEntities(): Promise<void> {
|
|
96
|
+
await Promise.all(
|
|
97
|
+
[POST_ENTITY_JSON, USER_ENTITY_JSON].map(async (entityJson) => {
|
|
98
|
+
const moduleName = entityJson.id.toLowerCase();
|
|
99
|
+
const directory = path.join(apiRoot, "src", "application", moduleName);
|
|
100
|
+
await mkdir(directory, { recursive: true });
|
|
101
|
+
await writeFile(
|
|
102
|
+
path.join(directory, `${moduleName}.entity.json`),
|
|
103
|
+
JSON.stringify(entityJson, null, 2),
|
|
104
|
+
"utf8",
|
|
105
|
+
);
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
EntityManager.isAutoloaded = false;
|
|
109
|
+
await EntityManager.autoload(true, apiRoot);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 관계 순회가 실제로 일어나야만 호출되는 최소 Knex 대역입니다. */
|
|
113
|
+
function createStubDb(rowsByTable: Record<string, FixtureSourceRecord>) {
|
|
114
|
+
const queriedTables: string[] = [];
|
|
115
|
+
const query = (table: string) => {
|
|
116
|
+
queriedTables.push(table);
|
|
117
|
+
return {
|
|
118
|
+
where: () => ({
|
|
119
|
+
first: () => Promise.resolve(rowsByTable[table]),
|
|
120
|
+
pluck: () => Promise.resolve([]),
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
// SAFETY: createFixtureRecord가 쓰는 db(table).where(...).first() 경로만 구현한 테스트 대역입니다.
|
|
125
|
+
return { db: Object.assign(query, {} as Knex), queriedTables };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
beforeEach(async () => {
|
|
129
|
+
apiRoot = await attachSonamuTestRoot();
|
|
130
|
+
Sonamu.dbConfig = DB_CONFIG;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterEach(async () => {
|
|
134
|
+
vi.restoreAllMocks();
|
|
135
|
+
// 임시 루트에서 등록한 엔티티가 다음 케이스로 새지 않도록 되돌립니다.
|
|
136
|
+
EntityManager.isAutoloaded = false;
|
|
137
|
+
await detachSonamuTestRoot();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("fixture transfer의 depth 검증", () => {
|
|
141
|
+
it("--depth 0을 허용하고 maxDepth 0으로 관계 순회를 막는다", async () => {
|
|
142
|
+
const fixtures = stubFixtureManager();
|
|
143
|
+
|
|
144
|
+
await expect(
|
|
145
|
+
tooling.fixture.fetch(transferInput({ depth: 0, dryRun: true })),
|
|
146
|
+
).resolves.toMatchObject({ dryRun: true, records: [FIXTURE_RECORD] });
|
|
147
|
+
|
|
148
|
+
expect(fixtures.getFixtures).toHaveBeenCalledWith(
|
|
149
|
+
"development",
|
|
150
|
+
"fixture",
|
|
151
|
+
{ entityId: "Post", field: "id", value: "1", searchType: "equals" },
|
|
152
|
+
{ includeRelations: false, maxDepth: 0 },
|
|
153
|
+
);
|
|
154
|
+
expect(fixtures.insertFixtures).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("--depth 0으로 실제 이관까지 수행한다", async () => {
|
|
158
|
+
const fixtures = stubFixtureManager();
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
tooling.fixture.fetch(transferInput({ depth: 0, execute: true })),
|
|
162
|
+
).resolves.toMatchObject({ records: [FIXTURE_RECORD] });
|
|
163
|
+
|
|
164
|
+
expect(fixtures.insertFixtures).toHaveBeenCalledWith("fixture", [FIXTURE_RECORD]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("양의 정수 depth는 계속 허용한다", async () => {
|
|
168
|
+
const fixtures = stubFixtureManager();
|
|
169
|
+
|
|
170
|
+
await expect(
|
|
171
|
+
tooling.fixture.fetch(transferInput({ depth: 2, dryRun: true })),
|
|
172
|
+
).resolves.toMatchObject({ dryRun: true });
|
|
173
|
+
|
|
174
|
+
expect(fixtures.getFixtures).toHaveBeenCalledWith("development", "fixture", expect.anything(), {
|
|
175
|
+
includeRelations: false,
|
|
176
|
+
maxDepth: 2,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("음수 depth는 거절하고 fixture 조회를 시도하지 않는다", async () => {
|
|
181
|
+
const fixtures = stubFixtureManager();
|
|
182
|
+
|
|
183
|
+
await expect(
|
|
184
|
+
tooling.fixture.fetch(transferInput({ depth: -1, dryRun: true })),
|
|
185
|
+
).rejects.toMatchObject({ code: "INVALID_FIXTURE_TRANSFER", exitCode: 2 });
|
|
186
|
+
|
|
187
|
+
expect(fixtures.getFixtures).not.toHaveBeenCalled();
|
|
188
|
+
expect(fixtures.insertFixtures).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("정수가 아닌 depth는 거절한다", async () => {
|
|
192
|
+
const fixtures = stubFixtureManager();
|
|
193
|
+
|
|
194
|
+
await expect(
|
|
195
|
+
tooling.fixture.fetch(transferInput({ depth: 1.5, dryRun: true })),
|
|
196
|
+
).rejects.toMatchObject({ code: "INVALID_FIXTURE_TRANSFER", exitCode: 2 });
|
|
197
|
+
|
|
198
|
+
expect(fixtures.getFixtures).not.toHaveBeenCalled();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("설정되지 않은 DB preset은 depth 0이어도 거절한다", async () => {
|
|
202
|
+
const fixtures = stubFixtureManager();
|
|
203
|
+
|
|
204
|
+
await expect(
|
|
205
|
+
tooling.fixture.fetch(transferInput({ depth: 0, target: "unknown", dryRun: true })),
|
|
206
|
+
).rejects.toMatchObject({ code: "INVALID_FIXTURE_TRANSFER", exitCode: 2 });
|
|
207
|
+
|
|
208
|
+
expect(fixtures.getFixtures).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("relations include와 depth 0의 관계 순회 억제", () => {
|
|
213
|
+
it("relations include에서도 --depth 0은 maxDepth 0으로 전달한다", async () => {
|
|
214
|
+
const fixtures = stubFixtureManager();
|
|
215
|
+
|
|
216
|
+
await expect(
|
|
217
|
+
tooling.fixture.fetch(transferInput({ relations: "include", depth: 0, dryRun: true })),
|
|
218
|
+
).resolves.toMatchObject({ dryRun: true, records: [FIXTURE_RECORD] });
|
|
219
|
+
|
|
220
|
+
// includeRelations가 true인 상태에서는 maxDepth 0만이 순회를 막는 조건입니다.
|
|
221
|
+
expect(fixtures.getFixtures).toHaveBeenCalledWith("development", "fixture", expect.anything(), {
|
|
222
|
+
includeRelations: true,
|
|
223
|
+
maxDepth: 0,
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("includeRelations true여도 maxDepth 0이면 관계 레코드를 조회하지 않는다", async () => {
|
|
228
|
+
await registerRelatedEntities();
|
|
229
|
+
const { db, queriedTables } = createStubDb({ users: { id: 7, name: "글쓴이" } });
|
|
230
|
+
|
|
231
|
+
const records = await FixtureManager.createFixtureRecord(
|
|
232
|
+
EntityManager.get("Post"),
|
|
233
|
+
{ id: 1, title: "첫 글", author_id: 7 },
|
|
234
|
+
{ db, includeRelations: true, maxDepth: 0 },
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(records.map((record) => record.fixtureId)).toEqual(["Post#1"]);
|
|
238
|
+
expect(records[0].belongsRecords).toEqual(["User#7"]);
|
|
239
|
+
expect(queriedTables).toEqual([]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("maxDepth 1이면 같은 입력에서 관계 레코드까지 수집한다", async () => {
|
|
243
|
+
await registerRelatedEntities();
|
|
244
|
+
const { db, queriedTables } = createStubDb({ users: { id: 7, name: "글쓴이" } });
|
|
245
|
+
|
|
246
|
+
const records = await FixtureManager.createFixtureRecord(
|
|
247
|
+
EntityManager.get("Post"),
|
|
248
|
+
{ id: 1, title: "첫 글", author_id: 7 },
|
|
249
|
+
{ db, includeRelations: true, maxDepth: 1 },
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(records.map((record) => record.fixtureId).toSorted()).toEqual(["Post#1", "User#7"]);
|
|
253
|
+
expect(queriedTables).toEqual(["users"]);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { sonamuDictionary } from "../../dict/sonamu-dictionary";
|
|
4
|
+
import { type DictionaryResult } from "../../dict/types";
|
|
5
|
+
import { tooling } from "../cli-tooling";
|
|
6
|
+
import { attachSonamuTestRoot, detachSonamuTestRoot } from "./helpers/sonamu-test-root";
|
|
7
|
+
|
|
8
|
+
const DICTIONARY: DictionaryResult = {
|
|
9
|
+
locales: ["ko", "en"],
|
|
10
|
+
defaultLocale: "ko",
|
|
11
|
+
stats: {},
|
|
12
|
+
rows: [
|
|
13
|
+
{ key: "Post.title", source: "entity", isFunction: false, ko: "제목", en: "Title" },
|
|
14
|
+
{ key: "common.save", source: "project", isFunction: false, ko: "저장", en: "Save" },
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function stubDictionary() {
|
|
19
|
+
vi.spyOn(sonamuDictionary, "getDictionary").mockResolvedValue(DICTIONARY);
|
|
20
|
+
return vi.spyOn(sonamuDictionary, "updateEntry").mockResolvedValue(undefined);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
await attachSonamuTestRoot();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
await detachSonamuTestRoot();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("i18n update의 source 결정", () => {
|
|
33
|
+
it("entity 항목은 source를 생략해도 entity로 갱신한다", async () => {
|
|
34
|
+
const updateEntry = stubDictionary();
|
|
35
|
+
|
|
36
|
+
await tooling.i18n.update({ key: "Post.title", values: { ko: "게시글 제목" } });
|
|
37
|
+
|
|
38
|
+
expect(updateEntry).toHaveBeenCalledWith({
|
|
39
|
+
oldKey: "Post.title",
|
|
40
|
+
newKey: "Post.title",
|
|
41
|
+
source: "entity",
|
|
42
|
+
values: { ko: "게시글 제목" },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("project 항목은 project로 갱신한다", async () => {
|
|
47
|
+
const updateEntry = stubDictionary();
|
|
48
|
+
|
|
49
|
+
await tooling.i18n.update({ key: "common.save", values: { ko: "보관" } });
|
|
50
|
+
|
|
51
|
+
expect(updateEntry).toHaveBeenCalledWith(
|
|
52
|
+
expect.objectContaining({ oldKey: "common.save", source: "project" }),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("사전에 없는 key는 project로 폴백한다", async () => {
|
|
57
|
+
const updateEntry = stubDictionary();
|
|
58
|
+
|
|
59
|
+
await tooling.i18n.update({ key: "unknown.key", values: { ko: "값" } });
|
|
60
|
+
|
|
61
|
+
expect(updateEntry).toHaveBeenCalledWith(
|
|
62
|
+
expect.objectContaining({ oldKey: "unknown.key", source: "project" }),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("명시한 source가 사전에서 조회한 source보다 우선한다", async () => {
|
|
67
|
+
const updateEntry = stubDictionary();
|
|
68
|
+
|
|
69
|
+
await tooling.i18n.update({
|
|
70
|
+
key: "Post.title",
|
|
71
|
+
source: "project",
|
|
72
|
+
values: { ko: "게시글 제목" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(updateEntry).toHaveBeenCalledWith(expect.objectContaining({ source: "project" }));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("key 변경 시에도 기존 항목의 source를 유지한다", async () => {
|
|
79
|
+
const updateEntry = stubDictionary();
|
|
80
|
+
|
|
81
|
+
await tooling.i18n.update({
|
|
82
|
+
key: "Post.title",
|
|
83
|
+
newKey: "Post.headline",
|
|
84
|
+
values: { ko: "게시글 제목" },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(updateEntry).toHaveBeenCalledWith({
|
|
88
|
+
oldKey: "Post.title",
|
|
89
|
+
newKey: "Post.headline",
|
|
90
|
+
source: "entity",
|
|
91
|
+
values: { ko: "게시글 제목" },
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("dry run은 사전을 변경하지 않는다", async () => {
|
|
96
|
+
const updateEntry = stubDictionary();
|
|
97
|
+
|
|
98
|
+
await expect(
|
|
99
|
+
tooling.i18n.update({ key: "Post.title", values: { ko: "게시글 제목" }, dryRun: true }),
|
|
100
|
+
).resolves.toMatchObject({ operation: "update", dryRun: true });
|
|
101
|
+
|
|
102
|
+
expect(updateEntry).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Sonamu } from "../../api/sonamu";
|
|
6
|
+
import * as codeGenerator from "../../syncer/code-generator";
|
|
7
|
+
import { tooling } from "../cli-tooling";
|
|
8
|
+
import { attachSonamuTestRoot, detachSonamuTestRoot } from "./helpers/sonamu-test-root";
|
|
9
|
+
|
|
10
|
+
const RENDERED_PATH = "/:target/src/components/PostSearchInput.tsx";
|
|
11
|
+
|
|
12
|
+
function stubRenderTemplate() {
|
|
13
|
+
return vi
|
|
14
|
+
.spyOn(codeGenerator, "renderTemplate")
|
|
15
|
+
.mockResolvedValue([{ path: RENDERED_PATH, code: "// rendered" }]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
await attachSonamuTestRoot();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
await detachSonamuTestRoot();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("scaffold의 view_search_input 템플릿", () => {
|
|
28
|
+
it("status에서 view_search_input을 렌더링 대상으로 인식한다", async () => {
|
|
29
|
+
const renderTemplate = stubRenderTemplate();
|
|
30
|
+
|
|
31
|
+
await expect(
|
|
32
|
+
tooling.scaffold.status({ entities: ["Post"], templates: ["view_search_input"] }),
|
|
33
|
+
).resolves.toEqual([
|
|
34
|
+
{
|
|
35
|
+
entityId: "Post",
|
|
36
|
+
template: "view_search_input",
|
|
37
|
+
target: path.join(Sonamu.appRootPath, "web/src/components/PostSearchInput.tsx"),
|
|
38
|
+
exists: false,
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
expect(renderTemplate).toHaveBeenCalledWith("view_search_input", { entityId: "Post" });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("preview에서 view_search_input의 렌더링 내용을 반환한다", async () => {
|
|
46
|
+
stubRenderTemplate();
|
|
47
|
+
|
|
48
|
+
await expect(
|
|
49
|
+
tooling.scaffold.preview({ entities: ["Post"], templates: ["view_search_input"] }),
|
|
50
|
+
).resolves.toEqual([
|
|
51
|
+
expect.objectContaining({ template: "view_search_input", content: "// rendered" }),
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("batch dry run에서도 view_search_input을 렌더링한다", async () => {
|
|
56
|
+
const renderTemplate = stubRenderTemplate();
|
|
57
|
+
|
|
58
|
+
await expect(
|
|
59
|
+
tooling.scaffold.batch({
|
|
60
|
+
entities: ["Post"],
|
|
61
|
+
templates: ["view_search_input"],
|
|
62
|
+
dryRun: true,
|
|
63
|
+
}),
|
|
64
|
+
).resolves.toEqual([expect.objectContaining({ template: "view_search_input" })]);
|
|
65
|
+
|
|
66
|
+
expect(renderTemplate).toHaveBeenCalledWith("view_search_input", { entityId: "Post" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("기존 템플릿 키도 그대로 렌더링한다", async () => {
|
|
70
|
+
const renderTemplate = stubRenderTemplate();
|
|
71
|
+
|
|
72
|
+
await tooling.scaffold.status({ entities: ["Post"], templates: ["view_list"] });
|
|
73
|
+
|
|
74
|
+
expect(renderTemplate).toHaveBeenCalledWith("view_list", { entityId: "Post" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("지원하지 않는 템플릿 키는 거절한다", async () => {
|
|
78
|
+
const renderTemplate = stubRenderTemplate();
|
|
79
|
+
|
|
80
|
+
await expect(
|
|
81
|
+
tooling.scaffold.status({ entities: ["Post"], templates: ["bogus_template"] }),
|
|
82
|
+
).rejects.toThrow();
|
|
83
|
+
|
|
84
|
+
expect(renderTemplate).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|