swallowkit 2.0.0-alpha.1 → 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 (224) hide show
  1. package/README.ja.md +14 -0
  2. package/README.md +14 -0
  3. package/dist/cli/commands/add-auth.js +37 -6
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  6. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  7. package/dist/cli/commands/configure-mcp.js +42 -0
  8. package/dist/cli/commands/configure-mcp.js.map +1 -0
  9. package/dist/cli/commands/dev.d.ts +2 -2
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +39 -27
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +8 -2
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +67 -132
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/migrations.d.ts.map +1 -1
  18. package/dist/cli/commands/migrations.js +25 -0
  19. package/dist/cli/commands/migrations.js.map +1 -1
  20. package/dist/cli/commands/provision.d.ts +5 -0
  21. package/dist/cli/commands/provision.d.ts.map +1 -1
  22. package/dist/cli/commands/provision.js +278 -55
  23. package/dist/cli/commands/provision.js.map +1 -1
  24. package/dist/cli/commands/scaffold.d.ts +4 -0
  25. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  26. package/dist/cli/commands/scaffold.js +85 -3
  27. package/dist/cli/commands/scaffold.js.map +1 -1
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.d.ts.map +1 -1
  30. package/dist/cli/index.js +43 -5
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/core/operations/create-model.d.ts +2 -0
  33. package/dist/core/operations/create-model.d.ts.map +1 -1
  34. package/dist/core/operations/create-model.js +60 -8
  35. package/dist/core/operations/create-model.js.map +1 -1
  36. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  37. package/dist/core/operations/migration-generator.js +12 -1
  38. package/dist/core/operations/migration-generator.js.map +1 -1
  39. package/dist/core/operations/model-migration.d.ts +22 -0
  40. package/dist/core/operations/model-migration.d.ts.map +1 -0
  41. package/dist/core/operations/model-migration.js +36 -0
  42. package/dist/core/operations/model-migration.js.map +1 -0
  43. package/dist/core/operations/provision-deployment.d.ts +52 -0
  44. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  45. package/dist/core/operations/provision-deployment.js +164 -0
  46. package/dist/core/operations/provision-deployment.js.map +1 -0
  47. package/dist/core/operations/provision-operations.d.ts +62 -6
  48. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  49. package/dist/core/operations/provision-operations.js +653 -115
  50. package/dist/core/operations/provision-operations.js.map +1 -1
  51. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  52. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  53. package/dist/core/operations/scaffold-apply.js +1 -0
  54. package/dist/core/operations/scaffold-apply.js.map +1 -1
  55. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  56. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  57. package/dist/core/operations/scaffold-machine.js.map +1 -1
  58. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  59. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  60. package/dist/core/operations/scaffold-plan.js +3 -1
  61. package/dist/core/operations/scaffold-plan.js.map +1 -1
  62. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  63. package/dist/core/project/agent-instructions.js +4 -1
  64. package/dist/core/project/agent-instructions.js.map +1 -1
  65. package/dist/core/project/boundaries.d.ts +5 -2
  66. package/dist/core/project/boundaries.d.ts.map +1 -1
  67. package/dist/core/project/boundaries.js +51 -20
  68. package/dist/core/project/boundaries.js.map +1 -1
  69. package/dist/core/project/capabilities.d.ts +10 -0
  70. package/dist/core/project/capabilities.d.ts.map +1 -1
  71. package/dist/core/project/capabilities.js +38 -6
  72. package/dist/core/project/capabilities.js.map +1 -1
  73. package/dist/core/project/home-page.d.ts +4 -0
  74. package/dist/core/project/home-page.d.ts.map +1 -0
  75. package/dist/core/project/home-page.js +113 -0
  76. package/dist/core/project/home-page.js.map +1 -0
  77. package/dist/core/project/mcp-config.d.ts +26 -0
  78. package/dist/core/project/mcp-config.d.ts.map +1 -0
  79. package/dist/core/project/mcp-config.js +117 -0
  80. package/dist/core/project/mcp-config.js.map +1 -0
  81. package/dist/core/project/react-project.d.ts +1 -1
  82. package/dist/core/project/react-project.d.ts.map +1 -1
  83. package/dist/core/project/react-project.js +46 -25
  84. package/dist/core/project/react-project.js.map +1 -1
  85. package/dist/core/project/state.d.ts +7 -0
  86. package/dist/core/project/state.d.ts.map +1 -1
  87. package/dist/core/project/state.js +86 -0
  88. package/dist/core/project/state.js.map +1 -1
  89. package/dist/core/project/workflows.d.ts.map +1 -1
  90. package/dist/core/project/workflows.js +53 -7
  91. package/dist/core/project/workflows.js.map +1 -1
  92. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  93. package/dist/core/scaffold/auth-generator.js +26 -27
  94. package/dist/core/scaffold/auth-generator.js.map +1 -1
  95. package/dist/core/scaffold/model-parser.d.ts +23 -0
  96. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  97. package/dist/core/scaffold/model-parser.js +407 -137
  98. package/dist/core/scaffold/model-parser.js.map +1 -1
  99. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  100. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  101. package/dist/core/scaffold/native-schema-generator.js +294 -91
  102. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  103. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  104. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  105. package/dist/core/scaffold/openapi-generator.js +53 -3
  106. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  107. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  108. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  109. package/dist/core/scaffold/ui-generator.js +68 -7
  110. package/dist/core/scaffold/ui-generator.js.map +1 -1
  111. package/dist/core/verify/index.d.ts +4 -3
  112. package/dist/core/verify/index.d.ts.map +1 -1
  113. package/dist/core/verify/index.js +122 -5
  114. package/dist/core/verify/index.js.map +1 -1
  115. package/dist/machine/contracts.d.ts +1 -0
  116. package/dist/machine/contracts.d.ts.map +1 -1
  117. package/dist/machine/errors.d.ts +3 -2
  118. package/dist/machine/errors.d.ts.map +1 -1
  119. package/dist/machine/errors.js +2 -1
  120. package/dist/machine/errors.js.map +1 -1
  121. package/dist/machine/index.d.ts.map +1 -1
  122. package/dist/machine/index.js +59 -28
  123. package/dist/machine/index.js.map +1 -1
  124. package/dist/mcp/index.d.ts +6 -4
  125. package/dist/mcp/index.d.ts.map +1 -1
  126. package/dist/mcp/index.js +210 -25
  127. package/dist/mcp/index.js.map +1 -1
  128. package/dist/mcp/version-selection.d.ts +55 -0
  129. package/dist/mcp/version-selection.d.ts.map +1 -0
  130. package/dist/mcp/version-selection.js +152 -0
  131. package/dist/mcp/version-selection.js.map +1 -0
  132. package/dist/utils/azure-cli.d.ts +56 -0
  133. package/dist/utils/azure-cli.d.ts.map +1 -0
  134. package/dist/utils/azure-cli.js +326 -0
  135. package/dist/utils/azure-cli.js.map +1 -0
  136. package/dist/utils/azure-diagnostics.d.ts +16 -0
  137. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  138. package/dist/utils/azure-diagnostics.js +68 -0
  139. package/dist/utils/azure-diagnostics.js.map +1 -0
  140. package/dist/utils/package-manager.js +2 -2
  141. package/dist/utils/package-manager.js.map +1 -1
  142. package/dist/utils/python-uv.d.ts +5 -7
  143. package/dist/utils/python-uv.d.ts.map +1 -1
  144. package/dist/utils/python-uv.js +50 -24
  145. package/dist/utils/python-uv.js.map +1 -1
  146. package/dist/utils/react-dev.d.ts +4 -0
  147. package/dist/utils/react-dev.d.ts.map +1 -0
  148. package/dist/utils/react-dev.js +35 -0
  149. package/dist/utils/react-dev.js.map +1 -0
  150. package/dist/utils/semver.d.ts +14 -0
  151. package/dist/utils/semver.d.ts.map +1 -0
  152. package/dist/utils/semver.js +61 -0
  153. package/dist/utils/semver.js.map +1 -0
  154. package/package.json +6 -3
  155. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  156. package/src/__tests__/agent-instructions.test.ts +5 -0
  157. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  158. package/src/__tests__/auth.test.ts +9 -0
  159. package/src/__tests__/azure-cli.test.ts +202 -0
  160. package/src/__tests__/boundaries.test.ts +42 -0
  161. package/src/__tests__/csharp-schema.test.ts +261 -0
  162. package/src/__tests__/dev.test.ts +13 -6
  163. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  164. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  165. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  166. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  167. package/src/__tests__/home-page.test.ts +50 -0
  168. package/src/__tests__/init.test.ts +116 -0
  169. package/src/__tests__/mcp-version.test.ts +262 -0
  170. package/src/__tests__/mcp.test.ts +120 -3
  171. package/src/__tests__/multi-auth.test.ts +115 -4
  172. package/src/__tests__/package-manager.test.ts +17 -2
  173. package/src/__tests__/plan-apply.test.ts +179 -0
  174. package/src/__tests__/provision-runtime.test.ts +722 -0
  175. package/src/__tests__/python-uv.test.ts +50 -14
  176. package/src/__tests__/python-venv.test.ts +48 -0
  177. package/src/__tests__/react-dev.test.ts +72 -0
  178. package/src/__tests__/react.test.ts +19 -0
  179. package/src/__tests__/schema-analysis.test.ts +329 -0
  180. package/src/cli/commands/add-auth.ts +36 -6
  181. package/src/cli/commands/configure-mcp.ts +54 -0
  182. package/src/cli/commands/dev.ts +40 -31
  183. package/src/cli/commands/init.ts +73 -138
  184. package/src/cli/commands/migrations.ts +22 -0
  185. package/src/cli/commands/provision.ts +294 -56
  186. package/src/cli/commands/scaffold.ts +105 -3
  187. package/src/cli/index.ts +40 -7
  188. package/src/core/operations/create-model.ts +51 -13
  189. package/src/core/operations/migration-generator.ts +14 -1
  190. package/src/core/operations/model-migration.ts +52 -0
  191. package/src/core/operations/provision-deployment.ts +183 -0
  192. package/src/core/operations/provision-operations.ts +662 -154
  193. package/src/core/operations/scaffold-apply.ts +2 -0
  194. package/src/core/operations/scaffold-machine.ts +1 -0
  195. package/src/core/operations/scaffold-plan.ts +5 -1
  196. package/src/core/project/agent-instructions.ts +4 -1
  197. package/src/core/project/agent-templates/AGENTS.md +89 -6
  198. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  199. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  200. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  201. package/src/core/project/boundaries.ts +62 -20
  202. package/src/core/project/capabilities.ts +49 -6
  203. package/src/core/project/home-page.ts +114 -0
  204. package/src/core/project/mcp-config.ts +84 -0
  205. package/src/core/project/react-project.ts +50 -28
  206. package/src/core/project/state.ts +70 -0
  207. package/src/core/project/workflows.ts +53 -7
  208. package/src/core/scaffold/auth-generator.ts +26 -27
  209. package/src/core/scaffold/model-parser.ts +413 -129
  210. package/src/core/scaffold/native-schema-generator.ts +319 -103
  211. package/src/core/scaffold/openapi-generator.ts +63 -5
  212. package/src/core/scaffold/ui-generator.ts +70 -8
  213. package/src/core/verify/index.ts +118 -6
  214. package/src/machine/contracts.ts +1 -0
  215. package/src/machine/errors.ts +4 -2
  216. package/src/machine/index.ts +65 -34
  217. package/src/mcp/index.ts +221 -31
  218. package/src/mcp/version-selection.ts +163 -0
  219. package/src/utils/azure-cli.ts +315 -0
  220. package/src/utils/azure-diagnostics.ts +75 -0
  221. package/src/utils/package-manager.ts +2 -2
  222. package/src/utils/python-uv.ts +53 -33
  223. package/src/utils/react-dev.ts +31 -0
  224. package/src/utils/semver.ts +59 -0
@@ -35,6 +35,22 @@ export interface ModelInfo {
35
35
  semanticFingerprint: string;
36
36
  }
37
37
 
38
+ export type FieldFormat = "date-time" | "date" | "time" | "email" | "uuid" | "uri";
39
+
40
+ /** Zod validation constraints that native backends can enforce. For arrays, length bounds are minItems/maxItems and other values apply to each element. */
41
+ export interface FieldConstraints {
42
+ minLength?: number;
43
+ maxLength?: number;
44
+ pattern?: string;
45
+ patternFlags?: string;
46
+ minimum?: number;
47
+ maximum?: number;
48
+ exclusiveMinimum?: boolean;
49
+ exclusiveMaximum?: boolean;
50
+ minItems?: number;
51
+ maxItems?: number;
52
+ }
53
+
38
54
  export interface FieldInfo {
39
55
  name: string;
40
56
  type: string; // "string" | "number" | "boolean" | "date" | "object" | "array"
@@ -42,6 +58,10 @@ export interface FieldInfo {
42
58
  isNullable?: boolean;
43
59
  isArray: boolean;
44
60
  enumValues?: string[]; // enum の場合の選択肢
61
+ format?: FieldFormat; // 明示された文字列形式(z.iso.datetime() など)
62
+ isInteger?: boolean; // z.number().int() / z.int()
63
+ constraints?: FieldConstraints;
64
+ objectFields?: FieldInfo[]; // インライン z.object({...}) の形
45
65
  isForeignKey?: boolean; // 外部キーかどうか
46
66
  referencedModel?: string; // 参照先のモデル名(例: "Category")
47
67
  isNestedSchema?: boolean; // ネストしたスキーマ参照かどうか
@@ -113,6 +133,10 @@ export async function parseModelFile(modelPath: string, options: ModelParserOpti
113
133
  // フィールド情報を抽出(動的インポートを使用)
114
134
  const parsedSchema = await extractFieldsFromSchema(modelPath, schemaName, options.executeNode ?? execFileSync);
115
135
  const fields = parsedSchema.fields;
136
+ const parserWarnings = [...parsedSchema.warnings];
137
+
138
+ // 外部キーは参照先モデルが shared/models に実在する場合だけ推測する
139
+ applyForeignKeys(fields, modelName, modelPath, parseReferences(content), parserWarnings);
116
140
 
117
141
  // ネストスキーマ情報をフィールドにマージ
118
142
  mergeNestedSchemaInfo(fields, nestedSchemaRefs);
@@ -147,7 +171,7 @@ export async function parseModelFile(modelPath: string, options: ModelParserOpti
147
171
  ...(connectorConfig ? { connectorConfig } : {}),
148
172
  ...(authPolicy ? { authPolicy } : {}),
149
173
  parserMode: parsedSchema.mode,
150
- parserWarnings: parsedSchema.warnings,
174
+ parserWarnings,
151
175
  semanticFingerprint: "",
152
176
  };
153
177
  modelInfo.semanticFingerprint = createModelSemanticFingerprint(modelInfo);
@@ -396,6 +420,16 @@ interface ParsedSchemaFields {
396
420
  warnings: string[];
397
421
  }
398
422
 
423
+ const STRING_FORMATS: Record<string, FieldFormat> = {
424
+ email: "email",
425
+ url: "uri",
426
+ uuid: "uuid",
427
+ guid: "uuid",
428
+ datetime: "date-time",
429
+ date: "date",
430
+ time: "time",
431
+ };
432
+
399
433
  function staticParseFields(modelPath: string, schemaName: string): FieldInfo[] | null {
400
434
  const source = ts.createSourceFile(modelPath, fs.readFileSync(modelPath, "utf-8"), ts.ScriptTarget.Latest, true);
401
435
  const constants = new Map<string, ts.Expression>();
@@ -444,7 +478,137 @@ function staticParseFields(modelPath: string, schemaName: string): FieldInfo[] |
444
478
  return values;
445
479
  };
446
480
 
447
- const parseExpression = (expression: ts.Expression): Omit<FieldInfo, "name"> | null => {
481
+ type ParsedField = Omit<FieldInfo, "name">;
482
+
483
+ const numericArgument = (expression: ts.Expression | undefined): number | null => {
484
+ if (!expression) return null;
485
+ if (ts.isNumericLiteral(expression)) return Number(expression.text);
486
+ if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(expression.operand)) {
487
+ return -Number(expression.operand.text);
488
+ }
489
+ if (ts.isIdentifier(expression)) {
490
+ const resolved = constants.get(expression.text);
491
+ return resolved ? numericArgument(resolved) : null;
492
+ }
493
+ return null;
494
+ };
495
+
496
+ const withConstraints = (field: ParsedField, update: FieldConstraints): ParsedField => ({
497
+ ...field,
498
+ constraints: { ...(field.constraints ?? {}), ...update },
499
+ });
500
+
501
+ const lowerBound = (field: ParsedField, key: "minLength" | "minimum" | "minItems", value: number, exclusive = false): ParsedField => {
502
+ const current = field.constraints?.[key];
503
+ if (current !== undefined && current > value) return field;
504
+ const constraints: FieldConstraints = { ...(field.constraints ?? {}), [key]: value };
505
+ if (key === "minimum") {
506
+ // An equal bound stays exclusive if either check was exclusive (gt(0).min(0) still rejects 0).
507
+ if (exclusive || (current === value && field.constraints?.exclusiveMinimum)) constraints.exclusiveMinimum = true;
508
+ else delete constraints.exclusiveMinimum;
509
+ }
510
+ return { ...field, constraints };
511
+ };
512
+
513
+ const upperBound = (field: ParsedField, key: "maxLength" | "maximum" | "maxItems", value: number, exclusive = false): ParsedField => {
514
+ const current = field.constraints?.[key];
515
+ if (current !== undefined && current < value) return field;
516
+ const constraints: FieldConstraints = { ...(field.constraints ?? {}), [key]: value };
517
+ if (key === "maximum") {
518
+ if (exclusive || (current === value && field.constraints?.exclusiveMaximum)) constraints.exclusiveMaximum = true;
519
+ else delete constraints.exclusiveMaximum;
520
+ }
521
+ return { ...field, constraints };
522
+ };
523
+
524
+ const applyCheck = (inner: ParsedField, method: string, args: ts.NodeArray<ts.Expression>): ParsedField | null => {
525
+ const lengthTarget = inner.isArray ? "items" : inner.type === "string" ? "length" : inner.type === "number" ? "value" : null;
526
+ const value = numericArgument(args[0]);
527
+ switch (method) {
528
+ case "optional":
529
+ case "default":
530
+ return { ...inner, isOptional: true };
531
+ case "nullable":
532
+ return { ...inner, isNullable: true };
533
+ case "nullish":
534
+ return { ...inner, isOptional: true, isNullable: true };
535
+ case "describe":
536
+ case "trim":
537
+ case "toLowerCase":
538
+ case "toUpperCase":
539
+ case "strict":
540
+ case "strip":
541
+ case "passthrough":
542
+ case "loose":
543
+ return inner;
544
+ case "int":
545
+ return inner.type === "number" && !inner.isArray ? { ...inner, isInteger: true } : null;
546
+ case "positive":
547
+ return inner.type === "number" && !inner.isArray ? lowerBound(inner, "minimum", 0, true) : null;
548
+ case "nonnegative":
549
+ return inner.type === "number" && !inner.isArray ? lowerBound(inner, "minimum", 0) : null;
550
+ case "negative":
551
+ return inner.type === "number" && !inner.isArray ? upperBound(inner, "maximum", 0, true) : null;
552
+ case "nonpositive":
553
+ return inner.type === "number" && !inner.isArray ? upperBound(inner, "maximum", 0) : null;
554
+ case "gt":
555
+ case "gte":
556
+ return inner.type === "number" && !inner.isArray && value !== null ? lowerBound(inner, "minimum", value, method === "gt") : null;
557
+ case "lt":
558
+ case "lte":
559
+ return inner.type === "number" && !inner.isArray && value !== null ? upperBound(inner, "maximum", value, method === "lt") : null;
560
+ case "min":
561
+ if (value === null || !lengthTarget) return null;
562
+ return lowerBound(inner, lengthTarget === "items" ? "minItems" : lengthTarget === "length" ? "minLength" : "minimum", value);
563
+ case "max":
564
+ if (value === null || !lengthTarget) return null;
565
+ return upperBound(inner, lengthTarget === "items" ? "maxItems" : lengthTarget === "length" ? "maxLength" : "maximum", value);
566
+ case "length":
567
+ if (value === null || (lengthTarget !== "items" && lengthTarget !== "length")) return null;
568
+ return lengthTarget === "items"
569
+ ? upperBound(lowerBound(inner, "minItems", value), "maxItems", value)
570
+ : upperBound(lowerBound(inner, "minLength", value), "maxLength", value);
571
+ case "nonempty":
572
+ if (lengthTarget !== "items" && lengthTarget !== "length") return null;
573
+ return lowerBound(inner, lengthTarget === "items" ? "minItems" : "minLength", 1);
574
+ case "regex": {
575
+ const literal = args[0];
576
+ if (!literal || !ts.isRegularExpressionLiteral(literal) || inner.type !== "string" || inner.isArray) return null;
577
+ const text = literal.text;
578
+ const separator = text.lastIndexOf("/");
579
+ return withConstraints(inner, {
580
+ pattern: text.slice(1, separator),
581
+ ...(text.slice(separator + 1) ? { patternFlags: text.slice(separator + 1) } : {}),
582
+ });
583
+ }
584
+ case "email":
585
+ case "url":
586
+ case "uuid":
587
+ case "guid":
588
+ case "datetime":
589
+ case "date":
590
+ case "time":
591
+ if (inner.type !== "string" || inner.isArray) return null;
592
+ return { ...inner, format: STRING_FORMATS[method] };
593
+ default:
594
+ return null;
595
+ }
596
+ };
597
+
598
+ const parseObjectFields = (literal: ts.ObjectLiteralExpression): FieldInfo[] | null => {
599
+ const objectFields: FieldInfo[] = [];
600
+ for (const property of literal.properties) {
601
+ if (!ts.isPropertyAssignment(property)) return null;
602
+ const name = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) ? property.name.text : null;
603
+ if (!name) return null;
604
+ const parsed = parseExpression(property.initializer);
605
+ if (!parsed) return null;
606
+ objectFields.push({ name, ...parsed });
607
+ }
608
+ return objectFields;
609
+ };
610
+
611
+ function parseExpression(expression: ts.Expression): ParsedField | null {
448
612
  if (ts.isIdentifier(expression)) {
449
613
  const resolved = constants.get(expression.text);
450
614
  if (resolved) return parseExpression(resolved);
@@ -455,54 +619,48 @@ function staticParseFields(modelPath: string, schemaName: string): FieldInfo[] |
455
619
  if (!ts.isCallExpression(expression) || !ts.isPropertyAccessExpression(expression.expression)) return null;
456
620
  const method = expression.expression.name.text;
457
621
  const receiver = expression.expression.expression;
458
- const wrappers = new Set(["optional", "nullable", "default", "min", "max", "int", "positive", "negative", "nonnegative", "nonpositive", "regex", "email", "url", "uuid", "trim"]);
459
- if (wrappers.has(method)) {
622
+ const base = (type: string, extra: Partial<ParsedField> = {}): ParsedField => ({ type, isOptional: false, isNullable: false, isArray: false, ...extra });
623
+
624
+ // z.iso.datetime() / z.iso.date() / z.iso.time()
625
+ if (ts.isPropertyAccessExpression(receiver) && ts.isIdentifier(receiver.expression) && receiver.expression.text === "z" && receiver.name.text === "iso") {
626
+ return method === "datetime" || method === "date" || method === "time" ? base("string", { format: STRING_FORMATS[method] }) : null;
627
+ }
628
+ if (!(ts.isIdentifier(receiver) && receiver.text === "z")) {
460
629
  const inner = parseExpression(receiver);
461
- if (!inner) return null;
462
- if (method === "optional" || method === "default") inner.isOptional = true;
463
- if (method === "nullable") inner.isNullable = true;
464
- return inner;
630
+ return inner ? applyCheck(inner, method, expression.arguments) : null;
465
631
  }
466
- if (!ts.isIdentifier(receiver) || receiver.text !== "z") return null;
467
- if (["string", "number", "boolean", "date"].includes(method)) {
468
- return { type: method, isOptional: false, isNullable: false, isArray: false };
632
+ if (["string", "boolean", "date"].includes(method)) return base(method);
633
+ if (method === "number") return base("number");
634
+ if (method === "int") return base("number", { isInteger: true });
635
+ if (method === "email" || method === "url" || method === "uuid" || method === "guid") return base("string", { format: STRING_FORMATS[method] });
636
+ if (method === "object") {
637
+ const literal = expression.arguments[0];
638
+ if (!literal || !ts.isObjectLiteralExpression(literal)) return null;
639
+ // Unsupported members keep the previous untyped-object behavior instead of failing the whole model.
640
+ const objectFields = parseObjectFields(literal);
641
+ return objectFields ? base("object", { objectFields }) : base("object");
469
642
  }
470
- if (method === "object") return { type: "object", isOptional: false, isNullable: false, isArray: false };
471
643
  if (method === "array" && expression.arguments[0]) {
472
644
  const element = parseExpression(expression.arguments[0]);
473
645
  return element ? { ...element, isArray: true } : null;
474
646
  }
475
647
  if (method === "enum" && expression.arguments[0]) {
476
648
  const enumValues = literalValues(expression.arguments[0]);
477
- return enumValues ? { type: "string", isOptional: false, isNullable: false, isArray: false, enumValues } : null;
649
+ return enumValues ? base("string", { enumValues }) : null;
478
650
  }
479
651
  if (method === "literal" && expression.arguments[0]) {
480
652
  const value = expression.arguments[0];
481
653
  if (ts.isStringLiteral(value) || ts.isNumericLiteral(value)) {
482
- return { type: ts.isNumericLiteral(value) ? "number" : "string", isOptional: false, isNullable: false, isArray: false, enumValues: [value.text] };
654
+ return base(ts.isNumericLiteral(value) ? "number" : "string", { enumValues: [value.text] });
483
655
  }
484
656
  if (value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword) {
485
- return { type: "boolean", isOptional: false, isNullable: false, isArray: false, enumValues: [value.kind === ts.SyntaxKind.TrueKeyword ? "true" : "false"] };
657
+ return base("boolean", { enumValues: [value.kind === ts.SyntaxKind.TrueKeyword ? "true" : "false"] });
486
658
  }
487
659
  }
488
660
  return null;
489
- };
661
+ }
490
662
 
491
- const fields: FieldInfo[] = [];
492
- for (const property of schema.arguments[0].properties) {
493
- if (!ts.isPropertyAssignment(property)) return null;
494
- const name = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) ? property.name.text : null;
495
- if (!name) return null;
496
- const parsed = parseExpression(property.initializer);
497
- if (!parsed) return null;
498
- const isForeignKey = name.endsWith("Id") && name.length > 2 && parsed.type === "string";
499
- fields.push({
500
- name,
501
- ...parsed,
502
- ...(isForeignKey ? { isForeignKey: true, referencedModel: name.slice(0, -2).replace(/^./, char => char.toUpperCase()) } : {}),
503
- });
504
- }
505
- return fields;
663
+ return parseObjectFields(schema.arguments[0] as ts.ObjectLiteralExpression);
506
664
  }
507
665
 
508
666
  async function extractFieldsFromSchema(
@@ -532,110 +690,160 @@ async function extractFieldsFromSchema(
532
690
  const scriptCode = `
533
691
  import { ${schemaName} as schema } from './${path.basename(compiledModel)}';
534
692
 
535
- // Zod v3とv4の両方に対応
536
- const isObject = (schema && schema._def &&
537
- (schema._def.typeName === 'ZodObject' || schema.constructor?.name === 'ZodObject' || typeof schema._def.shape === 'function'));
538
-
539
- if (isObject) {
540
- const shape = typeof schema._def.shape === 'function' ? schema._def.shape() : schema._def.shape;
541
- const fields = Object.keys(shape).map(key => {
542
- const field = shape[key];
543
- let type = undefined;
544
- let isOptional = false;
545
- let isNullable = false;
546
- let isArray = false;
547
- let enumValues = undefined;
548
-
549
- // ZodOptional, ZodNullable, ZodDefault, ZodEffects を unwrap
550
- let fieldDef = field;
551
- const getTypeName = (def) => def?._def?.typeName || def?.constructor?.name || '';
552
-
553
- // 繰り返し unwrap(複数のラッパーがある場合に対応)
554
- let unwrapped = false;
555
- do {
556
- unwrapped = false;
557
- const typeName = getTypeName(fieldDef);
558
-
559
- if (typeName === 'ZodOptional') {
560
- isOptional = true;
561
- fieldDef = fieldDef._def.innerType;
562
- unwrapped = true;
563
- } else if (typeName === 'ZodNullable') {
564
- isNullable = true;
565
- fieldDef = fieldDef._def.innerType;
566
- unwrapped = true;
567
- } else if (typeName === 'ZodDefault') {
568
- // .default() は optional と同様に扱う
569
- isOptional = true;
570
- fieldDef = fieldDef._def.innerType;
571
- unwrapped = true;
572
- } else if (typeName === 'ZodEffects') {
573
- // .min(), .max(), .regex() などの Effects を unwrap
574
- fieldDef = fieldDef._def.schema;
575
- unwrapped = true;
576
- }
577
- } while (unwrapped);
578
-
579
- // ZodArray をチェック
580
- if (getTypeName(fieldDef) === 'ZodArray') {
581
- isArray = true;
582
- fieldDef = fieldDef._def.element || fieldDef._def.type;
693
+ // Zod v3 (_def.typeName) と v4 (_zod.def.type) の両方に対応
694
+ const FORMATS = { email: 'email', url: 'uri', uuid: 'uuid', guid: 'uuid', datetime: 'date-time', date: 'date', time: 'time' };
695
+ const v4 = (s) => (s && s._zod && s._zod.def) || null;
696
+ const v3Name = (s) => (s && s._def && s._def.typeName) || '';
697
+
698
+ function lower(c, key, value, exclusive) {
699
+ if (typeof value !== 'number' || (c[key] !== undefined && c[key] > value)) return;
700
+ const keepExclusive = c[key] === value && c.exclusiveMinimum;
701
+ c[key] = value;
702
+ if (key === 'minimum') { if (exclusive || keepExclusive) c.exclusiveMinimum = true; else delete c.exclusiveMinimum; }
703
+ }
704
+ function upper(c, key, value, exclusive) {
705
+ if (typeof value !== 'number' || (c[key] !== undefined && c[key] < value)) return;
706
+ const keepExclusive = c[key] === value && c.exclusiveMaximum;
707
+ c[key] = value;
708
+ if (key === 'maximum') { if (exclusive || keepExclusive) c.exclusiveMaximum = true; else delete c.exclusiveMaximum; }
709
+ }
710
+
711
+ function unwrap(s, info) {
712
+ for (;;) {
713
+ const d = v4(s);
714
+ if (d) {
715
+ if (d.type === 'optional' || d.type === 'default' || d.type === 'prefault') { info.isOptional = true; s = d.innerType; continue; }
716
+ if (d.type === 'nullable') { info.isNullable = true; s = d.innerType; continue; }
717
+ if (d.type === 'readonly' || d.type === 'nonoptional' || d.type === 'catch') { s = d.innerType; continue; }
718
+ return s;
583
719
  }
584
-
585
- // 基本型を判定
586
- const typeName = getTypeName(fieldDef);
587
- if (typeName === 'ZodString') type = 'string';
588
- else if (typeName === 'ZodNumber') type = 'number';
589
- else if (typeName === 'ZodBoolean') type = 'boolean';
590
- else if (typeName === 'ZodDate') type = 'date';
591
- else if (typeName === 'ZodObject') type = 'object';
592
- else if (typeName === 'ZodEnum' || typeName === 'ZodNativeEnum') {
593
- type = 'string';
594
- // enum の選択肢を取得(複数のZodバージョンに対応)
595
- if (fieldDef.options) {
596
- // Zod v3.23+ では options プロパティを使用
597
- enumValues = Array.isArray(fieldDef.options)
598
- ? fieldDef.options
599
- : Object.values(fieldDef.options);
600
- } else if (fieldDef._def.values) {
601
- // 古いバージョンでは _def.values を使用
602
- enumValues = Array.isArray(fieldDef._def.values)
603
- ? fieldDef._def.values
604
- : Object.values(fieldDef._def.values);
605
- } else if (fieldDef._def.entries) {
606
- // さらに古いバージョンでは _def.entries を使用
607
- enumValues = Array.isArray(fieldDef._def.entries)
608
- ? fieldDef._def.entries
609
- : Object.values(fieldDef._def.entries);
610
- }
611
- } else if (typeName === 'ZodLiteral') {
612
- const rawLiteralValues = fieldDef.values || fieldDef._def.values ||
613
- (fieldDef._def.value !== undefined ? [fieldDef._def.value] : undefined);
614
- const literalValues = rawLiteralValues instanceof Set ? Array.from(rawLiteralValues) : rawLiteralValues;
615
- if (Array.isArray(literalValues) && literalValues.length > 0) {
616
- const literalValue = literalValues[0];
617
- type = typeof literalValue === 'number' ? 'number' : typeof literalValue === 'boolean' ? 'boolean' : 'string';
618
- enumValues = literalValues.map(value => String(value));
720
+ const name = v3Name(s);
721
+ if (name === 'ZodOptional' || name === 'ZodDefault') { info.isOptional = true; s = s._def.innerType; continue; }
722
+ if (name === 'ZodNullable') { info.isNullable = true; s = s._def.innerType; continue; }
723
+ if (name === 'ZodEffects') { s = s._def.schema; continue; }
724
+ if (name === 'ZodReadonly' || name === 'ZodCatch') { s = s._def.innerType; continue; }
725
+ return s;
726
+ }
727
+ }
728
+
729
+ function describe(key, input) {
730
+ const info = { name: key, type: undefined, isOptional: false, isNullable: false, isArray: false };
731
+ const constraints = {};
732
+ let s = unwrap(input, info);
733
+
734
+ const arrayDef = v4(s);
735
+ if ((arrayDef && arrayDef.type === 'array') || v3Name(s) === 'ZodArray') {
736
+ info.isArray = true;
737
+ if (arrayDef) {
738
+ for (const check of arrayDef.checks || []) {
739
+ const cd = check._zod.def;
740
+ if (cd.check === 'min_length') lower(constraints, 'minItems', cd.minimum);
741
+ if (cd.check === 'max_length') upper(constraints, 'maxItems', cd.maximum);
742
+ if (cd.check === 'length_equals') { lower(constraints, 'minItems', cd.length); upper(constraints, 'maxItems', cd.length); }
619
743
  }
744
+ s = arrayDef.element;
745
+ } else {
746
+ const ad = s._def;
747
+ if (ad.minLength) lower(constraints, 'minItems', ad.minLength.value);
748
+ if (ad.maxLength) upper(constraints, 'maxItems', ad.maxLength.value);
749
+ if (ad.exactLength) { lower(constraints, 'minItems', ad.exactLength.value); upper(constraints, 'maxItems', ad.exactLength.value); }
750
+ s = ad.type;
620
751
  }
752
+ s = unwrap(s, {});
753
+ }
621
754
 
622
- if (!type || ((typeName === 'ZodEnum' || typeName === 'ZodNativeEnum') && (!enumValues || enumValues.length === 0))) {
623
- throw new Error('Unsupported or ambiguous Zod field "' + key + '" (' + (typeName || 'unknown') + ')');
755
+ const d = v4(s);
756
+ const name = v3Name(s);
757
+ let enumValues;
758
+ if (d) {
759
+ if (d.type === 'string') {
760
+ info.type = 'string';
761
+ if (d.format && FORMATS[d.format]) info.format = FORMATS[d.format];
762
+ if (d.format === 'regex' && d.pattern) { constraints.pattern = d.pattern.source; if (d.pattern.flags) constraints.patternFlags = d.pattern.flags; }
763
+ } else if (d.type === 'number') {
764
+ info.type = 'number';
765
+ if (d.format && /int/.test(d.format)) info.isInteger = true;
766
+ } else if (d.type === 'boolean') info.type = 'boolean';
767
+ else if (d.type === 'date') info.type = 'date';
768
+ else if (d.type === 'object') {
769
+ info.type = 'object';
770
+ const shape = typeof d.shape === 'function' ? d.shape() : d.shape;
771
+ info.objectFields = Object.keys(shape).map((child) => describe(child, shape[child]));
772
+ } else if (d.type === 'enum') {
773
+ info.type = 'string';
774
+ enumValues = Object.values(d.entries || {}).map(String);
775
+ } else if (d.type === 'literal') {
776
+ const values = Array.isArray(d.values) ? d.values : Array.from(d.values || []);
777
+ if (values.length > 0) {
778
+ info.type = typeof values[0] === 'number' ? 'number' : typeof values[0] === 'boolean' ? 'boolean' : 'string';
779
+ enumValues = values.map(String);
780
+ }
624
781
  }
625
-
626
- // 外部キー検出: フィールド名が <ModelName>Id のパターンの場合
627
- let isForeignKey = false;
628
- let referencedModel = undefined;
629
- if (key.endsWith('Id') && key.length > 2 && type === 'string') {
630
- // categoryId -> Category, userId -> User など
631
- const modelName = key.slice(0, -2); // "Id" を除去
632
- referencedModel = modelName.charAt(0).toUpperCase() + modelName.slice(1); // 先頭を大文字に
633
- isForeignKey = true;
782
+ for (const check of d.checks || []) {
783
+ const cd = check._zod.def;
784
+ const lengthKey = info.type === 'string';
785
+ if (cd.check === 'min_length' && lengthKey) lower(constraints, 'minLength', cd.minimum);
786
+ if (cd.check === 'max_length' && lengthKey) upper(constraints, 'maxLength', cd.maximum);
787
+ if (cd.check === 'length_equals' && lengthKey) { lower(constraints, 'minLength', cd.length); upper(constraints, 'maxLength', cd.length); }
788
+ if (cd.check === 'greater_than') lower(constraints, 'minimum', Number(cd.value), !cd.inclusive);
789
+ if (cd.check === 'less_than') upper(constraints, 'maximum', Number(cd.value), !cd.inclusive);
790
+ if (cd.check === 'number_format' && /int/.test(cd.format || '')) info.isInteger = true;
791
+ if (cd.check === 'string_format') {
792
+ if (cd.format === 'regex' && cd.pattern) { constraints.pattern = cd.pattern.source; if (cd.pattern.flags) constraints.patternFlags = cd.pattern.flags; }
793
+ else if (FORMATS[cd.format]) info.format = FORMATS[cd.format];
794
+ }
634
795
  }
635
-
636
- return { name: key, type, isOptional, isNullable, isArray, enumValues, isForeignKey, referencedModel };
637
- });
638
-
796
+ } else {
797
+ const def = s && s._def;
798
+ if (name === 'ZodString') {
799
+ info.type = 'string';
800
+ for (const check of def.checks || []) {
801
+ if (check.kind === 'min') lower(constraints, 'minLength', check.value);
802
+ if (check.kind === 'max') upper(constraints, 'maxLength', check.value);
803
+ if (check.kind === 'length') { lower(constraints, 'minLength', check.value); upper(constraints, 'maxLength', check.value); }
804
+ if (check.kind === 'regex') { constraints.pattern = check.regex.source; if (check.regex.flags) constraints.patternFlags = check.regex.flags; }
805
+ if (FORMATS[check.kind]) info.format = FORMATS[check.kind];
806
+ }
807
+ } else if (name === 'ZodNumber') {
808
+ info.type = 'number';
809
+ for (const check of def.checks || []) {
810
+ if (check.kind === 'int') info.isInteger = true;
811
+ if (check.kind === 'min') lower(constraints, 'minimum', check.value, !check.inclusive);
812
+ if (check.kind === 'max') upper(constraints, 'maximum', check.value, !check.inclusive);
813
+ }
814
+ } else if (name === 'ZodBoolean') info.type = 'boolean';
815
+ else if (name === 'ZodDate') info.type = 'date';
816
+ else if (name === 'ZodObject') {
817
+ info.type = 'object';
818
+ const shape = typeof def.shape === 'function' ? def.shape() : def.shape;
819
+ info.objectFields = Object.keys(shape).map((child) => describe(child, shape[child]));
820
+ } else if (name === 'ZodEnum' || name === 'ZodNativeEnum') {
821
+ info.type = 'string';
822
+ const values = s.options || def.values || def.entries;
823
+ enumValues = (Array.isArray(values) ? values : Object.values(values || {})).map(String);
824
+ } else if (name === 'ZodLiteral') {
825
+ const value = def.value;
826
+ info.type = typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string';
827
+ enumValues = [String(value)];
828
+ }
829
+ }
830
+
831
+ const typeLabel = (d && d.type) || name || 'unknown';
832
+ if (!info.type || ((typeLabel === 'enum' || name === 'ZodEnum' || name === 'ZodNativeEnum') && (!enumValues || enumValues.length === 0))) {
833
+ throw new Error('Unsupported or ambiguous Zod field "' + key + '" (' + typeLabel + ')');
834
+ }
835
+ if (enumValues) info.enumValues = enumValues;
836
+ if (Object.keys(constraints).length > 0) info.constraints = constraints;
837
+ return info;
838
+ }
839
+
840
+ const root = unwrap(schema, {});
841
+ const rootDef = v4(root);
842
+ const rootShape = rootDef && rootDef.type === 'object'
843
+ ? (typeof rootDef.shape === 'function' ? rootDef.shape() : rootDef.shape)
844
+ : v3Name(root) === 'ZodObject' ? (typeof root._def.shape === 'function' ? root._def.shape() : root._def.shape) : null;
845
+ if (rootShape) {
846
+ const fields = Object.keys(rootShape).map((key) => describe(key, rootShape[key]));
639
847
  console.log(JSON.stringify(fields));
640
848
  } else {
641
849
  throw new Error('Exported schema is not a Zod object');
@@ -862,6 +1070,82 @@ export function parseAuthPolicy(content: string): ModelAuthPolicy | undefined {
862
1070
  };
863
1071
  }
864
1072
 
1073
+ /**
1074
+ * `export const references = { userId: 'User', legacyId: false }` を静的に抽出する。
1075
+ * 値が false / null の場合は名前からの推測を無効にする。
1076
+ */
1077
+ export function parseReferences(content: string): Record<string, string | false> {
1078
+ const source = ts.createSourceFile("model.ts", content, ts.ScriptTarget.Latest, true);
1079
+ const references: Record<string, string | false> = {};
1080
+ for (const statement of source.statements) {
1081
+ if (!ts.isVariableStatement(statement)) continue;
1082
+ for (const declaration of statement.declarationList.declarations) {
1083
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "references" || !declaration.initializer) continue;
1084
+ let initializer = declaration.initializer;
1085
+ while (ts.isAsExpression(initializer) || ts.isSatisfiesExpression(initializer)) initializer = initializer.expression;
1086
+ if (!ts.isObjectLiteralExpression(initializer)) continue;
1087
+ for (const property of initializer.properties) {
1088
+ if (!ts.isPropertyAssignment(property)) continue;
1089
+ const key = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) ? property.name.text : null;
1090
+ if (!key) continue;
1091
+ const value = property.initializer;
1092
+ if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) references[key] = value.text;
1093
+ else if (value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword) references[key] = false;
1094
+ }
1095
+ }
1096
+ }
1097
+ return references;
1098
+ }
1099
+
1100
+ function listModelNames(modelPath: string): Set<string> {
1101
+ const directory = path.dirname(modelPath);
1102
+ const names = new Set<string>();
1103
+ for (const entry of fs.readdirSync(directory)) {
1104
+ if (!entry.endsWith(".ts") || entry.endsWith(".d.ts") || entry === "index.ts") continue;
1105
+ names.add(toPascalCase(path.basename(entry, ".ts")));
1106
+ }
1107
+ return names;
1108
+ }
1109
+
1110
+ function applyForeignKeys(
1111
+ fields: FieldInfo[],
1112
+ modelName: string,
1113
+ modelPath: string,
1114
+ references: Record<string, string | false>,
1115
+ warnings: string[]
1116
+ ): void {
1117
+ const models = listModelNames(modelPath);
1118
+ for (const field of fields) {
1119
+ delete field.isForeignKey;
1120
+ delete field.referencedModel;
1121
+ }
1122
+
1123
+ for (const [fieldName, target] of Object.entries(references)) {
1124
+ const field = fields.find((candidate) => candidate.name === fieldName);
1125
+ if (!field) {
1126
+ warnings.push(`reference-field-not-found: ${modelName}.references declares ${fieldName}, but the schema has no such field.`);
1127
+ } else if (target !== false && (field.type !== "string" || field.isArray || field.enumValues?.length)) {
1128
+ warnings.push(`reference-field-unsupported: ${modelName}.${fieldName} must be a single string id to reference ${target}.`);
1129
+ } else if (target !== false && !models.has(target)) {
1130
+ warnings.push(`reference-model-not-found: ${modelName}.${fieldName} references ${target}, but shared/models has no ${target} model; it is treated as a plain field.`);
1131
+ }
1132
+ }
1133
+
1134
+ for (const field of fields) {
1135
+ if (field.type !== "string" || field.isArray || field.enumValues?.length || field.isNestedSchema) continue;
1136
+ const declared = references[field.name];
1137
+ if (declared === false) continue;
1138
+ const inferred = field.name.length > 2 && field.name.endsWith("Id")
1139
+ ? field.name.slice(0, -2).replace(/^./, (char) => char.toUpperCase())
1140
+ : undefined;
1141
+ const target = declared ?? inferred;
1142
+ if (target && models.has(target)) {
1143
+ field.isForeignKey = true;
1144
+ field.referencedModel = target;
1145
+ }
1146
+ }
1147
+ }
1148
+
865
1149
  /**
866
1150
  * パーティションキーを抽出する
867
1151
  * export const partitionKey = '/tenantId' のようなエクスポートを検出