sonamu 0.9.20 → 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 (279) 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 +6 -6
  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/testing-devrunner.md +1 -1
  271. package/src/stream/ws-telemetry-memory.ts +2 -2
  272. package/src/testing/fixture-generator.ts +2 -1
  273. package/src/testing/fixture-manager.ts +3 -4
  274. package/src/testing/global-setup.ts +42 -18
  275. package/src/testing/vitest-helpers.ts +14 -0
  276. package/src/utils/controller.ts +14 -7
  277. package/tsdown.api.config.ts +6 -0
  278. package/dist/_virtual/rolldown_runtime.js +0 -39
  279. package/dist/ui-web/assets/index-D4rYm-Xz.css +0 -1
package/src/env.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import path from "path";
3
+
4
+ import dotenv from "dotenv";
5
+
6
+ export const SONAMU_ENVIRONMENTS = ["test", "development", "staging", "production"] as const;
7
+
8
+ export type SonamuEnvironment = (typeof SONAMU_ENVIRONMENTS)[number];
9
+ export type EnvironmentSnapshot = Record<string, string>;
10
+ export type EnvironmentSnapshots = Record<SonamuEnvironment, EnvironmentSnapshot>;
11
+
12
+ export function isSonamuEnvironment(value: string | undefined): value is SonamuEnvironment {
13
+ return SONAMU_ENVIRONMENTS.includes(value as SonamuEnvironment);
14
+ }
15
+
16
+ export function getSonamuEnvironment(env: NodeJS.ProcessEnv = process.env): SonamuEnvironment {
17
+ const nodeEnv = env.NODE_ENV;
18
+
19
+ if (nodeEnv === undefined || nodeEnv === "") {
20
+ return "development";
21
+ }
22
+
23
+ if (isSonamuEnvironment(nodeEnv)) {
24
+ return nodeEnv;
25
+ }
26
+
27
+ throw new Error(
28
+ `Invalid NODE_ENV "${nodeEnv}". Sonamu supports only ${SONAMU_ENVIRONMENTS.join(", ")}.`,
29
+ );
30
+ }
31
+
32
+ function readDotenvFile(filePath: string): EnvironmentSnapshot {
33
+ if (!existsSync(filePath)) {
34
+ return {};
35
+ }
36
+
37
+ return dotenv.parse(readFileSync(filePath));
38
+ }
39
+
40
+ function assertEnvironmentDotenvExists(rootPath: string, environment: SonamuEnvironment): void {
41
+ const commonEnvPath = path.join(rootPath, ".env");
42
+ const environmentEnvPath = path.join(rootPath, `.env.${environment}`);
43
+
44
+ if (!existsSync(commonEnvPath) && !existsSync(environmentEnvPath)) {
45
+ throw new Error(
46
+ `Missing Sonamu dotenv file. Create ${commonEnvPath} or ${environmentEnvPath}.`,
47
+ );
48
+ }
49
+ }
50
+
51
+ function removePreloadedCommonDotenvValues(
52
+ baseEnv: NodeJS.ProcessEnv,
53
+ commonEnv: EnvironmentSnapshot,
54
+ environmentEnv: EnvironmentSnapshot,
55
+ ): NodeJS.ProcessEnv {
56
+ const runtimeEnv = { ...baseEnv };
57
+
58
+ for (const [key, commonValue] of Object.entries(commonEnv)) {
59
+ if (environmentEnv[key] !== undefined && runtimeEnv[key] === commonValue) {
60
+ delete runtimeEnv[key];
61
+ }
62
+ }
63
+
64
+ return runtimeEnv;
65
+ }
66
+
67
+ export function readEnvironmentSnapshot(
68
+ rootPath: string,
69
+ environment: SonamuEnvironment,
70
+ baseEnv: NodeJS.ProcessEnv = process.env,
71
+ ): EnvironmentSnapshot {
72
+ assertEnvironmentDotenvExists(rootPath, environment);
73
+ const commonEnv = readDotenvFile(path.join(rootPath, ".env"));
74
+ const environmentEnv = readDotenvFile(path.join(rootPath, `.env.${environment}`));
75
+ const runtimeEnv = removePreloadedCommonDotenvValues(baseEnv, commonEnv, environmentEnv);
76
+
77
+ return {
78
+ ...commonEnv,
79
+ ...environmentEnv,
80
+ ...readDotenvFile(path.join(rootPath, ".env.local")),
81
+ ...runtimeEnv,
82
+ NODE_ENV: environment,
83
+ };
84
+ }
85
+
86
+ export function readAllEnvironmentSnapshots(
87
+ rootPath: string,
88
+ baseEnv: NodeJS.ProcessEnv = {},
89
+ ): EnvironmentSnapshots {
90
+ return Object.fromEntries(
91
+ SONAMU_ENVIRONMENTS.map((environment) => [
92
+ environment,
93
+ readEnvironmentSnapshot(rootPath, environment, baseEnv),
94
+ ]),
95
+ ) as EnvironmentSnapshots;
96
+ }
97
+
98
+ export function applyCurrentSnapshotToProcessEnv(rootPath: string): EnvironmentSnapshot {
99
+ const environment = getSonamuEnvironment();
100
+ const snapshot = readEnvironmentSnapshot(rootPath, environment);
101
+
102
+ for (const [key, value] of Object.entries(snapshot)) {
103
+ process.env[key] = value;
104
+ }
105
+
106
+ return snapshot;
107
+ }
108
+
109
+ export function isDevelopmentEnvironment(): boolean {
110
+ return getSonamuEnvironment() === "development";
111
+ }
112
+
113
+ export function isStagingEnvironment(): boolean {
114
+ return getSonamuEnvironment() === "staging";
115
+ }
116
+
117
+ export function isProductionEnvironment(): boolean {
118
+ return getSonamuEnvironment() === "production";
119
+ }
120
+
121
+ export function isTestEnvironment(): boolean {
122
+ return getSonamuEnvironment() === "test";
123
+ }
@@ -0,0 +1,149 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+
3
+ import { type SonamuConfig } from "../../api/config";
4
+ import { Sonamu } from "../../api/sonamu";
5
+ import { type SonamuDBConfig, type SonamuDBPreset } from "../../database/db";
6
+ import { setSDConfig } from "../../dict/sd";
7
+ import { Migrator } from "../migrator";
8
+ import { SlackConfirm } from "../slack-confirm";
9
+
10
+ describe("Migrator environment target filtering", () => {
11
+ const originalEnv = { ...process.env };
12
+ const presets: SonamuDBPreset[] = [
13
+ "test",
14
+ "fixture",
15
+ "development",
16
+ "staging",
17
+ "production",
18
+ "test_readonly",
19
+ "development_readonly",
20
+ "staging_readonly",
21
+ "production_readonly",
22
+ ];
23
+
24
+ const dbConfig = Object.fromEntries(
25
+ presets.map((preset) => [
26
+ preset,
27
+ {
28
+ client: "postgresql",
29
+ connection: {
30
+ host: "localhost",
31
+ database: preset,
32
+ },
33
+ },
34
+ ]),
35
+ ) as SonamuDBConfig;
36
+
37
+ afterEach(() => {
38
+ for (const key of Object.keys(process.env)) {
39
+ if (!(key in originalEnv)) {
40
+ delete process.env[key];
41
+ }
42
+ }
43
+ Object.assign(process.env, originalEnv);
44
+ });
45
+
46
+ const getMigrationTargetKeys = () => {
47
+ Sonamu.dbConfig = dbConfig;
48
+ return (
49
+ new Migrator() as unknown as {
50
+ getMigrationTargetKeys(): (keyof SonamuDBConfig)[];
51
+ }
52
+ ).getMigrationTargetKeys();
53
+ };
54
+
55
+ it("limits migration targets to production on a production server runtime", () => {
56
+ process.env.NODE_ENV = "production";
57
+
58
+ expect(getMigrationTargetKeys()).toEqual(["production"]);
59
+ });
60
+
61
+ it("keeps all writable targets available for local development UI workflows", () => {
62
+ process.env.NODE_ENV = "development";
63
+
64
+ expect(getMigrationTargetKeys()).toEqual([
65
+ "test",
66
+ "fixture",
67
+ "development",
68
+ "staging",
69
+ "production",
70
+ ]);
71
+ });
72
+ });
73
+
74
+ describe("SlackConfirm target validation", () => {
75
+ const presets: SonamuDBPreset[] = [
76
+ "test",
77
+ "fixture",
78
+ "development",
79
+ "staging",
80
+ "production",
81
+ "test_readonly",
82
+ "development_readonly",
83
+ "staging_readonly",
84
+ "production_readonly",
85
+ ];
86
+
87
+ const dbConfig = Object.fromEntries(
88
+ presets.map((preset) => [
89
+ preset,
90
+ {
91
+ client: "postgresql",
92
+ connection: {
93
+ host: "localhost",
94
+ database: preset,
95
+ },
96
+ },
97
+ ]),
98
+ ) as SonamuDBConfig;
99
+
100
+ const createConfig = (targets: string[]): SonamuConfig =>
101
+ ({
102
+ projectName: "sonamu-test",
103
+ api: {
104
+ dir: "src",
105
+ route: {
106
+ prefix: "/api",
107
+ },
108
+ },
109
+ i18n: {
110
+ defaultLocale: "ko",
111
+ supportedLocales: ["ko", "en"],
112
+ },
113
+ sync: {
114
+ targets: [],
115
+ },
116
+ database: {},
117
+ server: {},
118
+ apiConfig: {
119
+ contextProvider: (defaultContext) => defaultContext,
120
+ guardHandler: () => undefined,
121
+ },
122
+ slackConfirm: {
123
+ targets,
124
+ botToken: "xoxb-test",
125
+ channelId: "C123",
126
+ },
127
+ }) as SonamuConfig;
128
+
129
+ it("fails fast when slackConfirm.targets contains an unknown DB key", () => {
130
+ Sonamu.dbConfig = dbConfig;
131
+ Sonamu.config = createConfig(["production_old"]);
132
+ setSDConfig(Sonamu.config.i18n);
133
+
134
+ expect(() => new SlackConfirm().isTargetRequiresApproval("production")).toThrow(
135
+ /Slack Confirm targets/,
136
+ );
137
+ });
138
+
139
+ it("checks approval requirements only after configured targets match dbConfig keys", () => {
140
+ Sonamu.dbConfig = dbConfig;
141
+ Sonamu.config = createConfig(["production"]);
142
+ setSDConfig(Sonamu.config.i18n);
143
+
144
+ const slackConfirm = new SlackConfirm();
145
+
146
+ expect(slackConfirm.isTargetRequiresApproval("production")).toBe(true);
147
+ expect(slackConfirm.isTargetRequiresApproval("staging")).toBe(false);
148
+ });
149
+ });
@@ -12,10 +12,11 @@ import { type SonamuDBConfig } from "../database/db";
12
12
  import { createKnexInstance } from "../database/knex";
13
13
  import { SD } from "../dict/sd";
14
14
  import { EntityManager } from "../entity/entity-manager";
15
+ import { getSonamuEnvironment } from "../env";
15
16
  import { ServiceUnavailableException } from "../exceptions/so-exceptions";
16
17
  import { Naite } from "../naite/naite";
17
18
  import { type GenMigrationCode, type MigrationSet } from "../types/types";
18
- import { isTest } from "../utils/controller";
19
+ import { isLocal, isTest } from "../utils/controller";
19
20
  import { exists } from "../utils/fs-utils";
20
21
  import { generateAlterCode, generateCreateCode } from "./code-generation";
21
22
  import { getMigrationSetFromEntity } from "./migration-set";
@@ -29,6 +30,32 @@ export type MigrationResult = {
29
30
  }[];
30
31
 
31
32
  export class Migrator {
33
+ private isMissingMigrationTableError(error: unknown): boolean {
34
+ if (typeof error !== "object" || error === null) {
35
+ return false;
36
+ }
37
+
38
+ const maybePostgresError = error as { code?: unknown; message?: unknown };
39
+ return (
40
+ maybePostgresError.code === "42P01" &&
41
+ typeof maybePostgresError.message === "string" &&
42
+ maybePostgresError.message.includes("knex_migrations")
43
+ );
44
+ }
45
+
46
+ private getMigrationTargetKeys(): (keyof SonamuDBConfig)[] {
47
+ const connKeys = Object.keys(Sonamu.dbConfig).filter(
48
+ (key) => !key.endsWith("_readonly"),
49
+ ) as (keyof SonamuDBConfig)[];
50
+
51
+ if (isLocal()) {
52
+ return connKeys;
53
+ }
54
+
55
+ const environment = getSonamuEnvironment();
56
+ return connKeys.filter((key) => key === environment);
57
+ }
58
+
32
59
  private async runMigrationsSequentially(
33
60
  conns: { connKey: keyof SonamuDBConfig; knex: Knex }[],
34
61
  action: "apply" | "rollback",
@@ -83,9 +110,7 @@ export class Migrator {
83
110
  const codes = await this.getMigrationCodes();
84
111
  Naite.t("migrator:getStatus:codes", codes);
85
112
 
86
- const connKeys = Object.keys(Sonamu.dbConfig).filter(
87
- (key) => !key.endsWith("_slave"),
88
- ) as (keyof typeof Sonamu.dbConfig)[];
113
+ const connKeys = this.getMigrationTargetKeys();
89
114
 
90
115
  let migrationStatusError: string | undefined;
91
116
 
@@ -99,6 +124,10 @@ export class Migrator {
99
124
  try {
100
125
  return await tConn.migrate.status();
101
126
  } catch (err) {
127
+ if (this.isMissingMigrationTableError(err)) {
128
+ return codes.length;
129
+ }
130
+
102
131
  console.warn(
103
132
  chalk.yellow(
104
133
  `${connKey}의 마이그레이션 상태를 가져오는 데에 실패하였습니다. 데이터베이스가 올바르게 구성되지 않은 것 같습니다. 확인하시고 다시 시도해주세요.\n시도한 연결 설정:\n${JSON.stringify(knexOptions.connection, null, 2)}\n발생한 에러:\n${err}\n`,
@@ -113,6 +142,10 @@ export class Migrator {
113
142
  const [, fdList] = await tConn.migrate.list();
114
143
  return fdList.map((fd: { file: string }) => fd.file.replace(".ts", ""));
115
144
  } catch (err) {
145
+ if (this.isMissingMigrationTableError(err)) {
146
+ return codes.map((code) => code.name);
147
+ }
148
+
116
149
  migrationStatusError = err instanceof Error ? err.message : String(err);
117
150
  return [];
118
151
  }
@@ -121,6 +154,10 @@ export class Migrator {
121
154
  try {
122
155
  return await tConn.migrate.currentVersion();
123
156
  } catch (_err) {
157
+ if (this.isMissingMigrationTableError(_err)) {
158
+ return "none";
159
+ }
160
+
124
161
  migrationStatusError = _err instanceof Error ? _err.message : String(_err);
125
162
  return "error";
126
163
  }
@@ -130,7 +167,7 @@ export class Migrator {
130
167
  const connection = knexOptions.connection as Knex.PgConnectionConfig;
131
168
 
132
169
  return {
133
- name: connKey.replace("_master", ""),
170
+ name: connKey,
134
171
  connKey,
135
172
  connString: `pg://${connection.user ?? ""}@${connection.host}:${
136
173
  connection.port
@@ -193,6 +230,14 @@ export class Migrator {
193
230
  Naite.t("migrator:runAction:action", action);
194
231
  Naite.t("migrator:runAction:targets", targets);
195
232
 
233
+ const allowedTargets = new Set(this.getMigrationTargetKeys());
234
+ const disallowedTargets = targets.filter((target) => !allowedTargets.has(target));
235
+ if (disallowedTargets.length > 0) {
236
+ throw new Error(
237
+ `Migration targets are not allowed in NODE_ENV=${getSonamuEnvironment()}: ${disallowedTargets.join(", ")}`,
238
+ );
239
+ }
240
+
196
241
  // get uniq knex configs
197
242
  const configs = unique(
198
243
  targets
@@ -359,9 +404,12 @@ export class Migrator {
359
404
  // 조인테이블 포함하여 MigrationSet 배열
360
405
  const entitySets: MigrationSet[] = [...entitySetsWithJoinTable, ...joinTables];
361
406
 
362
- const codes: GenMigrationCode[] = (
363
- await Promise.all(
364
- entitySets.map(async (entitySet) => {
407
+ const codes: GenMigrationCode[] = [];
408
+ const batchSize = 4;
409
+
410
+ for (let i = 0; i < entitySets.length; i += batchSize) {
411
+ const batchCodes = await Promise.all(
412
+ entitySets.slice(i, i + batchSize).map(async (entitySet) => {
365
413
  const dbSet = await PostgreSQLSchemaReader.getMigrationSetFromDB(
366
414
  compareDB,
367
415
  entitySet.table,
@@ -377,8 +425,10 @@ export class Migrator {
377
425
  return await generateAlterCode(entitySet, dbSet, compareDB);
378
426
  }
379
427
  }),
380
- )
381
- ).flat();
428
+ );
429
+
430
+ codes.push(...batchCodes.flat());
431
+ }
382
432
 
383
433
  // normal 타입이 앞으로, foreign이 뒤로
384
434
  codes.sort((codeA, codeB) => {
@@ -402,20 +452,27 @@ export class Migrator {
402
452
  * @returns Shadow DB 테스트 결과
403
453
  */
404
454
  async runShadowTest(): Promise<MigrationResult> {
405
- const tdbConn = Sonamu.dbConfig.test.connection as Knex.PgConnectionConfig;
406
- const shadowDatabase = `${tdbConn.database}__migration_shadow`;
455
+ const baseTestConn = Sonamu.dbConfig.test.connection as Knex.PgConnectionConfig;
456
+ const workerId = process.env.SONAMU_WORKER_DB === "true" ? process.env.VITEST_POOL_ID : null;
457
+ const templateDatabase =
458
+ workerId !== null ? `${baseTestConn.database}_${workerId ?? "1"}` : baseTestConn.database;
459
+ const tdbConn = { ...baseTestConn, database: templateDatabase };
460
+ const shadowDatabase = `${templateDatabase}__migration_shadow`;
407
461
 
408
462
  // 테스트 상황에서는 트랜잭션을 초기화하고, 새 데이터베이스 커넥션을 가져와야 함
409
463
  if (isTest()) {
410
464
  await DB.clearTestTransaction();
411
- // 병렬 테스트 모드에서는 worker DB 연결 유지
412
- if (process.env.SONAMU_WORKER_DB !== "true") {
413
- await DB.destroy();
414
- }
465
+ await DB.destroy();
415
466
  }
416
467
 
417
468
  // 기존 Shadow DB 삭제 후 Shadow DB 생성
418
- const tdb = createKnexInstance(Sonamu.dbConfig.test);
469
+ const tdb = createKnexInstance({
470
+ ...Sonamu.dbConfig.test,
471
+ connection: {
472
+ ...baseTestConn,
473
+ database: "postgres",
474
+ },
475
+ });
419
476
  try {
420
477
  !isTest() && console.log(chalk.magenta(`${shadowDatabase} 삭제`));
421
478
  await tdb.raw(`DROP DATABASE IF EXISTS ${shadowDatabase}`);
@@ -25,10 +25,29 @@ export type SlackConfirmPendingResult = {
25
25
  export class SlackConfirm {
26
26
  private config = Sonamu.config.slackConfirm;
27
27
 
28
+ private validateConfiguredTargets(): void {
29
+ if (!this.config) {
30
+ return;
31
+ }
32
+
33
+ const validTargets = Object.keys(Sonamu.dbConfig);
34
+ const invalidTargets = this.config.targets.filter((target) => !validTargets.includes(target));
35
+
36
+ if (invalidTargets.length > 0) {
37
+ assert.fail(
38
+ SD("sonamu.error.slackConfirmInvalidTargets")(
39
+ invalidTargets.join(", "),
40
+ validTargets.join(", "),
41
+ ),
42
+ );
43
+ }
44
+ }
45
+
28
46
  /**
29
47
  * 설정이 있는지 확인합니다.
30
48
  */
31
49
  isConfigured(): boolean {
50
+ this.validateConfiguredTargets();
32
51
  return !!(this.config?.botToken && this.config?.channelId);
33
52
  }
34
53
 
@@ -36,6 +55,7 @@ export class SlackConfirm {
36
55
  * 해당 target이 승인 대상인지 확인합니다.
37
56
  */
38
57
  isTargetRequiresApproval(target: keyof SonamuDBConfig): boolean {
58
+ this.validateConfiguredTargets();
39
59
  return this.config?.targets?.includes(target) ?? false;
40
60
  }
41
61
 
@@ -98,6 +118,7 @@ export class SlackConfirm {
98
118
  requestor?: string,
99
119
  ): Promise<{ channel: string; ts: string }> {
100
120
  assert(this.config, SD("sonamu.error.slackConfirmNotConfigured"));
121
+ this.validateConfiguredTargets();
101
122
 
102
123
  const response = await fetch("https://slack.com/api/chat.postMessage", {
103
124
  method: "POST",
@@ -80,7 +80,7 @@ const fixtureDb = createKnexInstance(Sonamu.dbConfig.fixture);
80
80
  const generator = new FixtureGenerator(fixtureDb, fixtureDb, "fixture", EntityManager);
81
81
 
82
82
  // fixture fetch: production → fixture DB
83
- const sourceDb = DB.getDB("r"); // production_master
83
+ const sourceDb = DB.getDB("r"); // current NODE_ENV readonly DB
84
84
  const fixtureDb = createKnexInstance(Sonamu.dbConfig.fixture);
85
85
  const generator = new FixtureGenerator(sourceDb, fixtureDb, "fixture", EntityManager);
86
86
  ```
@@ -343,7 +343,7 @@ export default defineConfig({
343
343
 
344
344
  ### Activation Conditions
345
345
 
346
- DevRunner is registered in `sonamu.ts` under the condition `isLocal() && config.test?.devRunner?.enabled`. `isLocal()` returns true when the `LR` environment variable is undefined or `"local"` (`controller.ts`). It only works in local development environments and is disabled in remote (production/staging) environments.
346
+ DevRunner is registered in `sonamu.ts` under the condition `isLocal() && config.test?.devRunner?.enabled`. `isLocal()` returns true when `NODE_ENV` is `development` or `test` (`controller.ts`). It works in local development and test environments and is disabled in staging/production environments.
347
347
 
348
348
  ### Parallel Test DB Flow
349
349
 
@@ -40,8 +40,8 @@ class InMemoryRingBuffer<TRecord extends { timestamp: number }> {
40
40
  this.capacity = options.maxRecords ?? DEFAULT_MAX_RECORDS;
41
41
  this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
42
42
  this.estimateBytes = estimateBytes;
43
- this.slots = new Array(this.capacity);
44
- this.slotBytes = new Array(this.capacity).fill(0);
43
+ this.slots = Array.from({ length: this.capacity });
44
+ this.slotBytes = Array.from({ length: this.capacity }, () => 0);
45
45
  }
46
46
 
47
47
  push(record: TRecord): void {
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { type Knex } from "knex";
3
3
 
4
+ import { type SonamuDBPreset } from "../database/db";
4
5
  import { type Entity } from "../entity/entity";
5
6
  import { type EntityManager } from "../entity/entity-manager";
6
7
  import { type EntityProp, type FixtureImportResult, type FixtureRecord } from "../types/types";
@@ -45,7 +46,7 @@ export class FixtureGenerator {
45
46
  // FixtureManager.insertFixtures가 dbName 문자열을 받기 때문에 직접 사용하지 않습니다
46
47
  // 미래 확장성을 위해 API 시그니처에는 포함시켰습니다
47
48
  _targetDb: Knex,
48
- private targetDbName: "fixture" | "test" | "production_master",
49
+ private targetDbName: SonamuDBPreset,
49
50
  private entityManager: typeof EntityManager,
50
51
  options?: FixtureGeneratorOptions,
51
52
  ) {
@@ -79,11 +79,11 @@ export class FixtureManagerClass {
79
79
  if (this._tdb !== null) {
80
80
  return;
81
81
  }
82
- if (Sonamu.dbConfig.test && Sonamu.dbConfig.production_master) {
82
+ if (Sonamu.dbConfig.test && Sonamu.dbConfig.production) {
83
83
  const tConn = Sonamu.dbConfig.test.connection as Knex.ConnectionConfig & {
84
84
  port?: number;
85
85
  };
86
- const pConn = Sonamu.dbConfig.production_master.connection as Knex.ConnectionConfig & {
86
+ const pConn = Sonamu.dbConfig.production.connection as Knex.ConnectionConfig & {
87
87
  port?: number;
88
88
  };
89
89
  if (
@@ -229,8 +229,7 @@ export class FixtureManagerClass {
229
229
 
230
230
  // 픽스쳐DB, 실DB
231
231
  const fixtureDatabase = (Sonamu.dbConfig.fixture.connection as Knex.ConnectionConfig).database;
232
- const realDatabase = (Sonamu.dbConfig.production_master.connection as Knex.ConnectionConfig)
233
- .database;
232
+ const realDatabase = (Sonamu.dbConfig.production.connection as Knex.ConnectionConfig).database;
234
233
 
235
234
  const selfQuery = `INSERT IGNORE INTO \`${fixtureDatabase}\`.\`${entity.table}\` (SELECT * FROM \`${realDatabase}\`.\`${entity.table}\` WHERE \`id\` = ${id})`;
236
235
 
@@ -1,6 +1,19 @@
1
1
  import { loadConfig } from "../api/config";
2
+ import { DB } from "../database/db";
2
3
  import { ParallelDBManager } from "./parallel-db-manager";
3
4
 
5
+ function restoreProcessEnv(snapshot: NodeJS.ProcessEnv): void {
6
+ for (const key of Object.keys(process.env)) {
7
+ if (!(key in snapshot)) {
8
+ delete process.env[key];
9
+ }
10
+ }
11
+
12
+ for (const [key, value] of Object.entries(snapshot)) {
13
+ process.env[key] = value;
14
+ }
15
+ }
16
+
4
17
  /**
5
18
  * vitest globalSetup 함수를 생성합니다.
6
19
  * sonamu.config.ts의 test 설정을 읽어서 병렬 테스트 환경을 구성합니다.
@@ -22,30 +35,41 @@ export function createGlobalSetup() {
22
35
  return async function setup() {
23
36
  const { findApiRootPath } = await import("../utils/utils");
24
37
  const rootPath = findApiRootPath();
38
+ const envBeforeConfigLoad = { ...process.env };
25
39
  const config = await loadConfig(rootPath);
26
40
 
27
- // 병렬 테스트가 비활성화된 경우 아무것도 하지 않음
28
- if (!config.test?.parallel) {
29
- return async function teardown() {
30
- // no-op
31
- };
32
- }
41
+ try {
42
+ // 병렬 테스트가 비활성화된 경우 아무것도 하지 않음
43
+ if (!config.test?.parallel) {
44
+ return async function teardown() {
45
+ // no-op
46
+ };
47
+ }
33
48
 
34
- const maxWorkers = config.test.maxWorkers ?? 4;
35
- const templateDb = `${config.database.name}_test`;
49
+ const maxWorkers = config.test.maxWorkers ?? 4;
50
+ const dbConfig = DB.generateDBConfig(config.database, config.projectName);
51
+ const testConnection = dbConfig.test.connection;
52
+ const templateDb = (testConnection as { database: string }).database;
36
53
 
37
- const connectionConfig = {
38
- client: config.database.database ?? ("pg" as const),
39
- connection:
40
- config.database.environments?.test?.connection ?? config.database.defaultOptions.connection,
41
- };
54
+ const adminConnection = {
55
+ ...(testConnection as Record<string, unknown>),
56
+ database: "postgres",
57
+ };
58
+
59
+ const connectionConfig = {
60
+ client: config.database.database ?? ("pg" as const),
61
+ connection: adminConnection,
62
+ };
42
63
 
43
- const manager = new ParallelDBManager(maxWorkers, connectionConfig, templateDb);
44
- await manager.createWorkerDatabases();
64
+ const manager = new ParallelDBManager(maxWorkers, connectionConfig, templateDb);
65
+ await manager.createWorkerDatabases();
45
66
 
46
- return async function teardown() {
47
- await manager.dropWorkerDatabases();
48
- };
67
+ return async function teardown() {
68
+ await manager.dropWorkerDatabases();
69
+ };
70
+ } finally {
71
+ restoreProcessEnv(envBeforeConfigLoad);
72
+ }
49
73
  };
50
74
  }
51
75
 
@@ -4,6 +4,18 @@ import { loadConfig } from "../api/config";
4
4
 
5
5
  type VitestConfig = ViteUserConfig["test"];
6
6
 
7
+ function restoreProcessEnv(snapshot: NodeJS.ProcessEnv): void {
8
+ for (const key of Object.keys(process.env)) {
9
+ if (!(key in snapshot)) {
10
+ delete process.env[key];
11
+ }
12
+ }
13
+
14
+ for (const [key, value] of Object.entries(snapshot)) {
15
+ process.env[key] = value;
16
+ }
17
+ }
18
+
7
19
  /**
8
20
  * sonamu.config.ts의 test 설정을 기반으로 vitest 테스트 설정을 반환합니다.
9
21
  *
@@ -22,7 +34,9 @@ type VitestConfig = ViteUserConfig["test"];
22
34
  */
23
35
  export async function getSonamuTestConfig(options?: VitestConfig): Promise<VitestConfig> {
24
36
  const { findApiRootPath } = await import("../utils/utils");
37
+ const envBeforeConfigLoad = { ...process.env };
25
38
  const config = await loadConfig(findApiRootPath());
39
+ restoreProcessEnv(envBeforeConfigLoad);
26
40
 
27
41
  const isParallel = config.test?.parallel ?? false;
28
42
  const maxWorkers = config.test?.maxWorkers ?? 4;