sonamu 0.9.19 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +36 -0
  2. package/dist/ai/agents/agent.js +5 -7
  3. package/dist/ai/agents/index.js +1 -2
  4. package/dist/ai/agents/types.js +1 -1
  5. package/dist/ai/index.js +1 -2
  6. package/dist/ai/providers/rtzr/api.js +2 -3
  7. package/dist/ai/providers/rtzr/error.js +14 -29
  8. package/dist/ai/providers/rtzr/index.js +1 -2
  9. package/dist/ai/providers/rtzr/model.js +13 -20
  10. package/dist/ai/providers/rtzr/options.js +2 -3
  11. package/dist/ai/providers/rtzr/provider.js +2 -3
  12. package/dist/ai/providers/rtzr/utils.js +12 -21
  13. package/dist/api/base-frame.js +4 -4
  14. package/dist/api/caster.js +21 -38
  15. package/dist/api/code-converters.js +41 -98
  16. package/dist/api/config.d.ts +1 -10
  17. package/dist/api/config.d.ts.map +1 -1
  18. package/dist/api/config.js +9 -8
  19. package/dist/api/context.js +2 -3
  20. package/dist/api/decorators.js +80 -116
  21. package/dist/api/index.js +2 -3
  22. package/dist/api/secret.js +6 -10
  23. package/dist/api/sonamu.d.ts.map +1 -1
  24. package/dist/api/sonamu.js +200 -387
  25. package/dist/api/validator.js +5 -8
  26. package/dist/api/websocket-helpers.js +21 -32
  27. package/dist/auth/audit-log/builders.js +2 -3
  28. package/dist/auth/audit-log/events.js +2 -2
  29. package/dist/auth/audit-log/plugin.js +30 -61
  30. package/dist/auth/audit-log-ingestor.js +19 -41
  31. package/dist/auth/auth-generator.js +16 -41
  32. package/dist/auth/better-auth-entities.js +3 -4
  33. package/dist/auth/index.js +2 -3
  34. package/dist/auth/knex-adapter.js +18 -45
  35. package/dist/auth/plugins/entity-definitions/admin.js +2 -2
  36. package/dist/auth/plugins/entity-definitions/anonymous.js +2 -2
  37. package/dist/auth/plugins/entity-definitions/api-key.js +2 -2
  38. package/dist/auth/plugins/entity-definitions/audit-log.js +2 -2
  39. package/dist/auth/plugins/entity-definitions/index.js +2 -3
  40. package/dist/auth/plugins/entity-definitions/jwt.js +2 -2
  41. package/dist/auth/plugins/entity-definitions/organization.js +2 -2
  42. package/dist/auth/plugins/entity-definitions/passkey.js +2 -2
  43. package/dist/auth/plugins/entity-definitions/phone-number.js +2 -2
  44. package/dist/auth/plugins/entity-definitions/sso.js +2 -2
  45. package/dist/auth/plugins/entity-definitions/two-factor.js +2 -2
  46. package/dist/auth/plugins/entity-definitions/types.js +1 -1
  47. package/dist/auth/plugins/entity-definitions/username.js +2 -2
  48. package/dist/auth/plugins/index.js +1 -2
  49. package/dist/auth/plugins/wrappers/admin.js +2 -3
  50. package/dist/auth/plugins/wrappers/anonymous.js +2 -3
  51. package/dist/auth/plugins/wrappers/api-key.js +2 -3
  52. package/dist/auth/plugins/wrappers/index.js +1 -2
  53. package/dist/auth/plugins/wrappers/jwt.js +2 -3
  54. package/dist/auth/plugins/wrappers/organization.js +2 -3
  55. package/dist/auth/plugins/wrappers/passkey.js +2 -3
  56. package/dist/auth/plugins/wrappers/phone-number.js +2 -3
  57. package/dist/auth/plugins/wrappers/sso.js +2 -3
  58. package/dist/auth/plugins/wrappers/two-factor.js +2 -3
  59. package/dist/auth/plugins/wrappers/username.js +2 -3
  60. package/dist/bin/build-config.js +2 -2
  61. package/dist/bin/cli.js +151 -258
  62. package/dist/bin/fixture.d.ts.map +1 -1
  63. package/dist/bin/fixture.js +55 -97
  64. package/dist/bin/hmr-hook-register.js +3 -3
  65. package/dist/bin/migrate-targets.d.ts +3 -0
  66. package/dist/bin/migrate-targets.d.ts.map +1 -0
  67. package/dist/bin/migrate-targets.js +11 -0
  68. package/dist/bin/test-command.js +25 -55
  69. package/dist/bin/ts-loader-register.js +5 -6
  70. package/dist/bin/ts-loader-registration.js +6 -13
  71. package/dist/cache/cache-manager.js +3 -4
  72. package/dist/cache/decorator.js +11 -21
  73. package/dist/cache/drivers.js +2 -3
  74. package/dist/cache/index.js +2 -3
  75. package/dist/cache/types.js +1 -1
  76. package/dist/cache-control/cache-control.js +21 -34
  77. package/dist/cache-control/types.js +1 -1
  78. package/dist/compress/compress.js +10 -10
  79. package/dist/compress/index.js +1 -2
  80. package/dist/compress/types.js +1 -1
  81. package/dist/cone/cone-generator.js +25 -63
  82. package/dist/database/_batch_update.js +26 -46
  83. package/dist/database/base-model.js +44 -97
  84. package/dist/database/base-model.types.js +1 -1
  85. package/dist/database/db.d.ts +8 -14
  86. package/dist/database/db.d.ts.map +1 -1
  87. package/dist/database/db.js +127 -72
  88. package/dist/database/knex.js +5 -8
  89. package/dist/database/puri-subset.types.js +1 -1
  90. package/dist/database/puri-wrapper.js +11 -15
  91. package/dist/database/puri.js +117 -234
  92. package/dist/database/puri.types.js +3 -4
  93. package/dist/database/transaction-context.js +4 -5
  94. package/dist/database/upsert-builder.js +109 -176
  95. package/dist/dict/en.d.ts +1 -0
  96. package/dist/dict/en.d.ts.map +1 -1
  97. package/dist/dict/en.js +4 -4
  98. package/dist/dict/index.js +2 -3
  99. package/dist/dict/ko.d.ts +1 -0
  100. package/dist/dict/ko.d.ts.map +1 -1
  101. package/dist/dict/ko.js +4 -4
  102. package/dist/dict/rc-keys.js +3 -4
  103. package/dist/dict/sd.js +8 -19
  104. package/dist/dict/sonamu-dictionary.js +141 -284
  105. package/dist/dict/types.js +1 -1
  106. package/dist/dict/utils.js +4 -5
  107. package/dist/entity/entity-manager.d.ts +2 -2
  108. package/dist/entity/entity-manager.js +34 -82
  109. package/dist/entity/entity-template-cone.js +33 -66
  110. package/dist/entity/entity.js +156 -310
  111. package/dist/env.d.ts +14 -0
  112. package/dist/env.d.ts.map +1 -0
  113. package/dist/env.js +75 -0
  114. package/dist/exceptions/error-handler.js +2 -3
  115. package/dist/exceptions/so-exceptions.js +7 -5
  116. package/dist/filter/index.js +1 -2
  117. package/dist/filter/types.js +3 -4
  118. package/dist/filter/utils.js +21 -54
  119. package/dist/index.js +8 -7
  120. package/dist/logger/category.js +6 -12
  121. package/dist/logger/configure.js +23 -34
  122. package/dist/migration/code-generation.js +146 -314
  123. package/dist/migration/index-where-predicate.js +52 -144
  124. package/dist/migration/migration-set.js +19 -33
  125. package/dist/migration/migrator.d.ts +2 -0
  126. package/dist/migration/migrator.d.ts.map +1 -1
  127. package/dist/migration/migrator.js +69 -53
  128. package/dist/migration/postgresql-schema-reader.js +126 -225
  129. package/dist/migration/slack-confirm.d.ts +1 -0
  130. package/dist/migration/slack-confirm.d.ts.map +1 -1
  131. package/dist/migration/slack-confirm.js +28 -38
  132. package/dist/migration/types.js +1 -1
  133. package/dist/naite/messaging-types.js +1 -1
  134. package/dist/naite/naite-reporter.js +15 -32
  135. package/dist/naite/naite.js +43 -76
  136. package/dist/ssr/index.js +6 -9
  137. package/dist/ssr/registry.js +10 -18
  138. package/dist/ssr/renderer.js +10 -21
  139. package/dist/ssr/types.js +1 -1
  140. package/dist/storage/base-file.js +5 -10
  141. package/dist/storage/buffered-file.js +3 -4
  142. package/dist/storage/drivers.js +2 -3
  143. package/dist/storage/index.js +2 -3
  144. package/dist/storage/s3-driver.js +5 -9
  145. package/dist/storage/storage-manager.js +5 -5
  146. package/dist/storage/types.js +1 -1
  147. package/dist/storage/uploaded-file.js +4 -6
  148. package/dist/stream/index.js +1 -2
  149. package/dist/stream/sse.js +8 -13
  150. package/dist/stream/ws-audience-resolver.js +5 -5
  151. package/dist/stream/ws-audience.js +3 -4
  152. package/dist/stream/ws-cluster-bus.js +3 -4
  153. package/dist/stream/ws-core.js +1 -1
  154. package/dist/stream/ws-delivery.js +11 -25
  155. package/dist/stream/ws-local-connection-store.js +9 -18
  156. package/dist/stream/ws-presence-store.js +43 -97
  157. package/dist/stream/ws-registry.js +17 -22
  158. package/dist/stream/ws-telemetry-memory.js +38 -45
  159. package/dist/stream/ws-telemetry-trace.js +4 -6
  160. package/dist/stream/ws-telemetry.js +82 -135
  161. package/dist/stream/ws.js +47 -91
  162. package/dist/syncer/api-parser.js +81 -147
  163. package/dist/syncer/checksum.js +9 -20
  164. package/dist/syncer/code-generator.js +29 -47
  165. package/dist/syncer/entity-operations.js +17 -27
  166. package/dist/syncer/event-batcher.js +8 -15
  167. package/dist/syncer/file-patterns.js +3 -4
  168. package/dist/syncer/file-tracking.js +6 -10
  169. package/dist/syncer/index.js +1 -2
  170. package/dist/syncer/module-loader.js +10 -26
  171. package/dist/syncer/syncer-actions.js +19 -37
  172. package/dist/syncer/syncer.js +46 -98
  173. package/dist/syncer/watcher.js +12 -26
  174. package/dist/tasks/decorator.js +7 -11
  175. package/dist/tasks/step-wrapper.js +7 -8
  176. package/dist/tasks/workflow-manager.js +18 -25
  177. package/dist/template/entity-converter.js +40 -64
  178. package/dist/template/helpers.js +32 -63
  179. package/dist/template/implementations/entity.template.js +7 -11
  180. package/dist/template/implementations/entry-server.template.js +2 -3
  181. package/dist/template/implementations/generated.template.js +25 -51
  182. package/dist/template/implementations/generated_http.template.js +31 -58
  183. package/dist/template/implementations/generated_sso.template.js +45 -85
  184. package/dist/template/implementations/init_types.template.js +4 -7
  185. package/dist/template/implementations/model.template.js +5 -10
  186. package/dist/template/implementations/model_test.template.js +2 -3
  187. package/dist/template/implementations/queries.template.js +4 -7
  188. package/dist/template/implementations/sd.template.js +17 -35
  189. package/dist/template/implementations/services.template.js +18 -30
  190. package/dist/template/implementations/view_form.template.js +72 -125
  191. package/dist/template/implementations/view_id_all_select.template.js +2 -3
  192. package/dist/template/implementations/view_list.template.js +86 -143
  193. package/dist/template/implementations/view_search_input.template.js +2 -3
  194. package/dist/template/index.js +5 -8
  195. package/dist/template/template-manager.js +13 -26
  196. package/dist/template/template-types.js +2 -3
  197. package/dist/template/template.js +7 -11
  198. package/dist/template/zod-converter.js +173 -348
  199. package/dist/testing/_relation-graph.js +18 -37
  200. package/dist/testing/bootstrap.js +5 -8
  201. package/dist/testing/data-explorer.js +34 -78
  202. package/dist/testing/dev-test-routes.js +54 -60
  203. package/dist/testing/dev-vitest-manager.js +33 -84
  204. package/dist/testing/faker-mappings.js +3 -4
  205. package/dist/testing/fixture-generator.d.ts +2 -1
  206. package/dist/testing/fixture-generator.d.ts.map +1 -1
  207. package/dist/testing/fixture-generator.js +159 -321
  208. package/dist/testing/fixture-loader.js +2 -2
  209. package/dist/testing/fixture-manager.d.ts.map +1 -1
  210. package/dist/testing/fixture-manager.js +124 -227
  211. package/dist/testing/global-setup.d.ts.map +1 -1
  212. package/dist/testing/global-setup.js +29 -17
  213. package/dist/testing/index.js +1 -2
  214. package/dist/testing/naite-vitest-reporter.js +2 -3
  215. package/dist/testing/parallel-db-manager.js +5 -3
  216. package/dist/testing/vitest-helpers.d.ts.map +1 -1
  217. package/dist/testing/vitest-helpers.js +15 -12
  218. package/dist/types/types.d.ts +14 -14
  219. package/dist/types/types.js +27 -50
  220. package/dist/ui/ai-api.js +6 -11
  221. package/dist/ui/ai-client.js +86 -134
  222. package/dist/ui/api.js +99 -195
  223. package/dist/ui/cdd-service.js +78 -130
  224. package/dist/ui/cdd-types.js +1 -1
  225. package/dist/ui-web/assets/{index-Df8q-fhb.js → index-DFStGyd0.js} +49 -49
  226. package/dist/ui-web/assets/index-Dx4ap5i4.css +1 -0
  227. package/dist/ui-web/index.html +2 -2
  228. package/dist/utils/async-utils.js +13 -25
  229. package/dist/utils/class-name.js +3 -4
  230. package/dist/utils/console-util.js +11 -26
  231. package/dist/utils/controller.d.ts.map +1 -1
  232. package/dist/utils/controller.js +14 -12
  233. package/dist/utils/esm-utils.js +5 -8
  234. package/dist/utils/formatter.js +10 -22
  235. package/dist/utils/fs-utils.js +14 -25
  236. package/dist/utils/lodash-able.js +3 -4
  237. package/dist/utils/model.js +7 -14
  238. package/dist/utils/object-utils.js +41 -73
  239. package/dist/utils/path-utils.js +5 -9
  240. package/dist/utils/process-utils.js +4 -7
  241. package/dist/utils/sql-parser.js +6 -13
  242. package/dist/utils/type-utils.js +16 -26
  243. package/dist/utils/utils.js +18 -40
  244. package/dist/utils/zod-error.js +9 -16
  245. package/dist/vector/chunking.js +24 -37
  246. package/dist/vector/config.js +2 -2
  247. package/dist/vector/embedding.js +8 -19
  248. package/dist/vector/index.js +1 -2
  249. package/dist/vector/types.js +1 -1
  250. package/package.json +7 -7
  251. package/src/__tests__/env.test.ts +127 -0
  252. package/src/api/__tests__/config.test.ts +10 -1
  253. package/src/api/config.ts +4 -12
  254. package/src/api/sonamu.ts +14 -4
  255. package/src/bin/__tests__/migrate-targets.test.ts +28 -0
  256. package/src/bin/__tests__/test-command.test.ts +82 -1
  257. package/src/bin/cli.ts +9 -18
  258. package/src/bin/fixture.ts +5 -4
  259. package/src/bin/migrate-targets.ts +7 -0
  260. package/src/bin/test-command.ts +2 -2
  261. package/src/database/__tests__/db.test.ts +175 -0
  262. package/src/database/db.ts +193 -71
  263. package/src/dict/en.ts +2 -0
  264. package/src/dict/ko.ts +2 -0
  265. package/src/env.ts +123 -0
  266. package/src/migration/__tests__/migrator.test.ts +149 -0
  267. package/src/migration/migrator.ts +74 -17
  268. package/src/migration/slack-confirm.ts +21 -0
  269. package/src/skills/sonamu/database.md +1 -1
  270. package/src/skills/sonamu/entity-basic.md +31 -0
  271. package/src/skills/sonamu/puri.md +22 -0
  272. package/src/skills/sonamu/testing-devrunner.md +1 -1
  273. package/src/skills/sonamu/upsert.md +53 -6
  274. package/src/stream/ws-telemetry-memory.ts +2 -2
  275. package/src/testing/fixture-generator.ts +2 -1
  276. package/src/testing/fixture-manager.ts +3 -4
  277. package/src/testing/global-setup.ts +42 -18
  278. package/src/testing/vitest-helpers.ts +14 -0
  279. package/src/utils/controller.ts +14 -7
  280. package/tsdown.api.config.ts +6 -0
  281. package/dist/_virtual/rolldown_runtime.js +0 -39
  282. package/dist/ui-web/assets/index-D4rYm-Xz.css +0 -1
@@ -0,0 +1,7 @@
1
+ import { type SonamuDBConfig } from "../database/db";
2
+ import { getSonamuEnvironment } from "../env";
3
+
4
+ export function getMigrateRunTargets(): (keyof SonamuDBConfig)[] {
5
+ const environment = getSonamuEnvironment();
6
+ return environment === "test" ? ["test", "fixture"] : [environment];
7
+ }
@@ -7,7 +7,7 @@ import { loadConfig } from "../api/config";
7
7
  import { type RunResult, type TestCaseResult } from "../testing";
8
8
  import { findApiRootPath } from "../utils/utils";
9
9
 
10
- async function loadTestConfig(): Promise<SonamuConfig> {
10
+ async function loadDevServerConfig(): Promise<SonamuConfig> {
11
11
  const prevVitest = process.env.VITEST;
12
12
  process.env.VITEST = "true";
13
13
  try {
@@ -37,7 +37,7 @@ function resolveTestBaseUrl(config: SonamuConfig): {
37
37
  export async function testCommand(): Promise<void> {
38
38
  const args = process.argv.slice(3);
39
39
 
40
- const config = await loadTestConfig();
40
+ const config = await loadDevServerConfig();
41
41
 
42
42
  // process.argv 파싱: sonamu test [file...] --pattern "이름" --traces --status
43
43
  const files: string[] = [];
@@ -0,0 +1,175 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+
3
+ import { type EnvironmentSnapshots } from "../../env";
4
+ import { DBClass } from "../db";
5
+
6
+ describe("DBClass.generateDBConfig", () => {
7
+ const originalEnv = { ...process.env };
8
+ const emptySnapshots: EnvironmentSnapshots = {
9
+ test: {},
10
+ development: {},
11
+ staging: {},
12
+ production: {},
13
+ };
14
+
15
+ afterEach(() => {
16
+ for (const key of Object.keys(process.env)) {
17
+ if (!(key in originalEnv)) {
18
+ delete process.env[key];
19
+ }
20
+ }
21
+ Object.assign(process.env, originalEnv);
22
+ });
23
+
24
+ it("keeps defaultOptions.connection values when matching SONAMU_DB env vars are absent", () => {
25
+ delete process.env.SONAMU_DB_HOST;
26
+ delete process.env.SONAMU_DB_PORT;
27
+ delete process.env.SONAMU_DB_USER;
28
+ delete process.env.SONAMU_DB_PASSWORD;
29
+ delete process.env.SONAMU_DB_NAME;
30
+
31
+ const dbConfig = new DBClass().generateDBConfig(
32
+ {
33
+ database: "pg",
34
+ defaultOptions: {
35
+ connection: {
36
+ host: "default-host",
37
+ port: 15432,
38
+ user: "default-user",
39
+ password: "default-password",
40
+ },
41
+ },
42
+ },
43
+ "Miomock",
44
+ );
45
+
46
+ expect(dbConfig.development!.connection).toMatchObject({
47
+ host: "default-host",
48
+ port: 15432,
49
+ user: "default-user",
50
+ password: "default-password",
51
+ database: "miomock_development",
52
+ });
53
+ });
54
+
55
+ it("derives database names from projectName even when defaultOptions.connection.database is set", () => {
56
+ const dbConfig = new DBClass().generateDBConfig(
57
+ {
58
+ database: "pg",
59
+ defaultOptions: {
60
+ connection: {
61
+ database: "legacy_database",
62
+ },
63
+ },
64
+ },
65
+ "Miomock",
66
+ emptySnapshots,
67
+ );
68
+
69
+ expect(dbConfig.development!.connection).toMatchObject({
70
+ database: "miomock_development",
71
+ });
72
+ expect(dbConfig.production!.connection).toMatchObject({
73
+ database: "miomock_production",
74
+ });
75
+ });
76
+
77
+ it("fails fast when legacy database.name or database.environments keys are present", () => {
78
+ const legacyConfig = {
79
+ database: "pg",
80
+ name: "miomock",
81
+ environments: {
82
+ production: {},
83
+ },
84
+ } as Parameters<DBClass["generateDBConfig"]>[0];
85
+
86
+ expect(() => new DBClass().generateDBConfig(legacyConfig, "Miomock")).toThrow(
87
+ /database\.name and database\.environments were removed/,
88
+ );
89
+ });
90
+
91
+ it("lets readonly env vars override only the readonly connection fields", () => {
92
+ process.env.SONAMU_DB_HOST = "writer-host";
93
+ process.env.SONAMU_DB_USER = "writer-user";
94
+ process.env.SONAMU_DB_PASSWORD = "writer-password";
95
+ process.env.SONAMU_DB_READONLY_HOST = "readonly-host";
96
+
97
+ const dbConfig = new DBClass().generateDBConfig(
98
+ {
99
+ database: "pg",
100
+ defaultOptions: {
101
+ connection: {
102
+ port: 15432,
103
+ },
104
+ },
105
+ },
106
+ "Miomock",
107
+ );
108
+
109
+ expect(dbConfig.development_readonly!.connection).toMatchObject({
110
+ host: "readonly-host",
111
+ port: 15432,
112
+ user: "writer-user",
113
+ password: "writer-password",
114
+ database: "miomock_development",
115
+ });
116
+ });
117
+
118
+ it("does not let current process connection values leak into other environment snapshots", () => {
119
+ const dbConfig = new DBClass().generateDBConfig(
120
+ {
121
+ database: "pg",
122
+ defaultOptions: {
123
+ connection: {
124
+ host: "development-host",
125
+ port: 15432,
126
+ user: "development-user",
127
+ password: "development-password",
128
+ ssl: true,
129
+ },
130
+ },
131
+ },
132
+ "Miomock",
133
+ {
134
+ ...emptySnapshots,
135
+ staging: {
136
+ SONAMU_DB_HOST: "staging-host",
137
+ },
138
+ },
139
+ );
140
+
141
+ expect(dbConfig.staging!.connection).toMatchObject({
142
+ host: "staging-host",
143
+ port: 5432,
144
+ user: "postgres",
145
+ database: "miomock_staging",
146
+ ssl: true,
147
+ });
148
+ expect(dbConfig.staging!.connection).not.toMatchObject({
149
+ password: "development-password",
150
+ });
151
+ });
152
+
153
+ it("does not reuse SONAMU_DB_NAME as the fixture database name", () => {
154
+ const dbConfig = new DBClass().generateDBConfig(
155
+ {
156
+ database: "pg",
157
+ defaultOptions: {},
158
+ },
159
+ "Miomock",
160
+ {
161
+ ...emptySnapshots,
162
+ test: {
163
+ SONAMU_DB_NAME: "miomock_test",
164
+ },
165
+ },
166
+ );
167
+
168
+ expect(dbConfig.test!.connection).toMatchObject({
169
+ database: "miomock_test",
170
+ });
171
+ expect(dbConfig.fixture!.connection).toMatchObject({
172
+ database: "miomock_fixture",
173
+ });
174
+ });
175
+ });
@@ -4,13 +4,10 @@ import { AsyncLocalStorage } from "async_hooks";
4
4
  import { type Knex } from "knex";
5
5
 
6
6
  import { type DatabaseConfig, type SonamuConfig } from "../api/config";
7
+ import { getSonamuEnvironment, type EnvironmentSnapshots, type SonamuEnvironment } from "../env";
7
8
  import { createKnexInstance } from "./knex";
8
9
  import { TransactionContext } from "./transaction-context";
9
10
 
10
- /**
11
- * 여러 설정 객체를 순차적으로 deep merge합니다.
12
- * undefined/null인 인자는 무시됩니다.
13
- */
14
11
  function isPlainObject(value: unknown): value is Record<string, unknown> {
15
12
  return typeof value === "object" && value !== null && !Array.isArray(value);
16
13
  }
@@ -44,20 +41,115 @@ function mergeConfigs<T extends object>(...configs: (Partial<T> | undefined | nu
44
41
  return merged as T;
45
42
  }
46
43
 
47
- export type DBPreset = "w" | "r";
44
+ export type SonamuMainDBPreset = "test" | "fixture" | SonamuEnvironment;
45
+ export type SonamuReadonlyDBPreset =
46
+ | "test_readonly"
47
+ | "development_readonly"
48
+ | "staging_readonly"
49
+ | "production_readonly";
50
+ export type SonamuDBPreset = SonamuMainDBPreset | SonamuReadonlyDBPreset;
51
+ export type DBPreset = "w" | "r" | SonamuDBPreset;
48
52
 
49
- export type SonamuDBConfig = {
50
- development_master: Knex.Config;
51
- development_slave: Knex.Config;
52
- production_master: Knex.Config;
53
- production_slave: Knex.Config;
54
- fixture: Knex.Config;
55
- test: Knex.Config;
56
- };
53
+ export type SonamuDBConfig = Record<SonamuDBPreset, Knex.Config>;
54
+
55
+ function isConcretePreset(value: DBPreset): value is SonamuDBPreset {
56
+ return value !== "w" && value !== "r";
57
+ }
58
+
59
+ function getReadonlyPreset(environment: SonamuEnvironment): SonamuReadonlyDBPreset {
60
+ return `${environment}_readonly` as SonamuReadonlyDBPreset;
61
+ }
62
+
63
+ function getProjectDatabaseBaseName(projectName?: string): string {
64
+ return (projectName ?? "sonamu")
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9]+/g, "_")
67
+ .replace(/^_+|_+$/g, "");
68
+ }
69
+
70
+ function numberFromEnv(value: string | undefined, fallback: number): number {
71
+ if (value === undefined || value === "") {
72
+ return fallback;
73
+ }
74
+
75
+ const parsed = Number(value);
76
+ if (Number.isNaN(parsed)) {
77
+ throw new Error(`Invalid database port: ${value}`);
78
+ }
79
+
80
+ return parsed;
81
+ }
82
+
83
+ function connectionFromEnv(options: {
84
+ baseName: string;
85
+ suffix: SonamuMainDBPreset;
86
+ baseConnection?: Knex.PgConnectionConfig;
87
+ prefix?: "SONAMU_DB_READONLY" | "SONAMU_DB_FIXTURE";
88
+ env?: NodeJS.ProcessEnv;
89
+ }): Knex.PgConnectionConfig {
90
+ const {
91
+ baseName,
92
+ suffix,
93
+ baseConnection = {},
94
+ prefix = "SONAMU_DB",
95
+ env = process.env,
96
+ } = options;
97
+ const read = (key: "HOST" | "PORT" | "USER" | "PASSWORD" | "NAME") => {
98
+ const prefixedValue = env[`${prefix}_${key}`];
99
+ if (prefixedValue !== undefined) {
100
+ return prefixedValue;
101
+ }
102
+ if (prefix === "SONAMU_DB_FIXTURE" && key === "NAME") {
103
+ return undefined;
104
+ }
105
+
106
+ return env[`SONAMU_DB_${key}`];
107
+ };
108
+
109
+ return {
110
+ ...baseConnection,
111
+ host: read("HOST") ?? baseConnection.host ?? "0.0.0.0",
112
+ port: numberFromEnv(read("PORT"), baseConnection.port ?? 5432),
113
+ user: read("USER") ?? baseConnection.user ?? "postgres",
114
+ password: read("PASSWORD") ?? baseConnection.password,
115
+ database: read("NAME") ?? `${baseName}_${suffix}`,
116
+ };
117
+ }
118
+
119
+ function neutralizeEnvironmentConnectionFields(
120
+ connection: Knex.PgConnectionConfig | undefined,
121
+ ): Knex.PgConnectionConfig | undefined {
122
+ if (connection === undefined) {
123
+ return undefined;
124
+ }
125
+
126
+ const neutralConnection = { ...connection };
127
+ delete neutralConnection.host;
128
+ delete neutralConnection.port;
129
+ delete neutralConnection.user;
130
+ delete neutralConnection.password;
131
+ delete neutralConnection.database;
132
+
133
+ return neutralConnection;
134
+ }
135
+
136
+ function assertNoLegacyDatabaseConfig(config: SonamuConfig["database"]): void {
137
+ const legacyConfig = config as SonamuConfig["database"] & {
138
+ name?: unknown;
139
+ environments?: unknown;
140
+ };
141
+
142
+ if (legacyConfig.name !== undefined || legacyConfig.environments !== undefined) {
143
+ throw new Error(
144
+ "Sonamu database.name and database.environments were removed. Use SONAMU_DB_* dotenv variables instead.",
145
+ );
146
+ }
147
+ }
57
148
 
58
149
  export class DBClass {
59
150
  private wdb?: Knex;
60
151
  private rdb?: Knex;
152
+ private presetDBs: Map<SonamuDBPreset, Knex> = new Map();
61
153
  private workerDBs: Map<number, Knex> = new Map();
62
154
  private currentConfig: SonamuDBConfig | null = null;
63
155
 
@@ -86,14 +178,21 @@ export class DBClass {
86
178
  getDB(which: DBPreset): Knex {
87
179
  const dbConfig = this.getCurrentConfig();
88
180
 
89
- // 테스트 트랜잭션 격리
90
- if (process.env.NODE_ENV === "test") {
91
- // 병렬 테스트 모드: worker별 DB 사용
181
+ if (isConcretePreset(which)) {
182
+ if (!this.presetDBs.has(which)) {
183
+ this.presetDBs.set(which, createKnexInstance(dbConfig[which]));
184
+ }
185
+
186
+ const db = this.presetDBs.get(which);
187
+ assert(db, `DB preset ${which} not found`);
188
+ return db;
189
+ }
190
+
191
+ if (getSonamuEnvironment() === "test") {
92
192
  if (process.env.SONAMU_WORKER_DB === "true") {
93
193
  return this.getWorkerDB(dbConfig);
94
194
  }
95
195
 
96
- // 기존 단일 테스트 로직
97
196
  if (this.testTransaction) {
98
197
  return this.testTransaction;
99
198
  } else if (this.wdb) {
@@ -101,7 +200,6 @@ export class DBClass {
101
200
  } else {
102
201
  this.wdb = createKnexInstance({
103
202
  ...dbConfig.test,
104
- // 단일 풀
105
203
  pool: {
106
204
  min: 1,
107
205
  max: 1,
@@ -121,19 +219,13 @@ export class DBClass {
121
219
  return this[instanceName];
122
220
  }
123
221
 
124
- /**
125
- * 병렬 테스트에서 worker별 DB 인스턴스를 반환합니다.
126
- * VITEST_POOL_ID 환경변수로 worker를 식별하여 해당 DB에 연결합니다.
127
- */
128
222
  private getWorkerDB(dbConfig: SonamuDBConfig): Knex {
129
- // 트랜잭션이 있으면 트랜잭션 반환
130
223
  if (this.testTransaction) {
131
224
  return this.testTransaction;
132
225
  }
133
226
 
134
227
  const workerId = parseInt(process.env.VITEST_POOL_ID ?? "1", 10);
135
228
 
136
- // Worker별 DB 인스턴스 캐싱
137
229
  if (!this.workerDBs.has(workerId)) {
138
230
  const baseTestConfig = dbConfig.test;
139
231
  const connection = baseTestConfig.connection as { database: string };
@@ -158,29 +250,24 @@ export class DBClass {
158
250
 
159
251
  getDBConfig(which: DBPreset): Knex.Config {
160
252
  const dbConfig = this.getCurrentConfig();
161
- if (process.env.NODE_ENV === "test") {
253
+
254
+ if (isConcretePreset(which)) {
255
+ return dbConfig[which];
256
+ }
257
+
258
+ const environment = getSonamuEnvironment();
259
+ if (environment === "test") {
260
+ const target = which === "w" ? dbConfig.test : dbConfig.test_readonly;
162
261
  return {
163
- ...dbConfig.test,
164
- // 단일 풀
262
+ ...target,
165
263
  pool: {
166
264
  min: 1,
167
265
  max: 1,
168
266
  },
169
267
  };
170
268
  }
171
- switch (process.env.NODE_ENV ?? "development") {
172
- case "development":
173
- case "staging":
174
- return which === "w"
175
- ? dbConfig.development_master
176
- : (dbConfig.development_slave ?? dbConfig.development_master);
177
- case "production":
178
- return which === "w"
179
- ? dbConfig.production_master
180
- : (dbConfig.production_slave ?? dbConfig.production_master);
181
- default:
182
- throw new Error(`현재 ENV ${process.env.NODE_ENV}에는 설정 가능한 DB설정이 없습니다.`);
183
- }
269
+
270
+ return which === "w" ? dbConfig[environment] : dbConfig[getReadonlyPreset(environment)];
184
271
  }
185
272
 
186
273
  async destroy(): Promise<void> {
@@ -192,17 +279,27 @@ export class DBClass {
192
279
  await this.rdb.destroy();
193
280
  this.rdb = undefined;
194
281
  }
195
- // 병렬 테스트용 worker DB들도 정리
282
+ for (const db of this.presetDBs.values()) {
283
+ await db.destroy();
284
+ }
285
+ this.presetDBs.clear();
196
286
  for (const db of this.workerDBs.values()) {
197
287
  await db.destroy();
198
288
  }
199
289
  this.workerDBs.clear();
200
290
  }
201
291
 
202
- public generateDBConfig(config: SonamuConfig["database"]): SonamuDBConfig {
292
+ public generateDBConfig(
293
+ config: SonamuConfig["database"],
294
+ projectName?: string,
295
+ snapshots?: EnvironmentSnapshots,
296
+ ): SonamuDBConfig {
297
+ assertNoLegacyDatabaseConfig(config);
298
+
299
+ const baseName = getProjectDatabaseBaseName(projectName);
203
300
  const defaultKnexConfig = mergeConfigs<Partial<DatabaseConfig>>(
204
301
  {
205
- client: "postgresql",
302
+ client: config.database === "pgnative" ? "pgnative" : "postgresql",
206
303
  pool: {
207
304
  min: 1,
208
305
  max: 5,
@@ -210,42 +307,67 @@ export class DBClass {
210
307
  migrations: {
211
308
  directory: "./src/migrations",
212
309
  },
213
- connection: {
214
- database: config.name,
215
- },
216
310
  },
217
311
  config.defaultOptions,
218
312
  );
313
+ const baseConnection = defaultKnexConfig.connection as Knex.PgConnectionConfig | undefined;
314
+ const environmentFallbackConnection = snapshots
315
+ ? neutralizeEnvironmentConnectionFields(baseConnection)
316
+ : baseConnection;
317
+
318
+ const envForPreset = (preset: SonamuMainDBPreset): NodeJS.ProcessEnv => {
319
+ if (!snapshots) {
320
+ return process.env;
321
+ }
322
+ if (preset === "fixture") {
323
+ return snapshots.test;
324
+ }
325
+ return snapshots[preset];
326
+ };
327
+
328
+ const mainConfig = (preset: SonamuMainDBPreset): Knex.Config =>
329
+ mergeConfigs(defaultKnexConfig, {
330
+ connection: connectionFromEnv({
331
+ baseName,
332
+ suffix: preset,
333
+ baseConnection: environmentFallbackConnection,
334
+ env: envForPreset(preset),
335
+ }),
336
+ });
337
+ const readonlyConfig = (environment: SonamuEnvironment): Knex.Config =>
338
+ mergeConfigs<Knex.Config>(defaultKnexConfig, mainConfig(environment), {
339
+ connection: connectionFromEnv({
340
+ baseName,
341
+ suffix: environment,
342
+ baseConnection: mainConfig(environment).connection as Knex.PgConnectionConfig,
343
+ prefix: "SONAMU_DB_READONLY",
344
+ env: envForPreset(environment),
345
+ }),
346
+ });
347
+
348
+ const test = mainConfig("test");
219
349
 
220
- // oxfmt-ignore -- 설정 구조 가독성을 위해 여러 줄로 유지
221
350
  return {
222
- // 여기에 나열한 순서대로 Sonamu UI의 DB Migration 탭에 표시됩니다.
223
- test: mergeConfigs(
224
- defaultKnexConfig,
225
- { connection: { database: `${config.name}_test` } },
226
- config.environments?.test,
227
- ),
228
- fixture: mergeConfigs(
229
- defaultKnexConfig,
230
- { connection: { database: `${config.name}_fixture` } },
231
- config.environments?.fixture,
232
- ),
233
- development_master: mergeConfigs(defaultKnexConfig, config.environments?.development),
234
- development_slave: mergeConfigs(
235
- defaultKnexConfig,
236
- config.environments?.development,
237
- config.environments?.development_slave,
238
- ),
239
- production_master: mergeConfigs(defaultKnexConfig, config.environments?.production),
240
- production_slave: mergeConfigs(
241
- defaultKnexConfig,
242
- config.environments?.production,
243
- config.environments?.production_slave,
244
- ),
351
+ test,
352
+ test_readonly: readonlyConfig("test"),
353
+ fixture: mergeConfigs(defaultKnexConfig, {
354
+ connection: connectionFromEnv({
355
+ baseName,
356
+ suffix: "fixture",
357
+ baseConnection: environmentFallbackConnection,
358
+ prefix: "SONAMU_DB_FIXTURE",
359
+ env: envForPreset("fixture"),
360
+ }),
361
+ }),
362
+ development: mainConfig("development"),
363
+ development_readonly: readonlyConfig("development"),
364
+ staging: mainConfig("staging"),
365
+ staging_readonly: readonlyConfig("staging"),
366
+ production: mainConfig("production"),
367
+ production_readonly: readonlyConfig("production"),
245
368
  };
246
369
  }
247
370
 
248
- // Test 환경에서 트랜잭션 사용
249
371
  public testTransaction: Knex.Transaction | null = null;
250
372
  async createTestTransaction(): Promise<Knex.Transaction> {
251
373
  const db = this.getDB("w");
package/src/dict/en.ts CHANGED
@@ -80,6 +80,8 @@ export default {
80
80
  "sonamu.error.keyNotFound": (key: string) => `Key not found: ${key}`,
81
81
  "sonamu.error.migrationRejected": "Migration has been rejected",
82
82
  "sonamu.error.slackConfirmNotConfigured": "Slack Confirm is not configured",
83
+ "sonamu.error.slackConfirmInvalidTargets": (targets: string, validTargets: string) =>
84
+ `Slack Confirm targets include unknown DB keys: ${targets}. Valid targets: ${validTargets}`,
83
85
  "sonamu.error.devRunner.notEnabled":
84
86
  "devRunner is not enabled. Set test.devRunner.enabled: true in sonamu.config.ts",
85
87
  "sonamu.error.devRunner.notReady": "Vitest instance is not ready yet",
package/src/dict/ko.ts CHANGED
@@ -81,6 +81,8 @@ export default {
81
81
  "sonamu.error.keyNotFound": (key: string) => `키를 찾을 수 없습니다: ${key}`,
82
82
  "sonamu.error.migrationRejected": "마이그레이션이 거절되었습니다",
83
83
  "sonamu.error.slackConfirmNotConfigured": "Slack Confirm이 설정되지 않았습니다",
84
+ "sonamu.error.slackConfirmInvalidTargets": (targets: string, validTargets: string) =>
85
+ `Slack Confirm targets에 알 수 없는 DB 키가 포함되어 있습니다: ${targets}. 유효한 대상: ${validTargets}`,
84
86
  "sonamu.error.devRunner.notEnabled":
85
87
  "devRunner가 활성화되지 않았습니다. sonamu.config.ts에서 test.devRunner.enabled: true 설정이 필요합니다",
86
88
  "sonamu.error.devRunner.notReady": "Vitest 인스턴스가 아직 준비되지 않았습니다",