swallowkit 1.0.0-beta.14 → 1.0.0-beta.16

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 (39) hide show
  1. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  2. package/dist/cli/commands/add-auth.js +85 -5
  3. package/dist/cli/commands/add-auth.js.map +1 -1
  4. package/dist/cli/commands/create-model.js +1 -1
  5. package/dist/cli/commands/create-model.js.map +1 -1
  6. package/dist/cli/commands/dev-seeds.js +1 -1
  7. package/dist/cli/commands/dev-seeds.js.map +1 -1
  8. package/dist/cli/commands/dev.js +62 -21
  9. package/dist/cli/commands/dev.js.map +1 -1
  10. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  11. package/dist/cli/commands/scaffold.js +60 -4
  12. package/dist/cli/commands/scaffold.js.map +1 -1
  13. package/dist/core/mock/connector-mock-server.d.ts +7 -1
  14. package/dist/core/mock/connector-mock-server.d.ts.map +1 -1
  15. package/dist/core/mock/connector-mock-server.js +23 -26
  16. package/dist/core/mock/connector-mock-server.js.map +1 -1
  17. package/dist/core/scaffold/auth-generator.d.ts +1 -1
  18. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  19. package/dist/core/scaffold/auth-generator.js +17 -20
  20. package/dist/core/scaffold/auth-generator.js.map +1 -1
  21. package/dist/core/scaffold/functions-generator.d.ts +2 -2
  22. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  23. package/dist/core/scaffold/functions-generator.js +34 -22
  24. package/dist/core/scaffold/functions-generator.js.map +1 -1
  25. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  26. package/dist/core/scaffold/model-parser.js +2 -0
  27. package/dist/core/scaffold/model-parser.js.map +1 -1
  28. package/package.json +5 -5
  29. package/src/__tests__/auth.test.ts +13 -13
  30. package/src/__tests__/connector-mock-server.test.ts +75 -37
  31. package/src/cli/commands/add-auth.ts +95 -6
  32. package/src/cli/commands/create-model.ts +1 -1
  33. package/src/cli/commands/dev-seeds.ts +1 -1
  34. package/src/cli/commands/dev.ts +66 -21
  35. package/src/cli/commands/scaffold.ts +68 -9
  36. package/src/core/mock/connector-mock-server.ts +27 -27
  37. package/src/core/scaffold/auth-generator.ts +16 -19
  38. package/src/core/scaffold/functions-generator.ts +37 -23
  39. package/src/core/scaffold/model-parser.ts +2 -0
@@ -373,7 +373,7 @@ namespace Functions.Auth
373
373
  public async Task<HttpResponseData> Me(
374
374
  [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/me")] HttpRequestData request)
375
375
  {
376
- var (principal, errorResponse) = JwtHelper.Authorize(request);
376
+ var (principal, errorResponse) = await JwtHelper.Authorize(request);
377
377
  if (errorResponse != null) return errorResponse;
378
378
 
379
379
  var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
@@ -400,7 +400,9 @@ namespace Functions.Auth
400
400
 
401
401
  public class LoginRequest
402
402
  {
403
+ [System.Text.Json.Serialization.JsonPropertyName("loginId")]
403
404
  public string LoginId { get; set; } = "";
405
+ [System.Text.Json.Serialization.JsonPropertyName("password")]
404
406
  public string Password { get; set; } = "";
405
407
  }
406
408
  }
@@ -417,6 +419,7 @@ using System.IdentityModel.Tokens.Jwt;
417
419
  using System.Security.Claims;
418
420
  using System.Text;
419
421
  using System.Text.Json;
422
+ using System.Threading.Tasks;
420
423
  using Microsoft.Azure.Functions.Worker.Http;
421
424
  using Microsoft.IdentityModel.Tokens;
422
425
 
@@ -498,21 +501,21 @@ namespace Functions.Auth
498
501
  }
499
502
  }
500
503
 
501
- public static (JwtPayload?, HttpResponseData?) Authorize(
504
+ public static async Task<(JwtPayload?, HttpResponseData?)> Authorize(
502
505
  HttpRequestData request, params string[] requiredRoles)
503
506
  {
504
507
  var payload = ValidateToken(request);
505
508
  if (payload == null)
506
509
  {
507
510
  var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
508
- unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" }).Wait();
511
+ await unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" });
509
512
  return (null, unauthorized);
510
513
  }
511
514
 
512
515
  if (requiredRoles.Length > 0 && !requiredRoles.Any(r => payload.Roles.Contains(r)))
513
516
  {
514
517
  var forbidden = request.CreateResponse(System.Net.HttpStatusCode.Forbidden);
515
- forbidden.WriteAsJsonAsync(new { error = "Forbidden" }).Wait();
518
+ await forbidden.WriteAsJsonAsync(new { error = "Forbidden" });
516
519
  return (null, forbidden);
517
520
  }
518
521
 
@@ -765,9 +768,8 @@ export function generateBFFAuthLoginRoute(projectName: string, sharedPackageName
765
768
  return `import { NextRequest, NextResponse } from 'next/server';
766
769
  import { LoginRequest, LoginResponse } from '${sharedPackageName}';
767
770
 
768
- const FUNCTIONS_BASE_URL = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
769
-
770
771
  export async function POST(request: NextRequest) {
772
+ const FUNCTIONS_BASE_URL = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
771
773
  try {
772
774
  const body = await request.json();
773
775
  const validated = LoginRequest.parse(body);
@@ -818,9 +820,8 @@ export function generateBFFAuthMeRoute(sharedPackageName: string): string {
818
820
  return `import { NextResponse } from 'next/server';
819
821
  import { headers } from 'next/headers';
820
822
 
821
- const FUNCTIONS_BASE_URL = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
822
-
823
823
  export async function GET() {
824
+ const FUNCTIONS_BASE_URL = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
824
825
  try {
825
826
  const reqHeaders = await headers();
826
827
  const authorization = reqHeaders.get('authorization');
@@ -848,10 +849,10 @@ export async function GET() {
848
849
  }
849
850
 
850
851
  // ============================================================
851
- // 9. Next.js Middleware
852
+ // 9. Next.js Proxy (formerly Middleware)
852
853
  // ============================================================
853
854
 
854
- export function generateMiddleware(projectName: string): string {
855
+ export function generateProxy(projectName: string): string {
855
856
  const cookieName = projectName.replace(/^@[^/]+\//, '').replace(/[^a-z0-9-]/g, '-') + '-auth-token';
856
857
  return `import { NextResponse } from 'next/server';
857
858
  import type { NextRequest } from 'next/server';
@@ -859,7 +860,7 @@ import type { NextRequest } from 'next/server';
859
860
  const AUTH_COOKIE_NAME = '${cookieName}';
860
861
  const PUBLIC_PATHS = ['/login', '/api/auth/login', '/api/auth/logout'];
861
862
 
862
- export function middleware(request: NextRequest) {
863
+ export function proxy(request: NextRequest) {
863
864
  const { pathname } = request.nextUrl;
864
865
 
865
866
  // 公開パス・静的アセットはスキップ
@@ -877,7 +878,7 @@ export function middleware(request: NextRequest) {
877
878
  return NextResponse.redirect(new URL('/login', request.url));
878
879
  }
879
880
 
880
- // JWT 有効期限の簡易チェック(署名検証なし = Edge Runtime 互換)
881
+ // JWT 有効期限の簡易チェック(署名検証なし)
881
882
  try {
882
883
  const payload = JSON.parse(atob(token.split('.')[1]));
883
884
  if (payload.exp && payload.exp * 1000 < Date.now()) {
@@ -902,10 +903,6 @@ export function middleware(request: NextRequest) {
902
903
  request: { headers: requestHeaders },
903
904
  });
904
905
  }
905
-
906
- export const config = {
907
- matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
908
- };
909
906
  `;
910
907
  }
911
908
 
@@ -1164,7 +1161,7 @@ export async function callFunction<TInput = any, TOutput = any>(
1164
1161
  const url = functionsBaseUrl + path;
1165
1162
  console.log(\`[BFF] \${method} \${url}\`);
1166
1163
 
1167
- // Authorization ヘッダーの転送(Middleware が cookie → Authorization に変換済み)
1164
+ // Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)
1168
1165
  const fetchHeaders: Record<string, string> = {
1169
1166
  'Content-Type': 'application/json',
1170
1167
  };
@@ -1262,10 +1259,10 @@ export function generateAuthGuardCSharp(policy: ModelAuthPolicy, operation: 'rea
1262
1259
 
1263
1260
  if (roles.length > 0) {
1264
1261
  const rolesStr = roles.map(r => `"${r}"`).join(', ');
1265
- return ` var (principal, errorResponse) = JwtHelper.Authorize(request, ${rolesStr});
1262
+ return ` var (principal, errorResponse) = await JwtHelper.Authorize(request, ${rolesStr});
1266
1263
  if (errorResponse != null) return errorResponse;`;
1267
1264
  }
1268
- return ` var (principal, errorResponse) = JwtHelper.Authorize(request);
1265
+ return ` var (principal, errorResponse) = await JwtHelper.Authorize(request);
1269
1266
  if (errorResponse != null) return errorResponse;`;
1270
1267
  }
1271
1268
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
8
8
  import { ModelAuthPolicy } from "../../types";
9
- import { generateAuthImportTS, generateAuthGuardTS } from "./auth-generator";
9
+ import { generateAuthImportTS, generateAuthGuardTS, generateAuthGuardCSharp, generateAuthGuardPython } from "./auth-generator";
10
10
 
11
11
  /**
12
12
  * Azure Functions エンティティファイルを生成(インラインハンドラー方式)
@@ -32,7 +32,7 @@ import { z } from 'zod/v4';
32
32
  import crypto from 'crypto';
33
33
  import { ${schemaName} } from '${sharedPackageName}';${authImport}
34
34
 
35
- const containerName = '${modelName}s';
35
+ const containerName = '${modelName.endsWith('s') ? modelName : modelName + 's'}';
36
36
 
37
37
  // GET /api/${modelCamel} - 全件取得
38
38
  app.http('${modelCamel}-get-all', {
@@ -248,11 +248,16 @@ ${authCatchBlock} context.error(\`Error deleting item from \${containerName
248
248
  `;
249
249
  }
250
250
 
251
- export function generateCSharpAzureFunctionsCRUD(model: ModelInfo): string {
251
+ export function generateCSharpAzureFunctionsCRUD(model: ModelInfo, authPolicy?: ModelAuthPolicy): string {
252
252
  const modelName = model.name;
253
253
  const modelCamel = toCamelCase(modelName);
254
254
  const className = `${modelName}Functions`;
255
- const containerName = `${modelName}s`;
255
+ const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
256
+
257
+ const hasAuth = !!authPolicy;
258
+ const authUsing = hasAuth ? 'using Functions.Auth;\n' : '';
259
+ const readGuard = hasAuth ? `\n${generateAuthGuardCSharp(authPolicy!, 'read')}\n` : '';
260
+ const writeGuard = hasAuth ? `\n${generateAuthGuardCSharp(authPolicy!, 'write')}\n` : '';
256
261
 
257
262
  return `using System.Net;
258
263
  using System.Text;
@@ -263,7 +268,7 @@ using Microsoft.Azure.Cosmos;
263
268
  using Microsoft.Azure.Functions.Worker;
264
269
  using Microsoft.Azure.Functions.Worker.Http;
265
270
  using Microsoft.Extensions.Logging;
266
-
271
+ ${authUsing}
267
272
  namespace SwallowKit.Functions;
268
273
 
269
274
  public sealed class ${className}
@@ -376,7 +381,7 @@ public sealed class ${className}
376
381
  [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}")] HttpRequestData request)
377
382
  {
378
383
  try
379
- {
384
+ {${readGuard}
380
385
  using var client = CreateCosmosClient();
381
386
  var container = GetContainer(client);
382
387
  using var iterator = container.GetItemQueryStreamIterator("SELECT * FROM c");
@@ -418,7 +423,7 @@ public sealed class ${className}
418
423
  string id)
419
424
  {
420
425
  try
421
- {
426
+ {${readGuard}
422
427
  using var client = CreateCosmosClient();
423
428
  var container = GetContainer(client);
424
429
  var item = await ReadCosmosItemAsync(container, id);
@@ -440,7 +445,7 @@ public sealed class ${className}
440
445
  [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "${modelCamel}")] HttpRequestData request)
441
446
  {
442
447
  try
443
- {
448
+ {${writeGuard}
444
449
  var body = await ReadRequestBodyAsync(request);
445
450
  var now = DateTimeOffset.UtcNow.ToString("O");
446
451
  var id = body["id"]?.GetValue<string>() ?? Guid.NewGuid().ToString();
@@ -475,7 +480,7 @@ public sealed class ${className}
475
480
  string id)
476
481
  {
477
482
  try
478
- {
483
+ {${writeGuard}
479
484
  using var client = CreateCosmosClient();
480
485
  var container = GetContainer(client);
481
486
 
@@ -520,7 +525,7 @@ public sealed class ${className}
520
525
  string id)
521
526
  {
522
527
  try
523
- {
528
+ {${writeGuard}
524
529
  using var client = CreateCosmosClient();
525
530
  var container = GetContainer(client);
526
531
  await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(id));
@@ -541,14 +546,23 @@ public sealed class ${className}
541
546
  `;
542
547
  }
543
548
 
544
- export function generatePythonAzureFunctionsCRUD(model: ModelInfo): {
549
+ export function generatePythonAzureFunctionsCRUD(model: ModelInfo, authPolicy?: ModelAuthPolicy): {
545
550
  blueprint: string;
546
551
  registration: string;
547
552
  } {
548
553
  const modelName = model.name;
549
554
  const modelCamel = toCamelCase(modelName);
550
555
  const modelSnake = toKebabCase(modelName).replace(/-/g, "_");
551
- const containerName = `${modelName}s`;
556
+ const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
557
+
558
+ const hasAuth = !!authPolicy;
559
+ const authImport = hasAuth ? '\nfrom auth.jwt_helper import require_auth, require_roles, handle_auth_error\n' : '';
560
+ // generateAuthGuardPython outputs at 4-space indent; inside try: we need 8-space
561
+ const readGuardRaw = hasAuth ? generateAuthGuardPython(authPolicy!, 'read') : '';
562
+ const writeGuardRaw = hasAuth ? generateAuthGuardPython(authPolicy!, 'write') : '';
563
+ const readGuard = hasAuth ? '\n' + readGuardRaw.split('\n').map(l => ' ' + l).join('\n') : '';
564
+ const writeGuard = hasAuth ? '\n' + writeGuardRaw.split('\n').map(l => ' ' + l).join('\n') : '';
565
+ const authCatch = hasAuth ? `\n auth_err = handle_auth_error(exc)\n if auth_err:\n return auth_err` : '';
552
566
 
553
567
  return {
554
568
  registration: `from blueprints.${modelSnake} import bp as ${modelSnake}_bp\napp.register_blueprint(${modelSnake}_bp)`,
@@ -561,7 +575,7 @@ import os
561
575
  import azure.functions as func
562
576
  from azure.cosmos import CosmosClient, exceptions
563
577
  from azure.identity import DefaultAzureCredential
564
-
578
+ ${authImport}
565
579
  bp = func.Blueprint()
566
580
  CONTAINER_NAME = "${containerName}"
567
581
  DATABASE_NAME = os.environ.get("COSMOS_DB_DATABASE_NAME", "AppDatabase")
@@ -603,7 +617,7 @@ def _build_managed_document(source: dict[str, Any], item_id: str, created_at: st
603
617
 
604
618
  @bp.route(route="${modelCamel}", methods=["GET"])
605
619
  def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
606
- try:
620
+ try:${readGuard}
607
621
  container = _get_container()
608
622
  items = list(
609
623
  container.query_items(
@@ -612,26 +626,26 @@ def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
612
626
  )
613
627
  )
614
628
  return _json_response(items, 200)
615
- except Exception as exc:
629
+ except Exception as exc:${authCatch}
616
630
  return _json_response({"error": "Failed to fetch items", "details": str(exc)}, 500)
617
631
 
618
632
 
619
633
  @bp.route(route="${modelCamel}/{id}", methods=["GET"])
620
634
  def ${modelSnake}_get_by_id(req: func.HttpRequest) -> func.HttpResponse:
621
635
  item_id = req.route_params.get("id")
622
- try:
636
+ try:${readGuard}
623
637
  container = _get_container()
624
638
  item = container.read_item(item=item_id, partition_key=item_id)
625
639
  return _json_response(item, 200)
626
640
  except exceptions.CosmosResourceNotFoundError:
627
641
  return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
628
- except Exception as exc:
642
+ except Exception as exc:${authCatch}
629
643
  return _json_response({"error": "Failed to fetch item", "id": item_id, "details": str(exc)}, 500)
630
644
 
631
645
 
632
646
  @bp.route(route="${modelCamel}", methods=["POST"])
633
647
  def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
634
- try:
648
+ try:${writeGuard}
635
649
  body = req.get_json()
636
650
  now = datetime.now(timezone.utc).isoformat()
637
651
  item_id = body.get("id") or str(uuid4())
@@ -642,14 +656,14 @@ def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
642
656
  return _json_response(payload, 201)
643
657
  except ValueError:
644
658
  return _json_response({"error": "Request body must be a JSON object."}, 400)
645
- except Exception as exc:
659
+ except Exception as exc:${authCatch}
646
660
  return _json_response({"error": "Failed to create item", "details": str(exc)}, 500)
647
661
 
648
662
 
649
663
  @bp.route(route="${modelCamel}/{id}", methods=["PUT"])
650
664
  def ${modelSnake}_update(req: func.HttpRequest) -> func.HttpResponse:
651
665
  item_id = req.route_params.get("id")
652
- try:
666
+ try:${writeGuard}
653
667
  container = _get_container()
654
668
  existing = container.read_item(item=item_id, partition_key=item_id)
655
669
  body = req.get_json()
@@ -665,20 +679,20 @@ def ${modelSnake}_update(req: func.HttpRequest) -> func.HttpResponse:
665
679
  return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
666
680
  except ValueError:
667
681
  return _json_response({"error": "Request body must be a JSON object.", "id": item_id}, 400)
668
- except Exception as exc:
682
+ except Exception as exc:${authCatch}
669
683
  return _json_response({"error": "Failed to update item", "id": item_id, "details": str(exc)}, 500)
670
684
 
671
685
 
672
686
  @bp.route(route="${modelCamel}/{id}", methods=["DELETE"])
673
687
  def ${modelSnake}_delete(req: func.HttpRequest) -> func.HttpResponse:
674
688
  item_id = req.route_params.get("id")
675
- try:
689
+ try:${writeGuard}
676
690
  container = _get_container()
677
691
  container.delete_item(item=item_id, partition_key=item_id)
678
692
  return func.HttpResponse(status_code=204)
679
693
  except exceptions.CosmosResourceNotFoundError:
680
694
  return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
681
- except Exception as exc:
695
+ except Exception as exc:${authCatch}
682
696
  return _json_response({"error": "Failed to delete item", "id": item_id, "details": str(exc)}, 500)
683
697
  `,
684
698
  };
@@ -410,6 +410,8 @@ async function extractFieldsFromSchema(modelPath: string, schemaName: string): P
410
410
  // コメントを削除
411
411
  modelContent = modelContent.replace(/\/\*[\s\S]*?\*\//g, '');
412
412
  modelContent = modelContent.replace(/\/\/.*/g, '');
413
+ // TypeScript の `as const` アサーションを削除(.mjs では構文エラーになる)
414
+ modelContent = modelContent.replace(/\s+as\s+const\b/g, '');
413
415
 
414
416
  // インライン化したローカルインポートを先頭に追加
415
417
  const inlinedDeps = localImports.length > 0 ? localImports.join('\n\n') + '\n\n' : '';