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.
Files changed (262) hide show
  1. package/README.ja.md +61 -7
  2. package/README.md +61 -7
  3. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  4. package/dist/cli/commands/add-auth.js +103 -27
  5. package/dist/cli/commands/add-auth.js.map +1 -1
  6. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  7. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  8. package/dist/cli/commands/configure-mcp.js +42 -0
  9. package/dist/cli/commands/configure-mcp.js.map +1 -0
  10. package/dist/cli/commands/dev.d.ts +5 -2
  11. package/dist/cli/commands/dev.d.ts.map +1 -1
  12. package/dist/cli/commands/dev.js +143 -67
  13. package/dist/cli/commands/dev.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts +14 -7
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/init.js +149 -170
  17. package/dist/cli/commands/init.js.map +1 -1
  18. package/dist/cli/commands/migrations.d.ts.map +1 -1
  19. package/dist/cli/commands/migrations.js +25 -0
  20. package/dist/cli/commands/migrations.js.map +1 -1
  21. package/dist/cli/commands/provision.d.ts +5 -0
  22. package/dist/cli/commands/provision.d.ts.map +1 -1
  23. package/dist/cli/commands/provision.js +278 -55
  24. package/dist/cli/commands/provision.js.map +1 -1
  25. package/dist/cli/commands/scaffold.d.ts +5 -1
  26. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  27. package/dist/cli/commands/scaffold.js +123 -18
  28. package/dist/cli/commands/scaffold.js.map +1 -1
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +49 -8
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/core/config.d.ts +2 -1
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +19 -0
  36. package/dist/core/config.js.map +1 -1
  37. package/dist/core/operations/create-model.d.ts +2 -0
  38. package/dist/core/operations/create-model.d.ts.map +1 -1
  39. package/dist/core/operations/create-model.js +60 -8
  40. package/dist/core/operations/create-model.js.map +1 -1
  41. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  42. package/dist/core/operations/migration-generator.js +12 -1
  43. package/dist/core/operations/migration-generator.js.map +1 -1
  44. package/dist/core/operations/model-migration.d.ts +22 -0
  45. package/dist/core/operations/model-migration.d.ts.map +1 -0
  46. package/dist/core/operations/model-migration.js +36 -0
  47. package/dist/core/operations/model-migration.js.map +1 -0
  48. package/dist/core/operations/provision-deployment.d.ts +52 -0
  49. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  50. package/dist/core/operations/provision-deployment.js +164 -0
  51. package/dist/core/operations/provision-deployment.js.map +1 -0
  52. package/dist/core/operations/provision-operations.d.ts +62 -6
  53. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  54. package/dist/core/operations/provision-operations.js +653 -115
  55. package/dist/core/operations/provision-operations.js.map +1 -1
  56. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  57. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  58. package/dist/core/operations/scaffold-apply.js +1 -0
  59. package/dist/core/operations/scaffold-apply.js.map +1 -1
  60. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  61. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  62. package/dist/core/operations/scaffold-machine.js.map +1 -1
  63. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  64. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  65. package/dist/core/operations/scaffold-plan.js +3 -1
  66. package/dist/core/operations/scaffold-plan.js.map +1 -1
  67. package/dist/core/project/agent-instructions.d.ts +2 -1
  68. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  69. package/dist/core/project/agent-instructions.js +15 -0
  70. package/dist/core/project/agent-instructions.js.map +1 -1
  71. package/dist/core/project/boundaries.d.ts +5 -2
  72. package/dist/core/project/boundaries.d.ts.map +1 -1
  73. package/dist/core/project/boundaries.js +51 -20
  74. package/dist/core/project/boundaries.js.map +1 -1
  75. package/dist/core/project/capabilities.d.ts +17 -0
  76. package/dist/core/project/capabilities.d.ts.map +1 -1
  77. package/dist/core/project/capabilities.js +45 -6
  78. package/dist/core/project/capabilities.js.map +1 -1
  79. package/dist/core/project/home-page.d.ts +4 -0
  80. package/dist/core/project/home-page.d.ts.map +1 -0
  81. package/dist/core/project/home-page.js +113 -0
  82. package/dist/core/project/home-page.js.map +1 -0
  83. package/dist/core/project/manifest.d.ts +3 -1
  84. package/dist/core/project/manifest.d.ts.map +1 -1
  85. package/dist/core/project/manifest.js +47 -25
  86. package/dist/core/project/manifest.js.map +1 -1
  87. package/dist/core/project/mcp-config.d.ts +26 -0
  88. package/dist/core/project/mcp-config.d.ts.map +1 -0
  89. package/dist/core/project/mcp-config.js +117 -0
  90. package/dist/core/project/mcp-config.js.map +1 -0
  91. package/dist/core/project/react-project.d.ts +26 -0
  92. package/dist/core/project/react-project.d.ts.map +1 -0
  93. package/dist/core/project/react-project.js +443 -0
  94. package/dist/core/project/react-project.js.map +1 -0
  95. package/dist/core/project/state.d.ts +7 -0
  96. package/dist/core/project/state.d.ts.map +1 -1
  97. package/dist/core/project/state.js +86 -0
  98. package/dist/core/project/state.js.map +1 -1
  99. package/dist/core/project/validation.d.ts.map +1 -1
  100. package/dist/core/project/validation.js +32 -10
  101. package/dist/core/project/validation.js.map +1 -1
  102. package/dist/core/project/workflows.d.ts.map +1 -1
  103. package/dist/core/project/workflows.js +53 -7
  104. package/dist/core/project/workflows.js.map +1 -1
  105. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  106. package/dist/core/scaffold/auth-generator.js +26 -27
  107. package/dist/core/scaffold/auth-generator.js.map +1 -1
  108. package/dist/core/scaffold/model-parser.d.ts +23 -0
  109. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  110. package/dist/core/scaffold/model-parser.js +407 -137
  111. package/dist/core/scaffold/model-parser.js.map +1 -1
  112. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  113. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  114. package/dist/core/scaffold/native-schema-generator.js +294 -91
  115. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  116. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  117. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  118. package/dist/core/scaffold/openapi-generator.js +53 -3
  119. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  120. package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
  121. package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
  122. package/dist/core/scaffold/react-bff-generator.js +252 -0
  123. package/dist/core/scaffold/react-bff-generator.js.map +1 -0
  124. package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
  125. package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
  126. package/dist/core/scaffold/react-ui-generator.js +41 -0
  127. package/dist/core/scaffold/react-ui-generator.js.map +1 -0
  128. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  129. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  130. package/dist/core/scaffold/ui-generator.js +68 -7
  131. package/dist/core/scaffold/ui-generator.js.map +1 -1
  132. package/dist/core/verify/index.d.ts +4 -3
  133. package/dist/core/verify/index.d.ts.map +1 -1
  134. package/dist/core/verify/index.js +122 -5
  135. package/dist/core/verify/index.js.map +1 -1
  136. package/dist/index.d.ts +3 -3
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +3 -2
  139. package/dist/index.js.map +1 -1
  140. package/dist/machine/contracts.d.ts +1 -0
  141. package/dist/machine/contracts.d.ts.map +1 -1
  142. package/dist/machine/errors.d.ts +3 -2
  143. package/dist/machine/errors.d.ts.map +1 -1
  144. package/dist/machine/errors.js +2 -1
  145. package/dist/machine/errors.js.map +1 -1
  146. package/dist/machine/index.d.ts.map +1 -1
  147. package/dist/machine/index.js +61 -30
  148. package/dist/machine/index.js.map +1 -1
  149. package/dist/mcp/index.d.ts +6 -4
  150. package/dist/mcp/index.d.ts.map +1 -1
  151. package/dist/mcp/index.js +210 -25
  152. package/dist/mcp/index.js.map +1 -1
  153. package/dist/mcp/version-selection.d.ts +55 -0
  154. package/dist/mcp/version-selection.d.ts.map +1 -0
  155. package/dist/mcp/version-selection.js +152 -0
  156. package/dist/mcp/version-selection.js.map +1 -0
  157. package/dist/types/index.d.ts +4 -0
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/utils/azure-cli.d.ts +56 -0
  160. package/dist/utils/azure-cli.d.ts.map +1 -0
  161. package/dist/utils/azure-cli.js +326 -0
  162. package/dist/utils/azure-cli.js.map +1 -0
  163. package/dist/utils/azure-diagnostics.d.ts +16 -0
  164. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  165. package/dist/utils/azure-diagnostics.js +68 -0
  166. package/dist/utils/azure-diagnostics.js.map +1 -0
  167. package/dist/utils/package-manager.d.ts +2 -2
  168. package/dist/utils/package-manager.d.ts.map +1 -1
  169. package/dist/utils/package-manager.js +6 -3
  170. package/dist/utils/package-manager.js.map +1 -1
  171. package/dist/utils/python-uv.d.ts +5 -7
  172. package/dist/utils/python-uv.d.ts.map +1 -1
  173. package/dist/utils/python-uv.js +50 -24
  174. package/dist/utils/python-uv.js.map +1 -1
  175. package/dist/utils/react-dev.d.ts +4 -0
  176. package/dist/utils/react-dev.d.ts.map +1 -0
  177. package/dist/utils/react-dev.js +35 -0
  178. package/dist/utils/react-dev.js.map +1 -0
  179. package/dist/utils/semver.d.ts +14 -0
  180. package/dist/utils/semver.d.ts.map +1 -0
  181. package/dist/utils/semver.js +61 -0
  182. package/dist/utils/semver.js.map +1 -0
  183. package/package.json +7 -4
  184. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  185. package/src/__tests__/agent-instructions.test.ts +6 -1
  186. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  187. package/src/__tests__/auth.test.ts +9 -0
  188. package/src/__tests__/azure-cli.test.ts +202 -0
  189. package/src/__tests__/boundaries.test.ts +42 -0
  190. package/src/__tests__/csharp-schema.test.ts +261 -0
  191. package/src/__tests__/dev.test.ts +13 -6
  192. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  193. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  194. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  195. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  196. package/src/__tests__/home-page.test.ts +50 -0
  197. package/src/__tests__/init.test.ts +145 -5
  198. package/src/__tests__/mcp-version.test.ts +262 -0
  199. package/src/__tests__/mcp.test.ts +120 -3
  200. package/src/__tests__/multi-auth.test.ts +115 -4
  201. package/src/__tests__/package-manager.test.ts +17 -2
  202. package/src/__tests__/plan-apply.test.ts +179 -0
  203. package/src/__tests__/provision-runtime.test.ts +722 -0
  204. package/src/__tests__/python-uv.test.ts +50 -14
  205. package/src/__tests__/python-venv.test.ts +48 -0
  206. package/src/__tests__/react-dev.test.ts +72 -0
  207. package/src/__tests__/react.test.ts +838 -0
  208. package/src/__tests__/release-metadata.test.ts +121 -0
  209. package/src/__tests__/schema-analysis.test.ts +329 -0
  210. package/src/cli/commands/add-auth.ts +103 -29
  211. package/src/cli/commands/configure-mcp.ts +54 -0
  212. package/src/cli/commands/dev.ts +145 -73
  213. package/src/cli/commands/init.ts +147 -172
  214. package/src/cli/commands/migrations.ts +22 -0
  215. package/src/cli/commands/provision.ts +294 -56
  216. package/src/cli/commands/scaffold.ts +145 -22
  217. package/src/cli/index.ts +46 -10
  218. package/src/core/config.ts +21 -1
  219. package/src/core/operations/create-model.ts +51 -13
  220. package/src/core/operations/migration-generator.ts +14 -1
  221. package/src/core/operations/model-migration.ts +52 -0
  222. package/src/core/operations/provision-deployment.ts +183 -0
  223. package/src/core/operations/provision-operations.ts +662 -154
  224. package/src/core/operations/scaffold-apply.ts +2 -0
  225. package/src/core/operations/scaffold-machine.ts +1 -0
  226. package/src/core/operations/scaffold-plan.ts +5 -1
  227. package/src/core/project/agent-instructions.ts +17 -1
  228. package/src/core/project/agent-templates/AGENTS.md +113 -15
  229. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  230. package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
  231. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  232. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  233. package/src/core/project/boundaries.ts +62 -20
  234. package/src/core/project/capabilities.ts +63 -6
  235. package/src/core/project/home-page.ts +114 -0
  236. package/src/core/project/manifest.ts +52 -27
  237. package/src/core/project/mcp-config.ts +84 -0
  238. package/src/core/project/react-project.ts +529 -0
  239. package/src/core/project/state.ts +70 -0
  240. package/src/core/project/validation.ts +32 -10
  241. package/src/core/project/workflows.ts +53 -7
  242. package/src/core/scaffold/auth-generator.ts +26 -27
  243. package/src/core/scaffold/model-parser.ts +413 -129
  244. package/src/core/scaffold/native-schema-generator.ts +319 -103
  245. package/src/core/scaffold/openapi-generator.ts +63 -5
  246. package/src/core/scaffold/react-bff-generator.ts +292 -0
  247. package/src/core/scaffold/react-ui-generator.ts +51 -0
  248. package/src/core/scaffold/ui-generator.ts +70 -8
  249. package/src/core/verify/index.ts +118 -6
  250. package/src/index.ts +3 -1
  251. package/src/machine/contracts.ts +1 -0
  252. package/src/machine/errors.ts +4 -2
  253. package/src/machine/index.ts +67 -36
  254. package/src/mcp/index.ts +221 -31
  255. package/src/mcp/version-selection.ts +163 -0
  256. package/src/types/index.ts +4 -0
  257. package/src/utils/azure-cli.ts +315 -0
  258. package/src/utils/azure-diagnostics.ts +75 -0
  259. package/src/utils/package-manager.ts +7 -4
  260. package/src/utils/python-uv.ts +53 -33
  261. package/src/utils/react-dev.ts +31 -0
  262. 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: field.name.toLowerCase().endsWith("at") ? "date-time" : undefined,
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(f => f.isArray && !f.isNestedSchema).map(f => ` if (typeof submitData.${f.name} === 'string') {
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>