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,1211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SwallowKit Scaffold ć³ćć³ć
|
|
3
|
+
* Zod ć¢ćć«ćć Azure Functions 㨠Next.js BFF ć® CRUD ć³ć¼ććēę
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { spawn, spawnSync } from "child_process";
|
|
9
|
+
import { getBackendLanguage, getConnectorDefinition, getAuthConfig, ensureSwallowKitProject } from "../../core/config";
|
|
10
|
+
import { ModelInfo, parseModelFile, toKebabCase, toPascalCase, toCamelCase } from "../../core/scaffold/model-parser";
|
|
11
|
+
import {
|
|
12
|
+
generateCSharpAzureFunctionsCRUD,
|
|
13
|
+
generateCompactAzureFunctionsCRUD,
|
|
14
|
+
generatePythonAzureFunctionsCRUD,
|
|
15
|
+
} from "../../core/scaffold/functions-generator";
|
|
16
|
+
import {
|
|
17
|
+
generateRdbConnectorFunctionTS,
|
|
18
|
+
generateApiConnectorFunctionTS,
|
|
19
|
+
generateRdbConnectorFunctionCSharp,
|
|
20
|
+
generateApiConnectorFunctionCSharp,
|
|
21
|
+
generateRdbConnectorFunctionPython,
|
|
22
|
+
generateApiConnectorFunctionPython,
|
|
23
|
+
} from "../../core/scaffold/connector-functions-generator";
|
|
24
|
+
import { generateCompactBFFRoutes, generateBFFCallFunction, generateConnectorBFFRoutes } from "../../core/scaffold/nextjs-generator";
|
|
25
|
+
import { generateOpenApiDocument } from "../../core/scaffold/openapi-generator";
|
|
26
|
+
import {
|
|
27
|
+
generateListPage,
|
|
28
|
+
generateDetailPage,
|
|
29
|
+
generateFormComponent,
|
|
30
|
+
generateNewPage,
|
|
31
|
+
generateEditPage,
|
|
32
|
+
UIAuthOptions,
|
|
33
|
+
} from "../../core/scaffold/ui-generator";
|
|
34
|
+
import { detectFromProject, getCommands } from "../../utils/package-manager";
|
|
35
|
+
import {
|
|
36
|
+
BackendLanguage,
|
|
37
|
+
RdbConnectorConfig,
|
|
38
|
+
ApiConnectorConfig,
|
|
39
|
+
RdbModelConnectorConfig,
|
|
40
|
+
ApiModelConnectorConfig,
|
|
41
|
+
ModelAuthPolicy,
|
|
42
|
+
AuthConfig,
|
|
43
|
+
} from "../../types";
|
|
44
|
+
import { syncProjectManifest } from "../../core/project/manifest";
|
|
45
|
+
|
|
46
|
+
interface ScaffoldOptions {
|
|
47
|
+
model: string; // ć¢ćć«ćć”ć¤ć«ć®ćć¹ļ¼ä¾: "lib/models/todo.ts" or "todo"ļ¼
|
|
48
|
+
functionsDir?: string; // Azure Functions ć®ćć£ć¬ćÆććŖļ¼ććć©ć«ć: "functions"ļ¼
|
|
49
|
+
apiDir?: string; // Next.js API routes ć®ćć£ć¬ćÆććŖļ¼ććć©ć«ć: "app/api"ļ¼
|
|
50
|
+
apiOnly?: boolean; // true ć®å “åćUI ćēęććŖćļ¼ććć©ć«ć: falseļ¼
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getMachineAwareStdio(): "inherit" | "pipe" {
|
|
54
|
+
return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runSpawnSyncCommand(command: string, args: string[], cwd: string): void {
|
|
58
|
+
const result = spawnSync(command, args, {
|
|
59
|
+
cwd,
|
|
60
|
+
stdio: getMachineAwareStdio(),
|
|
61
|
+
shell: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
65
|
+
throw new Error(`${command} ${args.join(" ")} exited with code ${result.status}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (result.error) {
|
|
69
|
+
throw result.error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
74
|
+
// SwallowKit ćććøć§ćÆććć£ć¬ćÆććŖćć©ćććę¤čؼ
|
|
75
|
+
ensureSwallowKitProject("scaffold");
|
|
76
|
+
|
|
77
|
+
console.log("šļø SwallowKit Scaffold: Generating CRUD operations...\n");
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// 1. Resolve model file path
|
|
81
|
+
const modelPath = resolveModelPath(options.model);
|
|
82
|
+
console.log(`š Model file: ${modelPath}`);
|
|
83
|
+
|
|
84
|
+
// 2. Parse model file
|
|
85
|
+
console.log("š Parsing model file...");
|
|
86
|
+
const modelInfo = await parseModelFile(modelPath);
|
|
87
|
+
console.log(`ā
Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
|
|
88
|
+
|
|
89
|
+
const backendLanguage = getBackendLanguage();
|
|
90
|
+
console.log(`š§ Backend language: ${backendLanguage}`);
|
|
91
|
+
|
|
92
|
+
// ć³ććÆćæć¢ćć«ćć©ćććå¤å®
|
|
93
|
+
const isConnectorModel = !!modelInfo.connectorConfig;
|
|
94
|
+
if (isConnectorModel) {
|
|
95
|
+
console.log(`š Connector model detected: ${modelInfo.connectorConfig!.connector}`);
|
|
96
|
+
console.log(` Operations: ${modelInfo.connectorConfig!.operations.join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ćć¹ćć¹ćć¼ćåē
§ćććć°č”Øē¤ŗ
|
|
100
|
+
if (modelInfo.nestedSchemaRefs.length > 0) {
|
|
101
|
+
console.log(`š Nested schema references detected:`);
|
|
102
|
+
for (const ref of modelInfo.nestedSchemaRefs) {
|
|
103
|
+
const relType = ref.isArray ? 'array' : 'single';
|
|
104
|
+
const optional = ref.isOptional ? ' (optional)' : '';
|
|
105
|
+
console.log(` - ${ref.fieldName}: ${ref.modelName} [${relType}]${optional}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. Check for ID field
|
|
110
|
+
if (!modelInfo.hasId) {
|
|
111
|
+
console.warn(
|
|
112
|
+
"ā ļø Warning: Model does not have an 'id' field. CRUD operations may not work correctly."
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4. Read shared package name
|
|
117
|
+
const functionsDir = options.functionsDir || "functions";
|
|
118
|
+
const sharedPackageName = readSharedPackageName();
|
|
119
|
+
const relatedModels = backendLanguage === "typescript"
|
|
120
|
+
? [modelInfo]
|
|
121
|
+
: await collectModelGraph(modelPath);
|
|
122
|
+
|
|
123
|
+
// 5. Generate BFF callFunction helper
|
|
124
|
+
await generateCallFunctionHelper();
|
|
125
|
+
|
|
126
|
+
// 6. Generate Azure Functions code
|
|
127
|
+
if (isConnectorModel) {
|
|
128
|
+
await generateConnectorFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
|
|
129
|
+
// 6b. Install connector driver dependencies
|
|
130
|
+
await installConnectorDriverDependencies(modelInfo, functionsDir, backendLanguage);
|
|
131
|
+
} else {
|
|
132
|
+
await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
|
|
133
|
+
|
|
134
|
+
if (backendLanguage !== "typescript") {
|
|
135
|
+
await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 7. Generate Next.js BFF API Routes
|
|
140
|
+
const apiDir = options.apiDir || "app/api";
|
|
141
|
+
if (isConnectorModel) {
|
|
142
|
+
await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName);
|
|
143
|
+
} else {
|
|
144
|
+
await generateBFFRoutes(modelInfo, apiDir, sharedPackageName);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 8. Generate Cosmos DB container Bicep file (skip for connector models)
|
|
148
|
+
if (!isConnectorModel) {
|
|
149
|
+
await generateCosmosContainer(modelInfo);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 9. Generate UI components (unless --api-only)
|
|
153
|
+
if (!options.apiOnly) {
|
|
154
|
+
const uiAuthConfig = getAuthConfig();
|
|
155
|
+
const uiAuthPolicy = resolveAuthPolicy(modelInfo, uiAuthConfig);
|
|
156
|
+
const uiAuthOptions: UIAuthOptions | undefined =
|
|
157
|
+
uiAuthPolicy && uiAuthConfig && uiAuthConfig.provider !== 'none'
|
|
158
|
+
? { authPolicy: uiAuthPolicy }
|
|
159
|
+
: undefined;
|
|
160
|
+
await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
|
|
161
|
+
await updateNavigationMenu(modelInfo);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await syncProjectManifest();
|
|
165
|
+
|
|
166
|
+
console.log("\nā
Scaffold completed successfully!");
|
|
167
|
+
console.log("\nš Next steps:");
|
|
168
|
+
console.log(` 1. Review generated files in ${describeFunctionsOutputPath(functionsDir, backendLanguage)} and ${apiDir}/`);
|
|
169
|
+
if (!options.apiOnly) {
|
|
170
|
+
console.log(` 2. Check the generated UI pages in app/${toKebabCase(modelInfo.name)}/`);
|
|
171
|
+
console.log(" 3. Navigate to the model from the homepage menu");
|
|
172
|
+
}
|
|
173
|
+
if (backendLanguage !== "typescript") {
|
|
174
|
+
console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
|
|
175
|
+
}
|
|
176
|
+
console.log(
|
|
177
|
+
` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in your .env.local file`
|
|
178
|
+
);
|
|
179
|
+
console.log(
|
|
180
|
+
` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
|
|
181
|
+
);
|
|
182
|
+
console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).dlx} swallowkit dev' to test the generated code`);
|
|
183
|
+
} catch (error: any) {
|
|
184
|
+
console.error("\nā Scaffold failed:", error.message);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* ć¢ćć«ć® authPolicy 㨠auth config ććå®å¹ēćŖčŖåÆććŖć·ć¼ć解決
|
|
191
|
+
* - ć¢ćć«ć« authPolicy ćććć°ććć使ēØ
|
|
192
|
+
* - ćŖććć° auth.authorization.defaultPolicy ć«å¾ć
|
|
193
|
+
* - auth čØå®ććŖććć° undefinedļ¼ć¬ć¼ććŖćļ¼
|
|
194
|
+
*/
|
|
195
|
+
function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
|
|
196
|
+
// ć¢ćć«ć«ę示ēćŖ authPolicy ćććå “åćÆććć使ēØ
|
|
197
|
+
if (modelInfo.authPolicy) {
|
|
198
|
+
return modelInfo.authPolicy;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// auth čØå®ććŖćå “åćÆć¬ć¼ććŖć
|
|
202
|
+
if (!authConfig || authConfig.provider === 'none') {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// defaultPolicy ć 'authenticated' ćŖćčŖčؼć®ćæļ¼ćć¼ć«ęå®ćŖćļ¼ć®ććŖć·ć¼ćčæć
|
|
207
|
+
const defaultPolicy = authConfig.authorization?.defaultPolicy ?? 'authenticated';
|
|
208
|
+
if (defaultPolicy === 'authenticated') {
|
|
209
|
+
return {}; // 空ć®ććŖć·ć¼ = čŖčؼć®ćæććć¼ć«å¶éćŖć
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// defaultPolicy ć 'anonymous' ćŖćčŖčؼäøč¦
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* ć¢ćć«ćć”ć¤ć«ć®ćć¹ć解決
|
|
218
|
+
*/
|
|
219
|
+
function resolveModelPath(modelInput: string): string {
|
|
220
|
+
const cwd = process.cwd();
|
|
221
|
+
|
|
222
|
+
// 絶対ćć¹ć®å “å
|
|
223
|
+
if (path.isAbsolute(modelInput)) {
|
|
224
|
+
return modelInput;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ę”å¼µåćććå “åļ¼ēøåƾćć¹ļ¼
|
|
228
|
+
if (modelInput.endsWith(".ts")) {
|
|
229
|
+
const fullPath = path.join(cwd, modelInput);
|
|
230
|
+
if (fs.existsSync(fullPath)) {
|
|
231
|
+
return fullPath;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ę”å¼µåććŖćå “åćshared/models/ ć§ę¢ć
|
|
236
|
+
const defaultPath = path.join(cwd, "shared", "models", `${modelInput}.ts`);
|
|
237
|
+
if (fs.existsSync(defaultPath)) {
|
|
238
|
+
return defaultPath;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// lib/models/ ć§ćę¢ćļ¼å¾ę¹äŗęę§ļ¼
|
|
242
|
+
const libPath = path.join(cwd, "lib", "models", `${modelInput}.ts`);
|
|
243
|
+
if (fs.existsSync(libPath)) {
|
|
244
|
+
return libPath;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/models/ ć§ćę¢ć
|
|
248
|
+
const srcPath = path.join(cwd, "src", "models", `${modelInput}.ts`);
|
|
249
|
+
if (fs.existsSync(srcPath)) {
|
|
250
|
+
return srcPath;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// č¦ć¤ćććŖćå “åćÆćØć©ć¼
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Model file not found: ${modelInput}\n` +
|
|
256
|
+
` Tried:\n` +
|
|
257
|
+
` - ${modelInput}\n` +
|
|
258
|
+
` - ${defaultPath}\n` +
|
|
259
|
+
` - ${libPath}\n` +
|
|
260
|
+
` - ${srcPath}\n` +
|
|
261
|
+
` Please specify a valid model file path.`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Read shared package name from shared/package.json
|
|
267
|
+
*/
|
|
268
|
+
function readSharedPackageName(): string {
|
|
269
|
+
const cwd = process.cwd();
|
|
270
|
+
const sharedPkgPath = path.join(cwd, "shared", "package.json");
|
|
271
|
+
|
|
272
|
+
if (!fs.existsSync(sharedPkgPath)) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"shared/package.json not found.\n" +
|
|
275
|
+
"The shared package is required for model imports.\n" +
|
|
276
|
+
`Run "${getCommands(detectFromProject()).dlx} swallowkit init" to set up your project.`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const pkg = JSON.parse(fs.readFileSync(sharedPkgPath, "utf-8"));
|
|
281
|
+
return pkg.name;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Generate BFF callFunction helper (lib/api/call-function.ts)
|
|
286
|
+
*/
|
|
287
|
+
async function generateCallFunctionHelper(): Promise<void> {
|
|
288
|
+
console.log("\nš¦ Generating BFF callFunction helper...");
|
|
289
|
+
|
|
290
|
+
const cwd = process.cwd();
|
|
291
|
+
|
|
292
|
+
const helperDir = path.join(cwd, "lib", "api");
|
|
293
|
+
const helperPath = path.join(helperDir, "call-function.ts");
|
|
294
|
+
|
|
295
|
+
if (!fs.existsSync(helperDir)) {
|
|
296
|
+
fs.mkdirSync(helperDir, { recursive: true });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// auth čØå®ćććå “å㯠Authorization ćććć¼č»¢éēćēę
|
|
300
|
+
const authConfig = getAuthConfig();
|
|
301
|
+
const hasAuth = authConfig && authConfig.provider !== 'none';
|
|
302
|
+
let helperCode: string;
|
|
303
|
+
if (hasAuth) {
|
|
304
|
+
const { generateBFFCallFunctionWithAuth } = await import("../../core/scaffold/auth-generator");
|
|
305
|
+
helperCode = generateBFFCallFunctionWithAuth();
|
|
306
|
+
} else {
|
|
307
|
+
helperCode = generateBFFCallFunction();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fs.writeFileSync(helperPath, helperCode, "utf-8");
|
|
311
|
+
console.log(`ā
Created: ${helperPath}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Generate Azure Functions CRUD code
|
|
316
|
+
*/
|
|
317
|
+
async function generateFunctionsCode(
|
|
318
|
+
modelInfo: ModelInfo,
|
|
319
|
+
functionsDir: string,
|
|
320
|
+
sharedPackageName: string,
|
|
321
|
+
backendLanguage: BackendLanguage
|
|
322
|
+
): Promise<void> {
|
|
323
|
+
console.log("\nšØ Generating Azure Functions CRUD code...");
|
|
324
|
+
|
|
325
|
+
// Resolve auth policy: model-level authPolicy or global defaultPolicy
|
|
326
|
+
const authConfig = getAuthConfig();
|
|
327
|
+
const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
|
|
328
|
+
if (authPolicy) {
|
|
329
|
+
console.log(`š Auth policy detected: ${JSON.stringify(authPolicy)}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const modelKebab = toKebabCase(modelInfo.name);
|
|
333
|
+
if (backendLanguage === "typescript") {
|
|
334
|
+
const functionFilePath = path.join(
|
|
335
|
+
process.cwd(),
|
|
336
|
+
functionsDir,
|
|
337
|
+
"src",
|
|
338
|
+
`${modelKebab}.ts`
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const functionDir = path.dirname(functionFilePath);
|
|
342
|
+
if (!fs.existsSync(functionDir)) {
|
|
343
|
+
fs.mkdirSync(functionDir, { recursive: true });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy);
|
|
347
|
+
fs.writeFileSync(functionFilePath, code, "utf-8");
|
|
348
|
+
console.log(`ā
Created: ${functionFilePath}`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (backendLanguage === "csharp") {
|
|
353
|
+
const crudDir = path.join(process.cwd(), functionsDir, "Crud");
|
|
354
|
+
const functionFilePath = path.join(crudDir, `${modelInfo.name}Functions.cs`);
|
|
355
|
+
fs.mkdirSync(crudDir, { recursive: true });
|
|
356
|
+
|
|
357
|
+
// Remove init-generated template (singular) to avoid route conflicts
|
|
358
|
+
const templatePath = path.join(crudDir, `${modelInfo.name}Function.cs`);
|
|
359
|
+
if (fs.existsSync(templatePath)) {
|
|
360
|
+
fs.unlinkSync(templatePath);
|
|
361
|
+
console.log(`šļø Removed template: ${templatePath}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fs.writeFileSync(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo, authPolicy), "utf-8");
|
|
365
|
+
console.log(`ā
Created: ${functionFilePath}`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
|
|
370
|
+
const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
|
|
371
|
+
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
372
|
+
|
|
373
|
+
const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo, authPolicy);
|
|
374
|
+
fs.writeFileSync(blueprintPath, blueprint, "utf-8");
|
|
375
|
+
updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration);
|
|
376
|
+
console.log(`ā
Created: ${blueprintPath}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* ć³ććÆćæć¢ćć«ēØ Azure Functions ć³ć¼ćēę
|
|
381
|
+
*/
|
|
382
|
+
async function generateConnectorFunctionsCode(
|
|
383
|
+
modelInfo: ModelInfo,
|
|
384
|
+
functionsDir: string,
|
|
385
|
+
sharedPackageName: string,
|
|
386
|
+
backendLanguage: BackendLanguage
|
|
387
|
+
): Promise<void> {
|
|
388
|
+
console.log("\nš Generating Connector Azure Functions code...");
|
|
389
|
+
|
|
390
|
+
const connectorConfig = modelInfo.connectorConfig!;
|
|
391
|
+
const connectorDef = getConnectorDefinition(connectorConfig.connector);
|
|
392
|
+
|
|
393
|
+
if (!connectorDef) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
`Connector '${connectorConfig.connector}' not found in swallowkit.config.js.\n` +
|
|
396
|
+
` Please add it to the 'connectors' section of your configuration.`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Resolve auth policy (same logic as Cosmos model scaffolding)
|
|
401
|
+
const authConfig = getAuthConfig();
|
|
402
|
+
const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
|
|
403
|
+
if (authPolicy) {
|
|
404
|
+
console.log(`š Auth policy detected: ${JSON.stringify(authPolicy)}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const modelKebab = toKebabCase(modelInfo.name);
|
|
408
|
+
|
|
409
|
+
if (backendLanguage === "typescript") {
|
|
410
|
+
const functionFilePath = path.join(
|
|
411
|
+
process.cwd(),
|
|
412
|
+
functionsDir,
|
|
413
|
+
"src",
|
|
414
|
+
`${modelKebab}.ts`
|
|
415
|
+
);
|
|
416
|
+
fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
|
|
417
|
+
|
|
418
|
+
let code: string;
|
|
419
|
+
if (connectorDef.type === "rdb") {
|
|
420
|
+
code = generateRdbConnectorFunctionTS(
|
|
421
|
+
modelInfo, sharedPackageName,
|
|
422
|
+
connectorDef as RdbConnectorConfig,
|
|
423
|
+
connectorConfig as RdbModelConnectorConfig,
|
|
424
|
+
authPolicy
|
|
425
|
+
);
|
|
426
|
+
} else {
|
|
427
|
+
code = generateApiConnectorFunctionTS(
|
|
428
|
+
modelInfo, sharedPackageName,
|
|
429
|
+
connectorDef as ApiConnectorConfig,
|
|
430
|
+
connectorConfig as ApiModelConnectorConfig,
|
|
431
|
+
authPolicy
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
fs.writeFileSync(functionFilePath, code, "utf-8");
|
|
436
|
+
console.log(`ā
Created: ${functionFilePath}`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (backendLanguage === "csharp") {
|
|
441
|
+
const functionFilePath = path.join(
|
|
442
|
+
process.cwd(),
|
|
443
|
+
functionsDir,
|
|
444
|
+
"Connectors",
|
|
445
|
+
`${modelInfo.name}ConnectorFunctions.cs`
|
|
446
|
+
);
|
|
447
|
+
fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
|
|
448
|
+
|
|
449
|
+
let code: string;
|
|
450
|
+
if (connectorDef.type === "rdb") {
|
|
451
|
+
code = generateRdbConnectorFunctionCSharp(
|
|
452
|
+
modelInfo,
|
|
453
|
+
connectorDef as RdbConnectorConfig,
|
|
454
|
+
connectorConfig as RdbModelConnectorConfig
|
|
455
|
+
);
|
|
456
|
+
} else {
|
|
457
|
+
code = generateApiConnectorFunctionCSharp(
|
|
458
|
+
modelInfo,
|
|
459
|
+
connectorDef as ApiConnectorConfig,
|
|
460
|
+
connectorConfig as ApiModelConnectorConfig
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
fs.writeFileSync(functionFilePath, code, "utf-8");
|
|
465
|
+
console.log(`ā
Created: ${functionFilePath}`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Python
|
|
470
|
+
const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
|
|
471
|
+
const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
|
|
472
|
+
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
473
|
+
|
|
474
|
+
let result: { blueprint: string; registration: string };
|
|
475
|
+
if (connectorDef.type === "rdb") {
|
|
476
|
+
result = generateRdbConnectorFunctionPython(
|
|
477
|
+
modelInfo,
|
|
478
|
+
connectorDef as RdbConnectorConfig,
|
|
479
|
+
connectorConfig as RdbModelConnectorConfig
|
|
480
|
+
);
|
|
481
|
+
} else {
|
|
482
|
+
result = generateApiConnectorFunctionPython(
|
|
483
|
+
modelInfo,
|
|
484
|
+
connectorDef as ApiConnectorConfig,
|
|
485
|
+
connectorConfig as ApiModelConnectorConfig
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
fs.writeFileSync(blueprintPath, result.blueprint, "utf-8");
|
|
490
|
+
updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), result.registration);
|
|
491
|
+
console.log(`ā
Created: ${blueprintPath}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* ć³ććÆćæć¢ćć«ć® scaffold å¾ć«ćēęć³ć¼ććåæ
č¦ćØćć RDB/API ćć©ć¤ćć
|
|
496
|
+
* functions ćć£ć¬ćÆććŖć«ć¤ć³ć¹ćć¼ć«ććć
|
|
497
|
+
*/
|
|
498
|
+
async function installConnectorDriverDependencies(
|
|
499
|
+
modelInfo: ModelInfo,
|
|
500
|
+
functionsDir: string,
|
|
501
|
+
backendLanguage: BackendLanguage
|
|
502
|
+
): Promise<void> {
|
|
503
|
+
const connectorConfig = modelInfo.connectorConfig!;
|
|
504
|
+
const connectorDef = getConnectorDefinition(connectorConfig.connector);
|
|
505
|
+
if (!connectorDef || connectorDef.type !== "rdb") return;
|
|
506
|
+
|
|
507
|
+
const rdbDef = connectorDef as RdbConnectorConfig;
|
|
508
|
+
const functionsPath = path.join(process.cwd(), functionsDir);
|
|
509
|
+
|
|
510
|
+
if (backendLanguage === "typescript") {
|
|
511
|
+
const driverMap: Record<string, { deps: string[]; devDeps: string[] }> = {
|
|
512
|
+
mysql: { deps: ["mysql2"], devDeps: [] },
|
|
513
|
+
postgres: { deps: ["pg"], devDeps: ["@types/pg"] },
|
|
514
|
+
sqlserver: { deps: ["mssql"], devDeps: [] },
|
|
515
|
+
};
|
|
516
|
+
const entry = driverMap[rdbDef.provider];
|
|
517
|
+
if (!entry) return;
|
|
518
|
+
|
|
519
|
+
// package.json ćčŖćć§ćę¢ć«ć¤ć³ć¹ćć¼ć«ęøćæćŖćć¹ććć
|
|
520
|
+
const pkgJsonPath = path.join(functionsPath, "package.json");
|
|
521
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
522
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
523
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
524
|
+
const missingDeps = entry.deps.filter(d => !allDeps[d]);
|
|
525
|
+
const missingDevDeps = entry.devDeps.filter(d => !allDeps[d]);
|
|
526
|
+
if (missingDeps.length === 0 && missingDevDeps.length === 0) return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
console.log(`\nš¦ Installing ${rdbDef.provider} driver dependencies...`);
|
|
530
|
+
const pm = detectFromProject(functionsPath);
|
|
531
|
+
const cmds = getCommands(pm);
|
|
532
|
+
|
|
533
|
+
if (entry.deps.length > 0) {
|
|
534
|
+
runSpawnSyncCommand(cmds.name, [pm === "pnpm" ? "add" : "install", ...entry.deps], functionsPath);
|
|
535
|
+
}
|
|
536
|
+
if (entry.devDeps.length > 0) {
|
|
537
|
+
runSpawnSyncCommand(cmds.name, [pm === "pnpm" ? "add" : "install", "-D", ...entry.devDeps], functionsPath);
|
|
538
|
+
}
|
|
539
|
+
console.log(`ā
${rdbDef.provider} driver installed`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (backendLanguage === "csharp") {
|
|
544
|
+
const nugetMap: Record<string, string> = {
|
|
545
|
+
mysql: "MySqlConnector",
|
|
546
|
+
postgres: "Npgsql",
|
|
547
|
+
sqlserver: "Microsoft.Data.SqlClient",
|
|
548
|
+
};
|
|
549
|
+
const pkg = nugetMap[rdbDef.provider];
|
|
550
|
+
if (!pkg) return;
|
|
551
|
+
console.log(`\nš¦ Installing ${rdbDef.provider} NuGet package...`);
|
|
552
|
+
runSpawnSyncCommand("dotnet", ["add", path.join(functionsPath, "functions.csproj"), "package", pkg], functionsPath);
|
|
553
|
+
console.log(`ā
${pkg} installed`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Python ā requirements.txt ć«čæ½čØ
|
|
558
|
+
const pipMap: Record<string, string> = {
|
|
559
|
+
mysql: "mysql-connector-python",
|
|
560
|
+
postgres: "psycopg2-binary",
|
|
561
|
+
sqlserver: "pymssql",
|
|
562
|
+
};
|
|
563
|
+
const pipPkg = pipMap[rdbDef.provider];
|
|
564
|
+
if (!pipPkg) return;
|
|
565
|
+
const reqPath = path.join(functionsPath, "requirements.txt");
|
|
566
|
+
if (fs.existsSync(reqPath)) {
|
|
567
|
+
const existing = fs.readFileSync(reqPath, "utf-8");
|
|
568
|
+
if (!existing.includes(pipPkg)) {
|
|
569
|
+
console.log(`\nš¦ Adding ${pipPkg} to requirements.txt...`);
|
|
570
|
+
fs.appendFileSync(reqPath, `\n${pipPkg}\n`);
|
|
571
|
+
console.log(`ā
${pipPkg} added`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* ć³ććÆćæć¢ćć«ēØ BFF ć«ć¼ćēęļ¼ęä½å¶éć«åƾåæļ¼
|
|
578
|
+
*/
|
|
579
|
+
async function generateConnectorBFFRoutesFiles(
|
|
580
|
+
modelInfo: ModelInfo,
|
|
581
|
+
apiDir: string,
|
|
582
|
+
sharedPackageName: string
|
|
583
|
+
): Promise<void> {
|
|
584
|
+
console.log("\nš Generating Connector BFF API routes...");
|
|
585
|
+
|
|
586
|
+
const modelCamel = toCamelCase(modelInfo.name);
|
|
587
|
+
const operations = modelInfo.connectorConfig!.operations;
|
|
588
|
+
|
|
589
|
+
const listRoutePath = path.join(process.cwd(), apiDir, modelCamel, "route.ts");
|
|
590
|
+
const detailRoutePath = path.join(process.cwd(), apiDir, modelCamel, "[id]", "route.ts");
|
|
591
|
+
|
|
592
|
+
fs.mkdirSync(path.dirname(listRoutePath), { recursive: true });
|
|
593
|
+
fs.mkdirSync(path.dirname(detailRoutePath), { recursive: true });
|
|
594
|
+
|
|
595
|
+
const routes = generateConnectorBFFRoutes(modelInfo, sharedPackageName, operations);
|
|
596
|
+
|
|
597
|
+
fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
|
|
598
|
+
fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
|
|
599
|
+
|
|
600
|
+
console.log(`ā
Created: ${listRoutePath}`);
|
|
601
|
+
console.log(`ā
Created: ${detailRoutePath}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function collectModelGraph(modelPath: string, seen = new Map<string, ModelInfo>()): Promise<ModelInfo[]> {
|
|
605
|
+
const resolvedPath = path.resolve(modelPath);
|
|
606
|
+
if (seen.has(resolvedPath)) {
|
|
607
|
+
return Array.from(seen.values());
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const modelInfo = await parseModelFile(resolvedPath);
|
|
611
|
+
seen.set(resolvedPath, modelInfo);
|
|
612
|
+
|
|
613
|
+
for (const ref of modelInfo.nestedSchemaRefs) {
|
|
614
|
+
const nestedPath = resolveNestedModelPath(resolvedPath, ref.importPath);
|
|
615
|
+
await collectModelGraph(nestedPath, seen);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return Array.from(seen.values());
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function resolveNestedModelPath(modelPath: string, importPath: string): string {
|
|
622
|
+
let resolvedPath = path.resolve(path.dirname(modelPath), importPath);
|
|
623
|
+
if (!resolvedPath.endsWith(".ts")) {
|
|
624
|
+
resolvedPath += ".ts";
|
|
625
|
+
}
|
|
626
|
+
return resolvedPath;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function describeFunctionsOutputPath(functionsDir: string, backendLanguage: BackendLanguage): string {
|
|
630
|
+
if (backendLanguage === "typescript") {
|
|
631
|
+
return `${functionsDir}/src/`;
|
|
632
|
+
}
|
|
633
|
+
if (backendLanguage === "csharp") {
|
|
634
|
+
return `${functionsDir}/Crud/`;
|
|
635
|
+
}
|
|
636
|
+
return `${functionsDir}/blueprints/`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function generateLanguageSchemaArtifacts(
|
|
640
|
+
models: ModelInfo[],
|
|
641
|
+
rootModel: ModelInfo,
|
|
642
|
+
functionsDir: string,
|
|
643
|
+
backendLanguage: Exclude<BackendLanguage, "typescript">
|
|
644
|
+
): Promise<void> {
|
|
645
|
+
console.log("\n𧬠Generating OpenAPI-backed schema artifacts...");
|
|
646
|
+
|
|
647
|
+
const openApiDir = path.join(process.cwd(), functionsDir, "openapi");
|
|
648
|
+
fs.mkdirSync(openApiDir, { recursive: true });
|
|
649
|
+
|
|
650
|
+
const specPath = path.join(openApiDir, `${toKebabCase(rootModel.name)}.openapi.json`);
|
|
651
|
+
fs.writeFileSync(specPath, generateOpenApiDocument(models, rootModel), "utf-8");
|
|
652
|
+
console.log(`ā
Created: ${specPath}`);
|
|
653
|
+
|
|
654
|
+
const outputDir = path.join(
|
|
655
|
+
process.cwd(),
|
|
656
|
+
functionsDir,
|
|
657
|
+
"generated",
|
|
658
|
+
backendLanguage === "csharp" ? "csharp-models" : "python-models"
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
662
|
+
await runOpenApiGenerator(specPath, outputDir, backendLanguage);
|
|
663
|
+
if (backendLanguage === "csharp") {
|
|
664
|
+
pruneGeneratedCSharpArtifacts(outputDir);
|
|
665
|
+
}
|
|
666
|
+
console.log(`ā
Generated ${backendLanguage} schema assets: ${outputDir}`);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* OpenAPI Generator ć®å®č”åć« Java ćć¼ćøć§ć³ć確čŖććć
|
|
671
|
+
* Java 11 ęŖęŗć®å “åćÆę確ćŖćØć©ć¼ć”ćć»ć¼ćøć蔨示ćć¦å¦ēćäøęććć
|
|
672
|
+
*/
|
|
673
|
+
function checkJavaVersion(): void {
|
|
674
|
+
try {
|
|
675
|
+
const result = spawnSync("java", ["-version"], { encoding: "utf8" });
|
|
676
|
+
// java -version 㯠stderr ć«åŗåćć
|
|
677
|
+
const versionOutput = (result.stderr || "") + (result.stdout || "");
|
|
678
|
+
const versionMatch = versionOutput.match(/version "(\d+)(?:\.(\d+))?/);
|
|
679
|
+
if (!versionMatch) return;
|
|
680
|
+
|
|
681
|
+
const major = parseInt(versionMatch[1]);
|
|
682
|
+
// Java 8 仄å㯠"1.8" å½¢å¼ćJava 9 仄é㯠"17" å½¢å¼
|
|
683
|
+
const effectiveMajor = major === 1 ? parseInt(versionMatch[2] ?? "0") : major;
|
|
684
|
+
|
|
685
|
+
if (effectiveMajor < 11) {
|
|
686
|
+
const versionStr = versionOutput.match(/version "([^"]+)"/)?.[1] ?? "unknown";
|
|
687
|
+
console.error(`\nā OpenAPI Generator requires Java 11 or later.`);
|
|
688
|
+
console.error(` Detected: Java ${versionStr}`);
|
|
689
|
+
if (process.env.JAVA_HOME) {
|
|
690
|
+
console.error(` Current JAVA_HOME: ${process.env.JAVA_HOME}`);
|
|
691
|
+
}
|
|
692
|
+
console.error(` Please set JAVA_HOME to a Java 11+ installation and retry.`);
|
|
693
|
+
console.error(` Example (Windows): $env:JAVA_HOME = "C:\\path\\to\\jdk-17"`);
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
// java ć³ćć³ććč¦ć¤ćććŖćå “å㯠OpenAPI Generator čŖčŗ«ććØć©ć¼ćåŗć
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function runOpenApiGenerator(
|
|
702
|
+
specPath: string,
|
|
703
|
+
outputDir: string,
|
|
704
|
+
backendLanguage: Exclude<BackendLanguage, "typescript">
|
|
705
|
+
): Promise<void> {
|
|
706
|
+
checkJavaVersion();
|
|
707
|
+
const pm = detectFromProject();
|
|
708
|
+
const command = pm === "pnpm" ? "pnpm" : "npx";
|
|
709
|
+
const args = pm === "pnpm"
|
|
710
|
+
? ["exec", "openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)]
|
|
711
|
+
: ["openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)];
|
|
712
|
+
|
|
713
|
+
await new Promise<void>((resolve, reject) => {
|
|
714
|
+
const child = spawn(command, args, {
|
|
715
|
+
cwd: process.cwd(),
|
|
716
|
+
shell: true,
|
|
717
|
+
stdio: getMachineAwareStdio(),
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
child.on("close", (code) => {
|
|
721
|
+
if (code === 0) {
|
|
722
|
+
resolve();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
reject(new Error(`OpenAPI generator exited with code ${code}`));
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
child.on("error", reject);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function getOpenApiGeneratorArgs(
|
|
734
|
+
specPath: string,
|
|
735
|
+
outputDir: string,
|
|
736
|
+
backendLanguage: Exclude<BackendLanguage, "typescript">
|
|
737
|
+
): string[] {
|
|
738
|
+
const globalProperty = backendLanguage === "csharp"
|
|
739
|
+
? "models,apis=false,modelDocs=false,modelTests=false"
|
|
740
|
+
: "models,apis=false,supportingFiles=false,modelDocs=false,modelTests=false";
|
|
741
|
+
|
|
742
|
+
const baseArgs = [
|
|
743
|
+
"generate",
|
|
744
|
+
"-i",
|
|
745
|
+
specPath,
|
|
746
|
+
"-o",
|
|
747
|
+
outputDir,
|
|
748
|
+
"--global-property",
|
|
749
|
+
globalProperty,
|
|
750
|
+
];
|
|
751
|
+
|
|
752
|
+
if (backendLanguage === "csharp") {
|
|
753
|
+
return [
|
|
754
|
+
...baseArgs,
|
|
755
|
+
"-g",
|
|
756
|
+
"csharp",
|
|
757
|
+
"--additional-properties",
|
|
758
|
+
"packageName=SwallowKitBackendModels,targetFramework=net8.0,nullableReferenceTypes=true",
|
|
759
|
+
];
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return [
|
|
763
|
+
...baseArgs,
|
|
764
|
+
"-g",
|
|
765
|
+
"python",
|
|
766
|
+
"--additional-properties",
|
|
767
|
+
"packageName=backend_models,projectName=backend-models",
|
|
768
|
+
];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export function getCSharpSchemaArtifactPruneTargets(outputDir: string): string[] {
|
|
772
|
+
return [
|
|
773
|
+
path.join(outputDir, "src", "SwallowKitBackendModels.Test"),
|
|
774
|
+
path.join(outputDir, "src", "SwallowKitBackendModels", "Api"),
|
|
775
|
+
path.join(outputDir, "src", "SwallowKitBackendModels", "Extensions"),
|
|
776
|
+
];
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function pruneGeneratedCSharpArtifacts(outputDir: string): void {
|
|
780
|
+
for (const target of getCSharpSchemaArtifactPruneTargets(outputDir)) {
|
|
781
|
+
if (fs.existsSync(target)) {
|
|
782
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const clientDir = path.join(outputDir, "src", "SwallowKitBackendModels", "Client");
|
|
787
|
+
if (!fs.existsSync(clientDir)) {
|
|
788
|
+
// --global-property models ć§ćÆClient/ćēęćććŖćććć
|
|
789
|
+
// ć¢ćć«ćä¾åććęå°éć® Option<T> ćčŖåć§ä½ęćć
|
|
790
|
+
fs.mkdirSync(clientDir, { recursive: true });
|
|
791
|
+
fs.writeFileSync(
|
|
792
|
+
path.join(clientDir, "Option.cs"),
|
|
793
|
+
generateMinimalOptionCs(),
|
|
794
|
+
"utf-8"
|
|
795
|
+
);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
for (const entry of fs.readdirSync(clientDir, { withFileTypes: true })) {
|
|
800
|
+
if (!entry.isFile() || entry.name === "Option.cs") {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
fs.rmSync(path.join(clientDir, entry.name), { force: true });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* OpenAPI Generator ć® csharp ćć³ćć¬ć¼ććēęććć¢ćć«ćÆ Option<T> ć«ä¾åćććć
|
|
810
|
+
* supportingFiles ćé¤å¤ćć¦ćććć Client/Option.cs ćēęćććŖćć
|
|
811
|
+
* Polly ēć®äøč¦ćŖä¾åćéæćć¤ć¤ć¢ćć«ćć³ć³ćć¤ć«åÆč½ć«ćććććęå°éć® Option<T> ćęä¾ććć
|
|
812
|
+
*/
|
|
813
|
+
function generateMinimalOptionCs(): string {
|
|
814
|
+
return `// <auto-generated>
|
|
815
|
+
// Minimal Option<T> for OpenAPI Generator model compatibility.
|
|
816
|
+
// Full client supporting files are excluded to avoid Polly version conflicts.
|
|
817
|
+
// </auto-generated>
|
|
818
|
+
|
|
819
|
+
#nullable enable
|
|
820
|
+
|
|
821
|
+
namespace SwallowKitBackendModels.Client
|
|
822
|
+
{
|
|
823
|
+
/// <summary>
|
|
824
|
+
/// A wrapper for nullable/optional properties generated by OpenAPI Generator.
|
|
825
|
+
/// Tracks whether a value has been explicitly set (distinguishing null from absent).
|
|
826
|
+
/// </summary>
|
|
827
|
+
public readonly struct Option<TValue>
|
|
828
|
+
{
|
|
829
|
+
/// <summary>Whether this option has been explicitly set.</summary>
|
|
830
|
+
public bool IsSet { get; }
|
|
831
|
+
|
|
832
|
+
/// <summary>The contained value (may be default if not set).</summary>
|
|
833
|
+
public TValue Value { get; }
|
|
834
|
+
|
|
835
|
+
/// <summary>Create an Option with an explicit value.</summary>
|
|
836
|
+
public Option(TValue value)
|
|
837
|
+
{
|
|
838
|
+
IsSet = true;
|
|
839
|
+
Value = value;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/// <summary>Implicit conversion from Option to its inner value.</summary>
|
|
843
|
+
public static implicit operator TValue(Option<TValue> option) => option.Value;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function updatePythonFunctionRegistrations(functionAppPath: string, registration: string): void {
|
|
850
|
+
if (!fs.existsSync(functionAppPath)) {
|
|
851
|
+
throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const content = fs.readFileSync(functionAppPath, "utf-8");
|
|
855
|
+
|
|
856
|
+
// Check if import line already exists (handles init-generated layout)
|
|
857
|
+
const importLine = registration.split("\n").find((l) => l.startsWith("from ") || l.startsWith("import "));
|
|
858
|
+
if (importLine && content.includes(importLine)) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (content.includes(registration)) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const marker = "# SwallowKit scaffold registrations";
|
|
866
|
+
if (!content.includes(marker)) {
|
|
867
|
+
throw new Error(`Could not find scaffold registration marker in ${functionAppPath}`);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const updated = content.replace(marker, `${registration}\n${marker}`);
|
|
871
|
+
fs.writeFileSync(functionAppPath, updated, "utf-8");
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Next.js BFF API Routes ćēę
|
|
876
|
+
*/
|
|
877
|
+
async function generateBFFRoutes(
|
|
878
|
+
modelInfo: any,
|
|
879
|
+
apiDir: string,
|
|
880
|
+
sharedPackageName: string
|
|
881
|
+
): Promise<void> {
|
|
882
|
+
console.log("\nšØ Generating Next.js BFF API routes...");
|
|
883
|
+
|
|
884
|
+
const modelCamel = modelInfo.name.charAt(0).toLowerCase() + modelInfo.name.slice(1);
|
|
885
|
+
|
|
886
|
+
// List route: app/api/[model]/route.ts
|
|
887
|
+
const listRoutePath = path.join(
|
|
888
|
+
process.cwd(),
|
|
889
|
+
apiDir,
|
|
890
|
+
modelCamel,
|
|
891
|
+
"route.ts"
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
// Detail route: app/api/[model]/[id]/route.ts
|
|
895
|
+
const detailRoutePath = path.join(
|
|
896
|
+
process.cwd(),
|
|
897
|
+
apiDir,
|
|
898
|
+
modelCamel,
|
|
899
|
+
"[id]",
|
|
900
|
+
"route.ts"
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// ćć£ć¬ćÆććŖćä½ę
|
|
904
|
+
const listRouteDir = path.dirname(listRoutePath);
|
|
905
|
+
const detailRouteDir = path.dirname(detailRoutePath);
|
|
906
|
+
|
|
907
|
+
if (!fs.existsSync(listRouteDir)) {
|
|
908
|
+
fs.mkdirSync(listRouteDir, { recursive: true });
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (!fs.existsSync(detailRouteDir)) {
|
|
912
|
+
fs.mkdirSync(detailRouteDir, { recursive: true });
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ć³ć¼ććēę
|
|
916
|
+
const routes = generateCompactBFFRoutes(modelInfo, sharedPackageName);
|
|
917
|
+
|
|
918
|
+
// ćć”ć¤ć«ć«ęøćč¾¼ćæ
|
|
919
|
+
fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
|
|
920
|
+
fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
|
|
921
|
+
|
|
922
|
+
console.log(`ā
Created: ${listRoutePath}`);
|
|
923
|
+
console.log(`ā
Created: ${detailRoutePath}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Generate Cosmos DB container Bicep file
|
|
928
|
+
*/
|
|
929
|
+
async function generateCosmosContainer(modelInfo: any): Promise<void> {
|
|
930
|
+
console.log("\nšļø Generating Cosmos DB container Bicep file...");
|
|
931
|
+
|
|
932
|
+
const modelKebab = toKebabCase(modelInfo.name);
|
|
933
|
+
const modelPascal = toPascalCase(modelInfo.name);
|
|
934
|
+
const cwd = process.cwd();
|
|
935
|
+
|
|
936
|
+
// Check if infra directory exists
|
|
937
|
+
const infraDir = path.join(cwd, "infra");
|
|
938
|
+
if (!fs.existsSync(infraDir)) {
|
|
939
|
+
console.log("ā¹ļø infra directory not found. Skipping Cosmos DB container generation.");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Check if containers directory exists, if not create it
|
|
944
|
+
const containersDir = path.join(infraDir, "containers");
|
|
945
|
+
if (!fs.existsSync(containersDir)) {
|
|
946
|
+
fs.mkdirSync(containersDir, { recursive: true });
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Generate container Bicep file
|
|
950
|
+
const containerFileName = `${modelKebab}-container.bicep`;
|
|
951
|
+
const containerFilePath = path.join(containersDir, containerFileName);
|
|
952
|
+
|
|
953
|
+
const bicepContent = `@description('Cosmos DB account name')
|
|
954
|
+
param cosmosAccountName string
|
|
955
|
+
|
|
956
|
+
@description('Database name')
|
|
957
|
+
param databaseName string
|
|
958
|
+
|
|
959
|
+
@description('Container name')
|
|
960
|
+
param containerName string = '${modelPascal}s'
|
|
961
|
+
|
|
962
|
+
@description('Partition key path')
|
|
963
|
+
param partitionKeyPath string = '${modelInfo.partitionKey}'
|
|
964
|
+
|
|
965
|
+
@description('Throughput (RU/s) - only used for Free Tier')
|
|
966
|
+
param throughput int = 400
|
|
967
|
+
|
|
968
|
+
@description('Cosmos DB mode: freetier or serverless')
|
|
969
|
+
param cosmosDbMode string
|
|
970
|
+
|
|
971
|
+
// Reference existing Cosmos DB account
|
|
972
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
973
|
+
name: cosmosAccountName
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Reference existing database
|
|
977
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
|
|
978
|
+
parent: cosmosAccount
|
|
979
|
+
name: databaseName
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Container for ${modelPascal}
|
|
983
|
+
resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-11-15' = {
|
|
984
|
+
parent: database
|
|
985
|
+
name: containerName
|
|
986
|
+
properties: {
|
|
987
|
+
resource: {
|
|
988
|
+
id: containerName
|
|
989
|
+
partitionKey: {
|
|
990
|
+
paths: [
|
|
991
|
+
partitionKeyPath
|
|
992
|
+
]
|
|
993
|
+
kind: 'Hash'
|
|
994
|
+
}
|
|
995
|
+
indexingPolicy: {
|
|
996
|
+
automatic: true
|
|
997
|
+
indexingMode: 'consistent'
|
|
998
|
+
includedPaths: [
|
|
999
|
+
{
|
|
1000
|
+
path: '/*'
|
|
1001
|
+
}
|
|
1002
|
+
]
|
|
1003
|
+
excludedPaths: [
|
|
1004
|
+
{
|
|
1005
|
+
path: '/_etag/?'
|
|
1006
|
+
}
|
|
1007
|
+
]
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
options: cosmosDbMode == 'freetier' ? {
|
|
1011
|
+
throughput: throughput
|
|
1012
|
+
} : {}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
output containerName string = container.name
|
|
1017
|
+
`;
|
|
1018
|
+
|
|
1019
|
+
// Write Bicep file (overwrite if exists)
|
|
1020
|
+
fs.writeFileSync(containerFilePath, bicepContent, "utf-8");
|
|
1021
|
+
console.log(`ā
Created: ${containerFilePath}`);
|
|
1022
|
+
|
|
1023
|
+
// Update main.bicep to include this container module
|
|
1024
|
+
await updateMainBicepWithContainer(modelKebab, modelPascal);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Update main.bicep to include new container module
|
|
1029
|
+
*/
|
|
1030
|
+
async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string): Promise<void> {
|
|
1031
|
+
const cwd = process.cwd();
|
|
1032
|
+
const mainBicepPath = path.join(cwd, "infra", "main.bicep");
|
|
1033
|
+
|
|
1034
|
+
if (!fs.existsSync(mainBicepPath)) {
|
|
1035
|
+
console.log("ā¹ļø main.bicep not found. Please manually add the container module.");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
let mainBicepContent = fs.readFileSync(mainBicepPath, "utf-8");
|
|
1040
|
+
|
|
1041
|
+
// Check if container module already exists
|
|
1042
|
+
const containerModuleName = `${modelKebab.replace(/-/g, '')}Container`;
|
|
1043
|
+
if (mainBicepContent.includes(`module ${containerModuleName}`)) {
|
|
1044
|
+
console.log(`ā¹ļø Container module '${containerModuleName}' already exists in main.bicep`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Find the position to insert the container module (after cosmosDb modules)
|
|
1049
|
+
// Look for the end of both cosmosDb modules (FreeTier and Serverless)
|
|
1050
|
+
const cosmosModulePattern = /module cosmosDbServerless ['"]modules\/cosmosdb-serverless\.bicep['"] = if \(cosmosDbMode == 'serverless'\) \{[\s\S]*?\n\}/;
|
|
1051
|
+
const cosmosModuleMatch = mainBicepContent.match(cosmosModulePattern);
|
|
1052
|
+
|
|
1053
|
+
if (!cosmosModuleMatch) {
|
|
1054
|
+
console.log("ā ļø Could not find Cosmos DB Serverless module in main.bicep. Please manually add the container module:");
|
|
1055
|
+
console.log(`\nmodule ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
|
|
1056
|
+
name: '${modelKebab}-container'
|
|
1057
|
+
params: {
|
|
1058
|
+
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
1059
|
+
databaseName: databaseName
|
|
1060
|
+
cosmosDbMode: cosmosDbMode
|
|
1061
|
+
}
|
|
1062
|
+
dependsOn: [
|
|
1063
|
+
cosmosDbFreeTier
|
|
1064
|
+
cosmosDbServerless
|
|
1065
|
+
]
|
|
1066
|
+
}\n`);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Find the end of the cosmosDbServerless module
|
|
1071
|
+
const insertPosition = cosmosModuleMatch.index! + cosmosModuleMatch[0].length;
|
|
1072
|
+
|
|
1073
|
+
// Create the container module declaration
|
|
1074
|
+
const containerModule = `
|
|
1075
|
+
|
|
1076
|
+
// ${modelPascal} Container
|
|
1077
|
+
module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
|
|
1078
|
+
name: '${modelKebab}-container'
|
|
1079
|
+
params: {
|
|
1080
|
+
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
1081
|
+
databaseName: '$` + `{projectName}Database'
|
|
1082
|
+
cosmosDbMode: cosmosDbMode
|
|
1083
|
+
}
|
|
1084
|
+
dependsOn: [
|
|
1085
|
+
cosmosDbFreeTier
|
|
1086
|
+
cosmosDbServerless
|
|
1087
|
+
]
|
|
1088
|
+
}`;
|
|
1089
|
+
|
|
1090
|
+
// Insert the module
|
|
1091
|
+
mainBicepContent =
|
|
1092
|
+
mainBicepContent.slice(0, insertPosition) +
|
|
1093
|
+
containerModule +
|
|
1094
|
+
mainBicepContent.slice(insertPosition);
|
|
1095
|
+
|
|
1096
|
+
fs.writeFileSync(mainBicepPath, mainBicepContent, "utf-8");
|
|
1097
|
+
console.log(`ā
Added container module to main.bicep`);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Generate UI components (list, detail, form, create, edit pages)
|
|
1102
|
+
*/
|
|
1103
|
+
async function generateUIComponents(modelInfo: any, sharedPackageName: string, authOptions?: UIAuthOptions): Promise<void> {
|
|
1104
|
+
console.log("\nšØ Generating UI components...");
|
|
1105
|
+
|
|
1106
|
+
const modelKebab = toKebabCase(modelInfo.name);
|
|
1107
|
+
const modelName = modelInfo.name;
|
|
1108
|
+
const cwd = process.cwd();
|
|
1109
|
+
|
|
1110
|
+
// Create directory structure: app/[model]/
|
|
1111
|
+
const modelDir = path.join(cwd, "app", modelKebab);
|
|
1112
|
+
const componentsDir = path.join(modelDir, "_components");
|
|
1113
|
+
const newDir = path.join(modelDir, "new");
|
|
1114
|
+
const idDir = path.join(modelDir, "[id]");
|
|
1115
|
+
const editDir = path.join(idDir, "edit");
|
|
1116
|
+
|
|
1117
|
+
// Create directories
|
|
1118
|
+
[componentsDir, newDir, editDir].forEach(dir => {
|
|
1119
|
+
if (!fs.existsSync(dir)) {
|
|
1120
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// Generate and write files
|
|
1125
|
+
const listPage = generateListPage(modelInfo, sharedPackageName, authOptions);
|
|
1126
|
+
const detailPage = generateDetailPage(modelInfo, sharedPackageName, authOptions);
|
|
1127
|
+
const formComponent = generateFormComponent(modelInfo, sharedPackageName);
|
|
1128
|
+
const newPage = generateNewPage(modelInfo, authOptions);
|
|
1129
|
+
const editPage = generateEditPage(modelInfo, sharedPackageName, authOptions);
|
|
1130
|
+
|
|
1131
|
+
fs.writeFileSync(path.join(modelDir, "page.tsx"), listPage, "utf-8");
|
|
1132
|
+
fs.writeFileSync(path.join(idDir, "page.tsx"), detailPage, "utf-8");
|
|
1133
|
+
fs.writeFileSync(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, "utf-8");
|
|
1134
|
+
fs.writeFileSync(path.join(newDir, "page.tsx"), newPage, "utf-8");
|
|
1135
|
+
fs.writeFileSync(path.join(editDir, "page.tsx"), editPage, "utf-8");
|
|
1136
|
+
|
|
1137
|
+
console.log(`ā
Created: ${path.join(modelDir, "page.tsx")}`);
|
|
1138
|
+
console.log(`ā
Created: ${path.join(idDir, "page.tsx")}`);
|
|
1139
|
+
console.log(`ā
Created: ${path.join(componentsDir, `${modelName}Form.tsx`)}`);
|
|
1140
|
+
console.log(`ā
Created: ${path.join(newDir, "page.tsx")}`);
|
|
1141
|
+
console.log(`ā
Created: ${path.join(editDir, "page.tsx")}`);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Update navigation menu on the homepage
|
|
1146
|
+
*/
|
|
1147
|
+
async function updateNavigationMenu(modelInfo: any): Promise<void> {
|
|
1148
|
+
console.log("\nš Updating navigation menu...");
|
|
1149
|
+
|
|
1150
|
+
const modelKebab = toKebabCase(modelInfo.name);
|
|
1151
|
+
const cwd = process.cwd();
|
|
1152
|
+
|
|
1153
|
+
// Update scaffold config
|
|
1154
|
+
const configPath = path.join(cwd, "lib", "scaffold-config.ts");
|
|
1155
|
+
|
|
1156
|
+
if (!fs.existsSync(configPath)) {
|
|
1157
|
+
console.log("ā ļø scaffold-config.ts not found. Skipping navigation menu update.");
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Read existing config
|
|
1162
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
1163
|
+
|
|
1164
|
+
// Parse models array - extract each model entry
|
|
1165
|
+
const models: Array<{ name: string; path: string; label: string }> = [];
|
|
1166
|
+
const modelsMatch = configContent.match(/models:\s*\[([\s\S]*?)\]\s*as\s*ScaffoldModel\[\]/);
|
|
1167
|
+
|
|
1168
|
+
if (modelsMatch) {
|
|
1169
|
+
const modelsArrayContent = modelsMatch[1];
|
|
1170
|
+
// Match each object in the array: { name: '...', path: '...', label: '...' }
|
|
1171
|
+
const modelPattern = /\{\s*name:\s*['"]([^'"]+)['"]\s*,\s*path:\s*['"]([^'"]+)['"]\s*,\s*label:\s*['"]([^'"]+)['"]\s*\}/g;
|
|
1172
|
+
let match;
|
|
1173
|
+
while ((match = modelPattern.exec(modelsArrayContent)) !== null) {
|
|
1174
|
+
models.push({
|
|
1175
|
+
name: match[1],
|
|
1176
|
+
path: match[2],
|
|
1177
|
+
label: match[3]
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Check if model already exists
|
|
1183
|
+
if (models.find(m => m.name === modelInfo.name)) {
|
|
1184
|
+
console.log(`ā¹ļø ${modelInfo.name} already in navigation menu`);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Add new model
|
|
1189
|
+
models.push({
|
|
1190
|
+
name: modelInfo.name,
|
|
1191
|
+
path: `/${modelKebab}`,
|
|
1192
|
+
label: modelInfo.displayName
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
// Generate new scaffold-config.ts content
|
|
1196
|
+
const newConfigContent = `export interface ScaffoldModel {
|
|
1197
|
+
name: string;
|
|
1198
|
+
path: string;
|
|
1199
|
+
label: string;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
export const scaffoldConfig = {
|
|
1203
|
+
models: [
|
|
1204
|
+
${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label}' },`).join('\n')}
|
|
1205
|
+
] as ScaffoldModel[]
|
|
1206
|
+
};
|
|
1207
|
+
`;
|
|
1208
|
+
|
|
1209
|
+
fs.writeFileSync(configPath, newConfigContent, "utf-8");
|
|
1210
|
+
console.log(`ā
Added ${modelInfo.name} to navigation menu`);
|
|
1211
|
+
}
|