swallowkit 1.1.2 → 2.0.0-alpha.10
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/README.ja.md +61 -7
- package/README.md +61 -7
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +103 -27
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +5 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +143 -67
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +14 -7
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +149 -170
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +5 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +123 -18
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +49 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +19 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts +2 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +15 -0
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +17 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +45 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/manifest.d.ts +3 -1
- package/dist/core/project/manifest.d.ts.map +1 -1
- package/dist/core/project/manifest.js +47 -25
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +26 -0
- package/dist/core/project/react-project.d.ts.map +1 -0
- package/dist/core/project/react-project.js +443 -0
- package/dist/core/project/react-project.js.map +1 -0
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +32 -10
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +53 -7
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +26 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +294 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
- package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-bff-generator.js +252 -0
- package/dist/core/scaffold/react-bff-generator.js.map +1 -0
- package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
- package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-ui-generator.js +41 -0
- package/dist/core/scaffold/react-ui-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +61 -30
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/azure-cli.d.ts +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/dist/utils/package-manager.d.ts +2 -2
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +6 -3
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +7 -4
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +6 -1
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +145 -5
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +838 -0
- package/src/__tests__/release-metadata.test.ts +121 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +103 -29
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +145 -73
- package/src/cli/commands/init.ts +147 -172
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +145 -22
- package/src/cli/index.ts +46 -10
- package/src/core/config.ts +21 -1
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +17 -1
- package/src/core/project/agent-templates/AGENTS.md +113 -15
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
- package/src/core/project/agent-templates/copilot-instructions.md +15 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +63 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/manifest.ts +52 -27
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +529 -0
- package/src/core/project/state.ts +70 -0
- package/src/core/project/validation.ts +32 -10
- package/src/core/project/workflows.ts +53 -7
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/react-bff-generator.ts +292 -0
- package/src/core/scaffold/react-ui-generator.ts +51 -0
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/index.ts +3 -1
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +67 -36
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/types/index.ts +4 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +7 -4
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
|
+
|
|
5
|
+
describe("npm release metadata", () => {
|
|
6
|
+
const repository = process.cwd();
|
|
7
|
+
const script = path.join(repository, "scripts", "release-metadata.cjs");
|
|
8
|
+
let directory: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
directory = fs.mkdtempSync(path.join(repository, ".tmp-release-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function run(
|
|
19
|
+
version: unknown,
|
|
20
|
+
options: {
|
|
21
|
+
refType?: string;
|
|
22
|
+
refName?: string;
|
|
23
|
+
lockVersion?: string;
|
|
24
|
+
rootVersion?: string;
|
|
25
|
+
noRootPackage?: boolean;
|
|
26
|
+
} = {}
|
|
27
|
+
) {
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
path.join(directory, "package.json"),
|
|
30
|
+
JSON.stringify({ name: "swallowkit", version })
|
|
31
|
+
);
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(directory, "package-lock.json"),
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
version: options.lockVersion ?? version,
|
|
36
|
+
packages: options.noRootPackage
|
|
37
|
+
? {}
|
|
38
|
+
: { "": { version: options.rootVersion ?? version } },
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
const outputPath = path.join(directory, "output.txt");
|
|
42
|
+
const result = spawnSync(process.execPath, [script], {
|
|
43
|
+
cwd: directory,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
env: {
|
|
46
|
+
...process.env,
|
|
47
|
+
GITHUB_REF_TYPE: options.refType ?? "tag",
|
|
48
|
+
GITHUB_REF_NAME: options.refName ?? `v${version}`,
|
|
49
|
+
GITHUB_OUTPUT: outputPath,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
expect(result.error).toBeUndefined();
|
|
53
|
+
const output = fs.existsSync(outputPath)
|
|
54
|
+
? fs.readFileSync(outputPath, "utf8")
|
|
55
|
+
: "";
|
|
56
|
+
return { ...result, output };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
it.each([
|
|
60
|
+
["2.0.0", "latest"],
|
|
61
|
+
["2.0.0-alpha.1", "alpha"],
|
|
62
|
+
["2.0.0-alpha.2", "alpha"],
|
|
63
|
+
["2.0.0-alpha", "alpha"],
|
|
64
|
+
["2.0.0-beta.1", "beta"],
|
|
65
|
+
["2.0.0-beta.12", "beta"],
|
|
66
|
+
["2.0.0-rc.1", "next"],
|
|
67
|
+
["2.0.0-next.1", "next"],
|
|
68
|
+
["2.0.0-alphabet.1", "next"],
|
|
69
|
+
["2.0.0-0", "next"],
|
|
70
|
+
["2.0.0+build.1", "latest"],
|
|
71
|
+
["2.0.0-alpha.1+build.1", "alpha"],
|
|
72
|
+
])("publishes %s on the %s channel", (version, channel) => {
|
|
73
|
+
const result = run(version);
|
|
74
|
+
expect(result.status).toBe(0);
|
|
75
|
+
expect(result.stderr).toBe("");
|
|
76
|
+
expect(result.output).toBe(`version=${version}\ndist_tag=${channel}\n`);
|
|
77
|
+
expect(result.stdout).toBe(result.output);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("supports manual branch dry runs without requiring a release tag", () => {
|
|
81
|
+
const result = run("2.0.0-alpha.1", {
|
|
82
|
+
refType: "branch",
|
|
83
|
+
refName: "main",
|
|
84
|
+
});
|
|
85
|
+
expect(result.status).toBe(0);
|
|
86
|
+
expect(result.output).toContain("dist_tag=alpha\n");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it.each([
|
|
90
|
+
{ refName: "v2.0.0" },
|
|
91
|
+
{ refName: "2.0.0-alpha.1" },
|
|
92
|
+
{ lockVersion: "1.1.2" },
|
|
93
|
+
{ rootVersion: "1.1.2" },
|
|
94
|
+
{ noRootPackage: true },
|
|
95
|
+
])("rejects inconsistent release metadata: %j", (options) => {
|
|
96
|
+
const result = run("2.0.0-alpha.1", options);
|
|
97
|
+
expect(result.status).not.toBe(0);
|
|
98
|
+
expect(result.stderr).toContain("must match");
|
|
99
|
+
expect(result.output).toBe("");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it.each([
|
|
103
|
+
undefined,
|
|
104
|
+
2,
|
|
105
|
+
"",
|
|
106
|
+
"v2.0.0-alpha.1",
|
|
107
|
+
"2.0",
|
|
108
|
+
"02.0.0",
|
|
109
|
+
"2.0.0-",
|
|
110
|
+
"2.0.0-alpha.01",
|
|
111
|
+
"2.0.0-alpha..1",
|
|
112
|
+
"2.0.0-alpha_1",
|
|
113
|
+
"2.0.0\n",
|
|
114
|
+
"2.0.0-alpha.1\ndist_tag=latest",
|
|
115
|
+
])("rejects invalid versions before emitting Actions outputs: %j", (version) => {
|
|
116
|
+
const result = run(version);
|
|
117
|
+
expect(result.status).not.toBe(0);
|
|
118
|
+
expect(result.stderr).toContain("must be a valid SemVer version");
|
|
119
|
+
expect(result.output).toBe("");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { parseModelFile } from "../core/scaffold/model-parser";
|
|
5
|
+
import { generateOpenApiDocument } from "../core/scaffold/openapi-generator";
|
|
6
|
+
import { generateListPage, generateFormComponent } from "../core/scaffold/ui-generator";
|
|
7
|
+
import { generateLanguageSchemaArtifacts, getPythonRegexWarnings, toPythonRegex } from "../core/scaffold/native-schema-generator";
|
|
8
|
+
import { generateDetailPage } from "../core/scaffold/ui-generator";
|
|
9
|
+
import { spawnSync } from "child_process";
|
|
10
|
+
import { FileOperationSession } from "../core/operations/file-session";
|
|
11
|
+
import { buildUserScopedDataWarning } from "../cli/commands/scaffold";
|
|
12
|
+
import { inspectCapabilities } from "../core/project/capabilities";
|
|
13
|
+
|
|
14
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
15
|
+
|
|
16
|
+
function model(name: string, fields: string, extra = ""): string {
|
|
17
|
+
const pascal = name.replace(/(^|-)([a-z])/g, (_, __, char: string) => char.toUpperCase());
|
|
18
|
+
return `import { z } from 'zod/v4';
|
|
19
|
+
export const ${pascal} = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
${fields}
|
|
22
|
+
});
|
|
23
|
+
export type ${pascal} = z.infer<typeof ${pascal}>;
|
|
24
|
+
${extra}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("foreign-key inference", () => {
|
|
28
|
+
let root: string;
|
|
29
|
+
const write = (name: string, source: string) => {
|
|
30
|
+
const file = path.join(root, "shared", "models", `${name}.ts`);
|
|
31
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
+
fs.writeFileSync(file, source);
|
|
33
|
+
return file;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-fk-"));
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
40
|
+
|
|
41
|
+
it("does not treat userId as a foreign key when no User model exists", async () => {
|
|
42
|
+
const info = await parseModelFile(write("probe-item", model("probe-item", " userId: z.string(),\n title: z.string(),", "export const partitionKey = '/userId';\n")));
|
|
43
|
+
const userId = info.fields.find((field) => field.name === "userId")!;
|
|
44
|
+
expect(userId.isForeignKey).toBeUndefined();
|
|
45
|
+
expect(userId.referencedModel).toBeUndefined();
|
|
46
|
+
expect(info.parserWarnings).toEqual([]);
|
|
47
|
+
|
|
48
|
+
const list = generateListPage(info, "@sample/shared");
|
|
49
|
+
const form = generateFormComponent(info, "@sample/shared");
|
|
50
|
+
expect(list).not.toMatch(/api\/user|userMap/);
|
|
51
|
+
expect(form).not.toMatch(/api\/user|userOptions/);
|
|
52
|
+
const schema = JSON.parse(generateOpenApiDocument([info], info)).components.schemas;
|
|
53
|
+
expect(schema.ProbeItem.properties.userId).toEqual({ type: "string" });
|
|
54
|
+
expect(Object.keys(schema)).toEqual(["ProbeItem"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("infers a foreign key when the referenced model exists", async () => {
|
|
58
|
+
write("category", model("category", " name: z.string(),"));
|
|
59
|
+
const info = await parseModelFile(write("todo", model("todo", " categoryId: z.string(),")));
|
|
60
|
+
expect(info.fields.find((field) => field.name === "categoryId")).toMatchObject({ isForeignKey: true, referencedModel: "Category" });
|
|
61
|
+
expect(generateListPage(info, "@sample/shared")).toContain("/api/category");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("supports explicit references, opt-out, and warns about missing targets", async () => {
|
|
65
|
+
write("user", model("user", " name: z.string(),"));
|
|
66
|
+
const info = await parseModelFile(write("post", model(
|
|
67
|
+
"post",
|
|
68
|
+
" authorId: z.string(),\n userId: z.string(),\n reviewerId: z.string(),\n tagIds: z.array(z.string()),",
|
|
69
|
+
"export const references = { authorId: 'User', userId: false, reviewerId: 'Reviewer', tagIds: 'Tag', missingId: 'User' };\n"
|
|
70
|
+
)));
|
|
71
|
+
const field = (name: string) => info.fields.find((candidate) => candidate.name === name)!;
|
|
72
|
+
expect(field("authorId")).toMatchObject({ isForeignKey: true, referencedModel: "User" });
|
|
73
|
+
expect(field("userId").isForeignKey).toBeUndefined();
|
|
74
|
+
expect(field("reviewerId").isForeignKey).toBeUndefined();
|
|
75
|
+
expect(field("tagIds").isForeignKey).toBeUndefined();
|
|
76
|
+
expect(info.parserWarnings).toEqual([
|
|
77
|
+
"reference-model-not-found: Post.reviewerId references Reviewer, but shared/models has no Reviewer model; it is treated as a plain field.",
|
|
78
|
+
"reference-field-unsupported: Post.tagIds must be a single string id to reference Tag.",
|
|
79
|
+
"reference-field-not-found: Post.references declares missingId, but the schema has no such field.",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("declares one lookup per referenced model when several fields reference it", async () => {
|
|
84
|
+
write("user", model("user", " name: z.string(),"));
|
|
85
|
+
const info = await parseModelFile(write("review", model(
|
|
86
|
+
"review",
|
|
87
|
+
" authorId: z.string(),\n reviewerId: z.string(),",
|
|
88
|
+
"export const references = { authorId: 'User', reviewerId: 'User' };\n"
|
|
89
|
+
)));
|
|
90
|
+
const count = (source: string, text: string) => source.split(text).length - 1;
|
|
91
|
+
for (const source of [generateListPage(info, "@sample/shared"), generateDetailPage(info, "@sample/shared")]) {
|
|
92
|
+
expect(count(source, "const [userMap, setUserMap]")).toBe(1);
|
|
93
|
+
expect(count(source, "fetch('/api/user')")).toBe(1);
|
|
94
|
+
expect(source).toContain("userMap[item.authorId]".replace("item", source.includes("ListPage") ? "item" : "review"));
|
|
95
|
+
}
|
|
96
|
+
const form = generateFormComponent(info, "@sample/shared");
|
|
97
|
+
expect(count(form, "const [userOptions, setUserOptions]")).toBe(1);
|
|
98
|
+
expect(count(form, "fetch('/api/user')")).toBe(1);
|
|
99
|
+
expect(form).toContain('id="authorId"');
|
|
100
|
+
expect(form).toContain('id="reviewerId"');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("documents the references declaration in capabilities", () => {
|
|
104
|
+
expect(inspectCapabilities().modelDeclarations.map((declaration) => declaration.name)).toContain("references");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("dynamic schema evaluation metadata", () => {
|
|
109
|
+
let root: string;
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-dynamic-"));
|
|
112
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "sample", type: "module" }));
|
|
113
|
+
fs.mkdirSync(path.join(root, "node_modules"), { recursive: true });
|
|
114
|
+
fs.symlinkSync(path.join(repoRoot, "node_modules", "zod"), path.join(root, "node_modules", "zod"), "junction");
|
|
115
|
+
});
|
|
116
|
+
afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
117
|
+
|
|
118
|
+
it("extracts formats, integers, constraints, and inline objects from Zod v4 at runtime", async () => {
|
|
119
|
+
const file = path.join(root, "shared", "models", "probe-item.ts");
|
|
120
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
121
|
+
// startsWith() is outside the static subset, which forces runtime evaluation.
|
|
122
|
+
fs.writeFileSync(file, model("probe-item", [
|
|
123
|
+
" userId: z.string().startsWith('u'),",
|
|
124
|
+
" title: z.string().min(1).max(5),",
|
|
125
|
+
" code: z.string().regex(/^a\\d$/i),",
|
|
126
|
+
" tags: z.array(z.string().min(2)).min(1),",
|
|
127
|
+
" score: z.number().gt(0).lte(1),",
|
|
128
|
+
" edge: z.number().gt(0).min(0).lt(1).max(1),",
|
|
129
|
+
" count: z.int(),",
|
|
130
|
+
" publishedAt: z.iso.datetime().optional(),",
|
|
131
|
+
" createdAt: z.string().optional(),",
|
|
132
|
+
" nested: z.object({ x: z.string() }).optional(),",
|
|
133
|
+
].join("\n")));
|
|
134
|
+
|
|
135
|
+
const info = await parseModelFile(file);
|
|
136
|
+
const field = (name: string) => info.fields.find((candidate) => candidate.name === name)!;
|
|
137
|
+
expect(info.parserMode).toBe("dynamic");
|
|
138
|
+
expect(field("userId").isForeignKey).toBeUndefined();
|
|
139
|
+
expect(field("title").constraints).toEqual({ minLength: 1, maxLength: 5 });
|
|
140
|
+
expect(field("code").constraints).toEqual({ pattern: "^a\\d$", patternFlags: "i" });
|
|
141
|
+
expect(field("tags")).toMatchObject({ isArray: true, constraints: { minLength: 2, minItems: 1 } });
|
|
142
|
+
expect(field("score").constraints).toEqual({ minimum: 0, exclusiveMinimum: true, maximum: 1 });
|
|
143
|
+
expect(field("edge").constraints).toEqual({ minimum: 0, exclusiveMinimum: true, maximum: 1, exclusiveMaximum: true });
|
|
144
|
+
expect(field("count").isInteger).toBe(true);
|
|
145
|
+
expect(field("publishedAt")).toMatchObject({ format: "date-time", isOptional: true });
|
|
146
|
+
expect(field("createdAt").format).toBeUndefined();
|
|
147
|
+
expect(field("nested")).toMatchObject({ type: "object", isOptional: true, objectFields: [expect.objectContaining({ name: "x", type: "string" })] });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("extracts the same metadata from Zod v3 schemas", async () => {
|
|
151
|
+
const file = path.join(root, "shared", "models", "legacy-item.ts");
|
|
152
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
153
|
+
fs.writeFileSync(file, model("legacy-item", [
|
|
154
|
+
" title: z.string().startsWith('t').min(2),",
|
|
155
|
+
" score: z.number().int().positive(),",
|
|
156
|
+
" publishedAt: z.string().datetime().optional(),",
|
|
157
|
+
" kind: z.enum(['a', 'b']),",
|
|
158
|
+
" nested: z.object({ x: z.string().max(3) }),",
|
|
159
|
+
].join("\n")).replace("from 'zod/v4'", "from 'zod'"));
|
|
160
|
+
|
|
161
|
+
const info = await parseModelFile(file);
|
|
162
|
+
const field = (name: string) => info.fields.find((candidate) => candidate.name === name)!;
|
|
163
|
+
expect(info.parserMode).toBe("dynamic");
|
|
164
|
+
expect(field("title").constraints).toEqual({ minLength: 2 });
|
|
165
|
+
expect(field("score")).toMatchObject({ isInteger: true, constraints: { minimum: 0, exclusiveMinimum: true } });
|
|
166
|
+
expect(field("publishedAt").format).toBe("date-time");
|
|
167
|
+
expect(field("kind").enumValues).toEqual(["a", "b"]);
|
|
168
|
+
expect(field("nested").objectFields).toEqual([expect.objectContaining({ name: "x", constraints: { maxLength: 3 } })]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("Python schema assets", () => {
|
|
173
|
+
let root: string;
|
|
174
|
+
const originalCwd = process.cwd();
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-schema-"));
|
|
177
|
+
});
|
|
178
|
+
afterEach(() => {
|
|
179
|
+
process.chdir(originalCwd);
|
|
180
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("keeps declared types and constraints instead of guessing from names", async () => {
|
|
184
|
+
const file = path.join(root, "shared", "models", "probe-item.ts");
|
|
185
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
186
|
+
fs.writeFileSync(file, model("probe-item", [
|
|
187
|
+
" title: z.string().min(1),",
|
|
188
|
+
" code: z.string().regex(/^a\\d$/i).optional(),",
|
|
189
|
+
" tags: z.array(z.string().min(2)).max(3),",
|
|
190
|
+
" score: z.number().min(0).max(1),",
|
|
191
|
+
" count: z.number().int(),",
|
|
192
|
+
" publishedAt: z.iso.datetime().optional(),",
|
|
193
|
+
" createdAt: z.string().optional(),",
|
|
194
|
+
" nested: z.object({ x: z.string() }).optional(),",
|
|
195
|
+
].join("\n")));
|
|
196
|
+
const info = await parseModelFile(file);
|
|
197
|
+
process.chdir(root);
|
|
198
|
+
await generateLanguageSchemaArtifacts([info], info, "functions", "python", new FileOperationSession("commit", root));
|
|
199
|
+
const modelsDir = path.join(root, "functions", "generated", "python-models", "backend_models", "models");
|
|
200
|
+
const source = fs.readFileSync(path.join(modelsDir, "probe_item.py"), "utf8");
|
|
201
|
+
|
|
202
|
+
expect(source).toContain("created_at: Optional[StrictStr] = Field(default=None, alias=\"createdAt\")");
|
|
203
|
+
expect(source).toContain("published_at: Optional[datetime] = Field(default=None, alias=\"publishedAt\")");
|
|
204
|
+
expect(source).toContain("title: StrictStr = Field(min_length=1)");
|
|
205
|
+
expect(source).toContain("tags: List[Annotated[StrictStr, Field(min_length=2)]] = Field(max_length=3)");
|
|
206
|
+
expect(source).toContain("score: Union[StrictFloat, StrictInt] = Field(ge=0, le=1)");
|
|
207
|
+
expect(source).toContain("count: StrictInt");
|
|
208
|
+
expect(source).toContain("nested: Optional[ProbeItemNested] = Field(default=None)");
|
|
209
|
+
expect(source).toContain('re.search("^a\\\\d$", item, re.IGNORECASE)');
|
|
210
|
+
expect(source).toContain("return self.model_dump_json(by_alias=True, exclude_none=True)");
|
|
211
|
+
expect(fs.readFileSync(path.join(modelsDir, "probe_item_nested.py"), "utf8")).toContain("class ProbeItemNested(BaseModel):");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("Python regex translation", () => {
|
|
216
|
+
it("translates JavaScript named groups and back-references", () => {
|
|
217
|
+
expect(toPythonRegex("^(?<code>[A-Z]+)-\\k<code>$")).toEqual({ pattern: "^(?P<code>[A-Z]+)-(?P=code)$" });
|
|
218
|
+
expect(toPythonRegex("^a\\d$", "i")).toEqual({ pattern: "^a\\d$" });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it.each([
|
|
222
|
+
["\\p{L}+", "", /Unicode property/],
|
|
223
|
+
["(?<=a)b", "", /lookbehind/],
|
|
224
|
+
["[^]", "", /empty character classes/],
|
|
225
|
+
["\\u{1F600}", "u", /flag\(s\) u|code point/],
|
|
226
|
+
["abc", "y", /flag\(s\) y/],
|
|
227
|
+
])("reports %s /%s as not enforceable", (pattern, flags, reason) => {
|
|
228
|
+
const result = toPythonRegex(pattern, flags);
|
|
229
|
+
expect("unsupported" in result && result.unsupported).toMatch(reason);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("skips unenforceable patterns with a warning and validates raw strings before conversion", async () => {
|
|
233
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-regex-"));
|
|
234
|
+
const originalCwd = process.cwd();
|
|
235
|
+
try {
|
|
236
|
+
const file = path.join(root, "shared", "models", "regex-item.ts");
|
|
237
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
238
|
+
fs.writeFileSync(file, model("regex-item", [
|
|
239
|
+
" code: z.string().regex(/^(?<code>[A-Z]+)$/),",
|
|
240
|
+
" letters: z.string().regex(/^\\p{L}+$/u),",
|
|
241
|
+
" when: z.string().datetime().regex(/Z$/),",
|
|
242
|
+
].join("\n")));
|
|
243
|
+
const info = await parseModelFile(file);
|
|
244
|
+
expect(getPythonRegexWarnings(info)).toEqual([
|
|
245
|
+
expect.stringMatching(/^python-regex-not-enforced: RegexItem\.letters uses Unicode property escapes/),
|
|
246
|
+
]);
|
|
247
|
+
process.chdir(root);
|
|
248
|
+
const session = new FileOperationSession("commit", root);
|
|
249
|
+
await generateLanguageSchemaArtifacts([info], info, "functions", "python", session);
|
|
250
|
+
expect(session.warnings).toEqual(getPythonRegexWarnings(info));
|
|
251
|
+
const source = fs.readFileSync(path.join(root, "functions", "generated", "python-models", "backend_models", "models", "regex_item.py"), "utf8");
|
|
252
|
+
expect(source).toContain('re.search("^(?P<code>[A-Z]+)$", item, 0)');
|
|
253
|
+
expect(source).not.toContain("letters_validate_pattern");
|
|
254
|
+
expect(source).toContain("@field_validator('when', mode='before')");
|
|
255
|
+
expect(source).toContain("if isinstance(item, str) and not re.search");
|
|
256
|
+
} finally {
|
|
257
|
+
process.chdir(originalCwd);
|
|
258
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const pydanticPython = ["python", "python3"].find((command) => spawnSync(command, ["-c", "import pydantic"], { stdio: "ignore" }).status === 0);
|
|
264
|
+
const itWithPydantic = pydanticPython ? it : it.skip;
|
|
265
|
+
|
|
266
|
+
describe("generated Python model runtime", () => {
|
|
267
|
+
itWithPydantic("accepts valid regex and timestamp values and rejects invalid ones", async () => {
|
|
268
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-runtime-"));
|
|
269
|
+
const originalCwd = process.cwd();
|
|
270
|
+
try {
|
|
271
|
+
const file = path.join(root, "shared", "models", "regex-item.ts");
|
|
272
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
273
|
+
fs.writeFileSync(file, model("regex-item", [
|
|
274
|
+
" code: z.string().regex(/^(?<code>[A-Z]+)$/),",
|
|
275
|
+
" when: z.iso.datetime().regex(/Z$/),",
|
|
276
|
+
" tags: z.array(z.string().regex(/^t/)),",
|
|
277
|
+
].join("\n")));
|
|
278
|
+
const info = await parseModelFile(file);
|
|
279
|
+
process.chdir(root);
|
|
280
|
+
await generateLanguageSchemaArtifacts([info], info, "functions", "python", new FileOperationSession("commit", root));
|
|
281
|
+
process.chdir(originalCwd);
|
|
282
|
+
const script = [
|
|
283
|
+
"import sys",
|
|
284
|
+
"sys.path.insert(0, 'generated/python-models')",
|
|
285
|
+
"from backend_models.models.regex_item import RegexItem",
|
|
286
|
+
"ok = {'id': '1', 'code': 'ABC', 'when': '2024-05-06T07:08:09Z', 'tags': ['t1']}",
|
|
287
|
+
"RegexItem.from_dict(ok)",
|
|
288
|
+
"for bad in ({**ok, 'code': 'abc'}, {**ok, 'when': '2024-05-06T07:08:09+09:00'}, {**ok, 'tags': ['x']}):",
|
|
289
|
+
" try:",
|
|
290
|
+
" RegexItem.from_dict(bad)",
|
|
291
|
+
" except ValueError:",
|
|
292
|
+
" continue",
|
|
293
|
+
" raise SystemExit('accepted ' + repr(bad))",
|
|
294
|
+
"print('ok')",
|
|
295
|
+
].join("\n");
|
|
296
|
+
const run = spawnSync(pydanticPython!, ["-c", script], { cwd: path.join(root, "functions"), encoding: "utf8" });
|
|
297
|
+
if (run.status !== 0) throw new Error(`${run.stdout}\n${run.stderr}`);
|
|
298
|
+
expect(run.stdout.trim()).toBe("ok");
|
|
299
|
+
} finally {
|
|
300
|
+
process.chdir(originalCwd);
|
|
301
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
}, 60000);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("user-scoped data warning", () => {
|
|
307
|
+
const info = (partitionKey: string, fields: string[]) => ({
|
|
308
|
+
name: "ProbeItem",
|
|
309
|
+
partitionKey,
|
|
310
|
+
fields: fields.map((name) => ({ name, type: "string", isOptional: false, isArray: false })),
|
|
311
|
+
}) as unknown as Parameters<typeof buildUserScopedDataWarning>[0];
|
|
312
|
+
|
|
313
|
+
it("warns when the partition key is a per-user id and auth is not configured", () => {
|
|
314
|
+
expect(buildUserScopedDataWarning(info("/userId", ["id", "userId"]), undefined))
|
|
315
|
+
.toMatch(/^user-scoped-data-without-auth: ProbeItem\.userId is the partition key and looks like a per-user owner id/);
|
|
316
|
+
expect(buildUserScopedDataWarning(info("/userId", ["id", "userId"]), { provider: "none" })).toBeDefined();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("warns for owner-like fields that are not the partition key", () => {
|
|
320
|
+
expect(buildUserScopedDataWarning(info("/id", ["id", "learnerId"]), undefined))
|
|
321
|
+
.toMatch(/ProbeItem\.learnerId looks like a per-user owner id/);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("stays quiet when authentication is configured or nothing looks user-scoped", () => {
|
|
325
|
+
expect(buildUserScopedDataWarning(info("/userId", ["id", "userId"]), { provider: "swa" })).toBeUndefined();
|
|
326
|
+
expect(buildUserScopedDataWarning(info("/userId", ["id", "userId"]), { schemes: { admin: { provider: "swa" } } } as never)).toBeUndefined();
|
|
327
|
+
expect(buildUserScopedDataWarning(info("/tenantId", ["id", "tenantId", "categoryId"]), undefined)).toBeUndefined();
|
|
328
|
+
});
|
|
329
|
+
});
|