swallowkit 2.0.0-alpha.3 → 2.0.0-alpha.5
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/dist/cli/commands/add-auth.js +37 -6
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/init.d.ts +6 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +19 -5
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +2 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +45 -16
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +85 -3
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +29 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +19 -1
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +62 -35
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +2 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +29 -5
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.js +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +23 -5
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +18 -19
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +289 -76
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +42 -17
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +154 -15
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +44 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +164 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-loop-phase34.test.ts +124 -0
- package/src/__tests__/azure-cli.test.ts +199 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +64 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/plan-apply.test.ts +170 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/init.ts +23 -8
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +53 -17
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +28 -5
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-operations.ts +86 -42
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-templates/AGENTS.md +67 -6
- package/src/core/project/agent-templates/CLAUDE.md +7 -1
- package/src/core/project/agent-templates/copilot-instructions.md +7 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +32 -5
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +1 -1
- package/src/core/project/workflows.ts +23 -5
- package/src/core/scaffold/auth-generator.ts +18 -19
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +313 -78
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +45 -20
- package/src/mcp/index.ts +173 -18
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +154 -0
- package/src/utils/semver.ts +59 -0
|
@@ -5,7 +5,7 @@ import { spawn, spawnSync } from "child_process";
|
|
|
5
5
|
import { BackendLanguage } from "../../types";
|
|
6
6
|
import { FileOperationSession } from "../operations/file-session";
|
|
7
7
|
import { ModelInfo, toKebabCase } from "./model-parser";
|
|
8
|
-
import { generateOpenApiDocument } from "./openapi-generator";
|
|
8
|
+
import { expandInlineObjectModels, generateOpenApiDocument } from "./openapi-generator";
|
|
9
9
|
import {
|
|
10
10
|
buildProjectLocalUvInstallerEnv,
|
|
11
11
|
buildUvPipInstallArgs,
|
|
@@ -154,8 +154,22 @@ function csharpStringLiteral(value: string): string {
|
|
|
154
154
|
return JSON.stringify(value);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
/** Date/time types are used only when the Zod schema declares them (z.iso.datetime(), z.iso.date(), z.date()). */
|
|
158
|
+
function getCSharpScalarType(field: ModelInfo["fields"][number]): string {
|
|
159
|
+
switch (field.type) {
|
|
160
|
+
case "string":
|
|
161
|
+
return field.format === "date-time" ? "DateTime" : field.format === "date" ? "DateOnly" : "string";
|
|
162
|
+
case "number":
|
|
163
|
+
return field.isInteger ? "long" : "double";
|
|
164
|
+
case "boolean":
|
|
165
|
+
return "bool";
|
|
166
|
+
case "date":
|
|
167
|
+
return "DateTime";
|
|
168
|
+
case "object":
|
|
169
|
+
return "Dictionary<string, object>";
|
|
170
|
+
default:
|
|
171
|
+
return "object";
|
|
172
|
+
}
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
export function getCSharpSchemaModelPath(outputDir: string, modelName: string): string {
|
|
@@ -211,6 +225,11 @@ export function getPythonNativeGeneratorArgs(specPath: string, outputPath: strin
|
|
|
211
225
|
}
|
|
212
226
|
|
|
213
227
|
function getCSharpFieldBaseType(field: ModelInfo["fields"][number]): string {
|
|
228
|
+
// Arrays first: arrays of nested models or enums are lists of the element type.
|
|
229
|
+
if (field.isArray) {
|
|
230
|
+
return `List<${getCSharpArrayElementType(field)}>`;
|
|
231
|
+
}
|
|
232
|
+
|
|
214
233
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
215
234
|
return field.nestedModelName;
|
|
216
235
|
}
|
|
@@ -219,24 +238,7 @@ function getCSharpFieldBaseType(field: ModelInfo["fields"][number]): string {
|
|
|
219
238
|
return `${toPascalIdentifier(field.name)}Enum`;
|
|
220
239
|
}
|
|
221
240
|
|
|
222
|
-
|
|
223
|
-
return `List<${getCSharpArrayElementType(field)}>`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
switch (field.type) {
|
|
227
|
-
case "string":
|
|
228
|
-
return isDateLikeField(field) ? "DateTime" : "string";
|
|
229
|
-
case "number":
|
|
230
|
-
return "decimal";
|
|
231
|
-
case "boolean":
|
|
232
|
-
return "bool";
|
|
233
|
-
case "date":
|
|
234
|
-
return "DateTime";
|
|
235
|
-
case "object":
|
|
236
|
-
return "Dictionary<string, object>";
|
|
237
|
-
default:
|
|
238
|
-
return "object";
|
|
239
|
-
}
|
|
241
|
+
return getCSharpScalarType(field);
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
function getCSharpArrayElementType(field: ModelInfo["fields"][number]): string {
|
|
@@ -248,25 +250,12 @@ function getCSharpArrayElementType(field: ModelInfo["fields"][number]): string {
|
|
|
248
250
|
return `${toPascalIdentifier(field.name)}Enum`;
|
|
249
251
|
}
|
|
250
252
|
|
|
251
|
-
|
|
252
|
-
case "string":
|
|
253
|
-
return isDateLikeField(field) ? "DateTime" : "string";
|
|
254
|
-
case "number":
|
|
255
|
-
return "decimal";
|
|
256
|
-
case "boolean":
|
|
257
|
-
return "bool";
|
|
258
|
-
case "date":
|
|
259
|
-
return "DateTime";
|
|
260
|
-
case "object":
|
|
261
|
-
return "Dictionary<string, object>";
|
|
262
|
-
default:
|
|
263
|
-
return "object";
|
|
264
|
-
}
|
|
253
|
+
return getCSharpScalarType(field);
|
|
265
254
|
}
|
|
266
255
|
|
|
267
256
|
function getCSharpPropertyType(field: ModelInfo["fields"][number]): string {
|
|
268
257
|
const baseType = getCSharpFieldBaseType(field);
|
|
269
|
-
return field.isOptional ? `${baseType}?` : baseType;
|
|
258
|
+
return field.isOptional || field.isNullable ? `${baseType}?` : baseType;
|
|
270
259
|
}
|
|
271
260
|
|
|
272
261
|
function getCSharpOptionType(field: ModelInfo["fields"][number]): string {
|
|
@@ -333,12 +322,120 @@ function buildCSharpEnumToJsonCases(field: ModelInfo["fields"][number], members:
|
|
|
333
322
|
.join("\n\n");
|
|
334
323
|
}
|
|
335
324
|
|
|
325
|
+
function csharpNumberLiteral(value: number): string {
|
|
326
|
+
return Number.isInteger(value) ? `${value}` : `${value}d`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function csharpRegexOptions(flags: string | undefined): string {
|
|
330
|
+
const options = [
|
|
331
|
+
flags?.includes("i") ? "RegexOptions.IgnoreCase" : null,
|
|
332
|
+
flags?.includes("m") ? "RegexOptions.Multiline" : null,
|
|
333
|
+
flags?.includes("s") ? "RegexOptions.Singleline" : null,
|
|
334
|
+
].filter(Boolean);
|
|
335
|
+
return options.length > 0 ? options.join(" | ") : "RegexOptions.None";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isCSharpReferenceType(field: ModelInfo["fields"][number]): boolean {
|
|
339
|
+
if (field.isArray || field.isNestedSchema || field.type === "object") return true;
|
|
340
|
+
if (field.enumValues?.length) return false;
|
|
341
|
+
return field.type === "string" && field.format !== "date-time" && field.format !== "date";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Translate Zod constraints into IValidatableObject checks. Regexes use search semantics like
|
|
346
|
+
* Zod's RegExp.test. String formats (email/uuid/url) and value-type presence stay with Zod.
|
|
347
|
+
*/
|
|
348
|
+
function buildCSharpValueChecks(
|
|
349
|
+
field: ModelInfo["fields"][number],
|
|
350
|
+
accessor: string,
|
|
351
|
+
member: string,
|
|
352
|
+
optionalValue: boolean
|
|
353
|
+
): string[] {
|
|
354
|
+
const constraints = field.constraints ?? {};
|
|
355
|
+
const checks: string[] = [];
|
|
356
|
+
const add = (condition: string, message: string) =>
|
|
357
|
+
checks.push(`if (${condition})\n yield return new ValidationResult(${csharpStringLiteral(message)}, new[] { ${member} });`);
|
|
358
|
+
|
|
359
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
360
|
+
// Pattern variables in an if condition stay in scope for the enclosing block, so keep names unique.
|
|
361
|
+
const variable = `validated${toPascalIdentifier(field.name)}${accessor === "item" ? "Item" : ""}`;
|
|
362
|
+
checks.push(`if (${accessor} is IValidatableObject ${variable})\n foreach (var ${variable}Result in ${variable}.Validate(new ValidationContext(${accessor})))\n yield return new ValidationResult(${variable}Result.ErrorMessage, ${variable}Result.MemberNames.Select(name => ${member} + "." + name).ToArray());`);
|
|
363
|
+
return checks;
|
|
364
|
+
}
|
|
365
|
+
if (field.enumValues?.length) return checks;
|
|
366
|
+
|
|
367
|
+
if (field.type === "string" && field.format !== "date-time" && field.format !== "date") {
|
|
368
|
+
if (constraints.minLength !== undefined) add(`${accessor} != null && ${accessor}.Length < ${constraints.minLength}`, `${field.name} must contain at least ${constraints.minLength} character(s).`);
|
|
369
|
+
if (constraints.maxLength !== undefined) add(`${accessor} != null && ${accessor}.Length > ${constraints.maxLength}`, `${field.name} must contain at most ${constraints.maxLength} character(s).`);
|
|
370
|
+
if (constraints.pattern !== undefined) {
|
|
371
|
+
add(
|
|
372
|
+
`${accessor} != null && !Regex.IsMatch(${accessor}, ${csharpStringLiteral(constraints.pattern)}, ${csharpRegexOptions(constraints.patternFlags)})`,
|
|
373
|
+
`${field.name} must match /${constraints.pattern}/${constraints.patternFlags ?? ""}.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (field.type === "number") {
|
|
379
|
+
const value = optionalValue ? `${accessor}.Value` : accessor;
|
|
380
|
+
const guard = optionalValue ? `${accessor}.HasValue && ` : "";
|
|
381
|
+
if (constraints.minimum !== undefined) {
|
|
382
|
+
add(`${guard}${value} ${constraints.exclusiveMinimum ? "<=" : "<"} ${csharpNumberLiteral(constraints.minimum)}`,
|
|
383
|
+
`${field.name} must be ${constraints.exclusiveMinimum ? "greater than" : "greater than or equal to"} ${constraints.minimum}.`);
|
|
384
|
+
}
|
|
385
|
+
if (constraints.maximum !== undefined) {
|
|
386
|
+
add(`${guard}${value} ${constraints.exclusiveMaximum ? ">=" : ">"} ${csharpNumberLiteral(constraints.maximum)}`,
|
|
387
|
+
`${field.name} must be ${constraints.exclusiveMaximum ? "less than" : "less than or equal to"} ${constraints.maximum}.`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return checks;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildCSharpValidationBody(model: ModelInfo): string {
|
|
394
|
+
const blocks: string[] = [];
|
|
395
|
+
for (const field of model.fields) {
|
|
396
|
+
const propertyName = toPascalIdentifier(field.name);
|
|
397
|
+
const member = csharpStringLiteral(field.name);
|
|
398
|
+
const constraints = field.constraints ?? {};
|
|
399
|
+
|
|
400
|
+
// .nullable() fields accept null in Zod, so only non-nullable required references are rejected.
|
|
401
|
+
if (!field.isOptional && !field.isNullable && isCSharpReferenceType(field)) {
|
|
402
|
+
blocks.push(`if (${propertyName} is null)\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} is required.`)}, new[] { ${member} });`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (field.isArray) {
|
|
406
|
+
if (constraints.minItems !== undefined) {
|
|
407
|
+
blocks.push(`if (${propertyName} != null && ${propertyName}.Count < ${constraints.minItems})\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} must contain at least ${constraints.minItems} item(s).`)}, new[] { ${member} });`);
|
|
408
|
+
}
|
|
409
|
+
if (constraints.maxItems !== undefined) {
|
|
410
|
+
blocks.push(`if (${propertyName} != null && ${propertyName}.Count > ${constraints.maxItems})\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} must contain at most ${constraints.maxItems} item(s).`)}, new[] { ${member} });`);
|
|
411
|
+
}
|
|
412
|
+
const itemChecks = buildCSharpValueChecks(
|
|
413
|
+
{ ...field, isArray: false },
|
|
414
|
+
"item",
|
|
415
|
+
`${member} + "[" + index + "]"`,
|
|
416
|
+
false
|
|
417
|
+
);
|
|
418
|
+
if (itemChecks.length > 0) {
|
|
419
|
+
blocks.push(`if (${propertyName} != null)\n {\n for (var index = 0; index < ${propertyName}.Count; index++)\n {\n var item = ${propertyName}[index];\n${itemChecks.map((check) => ` ${check.replace(/\n {16}/g, "\n ").replace(/\n {20}/g, "\n ")}`).join("\n")}\n }\n }`);
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const optionalValue = (field.isOptional || field.isNullable === true) && !isCSharpReferenceType(field);
|
|
425
|
+
blocks.push(...buildCSharpValueChecks(field, propertyName, member, optionalValue));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return blocks.length > 0
|
|
429
|
+
? blocks.map((block) => ` ${block}`).join("\n\n")
|
|
430
|
+
: " yield break;";
|
|
431
|
+
}
|
|
432
|
+
|
|
336
433
|
function generateLegacyCompatibleCSharpModelSource(model: ModelInfo): string {
|
|
337
434
|
const requiredFields = model.fields.filter((field) => !field.isOptional);
|
|
338
435
|
const optionalFields = model.fields.filter((field) => field.isOptional);
|
|
339
436
|
|
|
340
437
|
const constructorParams = [
|
|
341
|
-
...requiredFields.map((field) => `${
|
|
438
|
+
...requiredFields.map((field) => `${getCSharpPropertyType(field)} ${field.name}`),
|
|
342
439
|
...optionalFields.map((field) => `${getCSharpOptionType(field)} ${field.name} = default`),
|
|
343
440
|
].join(", ");
|
|
344
441
|
|
|
@@ -356,7 +453,7 @@ function generateLegacyCompatibleCSharpModelSource(model: ModelInfo): string {
|
|
|
356
453
|
return ` /// <summary>
|
|
357
454
|
/// Defines ${toPascalIdentifier(field.name)}
|
|
358
455
|
/// </summary>
|
|
359
|
-
[JsonConverter(typeof(
|
|
456
|
+
[JsonConverter(typeof(${enumType}JsonConverter))]
|
|
360
457
|
public enum ${enumType}
|
|
361
458
|
{
|
|
362
459
|
${buildCSharpEnumMembers(enumMembers)}
|
|
@@ -377,6 +474,22 @@ ${buildCSharpEnumFromStringCases(field, enumMembers, true)}
|
|
|
377
474
|
${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
378
475
|
|
|
379
476
|
throw new NotImplementedException($"Value could not be handled: '{value}'");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/// <summary>
|
|
480
|
+
/// Reads and writes ${enumType} with the exact Zod enum values.
|
|
481
|
+
/// </summary>
|
|
482
|
+
public sealed class ${enumType}JsonConverter : JsonConverter<${enumType}>
|
|
483
|
+
{
|
|
484
|
+
public override ${enumType} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
485
|
+
{
|
|
486
|
+
var value = reader.GetString();
|
|
487
|
+
var parsed = value == null ? null : ${enumType}FromStringOrDefault(value);
|
|
488
|
+
return parsed ?? throw new JsonException($"Unsupported ${enumType} value: '{value}'");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
public override void Write(Utf8JsonWriter writer, ${enumType} value, JsonSerializerOptions options)
|
|
492
|
+
=> writer.WriteStringValue(${enumType}ToJsonValue(value));
|
|
380
493
|
}`;
|
|
381
494
|
})
|
|
382
495
|
.join("\n\n");
|
|
@@ -404,7 +517,8 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
|
404
517
|
/// <summary>
|
|
405
518
|
/// Gets or Sets ${propertyName}
|
|
406
519
|
/// </summary>
|
|
407
|
-
[JsonPropertyName("${field.name}")]
|
|
520
|
+
[JsonPropertyName("${field.name}")]${field.isNullable ? "" : `
|
|
521
|
+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`}
|
|
408
522
|
public ${propertyType} ${propertyName} { get { return this.${propertyName}Option; } set { this.${propertyName}Option = new(value); } }`;
|
|
409
523
|
})
|
|
410
524
|
.join("\n\n");
|
|
@@ -428,8 +542,11 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
|
428
542
|
using System;
|
|
429
543
|
using System.Collections.Generic;
|
|
430
544
|
using System.ComponentModel.DataAnnotations;
|
|
545
|
+
using System.Linq;
|
|
431
546
|
using System.Text;
|
|
547
|
+
using System.Text.Json;
|
|
432
548
|
using System.Text.Json.Serialization;
|
|
549
|
+
using System.Text.RegularExpressions;
|
|
433
550
|
using SwallowKitBackendModels.Client;
|
|
434
551
|
|
|
435
552
|
namespace SwallowKitBackendModels.Model
|
|
@@ -439,12 +556,22 @@ namespace SwallowKitBackendModels.Model
|
|
|
439
556
|
/// </summary>
|
|
440
557
|
public partial class ${model.name} : IValidatableObject
|
|
441
558
|
{
|
|
559
|
+
/// <summary>
|
|
560
|
+
/// Used by System.Text.Json. Optional values are marked as set only when present in the JSON.
|
|
561
|
+
/// </summary>
|
|
562
|
+
#pragma warning disable CS8618
|
|
442
563
|
[JsonConstructor]
|
|
564
|
+
public ${model.name}()
|
|
565
|
+
{
|
|
566
|
+
OnCreated();
|
|
567
|
+
}
|
|
568
|
+
#pragma warning restore CS8618
|
|
569
|
+
${constructorParams ? `
|
|
443
570
|
public ${model.name}(${constructorParams})
|
|
444
571
|
{
|
|
445
572
|
${constructorAssignments}
|
|
446
573
|
}
|
|
447
|
-
|
|
574
|
+
` : ""}
|
|
448
575
|
partial void OnCreated();
|
|
449
576
|
${enumBlocks ? `\n\n${enumBlocks}` : ""}
|
|
450
577
|
|
|
@@ -461,27 +588,19 @@ ${toStringBody}
|
|
|
461
588
|
|
|
462
589
|
IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
|
|
463
590
|
{
|
|
464
|
-
|
|
591
|
+
${buildCSharpValidationBody(model)}
|
|
465
592
|
}
|
|
466
593
|
}
|
|
467
594
|
}
|
|
468
595
|
`;
|
|
469
596
|
}
|
|
470
597
|
|
|
471
|
-
function
|
|
472
|
-
if (field.isNestedSchema && field.nestedModelName) {
|
|
473
|
-
return field.nestedModelName;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (field.isArray) {
|
|
477
|
-
return `List[${getPythonArrayElementType(field)}]`;
|
|
478
|
-
}
|
|
479
|
-
|
|
598
|
+
function getPythonScalarType(field: ModelInfo["fields"][number]): string {
|
|
480
599
|
switch (field.type) {
|
|
481
600
|
case "string":
|
|
482
|
-
return
|
|
601
|
+
return field.format === "date-time" ? "datetime" : field.format === "date" ? "date" : "StrictStr";
|
|
483
602
|
case "number":
|
|
484
|
-
return "Union[StrictFloat, StrictInt]";
|
|
603
|
+
return field.isInteger ? "StrictInt" : "Union[StrictFloat, StrictInt]";
|
|
485
604
|
case "boolean":
|
|
486
605
|
return "StrictBool";
|
|
487
606
|
case "date":
|
|
@@ -493,43 +612,137 @@ function getPythonTypeName(field: ModelInfo["fields"][number]): string {
|
|
|
493
612
|
}
|
|
494
613
|
}
|
|
495
614
|
|
|
615
|
+
/** pydantic Field bounds for a scalar value (string length or numeric range). */
|
|
616
|
+
function getPythonValueConstraintArgs(field: ModelInfo["fields"][number]): string[] {
|
|
617
|
+
const constraints = field.constraints ?? {};
|
|
618
|
+
if (field.enumValues?.length || field.isNestedSchema) return [];
|
|
619
|
+
const args: string[] = [];
|
|
620
|
+
if (field.type === "string" && field.format !== "date-time" && field.format !== "date") {
|
|
621
|
+
if (constraints.minLength !== undefined) args.push(`min_length=${constraints.minLength}`);
|
|
622
|
+
if (constraints.maxLength !== undefined) args.push(`max_length=${constraints.maxLength}`);
|
|
623
|
+
}
|
|
624
|
+
if (field.type === "number") {
|
|
625
|
+
if (constraints.minimum !== undefined) args.push(`${constraints.exclusiveMinimum ? "gt" : "ge"}=${constraints.minimum}`);
|
|
626
|
+
if (constraints.maximum !== undefined) args.push(`${constraints.exclusiveMaximum ? "lt" : "le"}=${constraints.maximum}`);
|
|
627
|
+
}
|
|
628
|
+
return args;
|
|
629
|
+
}
|
|
630
|
+
|
|
496
631
|
function getPythonArrayElementType(field: ModelInfo["fields"][number]): string {
|
|
497
632
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
498
633
|
return field.nestedModelName;
|
|
499
634
|
}
|
|
500
635
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
return "Any";
|
|
636
|
+
const elementType = getPythonScalarType(field);
|
|
637
|
+
const elementArgs = getPythonValueConstraintArgs(field);
|
|
638
|
+
return elementArgs.length > 0 ? `Annotated[${elementType}, Field(${elementArgs.join(", ")})]` : elementType;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function getPythonTypeName(field: ModelInfo["fields"][number]): string {
|
|
642
|
+
if (field.isArray) {
|
|
643
|
+
return `List[${getPythonArrayElementType(field)}]`;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
647
|
+
return field.nestedModelName;
|
|
514
648
|
}
|
|
649
|
+
|
|
650
|
+
return getPythonScalarType(field);
|
|
515
651
|
}
|
|
516
652
|
|
|
517
653
|
function buildPythonFieldDeclaration(field: ModelInfo["fields"][number]): string {
|
|
518
654
|
const pythonName = toSnakeCase(field.name);
|
|
519
655
|
const typeName = field.isOptional ? `Optional[${getPythonTypeName(field)}]` : getPythonTypeName(field);
|
|
520
|
-
const
|
|
656
|
+
const fieldArgs = [
|
|
657
|
+
...(field.isOptional ? ["default=None"] : []),
|
|
658
|
+
...(pythonName !== field.name ? [`alias="${field.name}"`] : []),
|
|
659
|
+
...(field.isArray
|
|
660
|
+
? [
|
|
661
|
+
...(field.constraints?.minItems !== undefined ? [`min_length=${field.constraints.minItems}`] : []),
|
|
662
|
+
...(field.constraints?.maxItems !== undefined ? [`max_length=${field.constraints.maxItems}`] : []),
|
|
663
|
+
]
|
|
664
|
+
: getPythonValueConstraintArgs(field)),
|
|
665
|
+
];
|
|
521
666
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
667
|
+
return fieldArgs.length > 0
|
|
668
|
+
? `${pythonName}: ${typeName} = Field(${fieldArgs.join(", ")})`
|
|
669
|
+
: `${pythonName}: ${typeName}`;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function buildPythonRegexFlags(flags: string | undefined): string {
|
|
673
|
+
const values = [
|
|
674
|
+
flags?.includes("i") ? "re.IGNORECASE" : null,
|
|
675
|
+
flags?.includes("m") ? "re.MULTILINE" : null,
|
|
676
|
+
flags?.includes("s") ? "re.DOTALL" : null,
|
|
677
|
+
].filter(Boolean);
|
|
678
|
+
return values.length > 0 ? values.join(" | ") : "0";
|
|
679
|
+
}
|
|
525
680
|
|
|
526
|
-
|
|
527
|
-
|
|
681
|
+
export type PythonRegexTranslation = { pattern: string } | { unsupported: string };
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Translates a JavaScript regex (as used by Zod) to Python `re` syntax. Constructs Python cannot
|
|
685
|
+
* express identically are reported as unsupported so the validator is skipped instead of breaking imports.
|
|
686
|
+
*/
|
|
687
|
+
export function toPythonRegex(pattern: string, flags = ""): PythonRegexTranslation {
|
|
688
|
+
const unsupportedFlags = [...flags].filter((flag) => !"imsgu".includes(flag));
|
|
689
|
+
if (unsupportedFlags.length > 0) return { unsupported: `flag(s) ${unsupportedFlags.join("")} have no Python equivalent` };
|
|
690
|
+
const checks: Array<[RegExp, string]> = [
|
|
691
|
+
[/\\[pP]\{/, "Unicode property escapes (\\p{...})"],
|
|
692
|
+
[/\\u\{/, "code point escapes (\\u{...})"],
|
|
693
|
+
[/\\c[A-Za-z]/, "control escapes (\\cX)"],
|
|
694
|
+
[/\(\?<[=!]/, "lookbehind (JavaScript allows variable width; Python does not)"],
|
|
695
|
+
[/\[\^?\]/, "empty character classes ([] / [^])"],
|
|
696
|
+
];
|
|
697
|
+
for (const [construct, label] of checks) {
|
|
698
|
+
if (construct.test(pattern)) return { unsupported: label };
|
|
528
699
|
}
|
|
700
|
+
return {
|
|
701
|
+
pattern: pattern
|
|
702
|
+
.replace(/\(\?<([A-Za-z_][A-Za-z0-9_]*)>/g, "(?P<$1>")
|
|
703
|
+
.replace(/\\k<([A-Za-z_][A-Za-z0-9_]*)>/g, "(?P=$1)"),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function pythonRegexFields(model: ModelInfo): Array<{ field: ModelInfo["fields"][number]; translation: PythonRegexTranslation }> {
|
|
708
|
+
return model.fields
|
|
709
|
+
.filter((field) => field.type === "string" && !field.enumValues?.length && field.constraints?.pattern !== undefined)
|
|
710
|
+
.map((field) => ({ field, translation: toPythonRegex(field.constraints!.pattern!, field.constraints!.patternFlags) }));
|
|
711
|
+
}
|
|
529
712
|
|
|
530
|
-
|
|
713
|
+
export function getPythonRegexWarnings(model: ModelInfo): string[] {
|
|
714
|
+
return pythonRegexFields(model)
|
|
715
|
+
.filter((entry): entry is { field: ModelInfo["fields"][number]; translation: { unsupported: string } } => "unsupported" in entry.translation)
|
|
716
|
+
.map(({ field, translation }) =>
|
|
717
|
+
`python-regex-not-enforced: ${model.name}.${field.name} uses ${translation.unsupported}; the Python model does not enforce /${field.constraints!.pattern}/. The BFF Zod schema still validates it.`);
|
|
531
718
|
}
|
|
532
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Zod regex checks use RegExp.test (search), so validators use re.search instead of pydantic's anchored pattern.
|
|
722
|
+
* They run before type conversion so date/time strings are checked as the JSON text Zod sees; non-string
|
|
723
|
+
* values are left to the field's own type validation.
|
|
724
|
+
*/
|
|
725
|
+
function buildPythonPatternValidators(model: ModelInfo): string {
|
|
726
|
+
return pythonRegexFields(model)
|
|
727
|
+
.filter((entry): entry is { field: ModelInfo["fields"][number]; translation: { pattern: string } } => "pattern" in entry.translation)
|
|
728
|
+
.map(({ field, translation }) => {
|
|
729
|
+
const pythonName = toSnakeCase(field.name);
|
|
730
|
+
const pattern = JSON.stringify(translation.pattern);
|
|
731
|
+
const flags = buildPythonRegexFlags(field.constraints!.patternFlags);
|
|
732
|
+
const message = JSON.stringify(`${field.name} must match /${field.constraints!.pattern}/${field.constraints!.patternFlags ?? ""}`);
|
|
733
|
+
return ` @field_validator('${pythonName}', mode='before')
|
|
734
|
+
def ${pythonName}_validate_pattern(cls, value):
|
|
735
|
+
"""Validates the Zod regex constraint"""
|
|
736
|
+
if value is None:
|
|
737
|
+
return value
|
|
738
|
+
|
|
739
|
+
for item in (value if isinstance(value, list) else [value]):
|
|
740
|
+
if isinstance(item, str) and not re.search(${pattern}, item, ${flags}):
|
|
741
|
+
raise ValueError(${message})
|
|
742
|
+
return value`;
|
|
743
|
+
})
|
|
744
|
+
.join("\n\n");
|
|
745
|
+
}
|
|
533
746
|
function buildPythonEnumValidators(model: ModelInfo): string {
|
|
534
747
|
return model.fields
|
|
535
748
|
.filter((field) => field.enumValues?.length)
|
|
@@ -566,7 +779,7 @@ function buildPythonModelImports(model: ModelInfo): string {
|
|
|
566
779
|
function generateLegacyCompatiblePythonModelSource(model: ModelInfo): string {
|
|
567
780
|
const fieldDeclarations = model.fields.map((field) => ` ${buildPythonFieldDeclaration(field)}`).join("\n");
|
|
568
781
|
const propertyNames = model.fields.map((field) => `"${field.name}"`).join(", ");
|
|
569
|
-
const validators = buildPythonEnumValidators(model);
|
|
782
|
+
const validators = [buildPythonEnumValidators(model), buildPythonPatternValidators(model)].filter(Boolean).join("\n\n");
|
|
570
783
|
const dictAssignments = model.fields
|
|
571
784
|
.map((field) => ` "${field.name}": obj.get("${field.name}")`)
|
|
572
785
|
.join(",\n");
|
|
@@ -590,9 +803,9 @@ import pprint
|
|
|
590
803
|
import re # noqa: F401
|
|
591
804
|
import json
|
|
592
805
|
|
|
593
|
-
from datetime import datetime
|
|
806
|
+
from datetime import date, datetime
|
|
594
807
|
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, field_validator
|
|
595
|
-
from typing import Any, ClassVar, Dict, List, Optional, Set, Union
|
|
808
|
+
from typing import Annotated, Any, ClassVar, Dict, List, Optional, Set, Union
|
|
596
809
|
from typing_extensions import Self
|
|
597
810
|
|
|
598
811
|
${buildPythonModelImports(model)}class ${model.name}(BaseModel):
|
|
@@ -616,7 +829,7 @@ ${validators ? `\n\n${validators}` : ""}
|
|
|
616
829
|
|
|
617
830
|
def to_json(self) -> str:
|
|
618
831
|
"""Returns the JSON representation of the model using alias"""
|
|
619
|
-
return
|
|
832
|
+
return self.model_dump_json(by_alias=True, exclude_none=True)
|
|
620
833
|
|
|
621
834
|
@classmethod
|
|
622
835
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
|
@@ -726,6 +939,21 @@ function ensurePythonCodegenProjectFiles(
|
|
|
726
939
|
return requirementsPath;
|
|
727
940
|
}
|
|
728
941
|
|
|
942
|
+
/** Generated backend_models import pydantic v2 at runtime, so the deployed Functions app must install it. */
|
|
943
|
+
export const PYTHON_SCHEMA_RUNTIME_REQUIREMENT = "pydantic>=2.6,<3";
|
|
944
|
+
|
|
945
|
+
function ensurePythonRuntimeRequirement(functionsRoot: string, session: FileOperationSession): void {
|
|
946
|
+
const requirementsPath = path.join(functionsRoot, "requirements.txt");
|
|
947
|
+
const current = session.fileExists(requirementsPath) ? session.readFile(requirementsPath) : "";
|
|
948
|
+
const declared = current.split(/\r?\n/).some((line) => line.trim().split(/[<>=!~;[ ]/)[0].toLowerCase() === "pydantic");
|
|
949
|
+
if (declared) return;
|
|
950
|
+
const prefix = current && !current.endsWith("\n") ? `${current}\n` : current;
|
|
951
|
+
session.writeFile(requirementsPath, `${prefix}${PYTHON_SCHEMA_RUNTIME_REQUIREMENT}\n`, {
|
|
952
|
+
ownership: "extension-point",
|
|
953
|
+
generator: "native-schema",
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
729
957
|
async function ensureProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
|
|
730
958
|
const uvEnv = { ...process.env };
|
|
731
959
|
const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
|
|
@@ -869,6 +1097,7 @@ async function generatePythonSchemaArtifacts(
|
|
|
869
1097
|
sourceModel: string
|
|
870
1098
|
): Promise<void> {
|
|
871
1099
|
ensurePythonCodegenProjectFiles(functionsRoot, session, sourceModel);
|
|
1100
|
+
ensurePythonRuntimeRequirement(functionsRoot, session);
|
|
872
1101
|
|
|
873
1102
|
const packageRoot = path.join(outputDir, "backend_models");
|
|
874
1103
|
const modelsRoot = path.join(packageRoot, "models");
|
|
@@ -886,6 +1115,10 @@ async function generatePythonSchemaArtifacts(
|
|
|
886
1115
|
);
|
|
887
1116
|
|
|
888
1117
|
for (const model of models) {
|
|
1118
|
+
for (const warning of getPythonRegexWarnings(model)) {
|
|
1119
|
+
session.addWarning(warning);
|
|
1120
|
+
console.warn(`⚠️ ${warning}`);
|
|
1121
|
+
}
|
|
889
1122
|
const modelPath = getPythonSchemaModelPath(outputDir, model.name);
|
|
890
1123
|
session.writeFile(modelPath, generateLegacyCompatiblePythonModelSource(model), {
|
|
891
1124
|
ownership: "managed",
|
|
@@ -927,10 +1160,12 @@ export async function generateLanguageSchemaArtifacts(
|
|
|
927
1160
|
backendLanguage === "csharp" ? "csharp-models" : "python-models"
|
|
928
1161
|
);
|
|
929
1162
|
|
|
1163
|
+
// Inline z.object fields become named classes (e.g. ProbeItemNested) so their shape is preserved.
|
|
1164
|
+
const nativeModels = expandInlineObjectModels(models);
|
|
930
1165
|
if (backendLanguage === "csharp") {
|
|
931
|
-
await generateCSharpSchemaArtifacts(
|
|
1166
|
+
await generateCSharpSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
|
|
932
1167
|
} else {
|
|
933
|
-
await generatePythonSchemaArtifacts(
|
|
1168
|
+
await generatePythonSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
|
|
934
1169
|
}
|
|
935
1170
|
|
|
936
1171
|
console.log(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FieldInfo, ModelInfo, toCamelCase } from "./model-parser";
|
|
1
|
+
import { FieldInfo, ModelInfo, toCamelCase, toPascalCase } from "./model-parser";
|
|
2
2
|
|
|
3
3
|
type OpenApiSchema =
|
|
4
4
|
| {
|
|
@@ -11,9 +11,59 @@ type OpenApiSchema =
|
|
|
11
11
|
additionalProperties?: boolean;
|
|
12
12
|
nullable?: boolean;
|
|
13
13
|
$ref?: string;
|
|
14
|
+
minLength?: number;
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
pattern?: string;
|
|
17
|
+
minimum?: number;
|
|
18
|
+
maximum?: number;
|
|
19
|
+
exclusiveMinimum?: boolean;
|
|
20
|
+
exclusiveMaximum?: boolean;
|
|
21
|
+
minItems?: number;
|
|
22
|
+
maxItems?: number;
|
|
14
23
|
};
|
|
15
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Promote inline `z.object({...})` fields to named models (e.g. ProbeItem.nested -> ProbeItemNested)
|
|
27
|
+
* so OpenAPI components and native classes keep their shape. Expanded fields reference the new model.
|
|
28
|
+
*/
|
|
29
|
+
export function expandInlineObjectModels(models: ModelInfo[]): ModelInfo[] {
|
|
30
|
+
const names = new Set(models.map((model) => model.name));
|
|
31
|
+
const synthetic: ModelInfo[] = [];
|
|
32
|
+
|
|
33
|
+
const expand = (owner: ModelInfo): ModelInfo => ({
|
|
34
|
+
...owner,
|
|
35
|
+
fields: owner.fields.map((field) => {
|
|
36
|
+
if (!field.objectFields || field.isNestedSchema) return field;
|
|
37
|
+
let name = `${owner.name}${toPascalCase(field.name)}`;
|
|
38
|
+
while (names.has(name)) name = `${name}Object`;
|
|
39
|
+
names.add(name);
|
|
40
|
+
const nested = expand({
|
|
41
|
+
...owner,
|
|
42
|
+
name,
|
|
43
|
+
displayName: name,
|
|
44
|
+
schemaName: name,
|
|
45
|
+
fields: field.objectFields,
|
|
46
|
+
hasId: field.objectFields.some((child) => child.name === "id"),
|
|
47
|
+
hasCreatedAt: field.objectFields.some((child) => child.name === "createdAt"),
|
|
48
|
+
hasUpdatedAt: field.objectFields.some((child) => child.name === "updatedAt"),
|
|
49
|
+
nestedSchemaRefs: [],
|
|
50
|
+
partitionKey: "/id",
|
|
51
|
+
connectorConfig: undefined,
|
|
52
|
+
authPolicy: undefined,
|
|
53
|
+
});
|
|
54
|
+
synthetic.push(nested);
|
|
55
|
+
const rest = { ...field };
|
|
56
|
+
delete rest.objectFields;
|
|
57
|
+
return { ...rest, isNestedSchema: true, nestedModelName: name };
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const expanded = models.map(expand);
|
|
62
|
+
return [...expanded, ...synthetic];
|
|
63
|
+
}
|
|
64
|
+
|
|
16
65
|
function toScalarSchema(field: FieldInfo): OpenApiSchema {
|
|
66
|
+
const constraints = field.constraints ?? {};
|
|
17
67
|
if (field.enumValues?.length) {
|
|
18
68
|
return {
|
|
19
69
|
type: "string",
|
|
@@ -25,7 +75,10 @@ function toScalarSchema(field: FieldInfo): OpenApiSchema {
|
|
|
25
75
|
case "string":
|
|
26
76
|
return {
|
|
27
77
|
type: "string",
|
|
28
|
-
format
|
|
78
|
+
...(field.format && field.format !== "time" ? { format: field.format } : {}),
|
|
79
|
+
...(constraints.minLength !== undefined ? { minLength: constraints.minLength } : {}),
|
|
80
|
+
...(constraints.maxLength !== undefined ? { maxLength: constraints.maxLength } : {}),
|
|
81
|
+
...(constraints.pattern !== undefined ? { pattern: constraints.pattern } : {}),
|
|
29
82
|
};
|
|
30
83
|
case "date":
|
|
31
84
|
return {
|
|
@@ -34,7 +87,11 @@ function toScalarSchema(field: FieldInfo): OpenApiSchema {
|
|
|
34
87
|
};
|
|
35
88
|
case "number":
|
|
36
89
|
return {
|
|
37
|
-
type: "number",
|
|
90
|
+
...(field.isInteger ? { type: "integer", format: "int64" } : { type: "number", format: "double" }),
|
|
91
|
+
...(constraints.minimum !== undefined ? { minimum: constraints.minimum } : {}),
|
|
92
|
+
...(constraints.exclusiveMinimum ? { exclusiveMinimum: true } : {}),
|
|
93
|
+
...(constraints.maximum !== undefined ? { maximum: constraints.maximum } : {}),
|
|
94
|
+
...(constraints.exclusiveMaximum ? { exclusiveMaximum: true } : {}),
|
|
38
95
|
};
|
|
39
96
|
case "boolean":
|
|
40
97
|
return {
|
|
@@ -59,6 +116,8 @@ function toFieldSchema(field: FieldInfo): OpenApiSchema {
|
|
|
59
116
|
return {
|
|
60
117
|
type: "array",
|
|
61
118
|
items: schema,
|
|
119
|
+
...(field.constraints?.minItems !== undefined ? { minItems: field.constraints.minItems } : {}),
|
|
120
|
+
...(field.constraints?.maxItems !== undefined ? { maxItems: field.constraints.maxItems } : {}),
|
|
62
121
|
};
|
|
63
122
|
}
|
|
64
123
|
|
|
@@ -82,11 +141,10 @@ function createComponentSchema(model: ModelInfo): OpenApiSchema {
|
|
|
82
141
|
required,
|
|
83
142
|
};
|
|
84
143
|
}
|
|
85
|
-
|
|
86
144
|
export function generateOpenApiDocument(models: ModelInfo[], rootModel: ModelInfo): string {
|
|
87
145
|
const rootModelRoute = toCamelCase(rootModel.name);
|
|
88
146
|
const components = Object.fromEntries(
|
|
89
|
-
models.map((model) => [model.name, createComponentSchema(model)])
|
|
147
|
+
expandInlineObjectModels(models).map((model) => [model.name, createComponentSchema(model)])
|
|
90
148
|
);
|
|
91
149
|
|
|
92
150
|
const document = {
|