@aurios/mizzling 1.1.0 → 1.1.2

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.
@@ -0,0 +1,164 @@
1
+ import { expect, test, describe, beforeEach, afterEach, vi } from "vitest";
2
+ import { loadConfig, defineConfig, getClient } from "../src/config";
3
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+ import { tmpdir } from "os";
6
+ import * as credentialProviders from "@aws-sdk/credential-provider-ini";
7
+
8
+ vi.mock("@aws-sdk/credential-provider-ini", () => ({
9
+ fromIni: vi.fn(),
10
+ }));
11
+
12
+ const TEMP_DIR = join(tmpdir(), "mizzle-config-expansion-test-" + Date.now());
13
+
14
+ describe("Config Expansion", () => {
15
+ const originalCwd = process.cwd();
16
+
17
+ beforeEach(() => {
18
+ mkdirSync(TEMP_DIR, { recursive: true });
19
+ process.chdir(TEMP_DIR);
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ afterEach(() => {
24
+ process.chdir(originalCwd);
25
+ rmSync(TEMP_DIR, { recursive: true, force: true });
26
+ });
27
+
28
+ test("defineConfig should accept expanded properties", () => {
29
+ const config = defineConfig({
30
+ schema: "./schema.ts",
31
+ out: "./out",
32
+ profile: "test",
33
+ maxAttempts: 3,
34
+ });
35
+ expect(config.profile).toBe("test");
36
+ expect(config.maxAttempts).toBe(3);
37
+ });
38
+
39
+ test("should load config with expanded properties", async () => {
40
+ const configName = "mizzle.config.expanded.ts";
41
+ const configContent = `
42
+ export default {
43
+ schema: "./src/schema",
44
+ out: "./migrations",
45
+ credentials: {
46
+ accessKeyId: "AKIA",
47
+ secretAccessKey: "SECRET",
48
+ sessionToken: "TOKEN"
49
+ },
50
+ profile: "my-profile",
51
+ maxAttempts: 5
52
+ };
53
+ `;
54
+ writeFileSync(join(TEMP_DIR, configName), configContent);
55
+
56
+ const config = await loadConfig(configName);
57
+ expect(config).toEqual({
58
+ schema: "./src/schema",
59
+ out: "./migrations",
60
+ credentials: {
61
+ accessKeyId: "AKIA",
62
+ secretAccessKey: "SECRET",
63
+ sessionToken: "TOKEN",
64
+ },
65
+ profile: "my-profile",
66
+ maxAttempts: 5,
67
+ });
68
+ });
69
+
70
+ test("getClient should use provided credentials", async () => {
71
+ const config = {
72
+ schema: "./schema.ts",
73
+ out: "./out",
74
+ region: "us-west-2",
75
+ credentials: {
76
+ accessKeyId: "test-key",
77
+ secretAccessKey: "test-secret",
78
+ },
79
+ };
80
+ const client = getClient(config);
81
+ const resolvedConfig = await client.config.credentials();
82
+ expect(resolvedConfig.accessKeyId).toBe("test-key");
83
+ expect(resolvedConfig.secretAccessKey).toBe("test-secret");
84
+ expect(await client.config.region()).toBe("us-west-2");
85
+ });
86
+
87
+ test("getClient should use default credentials if no credentials or profile provided", async () => {
88
+ const originalEnv = { ...process.env };
89
+ process.env.AWS_ACCESS_KEY_ID = "env-key";
90
+ process.env.AWS_SECRET_ACCESS_KEY = "env-secret";
91
+ process.env.AWS_REGION = "us-east-1";
92
+
93
+ try {
94
+ const config = {
95
+ schema: "./schema.ts",
96
+ out: "./out",
97
+ };
98
+ const client = getClient(config);
99
+ const resolvedConfig = await client.config.credentials();
100
+ expect(resolvedConfig.accessKeyId).toBe("env-key");
101
+ } finally {
102
+ process.env = originalEnv;
103
+ }
104
+ });
105
+
106
+ test("getClient should use local defaults if endpoint is localhost and no credentials provided", async () => {
107
+ const config = {
108
+ schema: "./schema.ts",
109
+ out: "./out",
110
+ endpoint: "http://localhost:8000",
111
+ };
112
+ const client = getClient(config);
113
+ const resolvedConfig = await client.config.credentials();
114
+ expect(resolvedConfig.accessKeyId).toBe("local");
115
+ });
116
+
117
+ test("getClient should use profile if provided", async () => {
118
+ const config = {
119
+ schema: "./schema.ts",
120
+ out: "./out",
121
+ profile: "my-profile",
122
+ };
123
+ getClient(config);
124
+ expect(credentialProviders.fromIni).toHaveBeenCalledWith({ profile: "my-profile" });
125
+ });
126
+
127
+ test("getClient should use maxAttempts if provided", async () => {
128
+ const config = {
129
+ schema: "./schema.ts",
130
+ out: "./out",
131
+ maxAttempts: 10,
132
+ };
133
+ const client = getClient(config);
134
+ expect(await client.config.maxAttempts()).toBe(10);
135
+ });
136
+
137
+ test("loadConfig should support environment variable overrides", async () => {
138
+ const originalEnv = { ...process.env };
139
+ process.env.MIZZLE_REGION = "us-east-1-env";
140
+ process.env.MIZZLE_ENDPOINT = "http://env:8000";
141
+ process.env.MIZZLE_SCHEMA = "./src/schema-env";
142
+ process.env.MIZZLE_OUT = "./migrations-env";
143
+
144
+ try {
145
+ const configName = "mizzle.config.env.ts";
146
+ const configContent = `
147
+ export default {
148
+ schema: "./src/schema",
149
+ out: "./migrations",
150
+ region: "us-west-2"
151
+ };
152
+ `;
153
+ writeFileSync(join(TEMP_DIR, configName), configContent);
154
+
155
+ const config = await loadConfig(configName);
156
+ expect(config.region).toBe("us-east-1-env");
157
+ expect(config.endpoint).toBe("http://env:8000");
158
+ expect(config.schema).toBe("./src/schema-env");
159
+ expect(config.out).toBe("./migrations-env");
160
+ } finally {
161
+ process.env = originalEnv;
162
+ }
163
+ });
164
+ });
@@ -0,0 +1,72 @@
1
+ import { expect, test, describe, beforeEach, afterEach } from "vitest";
2
+ import { loadConfig } from "../src/config";
3
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+ import { tmpdir } from "os";
6
+
7
+ const TEMP_DIR = join(tmpdir(), "mizzle-config-test-" + Date.now());
8
+
9
+ describe("Config Loader", () => {
10
+ const originalCwd = process.cwd();
11
+
12
+ beforeEach(() => {
13
+ mkdirSync(TEMP_DIR, { recursive: true });
14
+ process.chdir(TEMP_DIR);
15
+ });
16
+
17
+ afterEach(() => {
18
+ process.chdir(originalCwd);
19
+ rmSync(TEMP_DIR, { recursive: true, force: true });
20
+ });
21
+
22
+ test("should throw if config file is missing", async () => {
23
+ await expect(loadConfig("non-existent.ts")).rejects.toThrow();
24
+ });
25
+
26
+ test("should load valid config", async () => {
27
+ const configName = "mizzle.config.valid.ts";
28
+ const configContent = `
29
+ export default {
30
+ schema: "./src/schema",
31
+ out: "./migrations"
32
+ };
33
+ `;
34
+ writeFileSync(join(TEMP_DIR, configName), configContent);
35
+
36
+ const config = await loadConfig(configName);
37
+ expect(config).toEqual({
38
+ schema: "./src/schema",
39
+ out: "./migrations",
40
+ });
41
+ });
42
+
43
+ test("should throw if config is invalid (missing out)", async () => {
44
+ const configName = "mizzle.config.invalid.ts";
45
+ const configContent = `
46
+ export default {
47
+ schema: "./src/schema"
48
+ };
49
+ `;
50
+ writeFileSync(join(TEMP_DIR, configName), configContent);
51
+
52
+ await expect(loadConfig(configName)).rejects.toThrow("Invalid config");
53
+ });
54
+
55
+ test("should throw if config is invalid (not an object)", async () => {
56
+ const configName = "mizzle.config.notobject.ts";
57
+ writeFileSync(join(TEMP_DIR, configName), 'export default "string";');
58
+ await expect(loadConfig(configName)).rejects.toThrow("Invalid config");
59
+ });
60
+
61
+ test("should throw if config is invalid (missing schema)", async () => {
62
+ const configName = "mizzle.config.noschema.ts";
63
+ writeFileSync(join(TEMP_DIR, configName), 'export default { out: "./" };');
64
+ await expect(loadConfig(configName)).rejects.toThrow("Invalid config: missing 'schema'");
65
+ });
66
+
67
+ test("should throw if config has syntax error", async () => {
68
+ const configName = "mizzle.config.syntax.ts";
69
+ writeFileSync(join(TEMP_DIR, configName), "export default { schema: ");
70
+ await expect(loadConfig(configName)).rejects.toThrow("Failed to load config");
71
+ });
72
+ });
@@ -0,0 +1,49 @@
1
+ import { expect, test, describe, beforeEach, afterEach } from "vitest";
2
+ import { loadConfig } from "../src/config";
3
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
4
+ import { join, resolve } from "path";
5
+ import { tmpdir } from "os";
6
+
7
+ const TEMP_DIR = join(tmpdir(), "mizzle-define-config-test-" + Date.now());
8
+
9
+ describe("defineConfig Integration", () => {
10
+ const originalCwd = process.cwd();
11
+ const projectRoot = originalCwd.includes("packages/mizzling")
12
+ ? join(originalCwd, "../..")
13
+ : originalCwd;
14
+ const mizzlingPath = join(projectRoot, "packages/mizzling/src/index.ts");
15
+
16
+ beforeEach(() => {
17
+ mkdirSync(TEMP_DIR, { recursive: true });
18
+ process.chdir(TEMP_DIR);
19
+ });
20
+
21
+ afterEach(() => {
22
+ process.chdir(originalCwd);
23
+ rmSync(TEMP_DIR, { recursive: true, force: true });
24
+ });
25
+
26
+ test("should load config using defineConfig helper", async () => {
27
+ const configName = "mizzle.config.ts";
28
+ // Using relative path to the source index.ts for the test to work without publishing
29
+ const configContent = `
30
+ import { defineConfig } from '${mizzlingPath}';
31
+
32
+ export default defineConfig({
33
+ schema: "./src/schema",
34
+ out: "./migrations",
35
+ verbose: true,
36
+ strict: false
37
+ });
38
+ `;
39
+ writeFileSync(join(TEMP_DIR, configName), configContent);
40
+
41
+ const config = await loadConfig(configName);
42
+ expect(config).toEqual({
43
+ schema: "./src/schema",
44
+ out: "./migrations",
45
+ verbose: true,
46
+ strict: false,
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,117 @@
1
+ import { expect, test, describe, beforeEach, afterEach } from "vitest";
2
+ import { discoverSchema } from "../src/discovery";
3
+ import { PhysicalTable, Entity } from "@aurios/mizzle/table";
4
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
5
+ import { join } from "path";
6
+ import { tmpdir } from "os";
7
+
8
+ const TEMP_DIR = join(tmpdir(), "mizzle-discovery-test-" + Date.now());
9
+
10
+ describe("Schema Discovery", () => {
11
+ const originalCwd = process.cwd();
12
+ // Robustly find project root
13
+ const projectRoot = originalCwd.includes("packages/mizzling")
14
+ ? join(originalCwd, "../..")
15
+ : originalCwd;
16
+ const mizzleCoreTablePath = join(projectRoot, "packages/mizzle/src/core/table");
17
+
18
+ beforeEach(() => {
19
+ mkdirSync(TEMP_DIR, { recursive: true });
20
+ });
21
+
22
+ afterEach(() => {
23
+ rmSync(TEMP_DIR, { recursive: true, force: true });
24
+ });
25
+
26
+ test("should discover tables in a file", async () => {
27
+ const schemaContent = `
28
+ import { dynamoTable } from "${mizzleCoreTablePath}";
29
+
30
+ export const UsersTable = dynamoTable("users", {
31
+ pk: { build: () => ({ _: { name: "id", type: "string" } }) }, // Mocking column builder
32
+ });
33
+ `;
34
+ writeFileSync(join(TEMP_DIR, "schema.ts"), schemaContent);
35
+
36
+ const result = await discoverSchema({
37
+ schema: join(TEMP_DIR, "schema.ts"),
38
+ out: "./migrations",
39
+ });
40
+
41
+ expect(result.tables).toHaveLength(1);
42
+ expect(result.tables[0]).toBeInstanceOf(PhysicalTable);
43
+ // @ts-expect-error test-internal
44
+ expect(result.tables[0][Symbol.for("mizzle:TableName")]).toBe("users");
45
+ });
46
+
47
+ test("should discover entities in a file", async () => {
48
+ const schemaContent = `
49
+ import { dynamoTable, dynamoEntity, Entity } from "${mizzleCoreTablePath}";
50
+
51
+ const UsersTable = dynamoTable("users", {
52
+ pk: { build: () => ({ _: { name: "id", type: "string" } }) },
53
+ });
54
+
55
+ export const UserEntity = dynamoEntity(UsersTable, "User", {
56
+ id: { build: () => ({ _: { name: "id", type: "string" } }), setName: () => {} } as any
57
+ });
58
+ `;
59
+ writeFileSync(join(TEMP_DIR, "entity.ts"), schemaContent);
60
+
61
+ const result = await discoverSchema({
62
+ schema: join(TEMP_DIR, "entity.ts"),
63
+ out: "./migrations",
64
+ });
65
+
66
+ expect(result.entities).toHaveLength(1);
67
+ expect(result.entities[0]).toBeInstanceOf(Entity);
68
+ // @ts-expect-error test-internal
69
+ expect(result.entities[0][Symbol.for("mizzle:EntityName")]).toBe("User");
70
+ });
71
+
72
+ test("should ignore non-table exports", async () => {
73
+ const schemaContent = `
74
+ export const SomethingElse = { foo: "bar" };
75
+ `;
76
+ writeFileSync(join(TEMP_DIR, "other.ts"), schemaContent);
77
+
78
+ const result = await discoverSchema({
79
+ schema: join(TEMP_DIR, "other.ts"),
80
+ out: "./migrations",
81
+ });
82
+
83
+ expect(result.tables).toHaveLength(0);
84
+ expect(result.entities).toHaveLength(0);
85
+ });
86
+
87
+ test("should discover tables in a directory", async () => {
88
+ const schemaContent = `
89
+ import { dynamoTable } from "${mizzleCoreTablePath}";
90
+ export const UsersTable = dynamoTable("users_dir", { pk: { build: () => ({ _: { name: "id", type: "string" } }) } });
91
+ `;
92
+ mkdirSync(join(TEMP_DIR, "tables"), { recursive: true });
93
+ writeFileSync(join(TEMP_DIR, "tables/users.ts"), schemaContent);
94
+
95
+ const result = await discoverSchema({
96
+ schema: join(TEMP_DIR, "tables"), // Directory path
97
+ out: "./migrations",
98
+ });
99
+
100
+ expect(result.tables).toHaveLength(1);
101
+ // @ts-expect-error test-internal
102
+ expect(result.tables[0][Symbol.for("mizzle:TableName")]).toBe("users_dir");
103
+ });
104
+
105
+ test("should handle import failures gracefully", async () => {
106
+ const schemaContent = `
107
+ export const UsersTable = dynamoTable("users", ...); // Syntax error
108
+ `;
109
+ writeFileSync(join(TEMP_DIR, "broken.ts"), schemaContent);
110
+
111
+ const result = await discoverSchema({
112
+ schema: join(TEMP_DIR, "broken.ts"),
113
+ out: "./migrations",
114
+ });
115
+ expect(result.tables).toHaveLength(0);
116
+ });
117
+ });
@@ -0,0 +1,145 @@
1
+ import { expect, test, describe, beforeEach, vi } from "vitest";
2
+ import { dropCommand } from "../src/commands/drop";
3
+ import * as prompts from "@clack/prompts";
4
+ import type { IMizzleClient } from "../../mizzle/src/core/client";
5
+
6
+ // Mock Console
7
+ vi.spyOn(console, "log").mockImplementation(() => {});
8
+
9
+ // Mock Prompts
10
+ vi.mock("@clack/prompts", () => ({
11
+ intro: vi.fn(),
12
+ outro: vi.fn(),
13
+ multiselect: vi.fn(),
14
+ confirm: vi.fn(),
15
+ cancel: vi.fn(),
16
+ isCancel: vi.fn((val) => val === Symbol.for("clack:cancel")),
17
+ spinner: vi.fn(() => ({
18
+ start: vi.fn(),
19
+ stop: vi.fn(),
20
+ message: vi.fn(),
21
+ })),
22
+ }));
23
+
24
+ // Manual Mock Client
25
+ const createMockClient = (tables: Record<string, string>[]) => {
26
+ return {
27
+ send: async (command: { constructor: { name: string } }) => {
28
+ const cmdName = command.constructor.name;
29
+ if (cmdName === "ListTablesCommand") {
30
+ return { TableNames: tables.map((t) => t.TableName) };
31
+ }
32
+ if (cmdName === "DeleteTableCommand") {
33
+ return {};
34
+ }
35
+ return {};
36
+ },
37
+ } as unknown;
38
+ };
39
+
40
+ describe("Drop Command", () => {
41
+ beforeEach(() => {
42
+ vi.clearAllMocks();
43
+ });
44
+
45
+ test("should handle no tables found", async () => {
46
+ const client = createMockClient([]) as unknown;
47
+ await dropCommand({
48
+ config: { schema: "dummy", out: "dummy" } as any,
49
+ client: client as any,
50
+ });
51
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining("No tables found"));
52
+ expect(prompts.multiselect).not.toHaveBeenCalled();
53
+ });
54
+
55
+ test("should delete selected tables after confirmation", async () => {
56
+ const mockTables = [{ TableName: "users" }, { TableName: "posts" }];
57
+ const client = createMockClient(mockTables) as { send: (cmd: unknown) => Promise<unknown> };
58
+ const sendSpy = vi.spyOn(client, "send");
59
+
60
+ (
61
+ prompts.multiselect as unknown as { mockResolvedValueOnce: (val: unknown) => void }
62
+ ).mockResolvedValueOnce(["users"]);
63
+ (
64
+ prompts.confirm as unknown as { mockResolvedValueOnce: (val: unknown) => void }
65
+ ).mockResolvedValueOnce(true);
66
+
67
+ await dropCommand({
68
+ config: { schema: "dummy", out: "dummy" } as any,
69
+ client: client as any,
70
+ });
71
+
72
+ // Verify ListTables was called
73
+ expect((sendSpy.mock.calls[0]![0] as { constructor: { name: string } }).constructor.name).toBe(
74
+ "ListTablesCommand",
75
+ );
76
+
77
+ // Verify multiselect was called with options
78
+ expect(prompts.multiselect).toHaveBeenCalled();
79
+
80
+ // Verify confirm was called
81
+ expect(prompts.confirm).toHaveBeenCalled();
82
+
83
+ // Verify DeleteTable was called for 'users'
84
+ // Note: The order of calls depends on implementation, but we expect at least one DeleteTableCommand
85
+ const calls = sendSpy.mock.calls as { input: { TableName: string } }[][];
86
+ const deleteCall = calls.find(
87
+ (call) =>
88
+ (call[0] as unknown as { constructor: { name: string } }).constructor.name ===
89
+ "DeleteTableCommand",
90
+ );
91
+ expect(deleteCall).toBeDefined();
92
+ expect(deleteCall![0]!.input.TableName).toBe("users");
93
+
94
+ expect(prompts.outro).toHaveBeenCalled();
95
+ });
96
+
97
+ test("should not delete if confirmation is rejected", async () => {
98
+ const mockTables = [{ TableName: "users" }];
99
+ const client = createMockClient(mockTables) as { send: (cmd: unknown) => Promise<unknown> };
100
+ const sendSpy = vi.spyOn(client, "send");
101
+
102
+ (
103
+ prompts.multiselect as unknown as { mockResolvedValueOnce: (val: unknown) => void }
104
+ ).mockResolvedValueOnce(["users"]);
105
+ (
106
+ prompts.confirm as unknown as { mockResolvedValueOnce: (val: unknown) => void }
107
+ ).mockResolvedValueOnce(false); // User says No
108
+
109
+ await dropCommand({
110
+ config: { schema: "dummy", out: "dummy" } as any,
111
+ client: client as any,
112
+ });
113
+
114
+ // Verify DeleteTable was NOT called
115
+ const calls = sendSpy.mock.calls;
116
+ const deleteCall = calls.find(
117
+ (call) =>
118
+ (call[0] as unknown as { constructor: { name: string } }).constructor.name ===
119
+ "DeleteTableCommand",
120
+ );
121
+ expect(deleteCall).toBeUndefined();
122
+
123
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining("Operation cancelled"));
124
+ });
125
+
126
+ test("should handle cancellation at selection", async () => {
127
+ const mockTables = [{ TableName: "users" }];
128
+ const client = createMockClient(mockTables) as unknown;
129
+
130
+ // Simulate cancellation symbol
131
+ const cancelSymbol = Symbol.for("clack:cancel");
132
+ (
133
+ prompts.multiselect as unknown as { mockResolvedValueOnce: (val: unknown) => void }
134
+ ).mockResolvedValueOnce(cancelSymbol);
135
+ vi.mocked(prompts.isCancel).mockReturnValueOnce(true);
136
+
137
+ await dropCommand({
138
+ config: { schema: "dummy", out: "dummy" } as any,
139
+ client: client as any,
140
+ });
141
+
142
+ expect(prompts.cancel).toHaveBeenCalled();
143
+ expect(prompts.confirm).not.toHaveBeenCalled();
144
+ });
145
+ });
@@ -0,0 +1,87 @@
1
+ import { expect, test, describe, beforeAll, afterAll } from "vitest";
2
+ import { spawnSync } from "child_process";
3
+ import { mkdirSync, rmSync, readdirSync, writeFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { tmpdir } from "os";
6
+
7
+ const TEMP_DIR = join(tmpdir(), "mizzle-e2e-env-test-" + Date.now());
8
+ const MIGRATIONS_DIR = join(TEMP_DIR, "migrations");
9
+ const ENV_MIGRATIONS_DIR = join(TEMP_DIR, "migrations-env");
10
+ const SCHEMA_FILE = join(TEMP_DIR, "schema.ts");
11
+ const CONFIG_FILE = join(TEMP_DIR, "mizzle.config.ts");
12
+
13
+ describe("CLI E2E Environment Overrides", () => {
14
+ const projectRoot = process.cwd().includes("packages/mizzling")
15
+ ? join(process.cwd(), "../..")
16
+ : process.cwd();
17
+ const mizzleTablePath = join(projectRoot, "packages/mizzle/src/core/table");
18
+ const mizzleColumnsPath = join(projectRoot, "packages/mizzle/src/columns/index");
19
+ const mizzlingConfigPath = join(projectRoot, "packages/mizzling/src/config");
20
+ const mizzlingCliPath = join(projectRoot, "packages/mizzling/src/cli.ts");
21
+
22
+ beforeAll(() => {
23
+ mkdirSync(TEMP_DIR, { recursive: true });
24
+ mkdirSync(MIGRATIONS_DIR, { recursive: true });
25
+ mkdirSync(ENV_MIGRATIONS_DIR, { recursive: true });
26
+
27
+ // Create a simple schema
28
+ writeFileSync(
29
+ SCHEMA_FILE,
30
+ `
31
+ import { dynamoTable } from "${mizzleTablePath}";
32
+ import { string } from "${mizzleColumnsPath}";
33
+
34
+ export const testTable = dynamoTable("e2e_env_test_table", {
35
+ pk: string("pk"),
36
+ });
37
+ `.trim(),
38
+ );
39
+
40
+ // Create config pointing to MIGRATIONS_DIR
41
+ writeFileSync(
42
+ CONFIG_FILE,
43
+ `
44
+ import { defineConfig } from "${mizzlingConfigPath}";
45
+ export default defineConfig({
46
+ schema: "${SCHEMA_FILE}",
47
+ out: "${MIGRATIONS_DIR}",
48
+ region: "us-east-1",
49
+ endpoint: "http://localhost:8000"
50
+ });
51
+ `.trim(),
52
+ );
53
+ });
54
+
55
+ afterAll(() => {
56
+ rmSync(TEMP_DIR, { recursive: true, force: true });
57
+ });
58
+
59
+ test("MIZZLE_OUT should override config.out", async () => {
60
+ // Run generate with MIZZLE_OUT pointing to ENV_MIGRATIONS_DIR
61
+ const result = spawnSync(
62
+ "bun",
63
+ [mizzlingCliPath, "generate", "--name", "env_override"],
64
+ {
65
+ cwd: process.cwd(),
66
+ env: {
67
+ ...process.env,
68
+ MIZZLE_CONFIG: CONFIG_FILE,
69
+ MIZZLE_OUT: ENV_MIGRATIONS_DIR,
70
+ },
71
+ encoding: "utf-8",
72
+ },
73
+ );
74
+
75
+ if (result.status !== 0) {
76
+ console.error("Generate failed!");
77
+ console.error("STDOUT:", result.stdout);
78
+ console.error("STDERR:", result.stderr);
79
+ }
80
+
81
+ expect(result.status).toBe(0);
82
+
83
+ // Should have created migration in ENV_MIGRATIONS_DIR, NOT MIGRATIONS_DIR
84
+ expect(readdirSync(ENV_MIGRATIONS_DIR).some((f) => f.endsWith("_env_override.ts"))).toBe(true);
85
+ expect(readdirSync(MIGRATIONS_DIR).length).toBe(0);
86
+ }, 20000);
87
+ });