swallowkit 1.1.2 → 2.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +61 -7
- package/README.md +61 -7
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +103 -27
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +5 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +143 -67
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +14 -7
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +149 -170
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +5 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +123 -18
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +49 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +19 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts +2 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +15 -0
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +17 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +45 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/manifest.d.ts +3 -1
- package/dist/core/project/manifest.d.ts.map +1 -1
- package/dist/core/project/manifest.js +47 -25
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +26 -0
- package/dist/core/project/react-project.d.ts.map +1 -0
- package/dist/core/project/react-project.js +443 -0
- package/dist/core/project/react-project.js.map +1 -0
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +32 -10
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +53 -7
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +26 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +294 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
- package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-bff-generator.js +252 -0
- package/dist/core/scaffold/react-bff-generator.js.map +1 -0
- package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
- package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-ui-generator.js +41 -0
- package/dist/core/scaffold/react-ui-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +61 -30
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/azure-cli.d.ts +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/dist/utils/package-manager.d.ts +2 -2
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +6 -3
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +7 -4
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +6 -1
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +145 -5
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +838 -0
- package/src/__tests__/release-metadata.test.ts +121 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +103 -29
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +145 -73
- package/src/cli/commands/init.ts +147 -172
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +145 -22
- package/src/cli/index.ts +46 -10
- package/src/core/config.ts +21 -1
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +17 -1
- package/src/core/project/agent-templates/AGENTS.md +113 -15
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
- package/src/core/project/agent-templates/copilot-instructions.md +15 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +63 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/manifest.ts +52 -27
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +529 -0
- package/src/core/project/state.ts +70 -0
- package/src/core/project/validation.ts +32 -10
- package/src/core/project/workflows.ts +53 -7
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/react-bff-generator.ts +292 -0
- package/src/core/scaffold/react-ui-generator.ts +51 -0
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/index.ts +3 -1
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +67 -36
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/types/index.ts +4 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +7 -4
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -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 = {
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { AuthConfig } from "../../types";
|
|
2
|
+
import { normalizeAuthConfig } from "../config";
|
|
3
|
+
import { ModelInfo, toCamelCase } from "./model-parser";
|
|
4
|
+
|
|
5
|
+
export function generateManagedFunctionConfig(
|
|
6
|
+
route: string,
|
|
7
|
+
methods: readonly string[],
|
|
8
|
+
entry: string
|
|
9
|
+
): string {
|
|
10
|
+
return (
|
|
11
|
+
JSON.stringify(
|
|
12
|
+
{
|
|
13
|
+
scriptFile: `../dist/${entry}/index.js`,
|
|
14
|
+
bindings: [
|
|
15
|
+
{
|
|
16
|
+
authLevel: "anonymous",
|
|
17
|
+
type: "httpTrigger",
|
|
18
|
+
direction: "in",
|
|
19
|
+
name: "req",
|
|
20
|
+
methods: methods.map((method) => method.toLowerCase()),
|
|
21
|
+
route,
|
|
22
|
+
},
|
|
23
|
+
{ type: "http", direction: "out", name: "$return" },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
null,
|
|
27
|
+
2
|
|
28
|
+
) + "\n"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getAuthCookieName(projectName: string): string {
|
|
33
|
+
return (
|
|
34
|
+
projectName.replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]/g, "-") +
|
|
35
|
+
"-auth-token"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Request-local credentials: never share authentication state between invocations. */
|
|
40
|
+
export function generateReactBffHelper(
|
|
41
|
+
projectName: string,
|
|
42
|
+
auth?: AuthConfig
|
|
43
|
+
): string {
|
|
44
|
+
const providers = new Set(
|
|
45
|
+
Object.values(normalizeAuthConfig(auth)?.schemes ?? {}).map(
|
|
46
|
+
(scheme) => scheme.provider
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
return `import type { HttpRequest, HttpResponse } from '@azure/functions';
|
|
50
|
+
import { z } from 'zod/v4';
|
|
51
|
+
|
|
52
|
+
// SwallowKit BFF Call Function Helper (SWA managed Functions)
|
|
53
|
+
const forwardSwa = ${providers.has("swa")};
|
|
54
|
+
const forwardBearer = ${providers.has("custom-jwt") || providers.has("external-token")};
|
|
55
|
+
const authCookieName = '${getAuthCookieName(projectName)}';
|
|
56
|
+
|
|
57
|
+
export class BffError extends Error {
|
|
58
|
+
constructor(public status: number, message: string) { super(message); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function json(body: unknown, status = 200): HttpResponse {
|
|
62
|
+
return { status, body, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseBody(request: HttpRequest): unknown {
|
|
66
|
+
if (request.body === undefined || request.body === '') throw new BffError(400, 'Request body is required');
|
|
67
|
+
if (typeof request.body !== 'string') return request.body;
|
|
68
|
+
try { return JSON.parse(request.body); }
|
|
69
|
+
catch { throw new BffError(400, 'Invalid JSON body'); }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function credentialHeaders(request: HttpRequest): Record<string, string> {
|
|
73
|
+
const result: Record<string, string> = {};
|
|
74
|
+
if (forwardBearer) {
|
|
75
|
+
const authorization = request.headers.authorization;
|
|
76
|
+
const cookie = request.headers.cookie?.split(';').map(value => value.trim()).find(value => value.startsWith(authCookieName + '='));
|
|
77
|
+
if (authorization?.startsWith('Bearer ')) result.Authorization = authorization;
|
|
78
|
+
else if (cookie) {
|
|
79
|
+
try { result.Authorization = 'Bearer ' + decodeURIComponent(cookie.slice(authCookieName.length + 1)); }
|
|
80
|
+
catch { throw new BffError(401, 'Invalid authentication cookie'); }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// SWA injects this header at the trusted managed API boundary. Never expose the backend key to clients.
|
|
84
|
+
if (forwardSwa && request.headers['x-ms-client-principal']) {
|
|
85
|
+
const encoded = request.headers['x-ms-client-principal'];
|
|
86
|
+
try {
|
|
87
|
+
const decoded = Buffer.from(encoded, 'base64');
|
|
88
|
+
if (decoded.toString('base64') !== encoded) throw new Error('Invalid encoding');
|
|
89
|
+
const value = JSON.parse(decoded.toString('utf8'));
|
|
90
|
+
if (typeof value.identityProvider !== 'string' || !value.identityProvider ||
|
|
91
|
+
typeof value.userId !== 'string' || !value.userId ||
|
|
92
|
+
!Array.isArray(value.userRoles) || !value.userRoles.includes('authenticated')) {
|
|
93
|
+
throw new Error('Invalid principal');
|
|
94
|
+
}
|
|
95
|
+
result['x-ms-client-principal'] = Buffer.from(JSON.stringify({
|
|
96
|
+
identityProvider: value.identityProvider, userId: value.userId,
|
|
97
|
+
userDetails: typeof value.userDetails === 'string' ? value.userDetails : '',
|
|
98
|
+
userRoles: value.userRoles.filter((role: unknown) => typeof role === 'string'),
|
|
99
|
+
})).toString('base64');
|
|
100
|
+
} catch {
|
|
101
|
+
throw new BffError(401, 'Invalid SWA client principal');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function backendRequest(request: HttpRequest, method: string, path: string, body?: unknown): Promise<Response> {
|
|
108
|
+
const baseUrl = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
109
|
+
const parsedUrl = new URL(baseUrl);
|
|
110
|
+
const local = parsedUrl.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(parsedUrl.hostname);
|
|
111
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
112
|
+
if (!key && !local) throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
113
|
+
if (!local && parsedUrl.protocol !== 'https:') throw new Error('Remote Functions calls require HTTPS');
|
|
114
|
+
return fetch(baseUrl.replace(/\\/$/, '') + path, {
|
|
115
|
+
method,
|
|
116
|
+
headers: { 'Content-Type': 'application/json', ...credentialHeaders(request), ...(key ? { 'x-functions-key': key } : {}) },
|
|
117
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
118
|
+
redirect: 'error',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface CallFunctionConfig {
|
|
123
|
+
request: HttpRequest;
|
|
124
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
125
|
+
path: string;
|
|
126
|
+
body?: unknown;
|
|
127
|
+
inputSchema?: z.ZodType;
|
|
128
|
+
responseSchema?: z.ZodType;
|
|
129
|
+
successStatus?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function callFunction(config: CallFunctionConfig): Promise<HttpResponse> {
|
|
133
|
+
try {
|
|
134
|
+
let body = config.body;
|
|
135
|
+
if (config.inputSchema) {
|
|
136
|
+
const result = config.inputSchema.safeParse(body);
|
|
137
|
+
if (!result.success) return json({ error: 'Validation failed', details: result.error.issues }, 400);
|
|
138
|
+
body = result.data;
|
|
139
|
+
}
|
|
140
|
+
const response = await backendRequest(config.request, config.method, config.path, body);
|
|
141
|
+
if (response.status === 204) return { status: 204 };
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
console.error('[BFF] Backend request failed:', response.status);
|
|
144
|
+
return json({ error: 'Backend request failed' }, response.status);
|
|
145
|
+
}
|
|
146
|
+
const data: unknown = await response.json();
|
|
147
|
+
if (config.responseSchema) {
|
|
148
|
+
const result = config.responseSchema.safeParse(data);
|
|
149
|
+
if (!result.success) {
|
|
150
|
+
console.error('[BFF] Backend response validation failed');
|
|
151
|
+
return json({ error: 'Invalid backend response' }, 502);
|
|
152
|
+
}
|
|
153
|
+
return json(result.data, config.successStatus ?? 200);
|
|
154
|
+
}
|
|
155
|
+
return json(data, config.successStatus ?? 200);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return handleError(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function handleError(error: unknown): HttpResponse {
|
|
162
|
+
if (error instanceof BffError) return json({ error: error.message }, error.status);
|
|
163
|
+
console.error('[BFF] Request failed:', error instanceof Error ? error.name : 'UnknownError');
|
|
164
|
+
return json({ error: 'Backend request failed' }, 502);
|
|
165
|
+
}
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function generateReactBffRoute(
|
|
170
|
+
model: ModelInfo,
|
|
171
|
+
sharedPackageName: string
|
|
172
|
+
): string {
|
|
173
|
+
const modelCamel = toCamelCase(model.name);
|
|
174
|
+
const operations = new Set(
|
|
175
|
+
model.connectorConfig?.operations ?? [
|
|
176
|
+
"getAll",
|
|
177
|
+
"getById",
|
|
178
|
+
"create",
|
|
179
|
+
"update",
|
|
180
|
+
"delete",
|
|
181
|
+
]
|
|
182
|
+
);
|
|
183
|
+
const managedFields = model.fields
|
|
184
|
+
.filter((field) => ["id", "createdAt", "updatedAt"].includes(field.name))
|
|
185
|
+
.map((field) => `${field.name}: true`)
|
|
186
|
+
.join(", ");
|
|
187
|
+
const clauses: string[] = [];
|
|
188
|
+
if (operations.has("getAll"))
|
|
189
|
+
clauses.push(
|
|
190
|
+
` if (request.method === 'GET' && !id) return callFunction({ request, method: 'GET', path, responseSchema: z.array(${model.schemaName}) });`
|
|
191
|
+
);
|
|
192
|
+
if (operations.has("getById"))
|
|
193
|
+
clauses.push(
|
|
194
|
+
` if (request.method === 'GET' && id) return callFunction({ request, method: 'GET', path, responseSchema: ${model.schemaName} });`
|
|
195
|
+
);
|
|
196
|
+
if (operations.has("create"))
|
|
197
|
+
clauses.push(
|
|
198
|
+
` if (request.method === 'POST' && !id) return callFunction({ request, method: 'POST', path, body: parseBody(request), inputSchema: InputSchema, responseSchema: ${model.schemaName}, successStatus: 201 });`
|
|
199
|
+
);
|
|
200
|
+
if (operations.has("update"))
|
|
201
|
+
clauses.push(
|
|
202
|
+
` if (request.method === 'PUT' && id) return callFunction({ request, method: 'PUT', path, body: parseBody(request), inputSchema: InputSchema, responseSchema: ${model.schemaName} });`
|
|
203
|
+
);
|
|
204
|
+
if (operations.has("delete"))
|
|
205
|
+
clauses.push(
|
|
206
|
+
` if (request.method === 'DELETE' && id) return callFunction({ request, method: 'DELETE', path });`
|
|
207
|
+
);
|
|
208
|
+
return `import type { AzureFunction, Context, HttpRequest } from '@azure/functions';
|
|
209
|
+
import { z } from 'zod/v4';
|
|
210
|
+
import { ${model.schemaName} } from '${sharedPackageName}';
|
|
211
|
+
import { callFunction, parseBody, json, handleError } from '../lib/call-function';
|
|
212
|
+
|
|
213
|
+
const InputSchema = ${model.schemaName}.omit({ ${managedFields} });
|
|
214
|
+
|
|
215
|
+
const handler: AzureFunction = async (_context: Context, request: HttpRequest) => {
|
|
216
|
+
try {
|
|
217
|
+
const id = request.params.id;
|
|
218
|
+
const path = '/api/${modelCamel}' + (id ? '/' + encodeURIComponent(id) : '');
|
|
219
|
+
${clauses.join("\n")}
|
|
220
|
+
return json({ error: 'Method not allowed' }, 405);
|
|
221
|
+
} catch (error) { return handleError(error); }
|
|
222
|
+
};
|
|
223
|
+
export default handler;
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function generateReactGreetBff(): string {
|
|
228
|
+
return `import type { AzureFunction, Context, HttpRequest } from '@azure/functions';
|
|
229
|
+
import { callFunction, parseBody, handleError } from '../lib/call-function';
|
|
230
|
+
|
|
231
|
+
const handler: AzureFunction = async (_context: Context, request: HttpRequest) => {
|
|
232
|
+
try {
|
|
233
|
+
const body = request.method === 'POST' ? parseBody(request) : undefined;
|
|
234
|
+
return callFunction({
|
|
235
|
+
request, method: request.method === 'POST' ? 'POST' : 'GET',
|
|
236
|
+
path: '/api/greet?name=' + encodeURIComponent(request.query.name || 'World'), body,
|
|
237
|
+
});
|
|
238
|
+
} catch (error) { return handleError(error); }
|
|
239
|
+
};
|
|
240
|
+
export default handler;
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function generateReactAuthBff(
|
|
245
|
+
projectName: string,
|
|
246
|
+
sharedPackageName: string,
|
|
247
|
+
customJwt: boolean
|
|
248
|
+
): string {
|
|
249
|
+
return `import type { AzureFunction, Context, HttpRequest } from '@azure/functions';
|
|
250
|
+
${customJwt ? `import { z } from 'zod/v4';\nimport { AuthUser, LoginRequest, LoginResponse } from '${sharedPackageName}';` : ""}
|
|
251
|
+
import { backendRequest, callFunction, parseBody, json, handleError } from '../lib/call-function';
|
|
252
|
+
|
|
253
|
+
const cookieName = '${getAuthCookieName(projectName)}';
|
|
254
|
+
${
|
|
255
|
+
customJwt
|
|
256
|
+
? `const CurrentUser = z.preprocess(value => {
|
|
257
|
+
if (!value || typeof value !== 'object') return value;
|
|
258
|
+
return { ...value, id: 'id' in value ? value.id : 'sub' in value ? value.sub : undefined };
|
|
259
|
+
}, AuthUser);
|
|
260
|
+
`
|
|
261
|
+
: ""
|
|
262
|
+
}
|
|
263
|
+
const handler: AzureFunction = async (_context: Context, request: HttpRequest) => {
|
|
264
|
+
try {
|
|
265
|
+
const action = request.params.action;
|
|
266
|
+
if (action === 'me' && request.method === 'GET') return callFunction({ request, method: 'GET', path: '/api/auth/me'${customJwt ? ", responseSchema: CurrentUser" : ""} });
|
|
267
|
+
${
|
|
268
|
+
customJwt
|
|
269
|
+
? ` if (action === 'logout' && request.method === 'POST') {
|
|
270
|
+
const response = json({ message: 'Logged out' });
|
|
271
|
+
response.headers = { ...response.headers, 'Set-Cookie': cookieName + '=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0' };
|
|
272
|
+
return response;
|
|
273
|
+
}
|
|
274
|
+
if (action === 'login' && request.method === 'POST') {
|
|
275
|
+
const input = LoginRequest.safeParse(parseBody(request));
|
|
276
|
+
if (!input.success) return json({ error: 'Validation failed', details: input.error.issues }, 400);
|
|
277
|
+
const result = await backendRequest(request, 'POST', '/api/auth/login', input.data);
|
|
278
|
+
if (!result.ok) return json({ error: 'Authentication failed' }, result.status);
|
|
279
|
+
const data = LoginResponse.parse(await result.json());
|
|
280
|
+
const response = json({ user: data.user });
|
|
281
|
+
const secure = process.env.WEBSITE_SITE_NAME || process.env.NODE_ENV === 'production';
|
|
282
|
+
response.headers = { ...response.headers, 'Set-Cookie': cookieName + '=' + encodeURIComponent(data.token) + '; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400' + (secure ? '; Secure' : '') };
|
|
283
|
+
return response;
|
|
284
|
+
}`
|
|
285
|
+
: ""
|
|
286
|
+
}
|
|
287
|
+
return json({ error: 'Not found' }, 404);
|
|
288
|
+
} catch (error) { return handleError(error); }
|
|
289
|
+
};
|
|
290
|
+
export default handler;
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ModelInfo, toKebabCase } from "./model-parser";
|
|
2
|
+
import { UIAuthOptions } from "./ui-generator";
|
|
3
|
+
|
|
4
|
+
/** Adapt the shared CRUD presentation templates to React Router, without a Next.js runtime. */
|
|
5
|
+
export function adaptReactPage(source: string): string {
|
|
6
|
+
return source
|
|
7
|
+
.replace(/^'use client';?\s*\r?\n/, "")
|
|
8
|
+
.replace(
|
|
9
|
+
/import Link from 'next\/link';/g,
|
|
10
|
+
"import { Link } from 'react-router-dom';"
|
|
11
|
+
)
|
|
12
|
+
.replace(
|
|
13
|
+
/import \{ useParams, useRouter \} from 'next\/navigation';/g,
|
|
14
|
+
"import { useParams, useNavigate } from 'react-router-dom';"
|
|
15
|
+
)
|
|
16
|
+
.replace(
|
|
17
|
+
/import \{ useRouter \} from 'next\/navigation';/g,
|
|
18
|
+
"import { useNavigate } from 'react-router-dom';"
|
|
19
|
+
)
|
|
20
|
+
.replace(/\buseRouter\(\)/g, "useNavigate()")
|
|
21
|
+
.replace(/\brouter\.(?:push|replace)\(/g, "router(")
|
|
22
|
+
.replace(/^\s*router\.refresh\(\);?\s*$/gm, "")
|
|
23
|
+
.replace(/(<Link\b[^>]*?)\bhref=/gs, "$1to=")
|
|
24
|
+
.replace(/\[params\]/g, "[params.id]");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function generateReactModelRoutes(
|
|
28
|
+
model: ModelInfo,
|
|
29
|
+
auth?: UIAuthOptions
|
|
30
|
+
): string {
|
|
31
|
+
const slug = toKebabCase(model.name);
|
|
32
|
+
return `import type { RouteObject } from 'react-router-dom';
|
|
33
|
+
${auth ? "import { Outlet } from 'react-router-dom';\nimport Layout from './layout';" : ""}
|
|
34
|
+
import List from './page';
|
|
35
|
+
import Detail from './[id]/page';
|
|
36
|
+
import New from './new/page';
|
|
37
|
+
import Edit from './[id]/edit/page';
|
|
38
|
+
|
|
39
|
+
const routes: RouteObject[] = [{
|
|
40
|
+
path: '/${slug}',
|
|
41
|
+
${auth ? "element: <Layout><Outlet /></Layout>," : ""}
|
|
42
|
+
children: [
|
|
43
|
+
{ index: true, element: <List /> },
|
|
44
|
+
{ path: 'new', element: <New /> },
|
|
45
|
+
{ path: ':id', element: <Detail /> },
|
|
46
|
+
{ path: ':id/edit', element: <Edit /> },
|
|
47
|
+
],
|
|
48
|
+
}];
|
|
49
|
+
export default routes;
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* CRUD 画面(一覧、詳細、新規作成、編集)を生成する
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
|
|
6
|
+
import { FieldInfo, ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
|
|
7
7
|
import { ModelAuthPolicy } from "../../types";
|
|
8
8
|
|
|
9
9
|
/** auth が有効かつ authPolicy が存在する場合に渡されるオプション */
|
|
@@ -18,6 +18,21 @@ export interface UIAuthOptions {
|
|
|
18
18
|
/**
|
|
19
19
|
* 認証付きCRUDルートを、ページ側のuseAuthと同じAuthProviderで囲むレイアウトを生成する。
|
|
20
20
|
*/
|
|
21
|
+
/** Lookup state and fetches are per referenced model; several fields may reference the same model. */
|
|
22
|
+
function uniqueByReferencedModel(fields: FieldInfo[]): FieldInfo[] {
|
|
23
|
+
const seen = new Set<string>();
|
|
24
|
+
return fields.filter((field) => {
|
|
25
|
+
if (!field.referencedModel || seen.has(field.referencedModel)) return false;
|
|
26
|
+
seen.add(field.referencedModel);
|
|
27
|
+
return true;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Untyped or inline z.object fields (not references to another model) are edited and shown as JSON. */
|
|
32
|
+
function isJsonField(f: FieldInfo): boolean {
|
|
33
|
+
return f.type === "object" && !f.isNestedSchema;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
export function generateAuthLayout(model: ModelInfo, authOptions?: UIAuthOptions): string | undefined {
|
|
22
37
|
if (!authOptions) return undefined;
|
|
23
38
|
|
|
@@ -59,14 +74,14 @@ export function generateListPage(model: ModelInfo, sharedPackageName: string, au
|
|
|
59
74
|
const hasForeignKeys = foreignKeyFields.length > 0;
|
|
60
75
|
|
|
61
76
|
// 外部キー用のstate定義を生成
|
|
62
|
-
const foreignKeyStates = foreignKeyFields.map(f => {
|
|
77
|
+
const foreignKeyStates = uniqueByReferencedModel(foreignKeyFields).map(f => {
|
|
63
78
|
const refModel = f.referencedModel!;
|
|
64
79
|
const refModelCamel = toCamelCase(refModel);
|
|
65
80
|
return ` const [${refModelCamel}Map, set${refModel}Map] = useState<Record<string, string>>({});`;
|
|
66
81
|
}).join('\n');
|
|
67
82
|
|
|
68
83
|
// 外部キーデータのフェッチロジックを生成
|
|
69
|
-
const foreignKeyFetches = foreignKeyFields.map(f => {
|
|
84
|
+
const foreignKeyFetches = uniqueByReferencedModel(foreignKeyFields).map(f => {
|
|
70
85
|
const refModel = f.referencedModel!;
|
|
71
86
|
const refModelCamel = toCamelCase(refModel);
|
|
72
87
|
return ` fetch('/api/${refModelCamel}')
|
|
@@ -234,6 +249,10 @@ ${displayFields.map(f => {
|
|
|
234
249
|
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
235
250
|
{${refModelCamel}Map[item.${f.name}] || item.${f.name}}
|
|
236
251
|
</td>`;
|
|
252
|
+
} else if (isJsonField(f)) {
|
|
253
|
+
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
254
|
+
{item.${f.name} === undefined ? '-' : JSON.stringify(item.${f.name})}
|
|
255
|
+
</td>`;
|
|
237
256
|
} else {
|
|
238
257
|
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
239
258
|
{String(item.${f.name})}
|
|
@@ -305,14 +324,14 @@ export function generateDetailPage(model: ModelInfo, sharedPackageName: string,
|
|
|
305
324
|
const hasForeignKeys = foreignKeyFields.length > 0;
|
|
306
325
|
|
|
307
326
|
// 外部キー用のstate定義を生成
|
|
308
|
-
const foreignKeyStates = foreignKeyFields.map(f => {
|
|
327
|
+
const foreignKeyStates = uniqueByReferencedModel(foreignKeyFields).map(f => {
|
|
309
328
|
const refModel = f.referencedModel!;
|
|
310
329
|
const refModelCamel = toCamelCase(refModel);
|
|
311
330
|
return ` const [${refModelCamel}Map, set${refModel}Map] = useState<Record<string, string>>({});`;
|
|
312
331
|
}).join('\n');
|
|
313
332
|
|
|
314
333
|
// 外部キーデータのフェッチロジックを生成
|
|
315
|
-
const foreignKeyFetches = foreignKeyFields.map(f => {
|
|
334
|
+
const foreignKeyFetches = uniqueByReferencedModel(foreignKeyFields).map(f => {
|
|
316
335
|
const refModel = f.referencedModel!;
|
|
317
336
|
const refModelCamel = toCamelCase(refModel);
|
|
318
337
|
return ` fetch('/api/${refModelCamel}')
|
|
@@ -476,6 +495,11 @@ ${model.fields.map(f => {
|
|
|
476
495
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${refModel}</dt>
|
|
477
496
|
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{${refModelCamel}Map[${modelCamel}.${f.name}] || ${modelCamel}.${f.name}}</dd>
|
|
478
497
|
</div>`;
|
|
498
|
+
} else if (isJsonField(f)) {
|
|
499
|
+
return ` <div>
|
|
500
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.name}</dt>
|
|
501
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100"><pre className="whitespace-pre-wrap">{${modelCamel}.${f.name} === undefined ? '-' : JSON.stringify(${modelCamel}.${f.name}, null, 2)}</pre></dd>
|
|
502
|
+
</div>`;
|
|
479
503
|
} else {
|
|
480
504
|
return ` <div>
|
|
481
505
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.name}</dt>
|
|
@@ -573,7 +597,7 @@ export default function ${modelName}Form({ initialData, isEdit = false }: ${mode
|
|
|
573
597
|
const router = useRouter();
|
|
574
598
|
const [loading, setLoading] = useState(false);
|
|
575
599
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
576
|
-
${hasForignKeys ? foreignKeyFields.map(f => ` const [${toCamelCase(f.referencedModel!)}Options, set${f.referencedModel}Options] = useState<Array<{ id: string; name: string }>>([]);`).join('\n') : ''}
|
|
600
|
+
${hasForignKeys ? uniqueByReferencedModel(foreignKeyFields).map(f => ` const [${toCamelCase(f.referencedModel!)}Options, set${f.referencedModel}Options] = useState<Array<{ id: string; name: string }>>([]);`).join('\n') : ''}
|
|
577
601
|
${hasNestedSchemas ? nestedSchemaStates : ''}
|
|
578
602
|
|
|
579
603
|
const [formData, setFormData] = useState({
|
|
@@ -587,6 +611,11 @@ ${formFields.map(f => {
|
|
|
587
611
|
}
|
|
588
612
|
}
|
|
589
613
|
|
|
614
|
+
// インラインオブジェクトは JSON テキストとして編集する
|
|
615
|
+
if (isJsonField(f)) {
|
|
616
|
+
return ` ${f.name}: initialData?.${f.name} !== undefined ? JSON.stringify(initialData.${f.name}, null, 2) : '',`;
|
|
617
|
+
}
|
|
618
|
+
|
|
590
619
|
let defaultValue = "''";
|
|
591
620
|
if (f.isArray) {
|
|
592
621
|
// Array の場合は空文字列(カンマ区切りで入力するため)
|
|
@@ -612,7 +641,7 @@ ${formFields.map(f => {
|
|
|
612
641
|
${needsUseEffect ? `
|
|
613
642
|
// 参照データの選択肢を取得
|
|
614
643
|
useEffect(() => {
|
|
615
|
-
${hasForignKeys ? foreignKeyFields.map(f => ` // ${f.referencedModel} の一覧を取得
|
|
644
|
+
${hasForignKeys ? uniqueByReferencedModel(foreignKeyFields).map(f => ` // ${f.referencedModel} の一覧を取得
|
|
616
645
|
fetch('/api/${toKebabCase(f.referencedModel!)}')
|
|
617
646
|
.then(res => res.json())
|
|
618
647
|
.then(data => set${f.referencedModel}Options(data.map((item: Record<string, string>) => ({ id: item.id, name: item.name || item.title || item.id }))))
|
|
@@ -628,7 +657,19 @@ ${hasNestedSchemas ? nestedSchemaFetches : ''}
|
|
|
628
657
|
try {
|
|
629
658
|
// Array フィールドをカンマ区切りから配列に変換
|
|
630
659
|
const submitData: Record<string, unknown> = { ...formData };
|
|
631
|
-
${formFields.filter(
|
|
660
|
+
${formFields.filter(isJsonField).length > 0 ? ` // JSON フィールド(インラインオブジェクト)を解析
|
|
661
|
+
${formFields.filter(isJsonField).map(f => ` if (submitData.${f.name} === '') {
|
|
662
|
+
submitData.${f.name} = undefined;
|
|
663
|
+
} else if (typeof submitData.${f.name} === 'string') {
|
|
664
|
+
try {
|
|
665
|
+
submitData.${f.name} = JSON.parse(submitData.${f.name});
|
|
666
|
+
} catch {
|
|
667
|
+
setErrors({ ${f.name}: 'Invalid JSON' });
|
|
668
|
+
setLoading(false);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}`).join('\n')}
|
|
672
|
+
` : ''}${formFields.filter(f => f.isArray && !f.isNestedSchema && !isJsonField(f)).map(f => ` if (typeof submitData.${f.name} === 'string') {
|
|
632
673
|
submitData.${f.name} = submitData.${f.name}.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
|
|
633
674
|
}`).join('\n')}
|
|
634
675
|
${formFields.filter(f => f.enumValues && f.enumValues.length > 0).length > 0 ? `
|
|
@@ -833,6 +874,27 @@ ${f.enumValues.map(v => ` <option value="${v}">${v}</option>`).join('\n
|
|
|
833
874
|
</div>`;
|
|
834
875
|
}
|
|
835
876
|
|
|
877
|
+
// インラインオブジェクトは JSON テキストエリア
|
|
878
|
+
if (isJsonField(f)) {
|
|
879
|
+
return ` <div>
|
|
880
|
+
<label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
881
|
+
${f.name}${!f.isOptional ? ' *' : ''} <span className="text-xs text-gray-500">(JSON)</span>
|
|
882
|
+
</label>
|
|
883
|
+
<textarea
|
|
884
|
+
id="${f.name}"
|
|
885
|
+
name="${f.name}"
|
|
886
|
+
rows={4}
|
|
887
|
+
value={formData.${f.name}}
|
|
888
|
+
onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
|
|
889
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 font-mono focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
890
|
+
${!f.isOptional ? 'required' : ''}
|
|
891
|
+
/>
|
|
892
|
+
{errors.${f.name} && (
|
|
893
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
894
|
+
)}
|
|
895
|
+
</div>`;
|
|
896
|
+
}
|
|
897
|
+
|
|
836
898
|
// Array の場合はカンマ区切りテキスト
|
|
837
899
|
if (f.isArray) {
|
|
838
900
|
return ` <div>
|