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,107 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } 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 { Sonamu } from "../../api/sonamu";
|
|
8
|
+
import { EntityManager } from "../entity-manager";
|
|
9
|
+
|
|
10
|
+
const temporaryRoots: string[] = [];
|
|
11
|
+
|
|
12
|
+
const POST_ENTITY_JSON = {
|
|
13
|
+
id: "Post",
|
|
14
|
+
table: "posts",
|
|
15
|
+
title: "게시글",
|
|
16
|
+
props: [
|
|
17
|
+
{ name: "id", type: "integer" },
|
|
18
|
+
{ name: "title", type: "string", length: 128 },
|
|
19
|
+
],
|
|
20
|
+
indexes: [],
|
|
21
|
+
subsets: {},
|
|
22
|
+
enums: {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function createApiRootWithEntity(): Promise<string> {
|
|
26
|
+
const apiRootPath = await mkdtemp(path.join(os.tmpdir(), "sonamu-explicit-root-"));
|
|
27
|
+
temporaryRoots.push(apiRootPath);
|
|
28
|
+
const entityDirectory = path.join(apiRootPath, "src", "application", "post");
|
|
29
|
+
await mkdir(entityDirectory, { recursive: true });
|
|
30
|
+
await writeFile(
|
|
31
|
+
path.join(entityDirectory, "post.entity.json"),
|
|
32
|
+
JSON.stringify(POST_ENTITY_JSON, null, 2),
|
|
33
|
+
"utf8",
|
|
34
|
+
);
|
|
35
|
+
return apiRootPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
await Promise.all(
|
|
40
|
+
temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("EntityManager 명시적 API 루트 등록", () => {
|
|
45
|
+
it("Sonamu 초기화 전에도 명시한 루트의 entity.json을 등록한다", async () => {
|
|
46
|
+
const apiRootPath = await createApiRootWithEntity();
|
|
47
|
+
const previousAutoloaded = EntityManager.isAutoloaded;
|
|
48
|
+
EntityManager.isAutoloaded = false;
|
|
49
|
+
|
|
50
|
+
// 명시적 루트 경로는 Sonamu singleton 초기화 없이 동작해야 합니다.
|
|
51
|
+
expect(() => Sonamu.apiRootPath).toThrow(/init/i);
|
|
52
|
+
try {
|
|
53
|
+
await expect(EntityManager.autoload(true, apiRootPath)).resolves.toBeUndefined();
|
|
54
|
+
|
|
55
|
+
expect(EntityManager.isAutoloaded).toBe(true);
|
|
56
|
+
expect(EntityManager.exists("Post")).toBe(true);
|
|
57
|
+
expect(EntityManager.get("Post").table).toBe("posts");
|
|
58
|
+
expect(EntityManager.modulePaths.get("PostBaseSchema")).toBe("sonamu.generated");
|
|
59
|
+
} finally {
|
|
60
|
+
EntityManager.isAutoloaded = previousAutoloaded;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("reload()는 autoload에 넘긴 명시적 루트를 그대로 다시 읽는다", async () => {
|
|
65
|
+
const apiRootPath = await createApiRootWithEntity();
|
|
66
|
+
const previousAutoloaded = EntityManager.isAutoloaded;
|
|
67
|
+
EntityManager.isAutoloaded = false;
|
|
68
|
+
|
|
69
|
+
expect(() => Sonamu.apiRootPath).toThrow(/init/i);
|
|
70
|
+
try {
|
|
71
|
+
await EntityManager.autoload(true, apiRootPath);
|
|
72
|
+
expect(EntityManager.exists("Post")).toBe(true);
|
|
73
|
+
|
|
74
|
+
// 보존한 루트를 잃으면 초기화되지 않은 Sonamu.apiRootPath로 폴백해 throw합니다.
|
|
75
|
+
await expect(EntityManager.reload(true)).resolves.toBeUndefined();
|
|
76
|
+
expect(EntityManager.exists("Post")).toBe(true);
|
|
77
|
+
expect(EntityManager.get("Post").table).toBe("posts");
|
|
78
|
+
} finally {
|
|
79
|
+
EntityManager.isAutoloaded = previousAutoloaded;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("reload()에 넘긴 루트는 보존된 루트보다 우선한다", async () => {
|
|
84
|
+
const firstRootPath = await createApiRootWithEntity();
|
|
85
|
+
const secondRootPath = await createApiRootWithEntity();
|
|
86
|
+
// 두 번째 루트에만 존재하는 entity로 어느 루트를 읽었는지 구분합니다.
|
|
87
|
+
const commentDirectory = path.join(secondRootPath, "src", "application", "comment");
|
|
88
|
+
await mkdir(commentDirectory, { recursive: true });
|
|
89
|
+
await writeFile(
|
|
90
|
+
path.join(commentDirectory, "comment.entity.json"),
|
|
91
|
+
JSON.stringify({ ...POST_ENTITY_JSON, id: "Comment", table: "comments" }, null, 2),
|
|
92
|
+
"utf8",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const previousAutoloaded = EntityManager.isAutoloaded;
|
|
96
|
+
EntityManager.isAutoloaded = false;
|
|
97
|
+
try {
|
|
98
|
+
await EntityManager.autoload(true, firstRootPath);
|
|
99
|
+
expect(EntityManager.exists("Comment")).toBe(false);
|
|
100
|
+
|
|
101
|
+
await EntityManager.reload(true, secondRootPath);
|
|
102
|
+
expect(EntityManager.exists("Comment")).toBe(true);
|
|
103
|
+
} finally {
|
|
104
|
+
EntityManager.isAutoloaded = previousAutoloaded;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -48,13 +48,17 @@ class EntityManagerClass {
|
|
|
48
48
|
public modulePaths: Map<string, string> = new Map();
|
|
49
49
|
private tableSpecs: Map<string, TableSpec> = new Map();
|
|
50
50
|
public isAutoloaded: boolean = false;
|
|
51
|
+
// reload()가 Sonamu 초기화 없이도 같은 루트를 다시 읽도록 명시적 api root를 보존합니다.
|
|
52
|
+
private explicitApiRootPath: string | undefined;
|
|
51
53
|
|
|
52
54
|
// 경로 전달받아 모든 entity.json 파일 로드
|
|
53
|
-
async autoload(doSilent: boolean = false) {
|
|
55
|
+
async autoload(doSilent: boolean = false, explicitApiRootPath?: string) {
|
|
54
56
|
if (this.isAutoloaded) {
|
|
55
57
|
return;
|
|
56
58
|
}
|
|
57
|
-
|
|
59
|
+
this.explicitApiRootPath = explicitApiRootPath;
|
|
60
|
+
const apiRootPath = explicitApiRootPath ?? Sonamu.apiRootPath;
|
|
61
|
+
const pathPattern = path.join(apiRootPath, "/src/application/**/*.entity.json");
|
|
58
62
|
|
|
59
63
|
for await (const file of glob(path.resolve(pathPattern))) {
|
|
60
64
|
const json = parseEntityJson((await readFile(file)).toString());
|
|
@@ -62,7 +66,7 @@ class EntityManagerClass {
|
|
|
62
66
|
// entity.json 스키마 검증
|
|
63
67
|
const error = this.schemaValidate(json);
|
|
64
68
|
if (error) {
|
|
65
|
-
const relativePath = path.relative(
|
|
69
|
+
const relativePath = path.relative(apiRootPath, file);
|
|
66
70
|
const errorMessage = prettifyError(error);
|
|
67
71
|
if (!doSilent) {
|
|
68
72
|
console.error(
|
|
@@ -71,11 +75,14 @@ class EntityManagerClass {
|
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
await this.register(json, {
|
|
78
|
+
await this.register(json, {
|
|
79
|
+
deferSearchTextJsonSourceValidation: true,
|
|
80
|
+
apiRootPath,
|
|
81
|
+
});
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
await this.registerNonEntityTypeModulePaths();
|
|
78
|
-
await this.validateAllRegisteredSearchTextJsonSources();
|
|
84
|
+
await this.registerNonEntityTypeModulePaths(apiRootPath);
|
|
85
|
+
await this.validateAllRegisteredSearchTextJsonSources(apiRootPath);
|
|
79
86
|
|
|
80
87
|
this.isAutoloaded = true;
|
|
81
88
|
}
|
|
@@ -85,36 +92,36 @@ class EntityManagerClass {
|
|
|
85
92
|
return result.success ? null : result.error;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
async reload(doSilent: boolean = false) {
|
|
95
|
+
async reload(doSilent: boolean = false, explicitApiRootPath?: string) {
|
|
89
96
|
this.entities.clear();
|
|
90
97
|
this.modulePaths.clear();
|
|
91
98
|
this.tableSpecs.clear();
|
|
92
99
|
this.isAutoloaded = false;
|
|
93
100
|
|
|
94
|
-
return await this.autoload(doSilent);
|
|
101
|
+
return await this.autoload(doSilent, explicitApiRootPath ?? this.explicitApiRootPath);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
async register(
|
|
98
105
|
json: EntityJson,
|
|
99
|
-
options: { deferSearchTextJsonSourceValidation?: boolean } = {},
|
|
106
|
+
options: { deferSearchTextJsonSourceValidation?: boolean; apiRootPath?: string } = {},
|
|
100
107
|
): Promise<void> {
|
|
101
108
|
const { Entity } = await import("./entity");
|
|
102
109
|
const entity = new Entity(json);
|
|
103
|
-
await entity.registerModulePaths();
|
|
110
|
+
await entity.registerModulePaths(options.apiRootPath);
|
|
104
111
|
if (!options.deferSearchTextJsonSourceValidation) {
|
|
105
|
-
await this.validateSearchTextJsonSources(entity);
|
|
112
|
+
await this.validateSearchTextJsonSources(entity, options.apiRootPath);
|
|
106
113
|
}
|
|
107
114
|
entity.registerTableSpecs();
|
|
108
115
|
this.entities.set(json.id, entity);
|
|
109
116
|
}
|
|
110
117
|
|
|
111
|
-
async validateAllRegisteredSearchTextJsonSources(): Promise<void> {
|
|
118
|
+
async validateAllRegisteredSearchTextJsonSources(apiRootPath?: string): Promise<void> {
|
|
112
119
|
for (const entity of this.entities.values()) {
|
|
113
|
-
await this.validateSearchTextJsonSources(entity);
|
|
120
|
+
await this.validateSearchTextJsonSources(entity, apiRootPath);
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
private async validateSearchTextJsonSources(entity: Entity): Promise<void> {
|
|
124
|
+
private async validateSearchTextJsonSources(entity: Entity, apiRootPath?: string): Promise<void> {
|
|
118
125
|
const propsByName = new Map(entity.props.map((prop) => [prop.name, prop]));
|
|
119
126
|
|
|
120
127
|
for (const prop of entity.props) {
|
|
@@ -128,7 +135,11 @@ class EntityManagerClass {
|
|
|
128
135
|
continue;
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
const zodType = await this.resolveSearchTextJsonSourceType(
|
|
138
|
+
const zodType = await this.resolveSearchTextJsonSourceType(
|
|
139
|
+
entity,
|
|
140
|
+
sourceProp.id,
|
|
141
|
+
apiRootPath,
|
|
142
|
+
);
|
|
132
143
|
if (!zodType) {
|
|
133
144
|
throw new Error(
|
|
134
145
|
`searchText source "${source.name}"의 json 타입 "${sourceProp.id}"을(를) 로드할 수 없습니다.`,
|
|
@@ -147,6 +158,7 @@ class EntityManagerClass {
|
|
|
147
158
|
private async resolveSearchTextJsonSourceType(
|
|
148
159
|
entity: Entity,
|
|
149
160
|
typeId: string,
|
|
161
|
+
explicitApiRootPath?: string,
|
|
150
162
|
): Promise<z.ZodTypeAny | null> {
|
|
151
163
|
const localType = entity.types[typeId];
|
|
152
164
|
if (localType instanceof z.ZodType) {
|
|
@@ -173,7 +185,7 @@ class EntityManagerClass {
|
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
const moduleFilePath = path.join(
|
|
176
|
-
Sonamu.apiRootPath,
|
|
188
|
+
explicitApiRootPath ?? Sonamu.apiRootPath,
|
|
177
189
|
runtimePath(`dist/application/${modulePath}.js`),
|
|
178
190
|
);
|
|
179
191
|
const importedMembers = await importMembers<unknown>(moduleFilePath);
|
|
@@ -296,10 +308,10 @@ class EntityManagerClass {
|
|
|
296
308
|
return inflection.camelize(entityBaseName.replace(/-/g, "_"));
|
|
297
309
|
}
|
|
298
310
|
|
|
299
|
-
private async registerNonEntityTypeModulePaths(): Promise<void> {
|
|
311
|
+
private async registerNonEntityTypeModulePaths(apiRootPath: string): Promise<void> {
|
|
300
312
|
const typePathsPatterns = [
|
|
301
|
-
path.join(
|
|
302
|
-
path.join(
|
|
313
|
+
path.join(apiRootPath, runtimePath("src/application/**/*.types.ts")),
|
|
314
|
+
path.join(apiRootPath, runtimePath("src/application/**/*.generated.ts")),
|
|
303
315
|
];
|
|
304
316
|
const typePaths = (
|
|
305
317
|
await Promise.all(typePathsPatterns.map((pattern) => globAsync(pattern)))
|
package/src/entity/entity.ts
CHANGED
|
@@ -827,7 +827,12 @@ export class Entity {
|
|
|
827
827
|
});
|
|
828
828
|
}
|
|
829
829
|
|
|
830
|
-
|
|
830
|
+
/**
|
|
831
|
+
* Entity가 노출하는 타입 이름과 모듈 경로를 EntityManager에 등록합니다.
|
|
832
|
+
*
|
|
833
|
+
* @param explicitApiRootPath - Sonamu 초기화 없이 등록할 때 사용할 API 루트 경로
|
|
834
|
+
*/
|
|
835
|
+
async registerModulePaths(explicitApiRootPath?: string) {
|
|
831
836
|
const basePath = `${this.names.parentFs}`;
|
|
832
837
|
|
|
833
838
|
// base-scheme
|
|
@@ -853,7 +858,7 @@ export class Entity {
|
|
|
853
858
|
// types
|
|
854
859
|
const typesModulePath = `${basePath}/${this.names.parentFs}.types`;
|
|
855
860
|
const typesFilePath = path.join(
|
|
856
|
-
Sonamu.apiRootPath,
|
|
861
|
+
explicitApiRootPath ?? Sonamu.apiRootPath,
|
|
857
862
|
runtimePath(`dist/application/${typesModulePath}.js`),
|
|
858
863
|
);
|
|
859
864
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { dispose, getLogger, type Config, type LogRecord } from "@logtape/logtape";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { configureLogTape } from "../configure";
|
|
5
|
+
|
|
6
|
+
describe("CLI LogTape 설정 합성", () => {
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
globalThis.sonamuKitCliLogTapeOverride = undefined;
|
|
9
|
+
await dispose();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("CLI 출력 설정과 프로젝트 카테고리 설정을 함께 유지한다", async () => {
|
|
13
|
+
const cliRecords: LogRecord[] = [];
|
|
14
|
+
const appRecords: LogRecord[] = [];
|
|
15
|
+
const cliConfig: Config<string, string> = {
|
|
16
|
+
sinks: { default: (record) => cliRecords.push(record) },
|
|
17
|
+
loggers: [{ category: [], lowestLevel: "debug", sinks: ["default"] }],
|
|
18
|
+
reset: true,
|
|
19
|
+
};
|
|
20
|
+
globalThis.sonamuKitCliLogTapeOverride = { config: cliConfig, applied: false };
|
|
21
|
+
|
|
22
|
+
await configureLogTape({
|
|
23
|
+
sinks: { default: (record) => appRecords.push(record) },
|
|
24
|
+
loggers: [{ category: ["app"], lowestLevel: "debug", sinks: ["default"] }],
|
|
25
|
+
});
|
|
26
|
+
getLogger(["cli"]).debug("CLI 로그");
|
|
27
|
+
getLogger(["app"]).debug("프로젝트 로그");
|
|
28
|
+
|
|
29
|
+
expect(globalThis.sonamuKitCliLogTapeOverride.applied).toBe(true);
|
|
30
|
+
expect(cliRecords.map((record) => record.category[0])).toEqual(["cli", "app"]);
|
|
31
|
+
expect(appRecords).toHaveLength(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/logger/configure.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { configure, getConsoleSink } from "@logtape/logtape";
|
|
2
2
|
import {
|
|
3
|
+
type Config,
|
|
3
4
|
type Filter,
|
|
4
5
|
type FilterLike,
|
|
5
6
|
type LoggerConfig,
|
|
@@ -12,6 +13,23 @@ import { type FastifyReply, type FastifyRequest } from "fastify";
|
|
|
12
13
|
|
|
13
14
|
import { isSameCategory } from "./category";
|
|
14
15
|
|
|
16
|
+
interface ExternalLogTapeConfiguration {
|
|
17
|
+
readonly config: Config<string, string>;
|
|
18
|
+
applied: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
var sonamuKitCliLogTapeOverride: ExternalLogTapeConfiguration | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getExternalLogTapeConfiguration(): ExternalLogTapeConfiguration | undefined {
|
|
26
|
+
return globalThis.sonamuKitCliLogTapeOverride;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hasExternalLogTapeConfiguration(): boolean {
|
|
30
|
+
return getExternalLogTapeConfiguration() !== undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export type SonamuLoggingOptions<TSinkId extends string, TFilterId extends string> = {
|
|
16
34
|
// fastify 로깅 카테고리 (a.b.c의 형태로 넣으면 [a, b, c]로 들어갑니다.)
|
|
17
35
|
// 기본값은 ["fastify"] 입니다.
|
|
@@ -107,19 +125,42 @@ function defaultFastifyFilter(fastifyCategory: readonly string[]): Filter {
|
|
|
107
125
|
export async function configureLogTape<TSinkId extends string, TFilterId extends string>(
|
|
108
126
|
options: SonamuLoggingOptions<TSinkId, TFilterId>,
|
|
109
127
|
) {
|
|
128
|
+
const external = getExternalLogTapeConfiguration();
|
|
110
129
|
const fastifyCategory = options.fastifyCategory ?? ["fastify"];
|
|
111
130
|
|
|
112
|
-
const
|
|
131
|
+
const projectSinks = {
|
|
113
132
|
"fastify-console": defaultFastifySink(fastifyCategory),
|
|
114
133
|
...options.sinks,
|
|
115
|
-
}
|
|
134
|
+
} satisfies Record<string, Sink>;
|
|
135
|
+
const externalSinkIds = new Map<string, string>();
|
|
136
|
+
const externalSinks: Record<string, Sink> = {};
|
|
137
|
+
for (const [sinkId, sink] of Object.entries(external?.config.sinks ?? {})) {
|
|
138
|
+
let namespacedId = `sonamu-cli:${sinkId}`;
|
|
139
|
+
while (
|
|
140
|
+
Object.hasOwn(projectSinks, namespacedId) ||
|
|
141
|
+
Object.hasOwn(externalSinks, namespacedId)
|
|
142
|
+
) {
|
|
143
|
+
namespacedId = `${namespacedId}-`;
|
|
144
|
+
}
|
|
145
|
+
externalSinkIds.set(sinkId, namespacedId);
|
|
146
|
+
externalSinks[namespacedId] = sink;
|
|
147
|
+
}
|
|
148
|
+
const sinks = { ...projectSinks, ...externalSinks };
|
|
116
149
|
|
|
117
150
|
const filters = {
|
|
118
151
|
"fastify-console": defaultFastifyFilter(fastifyCategory),
|
|
119
152
|
...options.filters,
|
|
153
|
+
...external?.config.filters,
|
|
120
154
|
};
|
|
121
155
|
|
|
122
|
-
const
|
|
156
|
+
const externalLoggers = (external?.config.loggers ?? []).map((logger) => ({
|
|
157
|
+
...logger,
|
|
158
|
+
sinks: logger.sinks?.map((sinkId) => externalSinkIds.get(sinkId) ?? sinkId),
|
|
159
|
+
}));
|
|
160
|
+
const loggers = new Set<LoggerConfig<string, string>>([
|
|
161
|
+
...externalLoggers,
|
|
162
|
+
...(options.loggers ?? []),
|
|
163
|
+
]);
|
|
123
164
|
|
|
124
165
|
// logtape의 meta logger 표시를 비활성화
|
|
125
166
|
loggers.add({
|
|
@@ -136,5 +177,6 @@ export async function configureLogTape<TSinkId extends string, TFilterId extends
|
|
|
136
177
|
});
|
|
137
178
|
}
|
|
138
179
|
|
|
180
|
+
if (external !== undefined) external.applied = true;
|
|
139
181
|
return configure({ sinks, filters, loggers: [...loggers], reset: true });
|
|
140
182
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type Knex } from "knex";
|
|
2
|
+
import knex from "knex";
|
|
1
3
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
4
|
|
|
3
5
|
import { type SonamuConfig } from "../../api/config";
|
|
@@ -89,6 +91,55 @@ describe("Migrator environment target filtering", () => {
|
|
|
89
91
|
});
|
|
90
92
|
});
|
|
91
93
|
|
|
94
|
+
describe("Migrator 최신 적용 batch 조회", () => {
|
|
95
|
+
const presets: SonamuDBPreset[] = [
|
|
96
|
+
"test",
|
|
97
|
+
"fixture",
|
|
98
|
+
"development",
|
|
99
|
+
"staging",
|
|
100
|
+
"production",
|
|
101
|
+
"test_readonly",
|
|
102
|
+
"development_readonly",
|
|
103
|
+
"staging_readonly",
|
|
104
|
+
"production_readonly",
|
|
105
|
+
];
|
|
106
|
+
const baseDbConfig =
|
|
107
|
+
/* SAFETY: 모든 Sonamu DB preset에 유효한 PostgreSQL 테스트 설정을 제공합니다. */ Object.fromEntries(
|
|
108
|
+
presets.map((preset) => [preset, { client: "postgresql", connection: { database: preset } }]),
|
|
109
|
+
) as SonamuDBConfig;
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
Sonamu.dbConfig = baseDbConfig;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("PostgreSQL 42P01은 사용자 migration table 이름과 오류 문구에 관계없이 미적용 상태로 처리한다", async () => {
|
|
116
|
+
const clientTemplate = knex({ client: "pg" });
|
|
117
|
+
// SAFETY: knex({ client: "pg" })가 생성한 실제 PostgreSQL client 생성자입니다.
|
|
118
|
+
const PostgreSqlClient = clientTemplate.client.constructor as typeof Knex.Client;
|
|
119
|
+
class MissingMigrationTableClient extends PostgreSqlClient {
|
|
120
|
+
async acquireRawConnection(): Promise<never> {
|
|
121
|
+
throw Object.assign(new Error('relation "sonamu_history" does not exist'), {
|
|
122
|
+
code: "42P01",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await clientTemplate.destroy();
|
|
127
|
+
Sonamu.dbConfig = {
|
|
128
|
+
...baseDbConfig,
|
|
129
|
+
development: {
|
|
130
|
+
client: MissingMigrationTableClient,
|
|
131
|
+
connection: { database: "sonamu_test" },
|
|
132
|
+
migrations: { tableName: "sonamu_history" },
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await expect(new Migrator().getLatestAppliedBatch("development")).resolves.toEqual({
|
|
137
|
+
batchNo: 0,
|
|
138
|
+
files: [],
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
92
143
|
describe("SlackConfirm target validation", () => {
|
|
93
144
|
const presets: SonamuDBPreset[] = [
|
|
94
145
|
"test",
|
|
@@ -41,6 +41,8 @@ export type MigrationResult = {
|
|
|
41
41
|
applied: string[];
|
|
42
42
|
}[];
|
|
43
43
|
|
|
44
|
+
type ShadowMigrationTarget = Extract<MigrationTarget, "fixture" | "test">;
|
|
45
|
+
|
|
44
46
|
function padDatePart(number: number, size: number = 2): string {
|
|
45
47
|
return number.toString().padStart(size, "0");
|
|
46
48
|
}
|
|
@@ -95,13 +97,9 @@ export class Migrator {
|
|
|
95
97
|
const maybePostgresError =
|
|
96
98
|
/* SAFETY: Knex와 PostgreSQL 스키마 조회 계약이 이 값의 타입을 보장한다. */ error as {
|
|
97
99
|
code?: unknown;
|
|
98
|
-
message?: unknown;
|
|
99
100
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
isStringValue(maybePostgresError.message) &&
|
|
103
|
-
maybePostgresError.message.includes("knex_migrations")
|
|
104
|
-
);
|
|
101
|
+
// PostgreSQL undefined_table 코드는 사용자 지정 migration table에도 동일하게 적용됩니다.
|
|
102
|
+
return maybePostgresError.code === "42P01";
|
|
105
103
|
}
|
|
106
104
|
|
|
107
105
|
/** 현재 실행 환경에서 허용된 마이그레이션 대상 키를 반환합니다. */
|
|
@@ -375,6 +373,61 @@ export class Migrator {
|
|
|
375
373
|
}
|
|
376
374
|
}
|
|
377
375
|
|
|
376
|
+
/**
|
|
377
|
+
* 대상 DB에서 가장 최근에 적용된 migration batch를 반환합니다.
|
|
378
|
+
*
|
|
379
|
+
* @category 분리형 마이그레이션 API
|
|
380
|
+
*/
|
|
381
|
+
async getLatestAppliedBatch(
|
|
382
|
+
connKey: MigrationTarget,
|
|
383
|
+
): Promise<{ batchNo: number; files: string[] }> {
|
|
384
|
+
this.assertMigrationTarget(connKey);
|
|
385
|
+
const knexOptions = Sonamu.dbConfig[connKey];
|
|
386
|
+
const connection =
|
|
387
|
+
/* SAFETY: Knex와 PostgreSQL 스키마 조회 계약이 이 값의 타입을 보장한다. */ knexOptions.connection as Knex.PgConnectionConfig;
|
|
388
|
+
const tConn = createKnexInstance({
|
|
389
|
+
...knexOptions,
|
|
390
|
+
connection: {
|
|
391
|
+
...connection,
|
|
392
|
+
connectionTimeoutMillis: MIGRATION_CONN_TIMEOUT_MS,
|
|
393
|
+
},
|
|
394
|
+
pool: {
|
|
395
|
+
...knexOptions.pool,
|
|
396
|
+
min: 0,
|
|
397
|
+
acquireTimeoutMillis: MIGRATION_CONN_TIMEOUT_MS,
|
|
398
|
+
createTimeoutMillis: MIGRATION_CONN_TIMEOUT_MS,
|
|
399
|
+
propagateCreateError: true,
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const tableName = knexOptions.migrations?.tableName ?? "knex_migrations";
|
|
405
|
+
const schemaName = knexOptions.migrations?.schemaName;
|
|
406
|
+
const migrationsTable =
|
|
407
|
+
schemaName === undefined
|
|
408
|
+
? tConn<{ id: number; name: string; batch: number }>(tableName)
|
|
409
|
+
: tConn<{ id: number; name: string; batch: number }>(tableName).withSchema(schemaName);
|
|
410
|
+
const latest = await migrationsTable.clone().select("batch").orderBy("batch", "desc").first();
|
|
411
|
+
if (latest === undefined) return { batchNo: 0, files: [] };
|
|
412
|
+
|
|
413
|
+
// 같은 batch 안에서는 Knex 실행 순서대로 롤백 계획을 고정한다.
|
|
414
|
+
const rows = await migrationsTable
|
|
415
|
+
.clone()
|
|
416
|
+
.select("name")
|
|
417
|
+
.where("batch", latest.batch)
|
|
418
|
+
.orderBy("id", "asc");
|
|
419
|
+
return {
|
|
420
|
+
batchNo: latest.batch,
|
|
421
|
+
files: rows.map(({ name }) => name.replace(/\.ts$/, "")),
|
|
422
|
+
};
|
|
423
|
+
} catch (caught) {
|
|
424
|
+
if (this.isMissingMigrationTableError(caught)) return { batchNo: 0, files: [] };
|
|
425
|
+
throw caught;
|
|
426
|
+
} finally {
|
|
427
|
+
await tConn.destroy();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
378
431
|
/**
|
|
379
432
|
* 최신 상태인 기준 DB와 엔티티 정의를 비교해 생성 예정 코드를 반환합니다.
|
|
380
433
|
*
|
|
@@ -659,15 +712,41 @@ export class Migrator {
|
|
|
659
712
|
*
|
|
660
713
|
* @category 분리형 마이그레이션 API
|
|
661
714
|
*/
|
|
662
|
-
async runShadowTest(
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
715
|
+
async runShadowTest(
|
|
716
|
+
options?: MigrationRunOptions & { target?: MigrationTarget },
|
|
717
|
+
): Promise<MigrationResult> {
|
|
718
|
+
const target = options?.target ?? "test";
|
|
719
|
+
if (target !== "test" && target !== "fixture") {
|
|
720
|
+
// 운영 계열 DB는 설정 조회나 연결 생성보다 먼저 차단한다.
|
|
721
|
+
throw Object.assign(new Error(`지원하지 않는 Shadow 마이그레이션 대상입니다: ${target}`), {
|
|
722
|
+
code: "INVALID_SHADOW_MIGRATION_TARGET",
|
|
723
|
+
target,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
const shadowTarget: ShadowMigrationTarget = target;
|
|
727
|
+
const baseTargetConn =
|
|
728
|
+
/* SAFETY: Knex와 PostgreSQL 스키마 조회 계약이 이 값의 타입을 보장한다. */ Sonamu.dbConfig[
|
|
729
|
+
shadowTarget
|
|
730
|
+
].connection as Knex.PgConnectionConfig;
|
|
731
|
+
const configuredDatabase = baseTargetConn.database;
|
|
732
|
+
if (!configuredDatabase) {
|
|
733
|
+
throw Object.assign(new Error(`Shadow 대상 DB 이름이 없습니다: ${shadowTarget}`), {
|
|
734
|
+
code: "INVALID_SHADOW_DATABASE_CONFIG",
|
|
735
|
+
target: shadowTarget,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const workerId =
|
|
739
|
+
shadowTarget === "test" && process.env.SONAMU_WORKER_DB === "true"
|
|
740
|
+
? process.env.VITEST_POOL_ID
|
|
741
|
+
: null;
|
|
667
742
|
const templateDatabase =
|
|
668
|
-
workerId !== null ? `${
|
|
669
|
-
const tdbConn = { ...baseTestConn, database: templateDatabase };
|
|
743
|
+
workerId !== null ? `${configuredDatabase}_${workerId ?? "1"}` : configuredDatabase;
|
|
670
744
|
const shadowDatabase = `${templateDatabase}__migration_shadow`;
|
|
745
|
+
const createShadowConnection = (database: string) =>
|
|
746
|
+
createKnexInstance({
|
|
747
|
+
...Sonamu.dbConfig[shadowTarget],
|
|
748
|
+
connection: { ...baseTargetConn, database },
|
|
749
|
+
});
|
|
671
750
|
|
|
672
751
|
// 테스트 상황에서는 트랜잭션을 초기화하고, 새 데이터베이스 커넥션을 가져와야 함
|
|
673
752
|
if (isTest()) {
|
|
@@ -676,33 +755,23 @@ export class Migrator {
|
|
|
676
755
|
}
|
|
677
756
|
|
|
678
757
|
// 기존 Shadow DB 삭제 후 Shadow DB 생성
|
|
679
|
-
const tdb =
|
|
680
|
-
...Sonamu.dbConfig.test,
|
|
681
|
-
connection: {
|
|
682
|
-
...baseTestConn,
|
|
683
|
-
database: "postgres",
|
|
684
|
-
},
|
|
685
|
-
});
|
|
758
|
+
const tdb = createShadowConnection("postgres");
|
|
686
759
|
try {
|
|
687
760
|
!isTest() && console.log(chalk.magenta(`${shadowDatabase} 삭제`));
|
|
688
|
-
await tdb.raw(
|
|
689
|
-
await tdb.raw(
|
|
761
|
+
await tdb.raw("DROP DATABASE IF EXISTS ??", [shadowDatabase]);
|
|
762
|
+
await tdb.raw(
|
|
763
|
+
`
|
|
690
764
|
SELECT pg_terminate_backend(pg_stat_activity.pid)
|
|
691
765
|
FROM pg_stat_activity
|
|
692
|
-
WHERE datname =
|
|
766
|
+
WHERE datname = ?
|
|
693
767
|
AND pid <> pg_backend_pid();
|
|
694
|
-
|
|
695
|
-
|
|
768
|
+
`,
|
|
769
|
+
[templateDatabase],
|
|
770
|
+
);
|
|
771
|
+
await tdb.raw("CREATE DATABASE ?? TEMPLATE ??", [shadowDatabase, templateDatabase]);
|
|
696
772
|
|
|
697
773
|
// Shadow DB에 연결
|
|
698
|
-
const sdb =
|
|
699
|
-
...Sonamu.dbConfig.test,
|
|
700
|
-
connection: {
|
|
701
|
-
...tdbConn,
|
|
702
|
-
database: shadowDatabase,
|
|
703
|
-
password: tdbConn.password,
|
|
704
|
-
},
|
|
705
|
-
});
|
|
774
|
+
const sdb = createShadowConnection(shadowDatabase);
|
|
706
775
|
|
|
707
776
|
// shadow DB 테스트 진행
|
|
708
777
|
try {
|
|
@@ -742,7 +811,7 @@ export class Migrator {
|
|
|
742
811
|
// Shadow DB 삭제
|
|
743
812
|
!isTest() && console.log(chalk.magenta(`${shadowDatabase} 삭제`));
|
|
744
813
|
try {
|
|
745
|
-
await tdb.raw(
|
|
814
|
+
await tdb.raw("DROP DATABASE IF EXISTS ??", [shadowDatabase]);
|
|
746
815
|
} catch (e) {
|
|
747
816
|
console.error("Shadow DB 정리 실패:", e); // 이게 없으면 조용히 누수
|
|
748
817
|
} finally {
|
package/src/syncer/api-parser.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "assert";
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
3
|
|
|
4
|
+
import ts from "@typescript/typescript6";
|
|
4
5
|
import inflection from "inflection";
|
|
5
|
-
import ts from "typescript";
|
|
6
6
|
|
|
7
7
|
import { registeredApis } from "../api/decorators";
|
|
8
8
|
import { type ExtendedApi } from "../api/decorators";
|