swallowkit 1.0.0-beta.2 → 1.0.0-beta.21
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 +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +164 -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 +154 -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 +193 -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,402 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { getBackendLanguage, getFullConfig, validateConfig } from "../config";
|
|
4
|
+
import { getAllModels, toCamelCase, toKebabCase } from "../scaffold/model-parser";
|
|
5
|
+
import { AuthConfig, BackendLanguage, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
|
|
6
|
+
import { captureConsoleMessages, withWorkingDirectory } from "../operations/runtime";
|
|
7
|
+
|
|
8
|
+
const CONFIG_CANDIDATES = [
|
|
9
|
+
"swallowkit.config.js",
|
|
10
|
+
"swallowkit.config.json",
|
|
11
|
+
".swallowkitrc.json",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export const SWALLOWKIT_MANIFEST_PATH = path.join(".swallowkit", "project.json");
|
|
15
|
+
export const SWALLOWKIT_MANIFEST_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
export interface ProjectManifestField {
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
isOptional: boolean;
|
|
21
|
+
isArray: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProjectManifestEntity {
|
|
25
|
+
name: string;
|
|
26
|
+
displayName: string;
|
|
27
|
+
schemaName: string;
|
|
28
|
+
filePath: string;
|
|
29
|
+
partitionKey: string;
|
|
30
|
+
hasId: boolean;
|
|
31
|
+
hasCreatedAt: boolean;
|
|
32
|
+
hasUpdatedAt: boolean;
|
|
33
|
+
connectorConfig?: ModelConnectorConfig;
|
|
34
|
+
authPolicy?: ModelAuthPolicy;
|
|
35
|
+
nestedModels: string[];
|
|
36
|
+
fields: ProjectManifestField[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ProjectManifestRoute {
|
|
40
|
+
name: string;
|
|
41
|
+
kind: "entity" | "system";
|
|
42
|
+
surface: "bff" | "functions";
|
|
43
|
+
entityName?: string;
|
|
44
|
+
methods: string[];
|
|
45
|
+
publicPath: string;
|
|
46
|
+
filePath: string;
|
|
47
|
+
exists: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ProjectManifestModule {
|
|
51
|
+
name: string;
|
|
52
|
+
kind: "shared-models" | "bff" | "functions" | "ui" | "infrastructure" | "auth";
|
|
53
|
+
rootPath: string;
|
|
54
|
+
exists: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ProjectManifestArtifacts {
|
|
58
|
+
callFunctionHelperPath: string;
|
|
59
|
+
callFunctionHelperExists: boolean;
|
|
60
|
+
openApiSpecs: string[];
|
|
61
|
+
generatedSchemaDirectories: string[];
|
|
62
|
+
proxyPath: string;
|
|
63
|
+
proxyExists: boolean;
|
|
64
|
+
loginPagePath: string;
|
|
65
|
+
loginPageExists: boolean;
|
|
66
|
+
authContextPath: string;
|
|
67
|
+
authContextExists: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ProjectManifestArchitecture {
|
|
71
|
+
pattern: string;
|
|
72
|
+
backendLanguage: BackendLanguage;
|
|
73
|
+
layerBoundaries: string[];
|
|
74
|
+
hasSharedWorkspace: boolean;
|
|
75
|
+
hasConnectors: boolean;
|
|
76
|
+
hasAuth: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ProjectManifest {
|
|
80
|
+
version: number;
|
|
81
|
+
generatedAt: string;
|
|
82
|
+
configPath: string | null;
|
|
83
|
+
backendLanguage: BackendLanguage;
|
|
84
|
+
configValidation: {
|
|
85
|
+
valid: boolean;
|
|
86
|
+
errors: string[];
|
|
87
|
+
};
|
|
88
|
+
connectors: Array<{ name: string; definition: ConnectorDefinition }>;
|
|
89
|
+
auth: AuthConfig | null;
|
|
90
|
+
entities: ProjectManifestEntity[];
|
|
91
|
+
routes: ProjectManifestRoute[];
|
|
92
|
+
modules: ProjectManifestModule[];
|
|
93
|
+
artifacts: ProjectManifestArtifacts;
|
|
94
|
+
architecture: ProjectManifestArchitecture;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface LoadedProjectManifest {
|
|
98
|
+
manifest: ProjectManifest;
|
|
99
|
+
source: "file" | "reconstructed";
|
|
100
|
+
diagnostics: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveConfigPath(projectRoot: string): string | null {
|
|
104
|
+
for (const candidate of CONFIG_CANDIDATES) {
|
|
105
|
+
const fullPath = path.join(projectRoot, candidate);
|
|
106
|
+
if (fs.existsSync(fullPath)) {
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function relativeProjectPath(projectRoot: string, targetPath: string): string {
|
|
115
|
+
return path.relative(projectRoot, targetPath).replace(/\\/g, "/");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildModules(projectRoot: string): ProjectManifestModule[] {
|
|
119
|
+
const candidates: ProjectManifestModule[] = [
|
|
120
|
+
{
|
|
121
|
+
name: "shared-models",
|
|
122
|
+
kind: "shared-models",
|
|
123
|
+
rootPath: "shared/models",
|
|
124
|
+
exists: fs.existsSync(path.join(projectRoot, "shared", "models")),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "bff",
|
|
128
|
+
kind: "bff",
|
|
129
|
+
rootPath: "app/api",
|
|
130
|
+
exists: fs.existsSync(path.join(projectRoot, "app", "api")),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "functions",
|
|
134
|
+
kind: "functions",
|
|
135
|
+
rootPath: "functions",
|
|
136
|
+
exists: fs.existsSync(path.join(projectRoot, "functions")),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "ui",
|
|
140
|
+
kind: "ui",
|
|
141
|
+
rootPath: "app",
|
|
142
|
+
exists: fs.existsSync(path.join(projectRoot, "app")),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "infrastructure",
|
|
146
|
+
kind: "infrastructure",
|
|
147
|
+
rootPath: "infra",
|
|
148
|
+
exists: fs.existsSync(path.join(projectRoot, "infra")),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "auth",
|
|
152
|
+
kind: "auth",
|
|
153
|
+
rootPath: "app/api/auth",
|
|
154
|
+
exists: fs.existsSync(path.join(projectRoot, "app", "api", "auth")),
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
return candidates;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
|
|
162
|
+
const openApiDir = path.join(projectRoot, "functions", "openapi");
|
|
163
|
+
const generatedDir = path.join(projectRoot, "functions", "generated");
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
callFunctionHelperPath: "lib/api/call-function.ts",
|
|
167
|
+
callFunctionHelperExists: fs.existsSync(path.join(projectRoot, "lib", "api", "call-function.ts")),
|
|
168
|
+
openApiSpecs: fs.existsSync(openApiDir)
|
|
169
|
+
? fs.readdirSync(openApiDir)
|
|
170
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
171
|
+
.map((entry) => `functions/openapi/${entry}`)
|
|
172
|
+
.sort()
|
|
173
|
+
: [],
|
|
174
|
+
generatedSchemaDirectories: fs.existsSync(generatedDir)
|
|
175
|
+
? fs.readdirSync(generatedDir)
|
|
176
|
+
.map((entry) => `functions/generated/${entry}`)
|
|
177
|
+
.sort()
|
|
178
|
+
: [],
|
|
179
|
+
proxyPath: "proxy.ts",
|
|
180
|
+
proxyExists: fs.existsSync(path.join(projectRoot, "proxy.ts")),
|
|
181
|
+
loginPagePath: "app/login/page.tsx",
|
|
182
|
+
loginPageExists: fs.existsSync(path.join(projectRoot, "app", "login", "page.tsx")),
|
|
183
|
+
authContextPath: "lib/auth/auth-context.tsx",
|
|
184
|
+
authContextExists: fs.existsSync(path.join(projectRoot, "lib", "auth", "auth-context.tsx")),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildEntityRoutes(
|
|
189
|
+
projectRoot: string,
|
|
190
|
+
backendLanguage: BackendLanguage,
|
|
191
|
+
entity: ProjectManifestEntity
|
|
192
|
+
): ProjectManifestRoute[] {
|
|
193
|
+
const modelCamel = toCamelCase(entity.name);
|
|
194
|
+
const modelKebab = toKebabCase(entity.name);
|
|
195
|
+
const isConnectorEntity = Boolean(entity.connectorConfig);
|
|
196
|
+
const functionFilePath = backendLanguage === "typescript"
|
|
197
|
+
? `functions/src/${modelKebab}.ts`
|
|
198
|
+
: backendLanguage === "csharp"
|
|
199
|
+
? isConnectorEntity
|
|
200
|
+
? `functions/Connectors/${entity.name}ConnectorFunctions.cs`
|
|
201
|
+
: `functions/Crud/${entity.name}Functions.cs`
|
|
202
|
+
: `functions/blueprints/${modelKebab.replace(/-/g, "_")}.py`;
|
|
203
|
+
|
|
204
|
+
const operations = entity.connectorConfig?.operations ?? ["getAll", "getById", "create", "update", "delete"];
|
|
205
|
+
const listMethods: string[] = [];
|
|
206
|
+
const detailMethods: string[] = [];
|
|
207
|
+
|
|
208
|
+
if (operations.includes("getAll")) {
|
|
209
|
+
listMethods.push("GET");
|
|
210
|
+
}
|
|
211
|
+
if (operations.includes("create")) {
|
|
212
|
+
listMethods.push("POST");
|
|
213
|
+
}
|
|
214
|
+
if (operations.includes("getById")) {
|
|
215
|
+
detailMethods.push("GET");
|
|
216
|
+
}
|
|
217
|
+
if (operations.includes("update")) {
|
|
218
|
+
detailMethods.push("PUT");
|
|
219
|
+
}
|
|
220
|
+
if (operations.includes("delete")) {
|
|
221
|
+
detailMethods.push("DELETE");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return [
|
|
225
|
+
{
|
|
226
|
+
name: `${entity.name}-bff-list`,
|
|
227
|
+
kind: "entity",
|
|
228
|
+
surface: "bff",
|
|
229
|
+
entityName: entity.name,
|
|
230
|
+
methods: listMethods,
|
|
231
|
+
publicPath: `/api/${modelCamel}`,
|
|
232
|
+
filePath: `app/api/${modelCamel}/route.ts`,
|
|
233
|
+
exists: fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "route.ts")),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: `${entity.name}-bff-detail`,
|
|
237
|
+
kind: "entity",
|
|
238
|
+
surface: "bff",
|
|
239
|
+
entityName: entity.name,
|
|
240
|
+
methods: detailMethods,
|
|
241
|
+
publicPath: `/api/${modelCamel}/{id}`,
|
|
242
|
+
filePath: `app/api/${modelCamel}/[id]/route.ts`,
|
|
243
|
+
exists: fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "[id]", "route.ts")),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: `${entity.name}-functions`,
|
|
247
|
+
kind: "entity",
|
|
248
|
+
surface: "functions",
|
|
249
|
+
entityName: entity.name,
|
|
250
|
+
methods: Array.from(new Set([...listMethods, ...detailMethods])),
|
|
251
|
+
publicPath: `/api/${modelCamel}`,
|
|
252
|
+
filePath: functionFilePath,
|
|
253
|
+
exists: fs.existsSync(path.join(projectRoot, functionFilePath)),
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildSystemRoutes(projectRoot: string): ProjectManifestRoute[] {
|
|
259
|
+
return [
|
|
260
|
+
{
|
|
261
|
+
name: "auth-login",
|
|
262
|
+
kind: "system",
|
|
263
|
+
surface: "bff",
|
|
264
|
+
methods: ["POST"],
|
|
265
|
+
publicPath: "/api/auth/login",
|
|
266
|
+
filePath: "app/api/auth/login/route.ts",
|
|
267
|
+
exists: fs.existsSync(path.join(projectRoot, "app", "api", "auth", "login", "route.ts")),
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "auth-logout",
|
|
271
|
+
kind: "system",
|
|
272
|
+
surface: "bff",
|
|
273
|
+
methods: ["POST"],
|
|
274
|
+
publicPath: "/api/auth/logout",
|
|
275
|
+
filePath: "app/api/auth/logout/route.ts",
|
|
276
|
+
exists: fs.existsSync(path.join(projectRoot, "app", "api", "auth", "logout", "route.ts")),
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "auth-me",
|
|
280
|
+
kind: "system",
|
|
281
|
+
surface: "bff",
|
|
282
|
+
methods: ["GET"],
|
|
283
|
+
publicPath: "/api/auth/me",
|
|
284
|
+
filePath: "app/api/auth/me/route.ts",
|
|
285
|
+
exists: fs.existsSync(path.join(projectRoot, "app", "api", "auth", "me", "route.ts")),
|
|
286
|
+
},
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function buildProjectManifest(projectRoot: string = process.cwd()): Promise<{
|
|
291
|
+
manifest: ProjectManifest;
|
|
292
|
+
diagnostics: string[];
|
|
293
|
+
}> {
|
|
294
|
+
const diagnostics: string[] = [];
|
|
295
|
+
|
|
296
|
+
return withWorkingDirectory(projectRoot, async () => {
|
|
297
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
298
|
+
const backendLanguage = getBackendLanguage(configPath || undefined);
|
|
299
|
+
const config = getFullConfig(configPath || undefined);
|
|
300
|
+
const configValidation = validateConfig(config);
|
|
301
|
+
const { result: entities, messages } = await captureConsoleMessages(async () => {
|
|
302
|
+
const models = await getAllModels("shared/models");
|
|
303
|
+
return models.map((model) => ({
|
|
304
|
+
name: model.name,
|
|
305
|
+
displayName: model.displayName,
|
|
306
|
+
schemaName: model.schemaName,
|
|
307
|
+
filePath: relativeProjectPath(projectRoot, model.filePath),
|
|
308
|
+
partitionKey: model.partitionKey,
|
|
309
|
+
hasId: model.hasId,
|
|
310
|
+
hasCreatedAt: model.hasCreatedAt,
|
|
311
|
+
hasUpdatedAt: model.hasUpdatedAt,
|
|
312
|
+
connectorConfig: model.connectorConfig,
|
|
313
|
+
authPolicy: model.authPolicy,
|
|
314
|
+
nestedModels: model.nestedSchemaRefs.map((ref) => ref.modelName),
|
|
315
|
+
fields: model.fields.map((field) => ({
|
|
316
|
+
name: field.name,
|
|
317
|
+
type: field.type,
|
|
318
|
+
isOptional: field.isOptional,
|
|
319
|
+
isArray: field.isArray,
|
|
320
|
+
})),
|
|
321
|
+
}));
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
diagnostics.push(
|
|
325
|
+
...messages.warnings.map((warning) => `warning:${warning}`),
|
|
326
|
+
...messages.errors.map((error) => `error:${error}`)
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const routes = [
|
|
330
|
+
...entities.flatMap((entity) => buildEntityRoutes(projectRoot, backendLanguage, entity)),
|
|
331
|
+
...buildSystemRoutes(projectRoot),
|
|
332
|
+
].sort((left, right) => left.name.localeCompare(right.name));
|
|
333
|
+
|
|
334
|
+
const manifest: ProjectManifest = {
|
|
335
|
+
version: SWALLOWKIT_MANIFEST_VERSION,
|
|
336
|
+
generatedAt: new Date().toISOString(),
|
|
337
|
+
configPath,
|
|
338
|
+
backendLanguage,
|
|
339
|
+
configValidation,
|
|
340
|
+
connectors: Object.entries(config.connectors || {})
|
|
341
|
+
.map(([name, definition]) => ({ name, definition }))
|
|
342
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
343
|
+
auth: config.auth || null,
|
|
344
|
+
entities: entities.sort((left, right) => left.name.localeCompare(right.name)),
|
|
345
|
+
routes,
|
|
346
|
+
modules: buildModules(projectRoot),
|
|
347
|
+
artifacts: buildArtifacts(projectRoot),
|
|
348
|
+
architecture: {
|
|
349
|
+
pattern: "Next.js BFF + Azure Functions + shared Zod models",
|
|
350
|
+
backendLanguage,
|
|
351
|
+
layerBoundaries: [
|
|
352
|
+
"shared/models -> schema source of truth",
|
|
353
|
+
"app/api -> BFF layer only",
|
|
354
|
+
"functions -> backend logic and data access",
|
|
355
|
+
"app -> UI and pages",
|
|
356
|
+
],
|
|
357
|
+
hasSharedWorkspace: fs.existsSync(path.join(projectRoot, "shared", "package.json")),
|
|
358
|
+
hasConnectors: Object.keys(config.connectors || {}).length > 0,
|
|
359
|
+
hasAuth: Boolean(config.auth && config.auth.provider !== "none"),
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return { manifest, diagnostics };
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function readProjectManifest(projectRoot: string = process.cwd()): ProjectManifest | null {
|
|
368
|
+
const manifestPath = path.join(projectRoot, SWALLOWKIT_MANIFEST_PATH);
|
|
369
|
+
if (!fs.existsSync(manifestPath)) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
374
|
+
return JSON.parse(raw) as ProjectManifest;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function loadProjectManifest(projectRoot: string = process.cwd()): Promise<LoadedProjectManifest> {
|
|
378
|
+
const manifestPath = path.join(projectRoot, SWALLOWKIT_MANIFEST_PATH);
|
|
379
|
+
if (fs.existsSync(manifestPath)) {
|
|
380
|
+
return {
|
|
381
|
+
manifest: readProjectManifest(projectRoot)!,
|
|
382
|
+
source: "file",
|
|
383
|
+
diagnostics: [],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const { manifest, diagnostics } = await buildProjectManifest(projectRoot);
|
|
388
|
+
diagnostics.unshift("manifest:not-found-reconstructed");
|
|
389
|
+
return {
|
|
390
|
+
manifest,
|
|
391
|
+
source: "reconstructed",
|
|
392
|
+
diagnostics,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export async function syncProjectManifest(projectRoot: string = process.cwd()): Promise<ProjectManifest> {
|
|
397
|
+
const { manifest } = await buildProjectManifest(projectRoot);
|
|
398
|
+
const manifestPath = path.join(projectRoot, SWALLOWKIT_MANIFEST_PATH);
|
|
399
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
400
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
401
|
+
return manifest;
|
|
402
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { loadProjectManifest, buildProjectManifest, ProjectManifest, ProjectManifestEntity, SWALLOWKIT_MANIFEST_PATH } from "./manifest";
|
|
4
|
+
import { toCamelCase, toKebabCase } from "../scaffold/model-parser";
|
|
5
|
+
|
|
6
|
+
export type ProjectViolationSeverity = "error" | "warning";
|
|
7
|
+
|
|
8
|
+
export interface ProjectViolation {
|
|
9
|
+
code: string;
|
|
10
|
+
severity: ProjectViolationSeverity;
|
|
11
|
+
message: string;
|
|
12
|
+
location?: {
|
|
13
|
+
path?: string;
|
|
14
|
+
entity?: string;
|
|
15
|
+
};
|
|
16
|
+
suggestedFix?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProjectValidationResult {
|
|
20
|
+
manifest: ProjectManifest;
|
|
21
|
+
manifestSource: "file" | "reconstructed";
|
|
22
|
+
diagnostics: string[];
|
|
23
|
+
violations: ProjectViolation[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pushViolation(
|
|
27
|
+
violations: ProjectViolation[],
|
|
28
|
+
violation: ProjectViolation
|
|
29
|
+
): void {
|
|
30
|
+
violations.push(violation);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateRequiredPaths(manifest: ProjectManifest, violations: ProjectViolation[]): void {
|
|
34
|
+
const requiredModules = manifest.modules.filter((module) =>
|
|
35
|
+
["shared-models", "bff", "functions"].includes(module.kind)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
for (const module of requiredModules) {
|
|
39
|
+
if (!module.exists) {
|
|
40
|
+
pushViolation(violations, {
|
|
41
|
+
code: "missing-required-module",
|
|
42
|
+
severity: "error",
|
|
43
|
+
message: `Required module path is missing: ${module.rootPath}`,
|
|
44
|
+
location: { path: module.rootPath },
|
|
45
|
+
suggestedFix: "Initialize or restore the standard SwallowKit project directories before generating code.",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!manifest.artifacts.callFunctionHelperExists) {
|
|
51
|
+
pushViolation(violations, {
|
|
52
|
+
code: "missing-call-function-helper",
|
|
53
|
+
severity: "warning",
|
|
54
|
+
message: "BFF helper lib/api/call-function.ts is missing.",
|
|
55
|
+
location: { path: manifest.artifacts.callFunctionHelperPath },
|
|
56
|
+
suggestedFix: "Run scaffold to regenerate the BFF helper.",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateSchemaNaming(entity: ProjectManifestEntity, violations: ProjectViolation[]): void {
|
|
62
|
+
const expectedFileName = `${toKebabCase(entity.name)}.ts`;
|
|
63
|
+
if (!entity.filePath.endsWith(expectedFileName)) {
|
|
64
|
+
pushViolation(violations, {
|
|
65
|
+
code: "naming-model-file",
|
|
66
|
+
severity: "warning",
|
|
67
|
+
message: `Model file should match the entity name in kebab-case: expected ${expectedFileName}.`,
|
|
68
|
+
location: { path: entity.filePath, entity: entity.name },
|
|
69
|
+
suggestedFix: `Rename the model file to ${expectedFileName} to keep generator conventions deterministic.`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const camelSchema = `${toCamelCase(entity.name)}Schema`;
|
|
74
|
+
if (entity.schemaName !== entity.name && entity.schemaName !== camelSchema) {
|
|
75
|
+
pushViolation(violations, {
|
|
76
|
+
code: "naming-schema-export",
|
|
77
|
+
severity: "warning",
|
|
78
|
+
message: `Schema export should be ${entity.name} or ${camelSchema}. Found ${entity.schemaName}.`,
|
|
79
|
+
location: { path: entity.filePath, entity: entity.name },
|
|
80
|
+
suggestedFix: "Use either the PascalCase Zod export or the camelCase Schema suffix pattern supported by SwallowKit.",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateGeneratedArtifacts(manifest: ProjectManifest, violations: ProjectViolation[]): void {
|
|
86
|
+
for (const route of manifest.routes.filter((candidate) => candidate.kind === "entity")) {
|
|
87
|
+
if (!route.exists) {
|
|
88
|
+
pushViolation(violations, {
|
|
89
|
+
code: route.surface === "bff" ? "missing-bff-route" : "missing-functions-artifact",
|
|
90
|
+
severity: "warning",
|
|
91
|
+
message: `${route.surface === "bff" ? "BFF route" : "Functions artifact"} is missing for ${route.entityName}.`,
|
|
92
|
+
location: {
|
|
93
|
+
path: route.filePath,
|
|
94
|
+
entity: route.entityName,
|
|
95
|
+
},
|
|
96
|
+
suggestedFix: `Run scaffold for ${route.entityName} to regenerate ${route.surface === "bff" ? "API routes" : "Functions code"}.`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (manifest.backendLanguage !== "typescript" && manifest.artifacts.openApiSpecs.length === 0) {
|
|
102
|
+
pushViolation(violations, {
|
|
103
|
+
code: "missing-openapi-artifacts",
|
|
104
|
+
severity: "warning",
|
|
105
|
+
message: `No OpenAPI schema artifacts were found for ${manifest.backendLanguage} backend generation.`,
|
|
106
|
+
location: { path: "functions/openapi" },
|
|
107
|
+
suggestedFix: "Run scaffold to regenerate the OpenAPI-backed backend schema artifacts.",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateLayeringViolations(projectRoot: string, violations: ProjectViolation[]): void {
|
|
113
|
+
const apiDir = path.join(projectRoot, "app", "api");
|
|
114
|
+
const modelsDir = path.join(projectRoot, "shared", "models");
|
|
115
|
+
|
|
116
|
+
if (fs.existsSync(apiDir)) {
|
|
117
|
+
for (const filePath of walkFiles(apiDir, [".ts", ".tsx"])) {
|
|
118
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
119
|
+
if (content.includes("@azure/cosmos") || content.match(/from\s+['"].*functions\//)) {
|
|
120
|
+
pushViolation(violations, {
|
|
121
|
+
code: "forbidden-bff-dependency",
|
|
122
|
+
severity: "error",
|
|
123
|
+
message: "BFF files must not depend directly on Azure Cosmos or Functions implementation paths.",
|
|
124
|
+
location: { path: path.relative(projectRoot, filePath).replace(/\\/g, "/") },
|
|
125
|
+
suggestedFix: "Keep app/api as a pure BFF layer and call the backend through generated helpers instead.",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (fs.existsSync(modelsDir)) {
|
|
132
|
+
for (const filePath of walkFiles(modelsDir, [".ts", ".tsx"])) {
|
|
133
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
134
|
+
if (content.match(/from\s+['"].*(app\/|functions\/|lib\/api\/)/)) {
|
|
135
|
+
pushViolation(violations, {
|
|
136
|
+
code: "forbidden-model-layer-import",
|
|
137
|
+
severity: "error",
|
|
138
|
+
message: "Shared models must not import from app/, functions/, or lib/api/ layers.",
|
|
139
|
+
location: { path: path.relative(projectRoot, filePath).replace(/\\/g, "/") },
|
|
140
|
+
suggestedFix: "Keep shared/models framework-agnostic so generators can reuse them across all layers.",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateManifestDrift(
|
|
148
|
+
projectRoot: string,
|
|
149
|
+
loadedManifest: ProjectManifest,
|
|
150
|
+
rebuiltManifest: ProjectManifest,
|
|
151
|
+
violations: ProjectViolation[]
|
|
152
|
+
): void {
|
|
153
|
+
const sameEntitySet = JSON.stringify(loadedManifest.entities.map((entity) => entity.name).sort()) ===
|
|
154
|
+
JSON.stringify(rebuiltManifest.entities.map((entity) => entity.name).sort());
|
|
155
|
+
const sameRouteSet = JSON.stringify(loadedManifest.routes.map((route) => route.name).sort()) ===
|
|
156
|
+
JSON.stringify(rebuiltManifest.routes.map((route) => route.name).sort());
|
|
157
|
+
|
|
158
|
+
if (!sameEntitySet || !sameRouteSet) {
|
|
159
|
+
pushViolation(violations, {
|
|
160
|
+
code: "manifest-out-of-date",
|
|
161
|
+
severity: "warning",
|
|
162
|
+
message: `${SWALLOWKIT_MANIFEST_PATH} is out of date with the current project structure.`,
|
|
163
|
+
location: { path: SWALLOWKIT_MANIFEST_PATH },
|
|
164
|
+
suggestedFix: "Run a SwallowKit generator command or refresh the manifest so inspection and validation use current metadata.",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function walkFiles(directory: string, extensions: string[]): string[] {
|
|
170
|
+
const collected: string[] = [];
|
|
171
|
+
|
|
172
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
173
|
+
const entryPath = path.join(directory, entry.name);
|
|
174
|
+
if (entry.isDirectory()) {
|
|
175
|
+
collected.push(...walkFiles(entryPath, extensions));
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (extensions.some((extension) => entry.name.endsWith(extension))) {
|
|
180
|
+
collected.push(entryPath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return collected;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function validateProject(projectRoot: string = process.cwd()): Promise<ProjectValidationResult> {
|
|
188
|
+
const loaded = await loadProjectManifest(projectRoot);
|
|
189
|
+
const rebuilt = await buildProjectManifest(projectRoot);
|
|
190
|
+
const violations: ProjectViolation[] = [];
|
|
191
|
+
|
|
192
|
+
validateRequiredPaths(loaded.manifest, violations);
|
|
193
|
+
|
|
194
|
+
for (const error of loaded.manifest.configValidation.errors) {
|
|
195
|
+
pushViolation(violations, {
|
|
196
|
+
code: "config-validation",
|
|
197
|
+
severity: "error",
|
|
198
|
+
message: error,
|
|
199
|
+
location: loaded.manifest.configPath ? { path: loaded.manifest.configPath } : undefined,
|
|
200
|
+
suggestedFix: "Fix the SwallowKit config so generators and validators have a stable project definition.",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const entity of loaded.manifest.entities) {
|
|
205
|
+
validateSchemaNaming(entity, violations);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
validateGeneratedArtifacts(loaded.manifest, violations);
|
|
209
|
+
validateLayeringViolations(projectRoot, violations);
|
|
210
|
+
|
|
211
|
+
if (loaded.source === "file") {
|
|
212
|
+
validateManifestDrift(projectRoot, loaded.manifest, rebuilt.manifest, violations);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
manifest: loaded.manifest,
|
|
217
|
+
manifestSource: loaded.source,
|
|
218
|
+
diagnostics: [...loaded.diagnostics, ...rebuilt.diagnostics],
|
|
219
|
+
violations: violations.sort((left, right) => left.code.localeCompare(right.code)),
|
|
220
|
+
};
|
|
221
|
+
}
|