@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.
- package/.turbo/turbo-build.log +15 -0
- package/LICENSE +21 -0
- package/dist/chunk-DKDRM5WU.js +2 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +1 -1
- package/package.json +38 -19
- package/src/cli.ts +26 -29
- package/src/commands/drop.ts +51 -51
- package/src/commands/generate.ts +88 -83
- package/src/commands/init.ts +1 -1
- package/src/commands/list.ts +39 -37
- package/src/commands/push.ts +54 -54
- package/src/config.ts +14 -14
- package/src/discovery.ts +33 -25
- package/test/config-expansion.test.ts +164 -0
- package/test/config.test.ts +72 -0
- package/test/define-config-integration.test.ts +49 -0
- package/test/discovery.test.ts +117 -0
- package/test/drop.test.ts +145 -0
- package/test/e2e-env.test.ts +87 -0
- package/test/e2e.test.ts +135 -0
- package/test/generate.test.ts +116 -0
- package/test/init-command.test.ts +78 -0
- package/test/init.test.ts +22 -0
- package/test/interactive.test.ts +96 -0
- package/test/list.test.ts +58 -0
- package/test/mizzling-exports.test.ts +18 -0
- package/test/push.test.ts +88 -0
- package/tsconfig.json +33 -11
- package/tsup.config.ts +5 -5
- package/vitest.config.ts +8 -0
- package/CHANGELOG.md +0 -45
- package/dist/chunk-SNI5EXF5.js +0 -2
|
@@ -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
|
+
});
|