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.
- package/dist/_virtual/_rolldown/runtime.js +36 -0
- package/dist/ai/agents/agent.js +5 -7
- package/dist/ai/agents/index.js +1 -2
- package/dist/ai/agents/types.js +1 -1
- package/dist/ai/index.js +1 -2
- package/dist/ai/providers/rtzr/api.js +2 -3
- package/dist/ai/providers/rtzr/error.js +14 -29
- package/dist/ai/providers/rtzr/index.js +1 -2
- package/dist/ai/providers/rtzr/model.js +13 -20
- package/dist/ai/providers/rtzr/options.js +2 -3
- package/dist/ai/providers/rtzr/provider.js +2 -3
- package/dist/ai/providers/rtzr/utils.js +12 -21
- package/dist/api/base-frame.js +4 -4
- package/dist/api/caster.js +21 -38
- package/dist/api/code-converters.js +41 -98
- package/dist/api/config.d.ts +1 -10
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +9 -8
- package/dist/api/context.js +2 -3
- package/dist/api/decorators.js +80 -116
- package/dist/api/index.js +2 -3
- package/dist/api/secret.js +6 -10
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +200 -387
- package/dist/api/validator.js +5 -8
- package/dist/api/websocket-helpers.js +21 -32
- package/dist/auth/audit-log/builders.js +2 -3
- package/dist/auth/audit-log/events.js +2 -2
- package/dist/auth/audit-log/plugin.js +30 -61
- package/dist/auth/audit-log-ingestor.js +19 -41
- package/dist/auth/auth-generator.js +16 -41
- package/dist/auth/better-auth-entities.js +3 -4
- package/dist/auth/index.js +2 -3
- package/dist/auth/knex-adapter.js +18 -45
- package/dist/auth/plugins/entity-definitions/admin.js +2 -2
- package/dist/auth/plugins/entity-definitions/anonymous.js +2 -2
- package/dist/auth/plugins/entity-definitions/api-key.js +2 -2
- package/dist/auth/plugins/entity-definitions/audit-log.js +2 -2
- package/dist/auth/plugins/entity-definitions/index.js +2 -3
- package/dist/auth/plugins/entity-definitions/jwt.js +2 -2
- package/dist/auth/plugins/entity-definitions/organization.js +2 -2
- package/dist/auth/plugins/entity-definitions/passkey.js +2 -2
- package/dist/auth/plugins/entity-definitions/phone-number.js +2 -2
- package/dist/auth/plugins/entity-definitions/sso.js +2 -2
- package/dist/auth/plugins/entity-definitions/two-factor.js +2 -2
- package/dist/auth/plugins/entity-definitions/types.js +1 -1
- package/dist/auth/plugins/entity-definitions/username.js +2 -2
- package/dist/auth/plugins/index.js +1 -2
- package/dist/auth/plugins/wrappers/admin.js +2 -3
- package/dist/auth/plugins/wrappers/anonymous.js +2 -3
- package/dist/auth/plugins/wrappers/api-key.js +2 -3
- package/dist/auth/plugins/wrappers/index.js +1 -2
- package/dist/auth/plugins/wrappers/jwt.js +2 -3
- package/dist/auth/plugins/wrappers/organization.js +2 -3
- package/dist/auth/plugins/wrappers/passkey.js +2 -3
- package/dist/auth/plugins/wrappers/phone-number.js +2 -3
- package/dist/auth/plugins/wrappers/sso.js +2 -3
- package/dist/auth/plugins/wrappers/two-factor.js +2 -3
- package/dist/auth/plugins/wrappers/username.js +2 -3
- package/dist/bin/build-config.js +2 -2
- package/dist/bin/cli.js +151 -258
- package/dist/bin/fixture.d.ts.map +1 -1
- package/dist/bin/fixture.js +55 -97
- package/dist/bin/hmr-hook-register.js +3 -3
- package/dist/bin/migrate-targets.d.ts +3 -0
- package/dist/bin/migrate-targets.d.ts.map +1 -0
- package/dist/bin/migrate-targets.js +11 -0
- package/dist/bin/test-command.js +25 -55
- package/dist/bin/ts-loader-register.js +5 -6
- package/dist/bin/ts-loader-registration.js +6 -13
- package/dist/cache/cache-manager.js +3 -4
- package/dist/cache/decorator.js +11 -21
- package/dist/cache/drivers.js +2 -3
- package/dist/cache/index.js +2 -3
- package/dist/cache/types.js +1 -1
- package/dist/cache-control/cache-control.js +21 -34
- package/dist/cache-control/types.js +1 -1
- package/dist/compress/compress.js +10 -10
- package/dist/compress/index.js +1 -2
- package/dist/compress/types.js +1 -1
- package/dist/cone/cone-generator.js +25 -63
- package/dist/database/_batch_update.js +26 -46
- package/dist/database/base-model.js +44 -97
- package/dist/database/base-model.types.js +1 -1
- package/dist/database/db.d.ts +8 -14
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +127 -72
- package/dist/database/knex.js +5 -8
- package/dist/database/puri-subset.types.js +1 -1
- package/dist/database/puri-wrapper.js +11 -15
- package/dist/database/puri.js +117 -234
- package/dist/database/puri.types.js +3 -4
- package/dist/database/transaction-context.js +4 -5
- package/dist/database/upsert-builder.js +109 -176
- package/dist/dict/en.d.ts +1 -0
- package/dist/dict/en.d.ts.map +1 -1
- package/dist/dict/en.js +4 -4
- package/dist/dict/index.js +2 -3
- package/dist/dict/ko.d.ts +1 -0
- package/dist/dict/ko.d.ts.map +1 -1
- package/dist/dict/ko.js +4 -4
- package/dist/dict/rc-keys.js +3 -4
- package/dist/dict/sd.js +8 -19
- package/dist/dict/sonamu-dictionary.js +141 -284
- package/dist/dict/types.js +1 -1
- package/dist/dict/utils.js +4 -5
- package/dist/entity/entity-manager.d.ts +2 -2
- package/dist/entity/entity-manager.js +34 -82
- package/dist/entity/entity-template-cone.js +33 -66
- package/dist/entity/entity.js +156 -310
- package/dist/env.d.ts +14 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +75 -0
- package/dist/exceptions/error-handler.js +2 -3
- package/dist/exceptions/so-exceptions.js +7 -5
- package/dist/filter/index.js +1 -2
- package/dist/filter/types.js +3 -4
- package/dist/filter/utils.js +21 -54
- package/dist/index.js +8 -7
- package/dist/logger/category.js +6 -12
- package/dist/logger/configure.js +23 -34
- package/dist/migration/code-generation.js +146 -314
- package/dist/migration/index-where-predicate.js +52 -144
- package/dist/migration/migration-set.js +19 -33
- package/dist/migration/migrator.d.ts +2 -0
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +69 -53
- package/dist/migration/postgresql-schema-reader.js +126 -225
- package/dist/migration/slack-confirm.d.ts +1 -0
- package/dist/migration/slack-confirm.d.ts.map +1 -1
- package/dist/migration/slack-confirm.js +28 -38
- package/dist/migration/types.js +1 -1
- package/dist/naite/messaging-types.js +1 -1
- package/dist/naite/naite-reporter.js +15 -32
- package/dist/naite/naite.js +43 -76
- package/dist/ssr/index.js +6 -9
- package/dist/ssr/registry.js +10 -18
- package/dist/ssr/renderer.js +10 -21
- package/dist/ssr/types.js +1 -1
- package/dist/storage/base-file.js +5 -10
- package/dist/storage/buffered-file.js +3 -4
- package/dist/storage/drivers.js +2 -3
- package/dist/storage/index.js +2 -3
- package/dist/storage/s3-driver.js +5 -9
- package/dist/storage/storage-manager.js +5 -5
- package/dist/storage/types.js +1 -1
- package/dist/storage/uploaded-file.js +4 -6
- package/dist/stream/index.js +1 -2
- package/dist/stream/sse.js +8 -13
- package/dist/stream/ws-audience-resolver.js +5 -5
- package/dist/stream/ws-audience.js +3 -4
- package/dist/stream/ws-cluster-bus.js +3 -4
- package/dist/stream/ws-core.js +1 -1
- package/dist/stream/ws-delivery.js +11 -25
- package/dist/stream/ws-local-connection-store.js +9 -18
- package/dist/stream/ws-presence-store.js +43 -97
- package/dist/stream/ws-registry.js +17 -22
- package/dist/stream/ws-telemetry-memory.js +38 -45
- package/dist/stream/ws-telemetry-trace.js +4 -6
- package/dist/stream/ws-telemetry.js +82 -135
- package/dist/stream/ws.js +47 -91
- package/dist/syncer/api-parser.js +81 -147
- package/dist/syncer/checksum.js +9 -20
- package/dist/syncer/code-generator.js +29 -47
- package/dist/syncer/entity-operations.js +17 -27
- package/dist/syncer/event-batcher.js +8 -15
- package/dist/syncer/file-patterns.js +3 -4
- package/dist/syncer/file-tracking.js +6 -10
- package/dist/syncer/index.js +1 -2
- package/dist/syncer/module-loader.js +10 -26
- package/dist/syncer/syncer-actions.js +19 -37
- package/dist/syncer/syncer.js +46 -98
- package/dist/syncer/watcher.js +12 -26
- package/dist/tasks/decorator.js +7 -11
- package/dist/tasks/step-wrapper.js +7 -8
- package/dist/tasks/workflow-manager.js +18 -25
- package/dist/template/entity-converter.js +40 -64
- package/dist/template/helpers.js +32 -63
- package/dist/template/implementations/entity.template.js +7 -11
- package/dist/template/implementations/entry-server.template.js +2 -3
- package/dist/template/implementations/generated.template.js +25 -51
- package/dist/template/implementations/generated_http.template.js +31 -58
- package/dist/template/implementations/generated_sso.template.js +45 -85
- package/dist/template/implementations/init_types.template.js +4 -7
- package/dist/template/implementations/model.template.js +5 -10
- package/dist/template/implementations/model_test.template.js +2 -3
- package/dist/template/implementations/queries.template.js +4 -7
- package/dist/template/implementations/sd.template.js +17 -35
- package/dist/template/implementations/services.template.js +18 -30
- package/dist/template/implementations/view_form.template.js +72 -125
- package/dist/template/implementations/view_id_all_select.template.js +2 -3
- package/dist/template/implementations/view_list.template.js +86 -143
- package/dist/template/implementations/view_search_input.template.js +2 -3
- package/dist/template/index.js +5 -8
- package/dist/template/template-manager.js +13 -26
- package/dist/template/template-types.js +2 -3
- package/dist/template/template.js +7 -11
- package/dist/template/zod-converter.js +173 -348
- package/dist/testing/_relation-graph.js +18 -37
- package/dist/testing/bootstrap.js +5 -8
- package/dist/testing/data-explorer.js +34 -78
- package/dist/testing/dev-test-routes.js +54 -60
- package/dist/testing/dev-vitest-manager.js +33 -84
- package/dist/testing/faker-mappings.js +3 -4
- package/dist/testing/fixture-generator.d.ts +2 -1
- package/dist/testing/fixture-generator.d.ts.map +1 -1
- package/dist/testing/fixture-generator.js +159 -321
- package/dist/testing/fixture-loader.js +2 -2
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +124 -227
- package/dist/testing/global-setup.d.ts.map +1 -1
- package/dist/testing/global-setup.js +29 -17
- package/dist/testing/index.js +1 -2
- package/dist/testing/naite-vitest-reporter.js +2 -3
- package/dist/testing/parallel-db-manager.js +5 -3
- package/dist/testing/vitest-helpers.d.ts.map +1 -1
- package/dist/testing/vitest-helpers.js +15 -12
- package/dist/types/types.d.ts +14 -14
- package/dist/types/types.js +27 -50
- package/dist/ui/ai-api.js +6 -11
- package/dist/ui/ai-client.js +86 -134
- package/dist/ui/api.js +99 -195
- package/dist/ui/cdd-service.js +78 -130
- package/dist/ui/cdd-types.js +1 -1
- package/dist/ui-web/assets/{index-Df8q-fhb.js → index-DFStGyd0.js} +49 -49
- package/dist/ui-web/assets/index-Dx4ap5i4.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/dist/utils/async-utils.js +13 -25
- package/dist/utils/class-name.js +3 -4
- package/dist/utils/console-util.js +11 -26
- package/dist/utils/controller.d.ts.map +1 -1
- package/dist/utils/controller.js +14 -12
- package/dist/utils/esm-utils.js +5 -8
- package/dist/utils/formatter.js +10 -22
- package/dist/utils/fs-utils.js +14 -25
- package/dist/utils/lodash-able.js +3 -4
- package/dist/utils/model.js +7 -14
- package/dist/utils/object-utils.js +41 -73
- package/dist/utils/path-utils.js +5 -9
- package/dist/utils/process-utils.js +4 -7
- package/dist/utils/sql-parser.js +6 -13
- package/dist/utils/type-utils.js +16 -26
- package/dist/utils/utils.js +18 -40
- package/dist/utils/zod-error.js +9 -16
- package/dist/vector/chunking.js +24 -37
- package/dist/vector/config.js +2 -2
- package/dist/vector/embedding.js +8 -19
- package/dist/vector/index.js +1 -2
- package/dist/vector/types.js +1 -1
- package/package.json +7 -7
- package/src/__tests__/env.test.ts +127 -0
- package/src/api/__tests__/config.test.ts +10 -1
- package/src/api/config.ts +4 -12
- package/src/api/sonamu.ts +14 -4
- package/src/bin/__tests__/migrate-targets.test.ts +28 -0
- package/src/bin/__tests__/test-command.test.ts +82 -1
- package/src/bin/cli.ts +9 -18
- package/src/bin/fixture.ts +5 -4
- package/src/bin/migrate-targets.ts +7 -0
- package/src/bin/test-command.ts +2 -2
- package/src/database/__tests__/db.test.ts +175 -0
- package/src/database/db.ts +193 -71
- package/src/dict/en.ts +2 -0
- package/src/dict/ko.ts +2 -0
- package/src/env.ts +123 -0
- package/src/migration/__tests__/migrator.test.ts +149 -0
- package/src/migration/migrator.ts +74 -17
- package/src/migration/slack-confirm.ts +21 -0
- package/src/skills/sonamu/database.md +1 -1
- package/src/skills/sonamu/entity-basic.md +31 -0
- package/src/skills/sonamu/puri.md +22 -0
- package/src/skills/sonamu/testing-devrunner.md +1 -1
- package/src/skills/sonamu/upsert.md +53 -6
- package/src/stream/ws-telemetry-memory.ts +2 -2
- package/src/testing/fixture-generator.ts +2 -1
- package/src/testing/fixture-manager.ts +3 -4
- package/src/testing/global-setup.ts +42 -18
- package/src/testing/vitest-helpers.ts +14 -0
- package/src/utils/controller.ts +14 -7
- package/tsdown.api.config.ts +6 -0
- package/dist/_virtual/rolldown_runtime.js +0 -39
- 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 =
|
|
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
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
|
406
|
-
const
|
|
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
|
-
|
|
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(
|
|
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"); //
|
|
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
|
```
|
|
@@ -436,6 +436,37 @@ SQL expressions per source column type:
|
|
|
436
436
|
}
|
|
437
437
|
```
|
|
438
438
|
|
|
439
|
+
### Partial index (`where`)
|
|
440
|
+
|
|
441
|
+
`where` declares a PostgreSQL partial index predicate. Provide a raw SQL condition **without** the `WHERE` keyword; it is appended to the generated `CREATE INDEX`. Works for every index type (`index`, `unique`, `hnsw`, `ivfflat`, pgroonga).
|
|
442
|
+
|
|
443
|
+
```json
|
|
444
|
+
{
|
|
445
|
+
"name": "uniq_users_email_active",
|
|
446
|
+
"type": "unique",
|
|
447
|
+
"columns": [{ "name": "email" }],
|
|
448
|
+
"where": "deleted_at IS NULL"
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
→ `CREATE UNIQUE INDEX uniq_users_email_active ON users (email) WHERE deleted_at IS NULL;`
|
|
453
|
+
(Enforces email uniqueness only among non-deleted rows.)
|
|
454
|
+
|
|
455
|
+
### `nullsNotDistinct` (unique only)
|
|
456
|
+
|
|
457
|
+
By default PostgreSQL treats `NULL`s as distinct, so a unique index allows multiple `NULL` rows. Set `nullsNotDistinct: true` to emit `NULLS NOT DISTINCT`, treating `NULL`s as equal (at most one `NULL` allowed).
|
|
458
|
+
|
|
459
|
+
```json
|
|
460
|
+
{
|
|
461
|
+
"name": "uniq_accounts_external_id",
|
|
462
|
+
"type": "unique",
|
|
463
|
+
"columns": [{ "name": "external_id" }],
|
|
464
|
+
"nullsNotDistinct": true
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
→ `CREATE UNIQUE INDEX uniq_accounts_external_id ON accounts (external_id) NULLS NOT DISTINCT;`
|
|
469
|
+
|
|
439
470
|
### IMPORTANT: Use the actual DB column name in indexes
|
|
440
471
|
|
|
441
472
|
**The way FK columns are referenced differs between indexes and subsets. Do not confuse them.**
|
|
@@ -180,6 +180,28 @@ db.table("orders")
|
|
|
180
180
|
db.orderBy("created_at", "desc").limit(20).offset(40); // Page 3
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
+
### NULLS position & array form
|
|
184
|
+
|
|
185
|
+
`orderBy` has two overloads:
|
|
186
|
+
|
|
187
|
+
1. Single column: `orderBy(column, direction?, nulls?)` — `nulls` is `"first" | "last"`
|
|
188
|
+
2. Array: `orderBy(entries[])` — each entry is a column string, a `Puri.raw*` SQL expression, or an object `{ column, order?, nulls? }`
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Single column with NULLS position
|
|
192
|
+
db.orderBy("published_at", "desc", "last");
|
|
193
|
+
|
|
194
|
+
// Array form: multiple sort keys, per-key direction and NULLS
|
|
195
|
+
db.orderBy([
|
|
196
|
+
{ column: "is_pinned", order: "desc" },
|
|
197
|
+
{ column: "published_at", order: "desc", nulls: "last" },
|
|
198
|
+
"title", // bare string defaults to asc
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
// Sort by a SQL expression
|
|
202
|
+
db.orderBy([Puri.rawNumber("view_count * 2"), { column: "id", order: "desc" }]);
|
|
203
|
+
```
|
|
204
|
+
|
|
183
205
|
## GROUP BY & HAVING
|
|
184
206
|
|
|
185
207
|
```typescript
|
|
@@ -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
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sonamu-upsert
|
|
3
|
-
description: Saving complex relational data with Sonamu UpsertBuilder. ubRegister, ubUpsert,
|
|
3
|
+
description: Saving complex relational data with Sonamu UpsertBuilder. ubRegister, ubUpsert, ubInsertOnly, ubUpdateBatch, bulk insert/upsert patterns, FK ordering, cleanOrphans. Use when saving related data with foreign key dependencies.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# UpsertBuilder
|
|
@@ -178,15 +178,62 @@ await wdb.transaction(async (trx) => {
|
|
|
178
178
|
});
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
##
|
|
181
|
+
## Bulk Insert / Bulk Upsert (Large Datasets)
|
|
182
|
+
|
|
183
|
+
**Principle (same as regular saves):** call `ubRegister` for every row **outside** the transaction, then call only `ubUpsert` / `ubInsertOnly` **inside** the transaction. Split large datasets with `chunkSize`. Calling `ubRegister` inside the transaction is reserved for the special case where a row depends on the real id produced by a preceding `ubUpsert`.
|
|
184
|
+
|
|
185
|
+
### (a) Bulk insert inside a Model
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
async createMany(params: PostCreateParams[]): Promise<number[]> {
|
|
189
|
+
const wdb = this.getPuri("w");
|
|
190
|
+
params.forEach((p) => wdb.ubRegister("posts", p));
|
|
191
|
+
|
|
192
|
+
return wdb.transaction(async (trx) => {
|
|
193
|
+
return trx.ubInsertOnly("posts", { chunkSize: 5000 });
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### (b) Standalone Puri (seed / batch scripts outside a Model)
|
|
199
|
+
|
|
200
|
+
Outside a Model there is no `this.getPuri("w")`, so build a `PuriWrapper` directly. Use `DB.getDB("w")` only in this script context — inside Models always use `getPuri("w")`.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { DB, PuriWrapper, UpsertBuilder } from "sonamu";
|
|
204
|
+
|
|
205
|
+
const puri = new PuriWrapper(DB.getDB("w"), new UpsertBuilder());
|
|
206
|
+
for (const row of rows) puri.ubRegister("audit_logs", row);
|
|
207
|
+
|
|
208
|
+
await puri.transaction(async (trx) => {
|
|
209
|
+
await trx.ubInsertOnly("audit_logs", { chunkSize: 5000 }); // or ubUpsert
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### (c) Multiple tables in one transaction (FK order)
|
|
214
|
+
|
|
215
|
+
Register every table outside the transaction, then upsert parents before children.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const puri = new PuriWrapper(DB.getDB("w"), new UpsertBuilder());
|
|
219
|
+
for (const row of postRows) puri.ubRegister("posts", row);
|
|
220
|
+
for (const row of tagRows) puri.ubRegister("post_tags", row);
|
|
221
|
+
|
|
222
|
+
await puri.transaction(async (trx) => {
|
|
223
|
+
await trx.ubInsertOnly("posts", { chunkSize: 5000 }); // parent first
|
|
224
|
+
await trx.ubInsertOnly("post_tags", { chunkSize: 5000 }); // child next
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## ubInsertOnly (INSERT Only)
|
|
182
229
|
|
|
183
230
|
Perform INSERT without UPDATE:
|
|
184
231
|
|
|
185
232
|
```typescript
|
|
186
|
-
await trx.
|
|
233
|
+
await trx.ubInsertOnly("logs", { chunkSize: 1000 });
|
|
187
234
|
```
|
|
188
235
|
|
|
189
|
-
##
|
|
236
|
+
## ubUpdateBatch (Batch Update)
|
|
190
237
|
|
|
191
238
|
Bulk UPDATE operations:
|
|
192
239
|
|
|
@@ -197,14 +244,14 @@ wdb.ubRegister("users", { id: 2, status: "active" });
|
|
|
197
244
|
wdb.ubRegister("users", { id: 3, status: "inactive" });
|
|
198
245
|
|
|
199
246
|
await wdb.transaction(async (trx) => {
|
|
200
|
-
await trx.
|
|
247
|
+
await trx.ubUpdateBatch("users", {
|
|
201
248
|
chunkSize: 500, // batch size (default: 500)
|
|
202
249
|
where: "id", // WHERE condition column (default: "id")
|
|
203
250
|
});
|
|
204
251
|
});
|
|
205
252
|
|
|
206
253
|
// Composite key for WHERE condition
|
|
207
|
-
await trx.
|
|
254
|
+
await trx.ubUpdateBatch("user_settings", {
|
|
208
255
|
where: ["user_id", "setting_key"],
|
|
209
256
|
});
|
|
210
257
|
```
|
|
@@ -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 =
|
|
44
|
-
this.slotBytes =
|
|
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 {
|