swallowkit 2.0.0-alpha.2 → 2.0.0-alpha.4

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 (176) hide show
  1. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  2. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  3. package/dist/cli/commands/configure-mcp.js +42 -0
  4. package/dist/cli/commands/configure-mcp.js.map +1 -0
  5. package/dist/cli/commands/dev.d.ts +2 -2
  6. package/dist/cli/commands/dev.d.ts.map +1 -1
  7. package/dist/cli/commands/dev.js +39 -27
  8. package/dist/cli/commands/dev.js.map +1 -1
  9. package/dist/cli/commands/init.d.ts +6 -2
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +43 -122
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/migrations.d.ts.map +1 -1
  14. package/dist/cli/commands/migrations.js +25 -0
  15. package/dist/cli/commands/migrations.js.map +1 -1
  16. package/dist/cli/commands/provision.d.ts +2 -0
  17. package/dist/cli/commands/provision.d.ts.map +1 -1
  18. package/dist/cli/commands/provision.js +45 -16
  19. package/dist/cli/commands/provision.js.map +1 -1
  20. package/dist/cli/commands/scaffold.d.ts +4 -0
  21. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  22. package/dist/cli/commands/scaffold.js +40 -2
  23. package/dist/cli/commands/scaffold.js.map +1 -1
  24. package/dist/cli/index.d.ts +2 -0
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/index.js +27 -2
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/core/operations/model-migration.d.ts +22 -0
  29. package/dist/core/operations/model-migration.d.ts.map +1 -0
  30. package/dist/core/operations/model-migration.js +36 -0
  31. package/dist/core/operations/model-migration.js.map +1 -0
  32. package/dist/core/operations/provision-operations.d.ts +19 -1
  33. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  34. package/dist/core/operations/provision-operations.js +62 -35
  35. package/dist/core/operations/provision-operations.js.map +1 -1
  36. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  37. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  38. package/dist/core/operations/scaffold-apply.js +1 -0
  39. package/dist/core/operations/scaffold-apply.js.map +1 -1
  40. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  41. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  42. package/dist/core/operations/scaffold-machine.js.map +1 -1
  43. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  44. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  45. package/dist/core/operations/scaffold-plan.js +3 -1
  46. package/dist/core/operations/scaffold-plan.js.map +1 -1
  47. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  48. package/dist/core/project/agent-instructions.js +4 -1
  49. package/dist/core/project/agent-instructions.js.map +1 -1
  50. package/dist/core/project/boundaries.d.ts +5 -2
  51. package/dist/core/project/boundaries.d.ts.map +1 -1
  52. package/dist/core/project/boundaries.js +49 -18
  53. package/dist/core/project/boundaries.js.map +1 -1
  54. package/dist/core/project/capabilities.d.ts +1 -0
  55. package/dist/core/project/capabilities.d.ts.map +1 -1
  56. package/dist/core/project/capabilities.js +20 -2
  57. package/dist/core/project/capabilities.js.map +1 -1
  58. package/dist/core/project/home-page.d.ts +4 -0
  59. package/dist/core/project/home-page.d.ts.map +1 -0
  60. package/dist/core/project/home-page.js +113 -0
  61. package/dist/core/project/home-page.js.map +1 -0
  62. package/dist/core/project/mcp-config.d.ts +26 -0
  63. package/dist/core/project/mcp-config.d.ts.map +1 -0
  64. package/dist/core/project/mcp-config.js +117 -0
  65. package/dist/core/project/mcp-config.js.map +1 -0
  66. package/dist/core/project/react-project.d.ts +1 -1
  67. package/dist/core/project/react-project.d.ts.map +1 -1
  68. package/dist/core/project/react-project.js +45 -24
  69. package/dist/core/project/react-project.js.map +1 -1
  70. package/dist/core/project/workflows.d.ts.map +1 -1
  71. package/dist/core/project/workflows.js +9 -1
  72. package/dist/core/project/workflows.js.map +1 -1
  73. package/dist/core/scaffold/auth-generator.js +8 -8
  74. package/dist/core/scaffold/model-parser.d.ts +23 -0
  75. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  76. package/dist/core/scaffold/model-parser.js +407 -137
  77. package/dist/core/scaffold/model-parser.js.map +1 -1
  78. package/dist/core/scaffold/native-schema-generator.d.ts +11 -0
  79. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  80. package/dist/core/scaffold/native-schema-generator.js +279 -91
  81. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  82. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  83. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  84. package/dist/core/scaffold/openapi-generator.js +53 -3
  85. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  86. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  87. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  88. package/dist/core/scaffold/ui-generator.js +68 -7
  89. package/dist/core/scaffold/ui-generator.js.map +1 -1
  90. package/dist/core/verify/index.d.ts.map +1 -1
  91. package/dist/core/verify/index.js +24 -1
  92. package/dist/core/verify/index.js.map +1 -1
  93. package/dist/machine/index.d.ts.map +1 -1
  94. package/dist/machine/index.js +39 -15
  95. package/dist/machine/index.js.map +1 -1
  96. package/dist/mcp/index.d.ts +3 -2
  97. package/dist/mcp/index.d.ts.map +1 -1
  98. package/dist/mcp/index.js +137 -13
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/mcp/version-selection.d.ts +55 -0
  101. package/dist/mcp/version-selection.d.ts.map +1 -0
  102. package/dist/mcp/version-selection.js +152 -0
  103. package/dist/mcp/version-selection.js.map +1 -0
  104. package/dist/utils/azure-cli.d.ts +44 -0
  105. package/dist/utils/azure-cli.d.ts.map +1 -0
  106. package/dist/utils/azure-cli.js +164 -0
  107. package/dist/utils/azure-cli.js.map +1 -0
  108. package/dist/utils/package-manager.js +1 -1
  109. package/dist/utils/package-manager.js.map +1 -1
  110. package/dist/utils/python-uv.d.ts +5 -7
  111. package/dist/utils/python-uv.d.ts.map +1 -1
  112. package/dist/utils/python-uv.js +50 -24
  113. package/dist/utils/python-uv.js.map +1 -1
  114. package/dist/utils/react-dev.d.ts +4 -0
  115. package/dist/utils/react-dev.d.ts.map +1 -0
  116. package/dist/utils/react-dev.js +35 -0
  117. package/dist/utils/react-dev.js.map +1 -0
  118. package/dist/utils/semver.d.ts +14 -0
  119. package/dist/utils/semver.d.ts.map +1 -0
  120. package/dist/utils/semver.js +61 -0
  121. package/dist/utils/semver.js.map +1 -0
  122. package/package.json +1 -1
  123. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  124. package/src/__tests__/agent-loop-phase34.test.ts +29 -0
  125. package/src/__tests__/auth.test.ts +9 -0
  126. package/src/__tests__/azure-cli.test.ts +199 -0
  127. package/src/__tests__/boundaries.test.ts +42 -0
  128. package/src/__tests__/csharp-schema.test.ts +261 -0
  129. package/src/__tests__/dev.test.ts +13 -6
  130. package/src/__tests__/home-page.test.ts +50 -0
  131. package/src/__tests__/init.test.ts +57 -0
  132. package/src/__tests__/mcp-version.test.ts +262 -0
  133. package/src/__tests__/mcp.test.ts +49 -3
  134. package/src/__tests__/package-manager.test.ts +2 -2
  135. package/src/__tests__/plan-apply.test.ts +68 -0
  136. package/src/__tests__/python-uv.test.ts +50 -14
  137. package/src/__tests__/python-venv.test.ts +48 -0
  138. package/src/__tests__/react-dev.test.ts +72 -0
  139. package/src/__tests__/react.test.ts +19 -0
  140. package/src/__tests__/schema-analysis.test.ts +329 -0
  141. package/src/cli/commands/configure-mcp.ts +54 -0
  142. package/src/cli/commands/dev.ts +40 -31
  143. package/src/cli/commands/init.ts +48 -125
  144. package/src/cli/commands/migrations.ts +22 -0
  145. package/src/cli/commands/provision.ts +53 -17
  146. package/src/cli/commands/scaffold.ts +40 -2
  147. package/src/cli/index.ts +26 -3
  148. package/src/core/operations/model-migration.ts +52 -0
  149. package/src/core/operations/provision-operations.ts +86 -42
  150. package/src/core/operations/scaffold-apply.ts +2 -0
  151. package/src/core/operations/scaffold-machine.ts +1 -0
  152. package/src/core/operations/scaffold-plan.ts +5 -1
  153. package/src/core/project/agent-instructions.ts +4 -1
  154. package/src/core/project/agent-templates/AGENTS.md +39 -3
  155. package/src/core/project/agent-templates/CLAUDE.md +3 -1
  156. package/src/core/project/agent-templates/copilot-instructions.md +3 -1
  157. package/src/core/project/boundaries.ts +60 -18
  158. package/src/core/project/capabilities.ts +22 -2
  159. package/src/core/project/home-page.ts +114 -0
  160. package/src/core/project/mcp-config.ts +84 -0
  161. package/src/core/project/react-project.ts +49 -27
  162. package/src/core/project/workflows.ts +9 -1
  163. package/src/core/scaffold/auth-generator.ts +8 -8
  164. package/src/core/scaffold/model-parser.ts +413 -129
  165. package/src/core/scaffold/native-schema-generator.ts +305 -105
  166. package/src/core/scaffold/openapi-generator.ts +63 -5
  167. package/src/core/scaffold/ui-generator.ts +70 -8
  168. package/src/core/verify/index.ts +23 -1
  169. package/src/machine/index.ts +42 -18
  170. package/src/mcp/index.ts +156 -14
  171. package/src/mcp/version-selection.ts +163 -0
  172. package/src/utils/azure-cli.ts +154 -0
  173. package/src/utils/package-manager.ts +1 -1
  174. package/src/utils/python-uv.ts +53 -33
  175. package/src/utils/react-dev.ts +31 -0
  176. package/src/utils/semver.ts +59 -0
@@ -42,6 +42,7 @@ exports.toPascalCase = toPascalCase;
42
42
  exports.toCamelCase = toCamelCase;
43
43
  exports.parseConnectorConfig = parseConnectorConfig;
44
44
  exports.parseAuthPolicy = parseAuthPolicy;
45
+ exports.parseReferences = parseReferences;
45
46
  exports.parsePartitionKey = parsePartitionKey;
46
47
  exports.toKebabCase = toKebabCase;
47
48
  exports.getAllModels = getAllModels;
@@ -91,6 +92,9 @@ async function parseModelFile(modelPath, options = {}) {
91
92
  // フィールド情報を抽出(動的インポートを使用)
92
93
  const parsedSchema = await extractFieldsFromSchema(modelPath, schemaName, options.executeNode ?? child_process_1.execFileSync);
93
94
  const fields = parsedSchema.fields;
95
+ const parserWarnings = [...parsedSchema.warnings];
96
+ // 外部キーは参照先モデルが shared/models に実在する場合だけ推測する
97
+ applyForeignKeys(fields, modelName, modelPath, parseReferences(content), parserWarnings);
94
98
  // ネストスキーマ情報をフィールドにマージ
95
99
  mergeNestedSchemaInfo(fields, nestedSchemaRefs);
96
100
  // id フィールドの存在確認
@@ -121,7 +125,7 @@ async function parseModelFile(modelPath, options = {}) {
121
125
  ...(connectorConfig ? { connectorConfig } : {}),
122
126
  ...(authPolicy ? { authPolicy } : {}),
123
127
  parserMode: parsedSchema.mode,
124
- parserWarnings: parsedSchema.warnings,
128
+ parserWarnings,
125
129
  semanticFingerprint: "",
126
130
  };
127
131
  modelInfo.semanticFingerprint = createModelSemanticFingerprint(modelInfo);
@@ -339,6 +343,15 @@ function compileModelModule(modelPath, directory, modules = new Map()) {
339
343
  fs.writeFileSync(outputPath, output.outputText);
340
344
  return outputPath;
341
345
  }
346
+ const STRING_FORMATS = {
347
+ email: "email",
348
+ url: "uri",
349
+ uuid: "uuid",
350
+ guid: "uuid",
351
+ datetime: "date-time",
352
+ date: "date",
353
+ time: "time",
354
+ };
342
355
  function staticParseFields(modelPath, schemaName) {
343
356
  const source = ts.createSourceFile(modelPath, fs.readFileSync(modelPath, "utf-8"), ts.ScriptTarget.Latest, true);
344
357
  const constants = new Map();
@@ -390,7 +403,146 @@ function staticParseFields(modelPath, schemaName) {
390
403
  }
391
404
  return values;
392
405
  };
393
- const parseExpression = (expression) => {
406
+ const numericArgument = (expression) => {
407
+ if (!expression)
408
+ return null;
409
+ if (ts.isNumericLiteral(expression))
410
+ return Number(expression.text);
411
+ if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(expression.operand)) {
412
+ return -Number(expression.operand.text);
413
+ }
414
+ if (ts.isIdentifier(expression)) {
415
+ const resolved = constants.get(expression.text);
416
+ return resolved ? numericArgument(resolved) : null;
417
+ }
418
+ return null;
419
+ };
420
+ const withConstraints = (field, update) => ({
421
+ ...field,
422
+ constraints: { ...(field.constraints ?? {}), ...update },
423
+ });
424
+ const lowerBound = (field, key, value, exclusive = false) => {
425
+ const current = field.constraints?.[key];
426
+ if (current !== undefined && current > value)
427
+ return field;
428
+ const constraints = { ...(field.constraints ?? {}), [key]: value };
429
+ if (key === "minimum") {
430
+ // An equal bound stays exclusive if either check was exclusive (gt(0).min(0) still rejects 0).
431
+ if (exclusive || (current === value && field.constraints?.exclusiveMinimum))
432
+ constraints.exclusiveMinimum = true;
433
+ else
434
+ delete constraints.exclusiveMinimum;
435
+ }
436
+ return { ...field, constraints };
437
+ };
438
+ const upperBound = (field, key, value, exclusive = false) => {
439
+ const current = field.constraints?.[key];
440
+ if (current !== undefined && current < value)
441
+ return field;
442
+ const constraints = { ...(field.constraints ?? {}), [key]: value };
443
+ if (key === "maximum") {
444
+ if (exclusive || (current === value && field.constraints?.exclusiveMaximum))
445
+ constraints.exclusiveMaximum = true;
446
+ else
447
+ delete constraints.exclusiveMaximum;
448
+ }
449
+ return { ...field, constraints };
450
+ };
451
+ const applyCheck = (inner, method, args) => {
452
+ const lengthTarget = inner.isArray ? "items" : inner.type === "string" ? "length" : inner.type === "number" ? "value" : null;
453
+ const value = numericArgument(args[0]);
454
+ switch (method) {
455
+ case "optional":
456
+ case "default":
457
+ return { ...inner, isOptional: true };
458
+ case "nullable":
459
+ return { ...inner, isNullable: true };
460
+ case "nullish":
461
+ return { ...inner, isOptional: true, isNullable: true };
462
+ case "describe":
463
+ case "trim":
464
+ case "toLowerCase":
465
+ case "toUpperCase":
466
+ case "strict":
467
+ case "strip":
468
+ case "passthrough":
469
+ case "loose":
470
+ return inner;
471
+ case "int":
472
+ return inner.type === "number" && !inner.isArray ? { ...inner, isInteger: true } : null;
473
+ case "positive":
474
+ return inner.type === "number" && !inner.isArray ? lowerBound(inner, "minimum", 0, true) : null;
475
+ case "nonnegative":
476
+ return inner.type === "number" && !inner.isArray ? lowerBound(inner, "minimum", 0) : null;
477
+ case "negative":
478
+ return inner.type === "number" && !inner.isArray ? upperBound(inner, "maximum", 0, true) : null;
479
+ case "nonpositive":
480
+ return inner.type === "number" && !inner.isArray ? upperBound(inner, "maximum", 0) : null;
481
+ case "gt":
482
+ case "gte":
483
+ return inner.type === "number" && !inner.isArray && value !== null ? lowerBound(inner, "minimum", value, method === "gt") : null;
484
+ case "lt":
485
+ case "lte":
486
+ return inner.type === "number" && !inner.isArray && value !== null ? upperBound(inner, "maximum", value, method === "lt") : null;
487
+ case "min":
488
+ if (value === null || !lengthTarget)
489
+ return null;
490
+ return lowerBound(inner, lengthTarget === "items" ? "minItems" : lengthTarget === "length" ? "minLength" : "minimum", value);
491
+ case "max":
492
+ if (value === null || !lengthTarget)
493
+ return null;
494
+ return upperBound(inner, lengthTarget === "items" ? "maxItems" : lengthTarget === "length" ? "maxLength" : "maximum", value);
495
+ case "length":
496
+ if (value === null || (lengthTarget !== "items" && lengthTarget !== "length"))
497
+ return null;
498
+ return lengthTarget === "items"
499
+ ? upperBound(lowerBound(inner, "minItems", value), "maxItems", value)
500
+ : upperBound(lowerBound(inner, "minLength", value), "maxLength", value);
501
+ case "nonempty":
502
+ if (lengthTarget !== "items" && lengthTarget !== "length")
503
+ return null;
504
+ return lowerBound(inner, lengthTarget === "items" ? "minItems" : "minLength", 1);
505
+ case "regex": {
506
+ const literal = args[0];
507
+ if (!literal || !ts.isRegularExpressionLiteral(literal) || inner.type !== "string" || inner.isArray)
508
+ return null;
509
+ const text = literal.text;
510
+ const separator = text.lastIndexOf("/");
511
+ return withConstraints(inner, {
512
+ pattern: text.slice(1, separator),
513
+ ...(text.slice(separator + 1) ? { patternFlags: text.slice(separator + 1) } : {}),
514
+ });
515
+ }
516
+ case "email":
517
+ case "url":
518
+ case "uuid":
519
+ case "guid":
520
+ case "datetime":
521
+ case "date":
522
+ case "time":
523
+ if (inner.type !== "string" || inner.isArray)
524
+ return null;
525
+ return { ...inner, format: STRING_FORMATS[method] };
526
+ default:
527
+ return null;
528
+ }
529
+ };
530
+ const parseObjectFields = (literal) => {
531
+ const objectFields = [];
532
+ for (const property of literal.properties) {
533
+ if (!ts.isPropertyAssignment(property))
534
+ return null;
535
+ const name = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) ? property.name.text : null;
536
+ if (!name)
537
+ return null;
538
+ const parsed = parseExpression(property.initializer);
539
+ if (!parsed)
540
+ return null;
541
+ objectFields.push({ name, ...parsed });
542
+ }
543
+ return objectFields;
544
+ };
545
+ function parseExpression(expression) {
394
546
  if (ts.isIdentifier(expression)) {
395
547
  const resolved = constants.get(expression.text);
396
548
  if (resolved)
@@ -403,61 +555,51 @@ function staticParseFields(modelPath, schemaName) {
403
555
  return null;
404
556
  const method = expression.expression.name.text;
405
557
  const receiver = expression.expression.expression;
406
- const wrappers = new Set(["optional", "nullable", "default", "min", "max", "int", "positive", "negative", "nonnegative", "nonpositive", "regex", "email", "url", "uuid", "trim"]);
407
- if (wrappers.has(method)) {
558
+ const base = (type, extra = {}) => ({ type, isOptional: false, isNullable: false, isArray: false, ...extra });
559
+ // z.iso.datetime() / z.iso.date() / z.iso.time()
560
+ if (ts.isPropertyAccessExpression(receiver) && ts.isIdentifier(receiver.expression) && receiver.expression.text === "z" && receiver.name.text === "iso") {
561
+ return method === "datetime" || method === "date" || method === "time" ? base("string", { format: STRING_FORMATS[method] }) : null;
562
+ }
563
+ if (!(ts.isIdentifier(receiver) && receiver.text === "z")) {
408
564
  const inner = parseExpression(receiver);
409
- if (!inner)
410
- return null;
411
- if (method === "optional" || method === "default")
412
- inner.isOptional = true;
413
- if (method === "nullable")
414
- inner.isNullable = true;
415
- return inner;
565
+ return inner ? applyCheck(inner, method, expression.arguments) : null;
416
566
  }
417
- if (!ts.isIdentifier(receiver) || receiver.text !== "z")
418
- return null;
419
- if (["string", "number", "boolean", "date"].includes(method)) {
420
- return { type: method, isOptional: false, isNullable: false, isArray: false };
567
+ if (["string", "boolean", "date"].includes(method))
568
+ return base(method);
569
+ if (method === "number")
570
+ return base("number");
571
+ if (method === "int")
572
+ return base("number", { isInteger: true });
573
+ if (method === "email" || method === "url" || method === "uuid" || method === "guid")
574
+ return base("string", { format: STRING_FORMATS[method] });
575
+ if (method === "object") {
576
+ const literal = expression.arguments[0];
577
+ if (!literal || !ts.isObjectLiteralExpression(literal))
578
+ return null;
579
+ // Unsupported members keep the previous untyped-object behavior instead of failing the whole model.
580
+ const objectFields = parseObjectFields(literal);
581
+ return objectFields ? base("object", { objectFields }) : base("object");
421
582
  }
422
- if (method === "object")
423
- return { type: "object", isOptional: false, isNullable: false, isArray: false };
424
583
  if (method === "array" && expression.arguments[0]) {
425
584
  const element = parseExpression(expression.arguments[0]);
426
585
  return element ? { ...element, isArray: true } : null;
427
586
  }
428
587
  if (method === "enum" && expression.arguments[0]) {
429
588
  const enumValues = literalValues(expression.arguments[0]);
430
- return enumValues ? { type: "string", isOptional: false, isNullable: false, isArray: false, enumValues } : null;
589
+ return enumValues ? base("string", { enumValues }) : null;
431
590
  }
432
591
  if (method === "literal" && expression.arguments[0]) {
433
592
  const value = expression.arguments[0];
434
593
  if (ts.isStringLiteral(value) || ts.isNumericLiteral(value)) {
435
- return { type: ts.isNumericLiteral(value) ? "number" : "string", isOptional: false, isNullable: false, isArray: false, enumValues: [value.text] };
594
+ return base(ts.isNumericLiteral(value) ? "number" : "string", { enumValues: [value.text] });
436
595
  }
437
596
  if (value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword) {
438
- return { type: "boolean", isOptional: false, isNullable: false, isArray: false, enumValues: [value.kind === ts.SyntaxKind.TrueKeyword ? "true" : "false"] };
597
+ return base("boolean", { enumValues: [value.kind === ts.SyntaxKind.TrueKeyword ? "true" : "false"] });
439
598
  }
440
599
  }
441
600
  return null;
442
- };
443
- const fields = [];
444
- for (const property of schema.arguments[0].properties) {
445
- if (!ts.isPropertyAssignment(property))
446
- return null;
447
- const name = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) ? property.name.text : null;
448
- if (!name)
449
- return null;
450
- const parsed = parseExpression(property.initializer);
451
- if (!parsed)
452
- return null;
453
- const isForeignKey = name.endsWith("Id") && name.length > 2 && parsed.type === "string";
454
- fields.push({
455
- name,
456
- ...parsed,
457
- ...(isForeignKey ? { isForeignKey: true, referencedModel: name.slice(0, -2).replace(/^./, char => char.toUpperCase()) } : {}),
458
- });
459
- }
460
- return fields;
601
+ }
602
+ return parseObjectFields(schema.arguments[0]);
461
603
  }
462
604
  async function extractFieldsFromSchema(modelPath, schemaName, executeNode) {
463
605
  const staticFields = staticParseFields(modelPath, schemaName);
@@ -481,110 +623,160 @@ async function extractFieldsFromSchema(modelPath, schemaName, executeNode) {
481
623
  const scriptCode = `
482
624
  import { ${schemaName} as schema } from './${path.basename(compiledModel)}';
483
625
 
484
- // Zod v3とv4の両方に対応
485
- const isObject = (schema && schema._def &&
486
- (schema._def.typeName === 'ZodObject' || schema.constructor?.name === 'ZodObject' || typeof schema._def.shape === 'function'));
626
+ // Zod v3 (_def.typeName) と v4 (_zod.def.type) の両方に対応
627
+ const FORMATS = { email: 'email', url: 'uri', uuid: 'uuid', guid: 'uuid', datetime: 'date-time', date: 'date', time: 'time' };
628
+ const v4 = (s) => (s && s._zod && s._zod.def) || null;
629
+ const v3Name = (s) => (s && s._def && s._def.typeName) || '';
630
+
631
+ function lower(c, key, value, exclusive) {
632
+ if (typeof value !== 'number' || (c[key] !== undefined && c[key] > value)) return;
633
+ const keepExclusive = c[key] === value && c.exclusiveMinimum;
634
+ c[key] = value;
635
+ if (key === 'minimum') { if (exclusive || keepExclusive) c.exclusiveMinimum = true; else delete c.exclusiveMinimum; }
636
+ }
637
+ function upper(c, key, value, exclusive) {
638
+ if (typeof value !== 'number' || (c[key] !== undefined && c[key] < value)) return;
639
+ const keepExclusive = c[key] === value && c.exclusiveMaximum;
640
+ c[key] = value;
641
+ if (key === 'maximum') { if (exclusive || keepExclusive) c.exclusiveMaximum = true; else delete c.exclusiveMaximum; }
642
+ }
643
+
644
+ function unwrap(s, info) {
645
+ for (;;) {
646
+ const d = v4(s);
647
+ if (d) {
648
+ if (d.type === 'optional' || d.type === 'default' || d.type === 'prefault') { info.isOptional = true; s = d.innerType; continue; }
649
+ if (d.type === 'nullable') { info.isNullable = true; s = d.innerType; continue; }
650
+ if (d.type === 'readonly' || d.type === 'nonoptional' || d.type === 'catch') { s = d.innerType; continue; }
651
+ return s;
652
+ }
653
+ const name = v3Name(s);
654
+ if (name === 'ZodOptional' || name === 'ZodDefault') { info.isOptional = true; s = s._def.innerType; continue; }
655
+ if (name === 'ZodNullable') { info.isNullable = true; s = s._def.innerType; continue; }
656
+ if (name === 'ZodEffects') { s = s._def.schema; continue; }
657
+ if (name === 'ZodReadonly' || name === 'ZodCatch') { s = s._def.innerType; continue; }
658
+ return s;
659
+ }
660
+ }
661
+
662
+ function describe(key, input) {
663
+ const info = { name: key, type: undefined, isOptional: false, isNullable: false, isArray: false };
664
+ const constraints = {};
665
+ let s = unwrap(input, info);
487
666
 
488
- if (isObject) {
489
- const shape = typeof schema._def.shape === 'function' ? schema._def.shape() : schema._def.shape;
490
- const fields = Object.keys(shape).map(key => {
491
- const field = shape[key];
492
- let type = undefined;
493
- let isOptional = false;
494
- let isNullable = false;
495
- let isArray = false;
496
- let enumValues = undefined;
497
-
498
- // ZodOptional, ZodNullable, ZodDefault, ZodEffects を unwrap
499
- let fieldDef = field;
500
- const getTypeName = (def) => def?._def?.typeName || def?.constructor?.name || '';
501
-
502
- // 繰り返し unwrap(複数のラッパーがある場合に対応)
503
- let unwrapped = false;
504
- do {
505
- unwrapped = false;
506
- const typeName = getTypeName(fieldDef);
507
-
508
- if (typeName === 'ZodOptional') {
509
- isOptional = true;
510
- fieldDef = fieldDef._def.innerType;
511
- unwrapped = true;
512
- } else if (typeName === 'ZodNullable') {
513
- isNullable = true;
514
- fieldDef = fieldDef._def.innerType;
515
- unwrapped = true;
516
- } else if (typeName === 'ZodDefault') {
517
- // .default() は optional と同様に扱う
518
- isOptional = true;
519
- fieldDef = fieldDef._def.innerType;
520
- unwrapped = true;
521
- } else if (typeName === 'ZodEffects') {
522
- // .min(), .max(), .regex() などの Effects を unwrap
523
- fieldDef = fieldDef._def.schema;
524
- unwrapped = true;
667
+ const arrayDef = v4(s);
668
+ if ((arrayDef && arrayDef.type === 'array') || v3Name(s) === 'ZodArray') {
669
+ info.isArray = true;
670
+ if (arrayDef) {
671
+ for (const check of arrayDef.checks || []) {
672
+ const cd = check._zod.def;
673
+ if (cd.check === 'min_length') lower(constraints, 'minItems', cd.minimum);
674
+ if (cd.check === 'max_length') upper(constraints, 'maxItems', cd.maximum);
675
+ if (cd.check === 'length_equals') { lower(constraints, 'minItems', cd.length); upper(constraints, 'maxItems', cd.length); }
676
+ }
677
+ s = arrayDef.element;
678
+ } else {
679
+ const ad = s._def;
680
+ if (ad.minLength) lower(constraints, 'minItems', ad.minLength.value);
681
+ if (ad.maxLength) upper(constraints, 'maxItems', ad.maxLength.value);
682
+ if (ad.exactLength) { lower(constraints, 'minItems', ad.exactLength.value); upper(constraints, 'maxItems', ad.exactLength.value); }
683
+ s = ad.type;
684
+ }
685
+ s = unwrap(s, {});
686
+ }
687
+
688
+ const d = v4(s);
689
+ const name = v3Name(s);
690
+ let enumValues;
691
+ if (d) {
692
+ if (d.type === 'string') {
693
+ info.type = 'string';
694
+ if (d.format && FORMATS[d.format]) info.format = FORMATS[d.format];
695
+ if (d.format === 'regex' && d.pattern) { constraints.pattern = d.pattern.source; if (d.pattern.flags) constraints.patternFlags = d.pattern.flags; }
696
+ } else if (d.type === 'number') {
697
+ info.type = 'number';
698
+ if (d.format && /int/.test(d.format)) info.isInteger = true;
699
+ } else if (d.type === 'boolean') info.type = 'boolean';
700
+ else if (d.type === 'date') info.type = 'date';
701
+ else if (d.type === 'object') {
702
+ info.type = 'object';
703
+ const shape = typeof d.shape === 'function' ? d.shape() : d.shape;
704
+ info.objectFields = Object.keys(shape).map((child) => describe(child, shape[child]));
705
+ } else if (d.type === 'enum') {
706
+ info.type = 'string';
707
+ enumValues = Object.values(d.entries || {}).map(String);
708
+ } else if (d.type === 'literal') {
709
+ const values = Array.isArray(d.values) ? d.values : Array.from(d.values || []);
710
+ if (values.length > 0) {
711
+ info.type = typeof values[0] === 'number' ? 'number' : typeof values[0] === 'boolean' ? 'boolean' : 'string';
712
+ enumValues = values.map(String);
713
+ }
714
+ }
715
+ for (const check of d.checks || []) {
716
+ const cd = check._zod.def;
717
+ const lengthKey = info.type === 'string';
718
+ if (cd.check === 'min_length' && lengthKey) lower(constraints, 'minLength', cd.minimum);
719
+ if (cd.check === 'max_length' && lengthKey) upper(constraints, 'maxLength', cd.maximum);
720
+ if (cd.check === 'length_equals' && lengthKey) { lower(constraints, 'minLength', cd.length); upper(constraints, 'maxLength', cd.length); }
721
+ if (cd.check === 'greater_than') lower(constraints, 'minimum', Number(cd.value), !cd.inclusive);
722
+ if (cd.check === 'less_than') upper(constraints, 'maximum', Number(cd.value), !cd.inclusive);
723
+ if (cd.check === 'number_format' && /int/.test(cd.format || '')) info.isInteger = true;
724
+ if (cd.check === 'string_format') {
725
+ if (cd.format === 'regex' && cd.pattern) { constraints.pattern = cd.pattern.source; if (cd.pattern.flags) constraints.patternFlags = cd.pattern.flags; }
726
+ else if (FORMATS[cd.format]) info.format = FORMATS[cd.format];
525
727
  }
526
- } while (unwrapped);
527
-
528
- // ZodArray をチェック
529
- if (getTypeName(fieldDef) === 'ZodArray') {
530
- isArray = true;
531
- fieldDef = fieldDef._def.element || fieldDef._def.type;
532
- }
533
-
534
- // 基本型を判定
535
- const typeName = getTypeName(fieldDef);
536
- if (typeName === 'ZodString') type = 'string';
537
- else if (typeName === 'ZodNumber') type = 'number';
538
- else if (typeName === 'ZodBoolean') type = 'boolean';
539
- else if (typeName === 'ZodDate') type = 'date';
540
- else if (typeName === 'ZodObject') type = 'object';
541
- else if (typeName === 'ZodEnum' || typeName === 'ZodNativeEnum') {
542
- type = 'string';
543
- // enum の選択肢を取得(複数のZodバージョンに対応)
544
- if (fieldDef.options) {
545
- // Zod v3.23+ では options プロパティを使用
546
- enumValues = Array.isArray(fieldDef.options)
547
- ? fieldDef.options
548
- : Object.values(fieldDef.options);
549
- } else if (fieldDef._def.values) {
550
- // 古いバージョンでは _def.values を使用
551
- enumValues = Array.isArray(fieldDef._def.values)
552
- ? fieldDef._def.values
553
- : Object.values(fieldDef._def.values);
554
- } else if (fieldDef._def.entries) {
555
- // さらに古いバージョンでは _def.entries を使用
556
- enumValues = Array.isArray(fieldDef._def.entries)
557
- ? fieldDef._def.entries
558
- : Object.values(fieldDef._def.entries);
728
+ }
729
+ } else {
730
+ const def = s && s._def;
731
+ if (name === 'ZodString') {
732
+ info.type = 'string';
733
+ for (const check of def.checks || []) {
734
+ if (check.kind === 'min') lower(constraints, 'minLength', check.value);
735
+ if (check.kind === 'max') upper(constraints, 'maxLength', check.value);
736
+ if (check.kind === 'length') { lower(constraints, 'minLength', check.value); upper(constraints, 'maxLength', check.value); }
737
+ if (check.kind === 'regex') { constraints.pattern = check.regex.source; if (check.regex.flags) constraints.patternFlags = check.regex.flags; }
738
+ if (FORMATS[check.kind]) info.format = FORMATS[check.kind];
559
739
  }
560
- } else if (typeName === 'ZodLiteral') {
561
- const rawLiteralValues = fieldDef.values || fieldDef._def.values ||
562
- (fieldDef._def.value !== undefined ? [fieldDef._def.value] : undefined);
563
- const literalValues = rawLiteralValues instanceof Set ? Array.from(rawLiteralValues) : rawLiteralValues;
564
- if (Array.isArray(literalValues) && literalValues.length > 0) {
565
- const literalValue = literalValues[0];
566
- type = typeof literalValue === 'number' ? 'number' : typeof literalValue === 'boolean' ? 'boolean' : 'string';
567
- enumValues = literalValues.map(value => String(value));
740
+ } else if (name === 'ZodNumber') {
741
+ info.type = 'number';
742
+ for (const check of def.checks || []) {
743
+ if (check.kind === 'int') info.isInteger = true;
744
+ if (check.kind === 'min') lower(constraints, 'minimum', check.value, !check.inclusive);
745
+ if (check.kind === 'max') upper(constraints, 'maximum', check.value, !check.inclusive);
568
746
  }
747
+ } else if (name === 'ZodBoolean') info.type = 'boolean';
748
+ else if (name === 'ZodDate') info.type = 'date';
749
+ else if (name === 'ZodObject') {
750
+ info.type = 'object';
751
+ const shape = typeof def.shape === 'function' ? def.shape() : def.shape;
752
+ info.objectFields = Object.keys(shape).map((child) => describe(child, shape[child]));
753
+ } else if (name === 'ZodEnum' || name === 'ZodNativeEnum') {
754
+ info.type = 'string';
755
+ const values = s.options || def.values || def.entries;
756
+ enumValues = (Array.isArray(values) ? values : Object.values(values || {})).map(String);
757
+ } else if (name === 'ZodLiteral') {
758
+ const value = def.value;
759
+ info.type = typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string';
760
+ enumValues = [String(value)];
569
761
  }
762
+ }
570
763
 
571
- if (!type || ((typeName === 'ZodEnum' || typeName === 'ZodNativeEnum') && (!enumValues || enumValues.length === 0))) {
572
- throw new Error('Unsupported or ambiguous Zod field "' + key + '" (' + (typeName || 'unknown') + ')');
573
- }
574
-
575
- // 外部キー検出: フィールド名が <ModelName>Id のパターンの場合
576
- let isForeignKey = false;
577
- let referencedModel = undefined;
578
- if (key.endsWith('Id') && key.length > 2 && type === 'string') {
579
- // categoryId -> Category, userId -> User など
580
- const modelName = key.slice(0, -2); // "Id" を除去
581
- referencedModel = modelName.charAt(0).toUpperCase() + modelName.slice(1); // 先頭を大文字に
582
- isForeignKey = true;
583
- }
584
-
585
- return { name: key, type, isOptional, isNullable, isArray, enumValues, isForeignKey, referencedModel };
586
- });
587
-
764
+ const typeLabel = (d && d.type) || name || 'unknown';
765
+ if (!info.type || ((typeLabel === 'enum' || name === 'ZodEnum' || name === 'ZodNativeEnum') && (!enumValues || enumValues.length === 0))) {
766
+ throw new Error('Unsupported or ambiguous Zod field "' + key + '" (' + typeLabel + ')');
767
+ }
768
+ if (enumValues) info.enumValues = enumValues;
769
+ if (Object.keys(constraints).length > 0) info.constraints = constraints;
770
+ return info;
771
+ }
772
+
773
+ const root = unwrap(schema, {});
774
+ const rootDef = v4(root);
775
+ const rootShape = rootDef && rootDef.type === 'object'
776
+ ? (typeof rootDef.shape === 'function' ? rootDef.shape() : rootDef.shape)
777
+ : v3Name(root) === 'ZodObject' ? (typeof root._def.shape === 'function' ? root._def.shape() : root._def.shape) : null;
778
+ if (rootShape) {
779
+ const fields = Object.keys(rootShape).map((key) => describe(key, rootShape[key]));
588
780
  console.log(JSON.stringify(fields));
589
781
  } else {
590
782
  throw new Error('Exported schema is not a Zod object');
@@ -788,6 +980,84 @@ function parseAuthPolicy(content) {
788
980
  ...(namedPolicy ? { policy: namedPolicy } : {}),
789
981
  };
790
982
  }
983
+ /**
984
+ * `export const references = { userId: 'User', legacyId: false }` を静的に抽出する。
985
+ * 値が false / null の場合は名前からの推測を無効にする。
986
+ */
987
+ function parseReferences(content) {
988
+ const source = ts.createSourceFile("model.ts", content, ts.ScriptTarget.Latest, true);
989
+ const references = {};
990
+ for (const statement of source.statements) {
991
+ if (!ts.isVariableStatement(statement))
992
+ continue;
993
+ for (const declaration of statement.declarationList.declarations) {
994
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "references" || !declaration.initializer)
995
+ continue;
996
+ let initializer = declaration.initializer;
997
+ while (ts.isAsExpression(initializer) || ts.isSatisfiesExpression(initializer))
998
+ initializer = initializer.expression;
999
+ if (!ts.isObjectLiteralExpression(initializer))
1000
+ continue;
1001
+ for (const property of initializer.properties) {
1002
+ if (!ts.isPropertyAssignment(property))
1003
+ continue;
1004
+ const key = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) ? property.name.text : null;
1005
+ if (!key)
1006
+ continue;
1007
+ const value = property.initializer;
1008
+ if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
1009
+ references[key] = value.text;
1010
+ else if (value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword)
1011
+ references[key] = false;
1012
+ }
1013
+ }
1014
+ }
1015
+ return references;
1016
+ }
1017
+ function listModelNames(modelPath) {
1018
+ const directory = path.dirname(modelPath);
1019
+ const names = new Set();
1020
+ for (const entry of fs.readdirSync(directory)) {
1021
+ if (!entry.endsWith(".ts") || entry.endsWith(".d.ts") || entry === "index.ts")
1022
+ continue;
1023
+ names.add(toPascalCase(path.basename(entry, ".ts")));
1024
+ }
1025
+ return names;
1026
+ }
1027
+ function applyForeignKeys(fields, modelName, modelPath, references, warnings) {
1028
+ const models = listModelNames(modelPath);
1029
+ for (const field of fields) {
1030
+ delete field.isForeignKey;
1031
+ delete field.referencedModel;
1032
+ }
1033
+ for (const [fieldName, target] of Object.entries(references)) {
1034
+ const field = fields.find((candidate) => candidate.name === fieldName);
1035
+ if (!field) {
1036
+ warnings.push(`reference-field-not-found: ${modelName}.references declares ${fieldName}, but the schema has no such field.`);
1037
+ }
1038
+ else if (target !== false && (field.type !== "string" || field.isArray || field.enumValues?.length)) {
1039
+ warnings.push(`reference-field-unsupported: ${modelName}.${fieldName} must be a single string id to reference ${target}.`);
1040
+ }
1041
+ else if (target !== false && !models.has(target)) {
1042
+ warnings.push(`reference-model-not-found: ${modelName}.${fieldName} references ${target}, but shared/models has no ${target} model; it is treated as a plain field.`);
1043
+ }
1044
+ }
1045
+ for (const field of fields) {
1046
+ if (field.type !== "string" || field.isArray || field.enumValues?.length || field.isNestedSchema)
1047
+ continue;
1048
+ const declared = references[field.name];
1049
+ if (declared === false)
1050
+ continue;
1051
+ const inferred = field.name.length > 2 && field.name.endsWith("Id")
1052
+ ? field.name.slice(0, -2).replace(/^./, (char) => char.toUpperCase())
1053
+ : undefined;
1054
+ const target = declared ?? inferred;
1055
+ if (target && models.has(target)) {
1056
+ field.isForeignKey = true;
1057
+ field.referencedModel = target;
1058
+ }
1059
+ }
1060
+ }
791
1061
  /**
792
1062
  * パーティションキーを抽出する
793
1063
  * export const partitionKey = '/tenantId' のようなエクスポートを検出