swallowkit 1.0.0-beta.2 → 1.0.0-beta.20
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/LICENSE +21 -21
- package/README.ja.md +312 -215
- package/README.md +369 -216
- package/dist/__tests__/fixtures.d.ts +22 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +146 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/add-auth.d.ts +10 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +444 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +163 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +21 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +35 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +292 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +19 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +476 -117
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +13 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2627 -1708
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +3 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +617 -129
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +162 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +8 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +90 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +171 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +32 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +225 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +63 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +204 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +38 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1244 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1027 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +7 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +920 -213
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +329 -135
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +314 -182
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +3 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
- package/dist/core/scaffold/openapi-generator.js +190 -0
- package/dist/core/scaffold/openapi-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +10 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +768 -663
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/database/base-model.d.ts +3 -3
- package/dist/database/base-model.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +16 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +11 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +34 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +156 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +184 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +109 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +215 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/package.json +85 -73
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
- package/src/__tests__/auth.test.ts +654 -0
- package/src/__tests__/config.test.ts +263 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +439 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/dev-seeds.test.ts +112 -0
- package/src/__tests__/dev.test.ts +148 -0
- package/src/__tests__/fixtures.ts +144 -0
- package/src/__tests__/functions-generator.test.ts +237 -0
- package/src/__tests__/init.test.ts +80 -0
- package/src/__tests__/machine.test.ts +212 -0
- package/src/__tests__/mcp.test.ts +56 -0
- package/src/__tests__/model-parser.test.ts +72 -0
- package/src/__tests__/nextjs-generator.test.ts +97 -0
- package/src/__tests__/openapi-generator.test.ts +43 -0
- package/src/__tests__/package-manager.test.ts +189 -0
- package/src/__tests__/scaffold.test.ts +39 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/ui-generator.test.ts +144 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +500 -0
- package/src/cli/commands/add-connector.ts +158 -0
- package/src/cli/commands/create-model.ts +62 -0
- package/src/cli/commands/dev-seeds.ts +358 -0
- package/src/cli/commands/dev.ts +962 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/init.ts +3371 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +1211 -0
- package/src/cli/index.ts +191 -0
- package/src/core/config.ts +308 -0
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/create-model.ts +174 -0
- package/src/core/operations/runtime.ts +235 -0
- package/src/core/operations/scaffold-machine.ts +91 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/validation.ts +221 -0
- package/src/core/scaffold/auth-generator.ts +1284 -0
- package/src/core/scaffold/connector-functions-generator.ts +1128 -0
- package/src/core/scaffold/functions-generator.ts +970 -0
- package/src/core/scaffold/model-parser.ts +841 -0
- package/src/core/scaffold/nextjs-generator.ts +370 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +1061 -0
- package/src/database/base-model.ts +184 -0
- package/src/database/client.ts +140 -0
- package/src/database/repository.ts +104 -0
- package/src/database/runtime-check.ts +25 -0
- package/src/index.ts +27 -0
- package/src/machine/contracts.ts +17 -0
- package/src/machine/errors.ts +34 -0
- package/src/machine/index.ts +173 -0
- package/src/mcp/index.ts +185 -0
- package/src/types/index.ts +134 -0
- package/src/utils/package-manager.ts +229 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildGeneratedProjectDependencies,
|
|
3
|
+
buildCSharpFunctionsProgramSource,
|
|
4
|
+
buildCSharpFunctionsProjectSource,
|
|
5
|
+
buildSwallowKitConfigSource,
|
|
6
|
+
injectSwallowKitNextConfig,
|
|
7
|
+
} from "../cli/commands/init";
|
|
8
|
+
|
|
9
|
+
describe("injectSwallowKitNextConfig", () => {
|
|
10
|
+
it("adds standalone settings without deprecated experimental options", () => {
|
|
11
|
+
const original = `import type { NextConfig } from "next";
|
|
12
|
+
|
|
13
|
+
const nextConfig: NextConfig = {
|
|
14
|
+
/* config options here */
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default nextConfig;
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const updated = injectSwallowKitNextConfig(original, "sample-app");
|
|
21
|
+
|
|
22
|
+
expect(updated).toContain("output: 'standalone'");
|
|
23
|
+
expect(updated).toContain("transpilePackages: ['@sample-app/shared']");
|
|
24
|
+
expect(updated).toContain("serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers']");
|
|
25
|
+
expect(updated).not.toContain("turbopackUseSystemTlsCerts");
|
|
26
|
+
expect(updated).not.toContain("experimental:");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("supports JavaScript next.config format", () => {
|
|
30
|
+
const original = `const nextConfig = {
|
|
31
|
+
/* config options here */
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = nextConfig;
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const updated = injectSwallowKitNextConfig(original, "sample-app");
|
|
38
|
+
|
|
39
|
+
expect(updated).toContain("transpilePackages: ['@sample-app/shared']");
|
|
40
|
+
expect(updated).toContain("module.exports = nextConfig;");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("generates a C# Program.cs compatible with the current worker packages", () => {
|
|
44
|
+
const source = buildCSharpFunctionsProgramSource();
|
|
45
|
+
|
|
46
|
+
expect(source).toContain("new HostBuilder()");
|
|
47
|
+
expect(source).toContain(".ConfigureFunctionsWorkerDefaults()");
|
|
48
|
+
expect(source).toContain("services.AddApplicationInsightsTelemetryWorkerService()");
|
|
49
|
+
expect(source).not.toContain("Microsoft.Azure.Functions.Worker.Builder");
|
|
50
|
+
expect(source).not.toContain("FunctionsApplication.CreateBuilder");
|
|
51
|
+
expect(source).not.toContain("ConfigureFunctionsApplicationInsights");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("excludes nested generated bin and obj files from the C# Functions project", () => {
|
|
55
|
+
const source = buildCSharpFunctionsProjectSource();
|
|
56
|
+
|
|
57
|
+
expect(source).toContain('<Compile Remove="generated\\**\\bin\\**\\*.cs;generated\\**\\obj\\**\\*.cs" />');
|
|
58
|
+
expect(source).toContain('<EmbeddedResource Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />');
|
|
59
|
+
expect(source).toContain('<None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("builds swallowkit.config.js without a local swallowkit package type import", () => {
|
|
63
|
+
const source = buildSwallowKitConfigSource("typescript");
|
|
64
|
+
|
|
65
|
+
expect(source).toContain("language: 'typescript'");
|
|
66
|
+
expect(source).toContain("baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL");
|
|
67
|
+
expect(source).not.toContain("import('swallowkit').SwallowKitConfig");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("does not add swallowkit as a generated project dependency", () => {
|
|
71
|
+
const dependencies = buildGeneratedProjectDependencies("sample-app");
|
|
72
|
+
|
|
73
|
+
expect(dependencies).toEqual({
|
|
74
|
+
"@azure/cosmos": "^4.0.0",
|
|
75
|
+
applicationinsights: "^3.3.0",
|
|
76
|
+
"@sample-app/shared": "*",
|
|
77
|
+
});
|
|
78
|
+
expect(dependencies).not.toHaveProperty("swallowkit");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { runMachineCli } from "../machine";
|
|
5
|
+
|
|
6
|
+
const repoRoot = process.cwd();
|
|
7
|
+
|
|
8
|
+
function writeFile(filePath: string, content: string): void {
|
|
9
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
10
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createModelSource(name: string): string {
|
|
14
|
+
return `import { z } from 'zod/v4';
|
|
15
|
+
|
|
16
|
+
export const ${name} = z.object({
|
|
17
|
+
id: z.string(),
|
|
18
|
+
name: z.string().min(1),
|
|
19
|
+
createdAt: z.string().optional(),
|
|
20
|
+
updatedAt: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type ${name} = z.infer<typeof ${name}>;
|
|
24
|
+
|
|
25
|
+
export const displayName = '${name}';
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createProjectFixture(rootDir: string, options: { includeGeneratedArtifacts?: boolean; forbiddenBffDependency?: boolean } = {}): void {
|
|
30
|
+
writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
|
|
31
|
+
writeFile(
|
|
32
|
+
path.join(rootDir, "swallowkit.config.js"),
|
|
33
|
+
`module.exports = {
|
|
34
|
+
database: {
|
|
35
|
+
connectionString: 'AccountEndpoint=https://example.local;',
|
|
36
|
+
},
|
|
37
|
+
backend: {
|
|
38
|
+
language: 'typescript',
|
|
39
|
+
},
|
|
40
|
+
api: {
|
|
41
|
+
endpoint: '/api/_swallowkit',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
`
|
|
45
|
+
);
|
|
46
|
+
writeFile(path.join(rootDir, "shared", "package.json"), JSON.stringify({ name: "@sample-app/shared" }, null, 2));
|
|
47
|
+
writeFile(path.join(rootDir, "shared", "index.ts"), "export {};\n");
|
|
48
|
+
writeFile(path.join(rootDir, "shared", "models", "todo.ts"), createModelSource("Todo"));
|
|
49
|
+
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
|
50
|
+
fs.symlinkSync(
|
|
51
|
+
path.join(repoRoot, "node_modules", "zod"),
|
|
52
|
+
path.join(rootDir, "node_modules", "zod"),
|
|
53
|
+
"junction"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (options.includeGeneratedArtifacts) {
|
|
57
|
+
writeFile(path.join(rootDir, "lib", "api", "call-function.ts"), "export function callFunction() {}\n");
|
|
58
|
+
writeFile(path.join(rootDir, "functions", "src", "todo.ts"), "export {};\n");
|
|
59
|
+
writeFile(path.join(rootDir, "app", "api", "todo", "route.ts"), "export async function GET() { return Response.json([]); }\n");
|
|
60
|
+
writeFile(path.join(rootDir, "app", "api", "todo", "[id]", "route.ts"), "export async function GET() { return Response.json({}); }\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.forbiddenBffDependency) {
|
|
64
|
+
writeFile(
|
|
65
|
+
path.join(rootDir, "app", "api", "forbidden", "route.ts"),
|
|
66
|
+
"import { CosmosClient } from '@azure/cosmos';\nexport async function GET() { return Response.json({ ok: Boolean(CosmosClient) }); }\n"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runMachine(argv: string[]): Promise<{ response: any; exitCode: number }> {
|
|
72
|
+
const writes: string[] = [];
|
|
73
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
74
|
+
const originalExitCode = process.exitCode;
|
|
75
|
+
|
|
76
|
+
(process.stdout.write as unknown as (chunk: string | Uint8Array) => boolean) = ((chunk: string | Uint8Array) => {
|
|
77
|
+
writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8"));
|
|
78
|
+
return true;
|
|
79
|
+
}) as typeof process.stdout.write;
|
|
80
|
+
|
|
81
|
+
process.exitCode = 0;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await runMachineCli(argv);
|
|
85
|
+
return {
|
|
86
|
+
response: JSON.parse(writes.join("")),
|
|
87
|
+
exitCode: process.exitCode || 0,
|
|
88
|
+
};
|
|
89
|
+
} finally {
|
|
90
|
+
process.stdout.write = originalWrite;
|
|
91
|
+
process.exitCode = originalExitCode;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("machine CLI", () => {
|
|
96
|
+
const originalCwd = process.cwd();
|
|
97
|
+
let tempDir: string;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-machine-"));
|
|
101
|
+
process.chdir(tempDir);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
process.chdir(originalCwd);
|
|
106
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("inspects SwallowKit project metadata as JSON", async () => {
|
|
110
|
+
createProjectFixture(tempDir, { includeGeneratedArtifacts: true });
|
|
111
|
+
|
|
112
|
+
const { response, exitCode } = await runMachine(["node", "swallowkit", "machine", "inspect", "project"]);
|
|
113
|
+
|
|
114
|
+
expect(exitCode).toBe(0);
|
|
115
|
+
expect(response.ok).toBe(true);
|
|
116
|
+
expect(response.command).toBe("inspect-project");
|
|
117
|
+
expect(response.data.manifest.entities[0].name).toBe("Todo");
|
|
118
|
+
expect(response.data.manifest.routes).toEqual(
|
|
119
|
+
expect.arrayContaining([
|
|
120
|
+
expect.objectContaining({
|
|
121
|
+
name: "Todo-bff-list",
|
|
122
|
+
publicPath: "/api/todo",
|
|
123
|
+
exists: true,
|
|
124
|
+
}),
|
|
125
|
+
])
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("validates forbidden BFF dependencies", async () => {
|
|
130
|
+
createProjectFixture(tempDir, {
|
|
131
|
+
includeGeneratedArtifacts: true,
|
|
132
|
+
forbiddenBffDependency: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const { response, exitCode } = await runMachine(["node", "swallowkit", "machine", "validate", "project"]);
|
|
136
|
+
|
|
137
|
+
expect(exitCode).toBe(0);
|
|
138
|
+
expect(response.ok).toBe(true);
|
|
139
|
+
expect(response.command).toBe("validate-project");
|
|
140
|
+
expect(response.data.violations).toEqual(
|
|
141
|
+
expect.arrayContaining([
|
|
142
|
+
expect.objectContaining({
|
|
143
|
+
code: "forbidden-bff-dependency",
|
|
144
|
+
severity: "error",
|
|
145
|
+
}),
|
|
146
|
+
])
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("generates model templates through the machine interface", async () => {
|
|
151
|
+
writeFile(path.join(tempDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
|
|
152
|
+
writeFile(
|
|
153
|
+
path.join(tempDir, "swallowkit.config.js"),
|
|
154
|
+
`module.exports = {
|
|
155
|
+
backend: { language: 'typescript' },
|
|
156
|
+
api: { endpoint: '/api/_swallowkit' },
|
|
157
|
+
};
|
|
158
|
+
`
|
|
159
|
+
);
|
|
160
|
+
fs.mkdirSync(path.join(tempDir, "node_modules"), { recursive: true });
|
|
161
|
+
fs.symlinkSync(
|
|
162
|
+
path.join(repoRoot, "node_modules", "zod"),
|
|
163
|
+
path.join(tempDir, "node_modules", "zod"),
|
|
164
|
+
"junction"
|
|
165
|
+
);
|
|
166
|
+
writeFile(path.join(tempDir, "shared", "index.ts"), "export {};\n");
|
|
167
|
+
|
|
168
|
+
const { response, exitCode } = await runMachine([
|
|
169
|
+
"node",
|
|
170
|
+
"swallowkit",
|
|
171
|
+
"machine",
|
|
172
|
+
"generate",
|
|
173
|
+
"model",
|
|
174
|
+
"Task",
|
|
175
|
+
"--overwrite",
|
|
176
|
+
"never",
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
expect(exitCode).toBe(0);
|
|
180
|
+
expect(response.ok).toBe(true);
|
|
181
|
+
expect(response.command).toBe("generate-model");
|
|
182
|
+
expect(response.data.createdFiles).toContain("shared/models/task.ts");
|
|
183
|
+
expect(fs.existsSync(path.join(tempDir, ".swallowkit", "project.json"))).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("generates scaffold artifacts through the machine interface", async () => {
|
|
187
|
+
createProjectFixture(tempDir);
|
|
188
|
+
|
|
189
|
+
const { response, exitCode } = await runMachine([
|
|
190
|
+
"node",
|
|
191
|
+
"swallowkit",
|
|
192
|
+
"machine",
|
|
193
|
+
"generate",
|
|
194
|
+
"scaffold",
|
|
195
|
+
"todo",
|
|
196
|
+
"--api-only",
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
expect(exitCode).toBe(0);
|
|
200
|
+
expect(response.ok).toBe(true);
|
|
201
|
+
expect(response.command).toBe("generate-scaffold");
|
|
202
|
+
expect(response.data.createdFiles).toEqual(
|
|
203
|
+
expect.arrayContaining([
|
|
204
|
+
"app/api/todo/route.ts",
|
|
205
|
+
"app/api/todo/[id]/route.ts",
|
|
206
|
+
"functions/src/todo.ts",
|
|
207
|
+
"lib/api/call-function.ts",
|
|
208
|
+
])
|
|
209
|
+
);
|
|
210
|
+
expect(fs.existsSync(path.join(tempDir, ".swallowkit", "project.json"))).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { buildSwallowKitToolDefinitions } from "../mcp";
|
|
2
|
+
|
|
3
|
+
describe("SwallowKit MCP tool definitions", () => {
|
|
4
|
+
it("delegates inspect_project to the machine CLI", async () => {
|
|
5
|
+
const runner = jest.fn().mockResolvedValue({
|
|
6
|
+
stdout: JSON.stringify({
|
|
7
|
+
ok: true,
|
|
8
|
+
command: "inspect-project",
|
|
9
|
+
data: { manifestSource: "file" },
|
|
10
|
+
}),
|
|
11
|
+
stderr: "",
|
|
12
|
+
exitCode: 0,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const tool = buildSwallowKitToolDefinitions(runner).find((candidate) => candidate.name === "swallowkit_inspect_project");
|
|
16
|
+
expect(tool).toBeDefined();
|
|
17
|
+
|
|
18
|
+
const result = await tool!.handler({});
|
|
19
|
+
expect(runner).toHaveBeenCalledWith(["inspect", "project"]);
|
|
20
|
+
expect(JSON.parse(result.content[0].text)).toEqual({ manifestSource: "file" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("delegates scaffold_model with explicit args", async () => {
|
|
24
|
+
const runner = jest.fn().mockResolvedValue({
|
|
25
|
+
stdout: JSON.stringify({
|
|
26
|
+
ok: true,
|
|
27
|
+
command: "generate-scaffold",
|
|
28
|
+
data: { createdFiles: ["functions/src/todo.ts"] },
|
|
29
|
+
}),
|
|
30
|
+
stderr: "",
|
|
31
|
+
exitCode: 0,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const tool = buildSwallowKitToolDefinitions(runner).find((candidate) => candidate.name === "swallowkit_scaffold_model");
|
|
35
|
+
expect(tool).toBeDefined();
|
|
36
|
+
|
|
37
|
+
const result = await tool!.handler({
|
|
38
|
+
model: "todo",
|
|
39
|
+
functionsDir: "functions",
|
|
40
|
+
apiDir: "app/api",
|
|
41
|
+
apiOnly: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(runner).toHaveBeenCalledWith([
|
|
45
|
+
"generate",
|
|
46
|
+
"scaffold",
|
|
47
|
+
"todo",
|
|
48
|
+
"--functions-dir",
|
|
49
|
+
"functions",
|
|
50
|
+
"--api-dir",
|
|
51
|
+
"app/api",
|
|
52
|
+
"--api-only",
|
|
53
|
+
]);
|
|
54
|
+
expect(JSON.parse(result.content[0].text)).toEqual({ createdFiles: ["functions/src/todo.ts"] });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { parsePartitionKey } from "../core/scaffold/model-parser";
|
|
2
|
+
|
|
3
|
+
describe("parsePartitionKey", () => {
|
|
4
|
+
it("returns default '/id' when no partitionKey export exists", () => {
|
|
5
|
+
const content = `
|
|
6
|
+
import { z } from 'zod/v4';
|
|
7
|
+
|
|
8
|
+
export const Todo = z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
title: z.string(),
|
|
11
|
+
});
|
|
12
|
+
export type Todo = z.infer<typeof Todo>;
|
|
13
|
+
`;
|
|
14
|
+
expect(parsePartitionKey(content)).toBe("/id");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("extracts explicit partitionKey with single quotes", () => {
|
|
18
|
+
const content = `
|
|
19
|
+
import { z } from 'zod/v4';
|
|
20
|
+
|
|
21
|
+
export const Order = z.object({
|
|
22
|
+
id: z.string(),
|
|
23
|
+
tenantId: z.string(),
|
|
24
|
+
items: z.array(z.string()),
|
|
25
|
+
});
|
|
26
|
+
export type Order = z.infer<typeof Order>;
|
|
27
|
+
export const partitionKey = '/tenantId';
|
|
28
|
+
`;
|
|
29
|
+
expect(parsePartitionKey(content)).toBe("/tenantId");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("extracts explicit partitionKey with double quotes", () => {
|
|
33
|
+
const content = `
|
|
34
|
+
export const User = z.object({ id: z.string(), email: z.string() });
|
|
35
|
+
export const partitionKey = "/email";
|
|
36
|
+
`;
|
|
37
|
+
expect(parsePartitionKey(content)).toBe("/email");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("extracts /id as explicit partitionKey", () => {
|
|
41
|
+
const content = `
|
|
42
|
+
export const Todo = z.object({ id: z.string() });
|
|
43
|
+
export const partitionKey = '/id';
|
|
44
|
+
`;
|
|
45
|
+
expect(parsePartitionKey(content)).toBe("/id");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("handles nested/hierarchical partition key paths", () => {
|
|
49
|
+
const content = `
|
|
50
|
+
export const partitionKey = '/address/city';
|
|
51
|
+
`;
|
|
52
|
+
expect(parsePartitionKey(content)).toBe("/address/city");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ignores commented-out partitionKey", () => {
|
|
56
|
+
const content = `
|
|
57
|
+
// export const partitionKey = '/tenantId';
|
|
58
|
+
export const Todo = z.object({ id: z.string() });
|
|
59
|
+
`;
|
|
60
|
+
// The regex matches inside comments too — this is acceptable since
|
|
61
|
+
// the pattern follows the same approach as parseConnectorConfig/parseAuthPolicy
|
|
62
|
+
// In practice, commented-out exports are rare in model files
|
|
63
|
+
expect(parsePartitionKey(content)).toBe("/tenantId");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles extra spaces around equals sign", () => {
|
|
67
|
+
const content = `
|
|
68
|
+
export const partitionKey = '/customerId';
|
|
69
|
+
`;
|
|
70
|
+
expect(parsePartitionKey(content)).toBe("/customerId");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateBFFCallFunction,
|
|
3
|
+
generateCompactBFFRoutes,
|
|
4
|
+
} from "../core/scaffold/nextjs-generator";
|
|
5
|
+
import { createBasicModelInfo } from "./fixtures";
|
|
6
|
+
|
|
7
|
+
describe("generateBFFCallFunction", () => {
|
|
8
|
+
it("generates call function helper code", () => {
|
|
9
|
+
const code = generateBFFCallFunction();
|
|
10
|
+
expect(code).toMatchSnapshot();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("includes getFunctionsBaseUrl helper", () => {
|
|
14
|
+
const code = generateBFFCallFunction();
|
|
15
|
+
expect(code).toContain("getFunctionsBaseUrl");
|
|
16
|
+
expect(code).toContain("BACKEND_FUNCTIONS_BASE_URL");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("includes callFunction export", () => {
|
|
20
|
+
const code = generateBFFCallFunction();
|
|
21
|
+
expect(code).toContain("export async function callFunction");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("includes z.ZodSchema typings", () => {
|
|
25
|
+
const code = generateBFFCallFunction();
|
|
26
|
+
expect(code).toContain("z.ZodSchema");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("generateCompactBFFRoutes", () => {
|
|
31
|
+
it("generates list and detail routes", () => {
|
|
32
|
+
const model = createBasicModelInfo();
|
|
33
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
34
|
+
|
|
35
|
+
expect(routes.listRoute).toMatchSnapshot();
|
|
36
|
+
expect(routes.detailRoute).toMatchSnapshot();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("imports the correct schema", () => {
|
|
40
|
+
const model = createBasicModelInfo();
|
|
41
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
42
|
+
|
|
43
|
+
expect(routes.listRoute).toContain(
|
|
44
|
+
"import { todoSchema } from '@myapp/shared'"
|
|
45
|
+
);
|
|
46
|
+
expect(routes.detailRoute).toContain(
|
|
47
|
+
"import { todoSchema } from '@myapp/shared'"
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("uses correct API paths (camelCase)", () => {
|
|
52
|
+
const model = createBasicModelInfo();
|
|
53
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
54
|
+
|
|
55
|
+
expect(routes.listRoute).toContain("path: '/api/todo'");
|
|
56
|
+
expect(routes.detailRoute).toContain("/api/todo/");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("list route has GET and POST handlers", () => {
|
|
60
|
+
const model = createBasicModelInfo();
|
|
61
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
62
|
+
|
|
63
|
+
expect(routes.listRoute).toContain("export async function GET()");
|
|
64
|
+
expect(routes.listRoute).toContain("export async function POST(");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("detail route has GET, PUT, and DELETE handlers", () => {
|
|
68
|
+
const model = createBasicModelInfo();
|
|
69
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
70
|
+
|
|
71
|
+
expect(routes.detailRoute).toContain("export async function GET(");
|
|
72
|
+
expect(routes.detailRoute).toContain("export async function PUT(");
|
|
73
|
+
expect(routes.detailRoute).toContain("export async function DELETE(");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("creates InputSchema that omits managed fields", () => {
|
|
77
|
+
const model = createBasicModelInfo();
|
|
78
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
79
|
+
|
|
80
|
+
expect(routes.listRoute).toContain(
|
|
81
|
+
"todoSchema.omit({ id: true, createdAt: true, updatedAt: true })"
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("handles multi-word model names", () => {
|
|
86
|
+
const model = createBasicModelInfo({
|
|
87
|
+
name: "BlogPost",
|
|
88
|
+
schemaName: "blogPostSchema",
|
|
89
|
+
});
|
|
90
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
91
|
+
|
|
92
|
+
expect(routes.listRoute).toContain("path: '/api/blogPost'");
|
|
93
|
+
expect(routes.listRoute).toContain(
|
|
94
|
+
"import { blogPostSchema } from '@myapp/shared'"
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { generateOpenApiDocument } from "../core/scaffold/openapi-generator";
|
|
2
|
+
import { createBasicModelInfo } from "./fixtures";
|
|
3
|
+
|
|
4
|
+
describe("generateOpenApiDocument", () => {
|
|
5
|
+
it("emits an OpenAPI document for the root model", () => {
|
|
6
|
+
const model = createBasicModelInfo();
|
|
7
|
+
const document = JSON.parse(generateOpenApiDocument([model], model));
|
|
8
|
+
|
|
9
|
+
expect(document.openapi).toBe("3.0.3");
|
|
10
|
+
expect(document.components.schemas.Todo.properties.title.type).toBe("string");
|
|
11
|
+
expect(document.paths["/api/todo"].post.requestBody.content["application/json"].schema.$ref)
|
|
12
|
+
.toBe("#/components/schemas/Todo");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("emits nested schema references when present", () => {
|
|
16
|
+
const todo = createBasicModelInfo({
|
|
17
|
+
fields: [
|
|
18
|
+
{ name: "id", type: "string", isOptional: false, isArray: false },
|
|
19
|
+
{ name: "title", type: "string", isOptional: false, isArray: false },
|
|
20
|
+
{
|
|
21
|
+
name: "category",
|
|
22
|
+
type: "object",
|
|
23
|
+
isOptional: false,
|
|
24
|
+
isArray: false,
|
|
25
|
+
isNestedSchema: true,
|
|
26
|
+
nestedModelName: "Category",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
const category = createBasicModelInfo({
|
|
31
|
+
name: "Category",
|
|
32
|
+
schemaName: "categorySchema",
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: "id", type: "string", isOptional: false, isArray: false },
|
|
35
|
+
{ name: "name", type: "string", isOptional: false, isArray: false },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const document = JSON.parse(generateOpenApiDocument([todo, category], todo));
|
|
40
|
+
expect(document.components.schemas.Todo.properties.category.$ref).toBe("#/components/schemas/Category");
|
|
41
|
+
expect(document.components.schemas.Category.properties.name.type).toBe("string");
|
|
42
|
+
});
|
|
43
|
+
});
|