swallowkit 1.0.0-beta.15 โ†’ 1.0.0-beta.17

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 (43) hide show
  1. package/dist/__tests__/fixtures.d.ts.map +1 -1
  2. package/dist/__tests__/fixtures.js +1 -0
  3. package/dist/__tests__/fixtures.js.map +1 -1
  4. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  5. package/dist/cli/commands/add-auth.js +85 -5
  6. package/dist/cli/commands/add-auth.js.map +1 -1
  7. package/dist/cli/commands/create-model.js +1 -1
  8. package/dist/cli/commands/create-model.js.map +1 -1
  9. package/dist/cli/commands/dev-seeds.js +5 -5
  10. package/dist/cli/commands/dev-seeds.js.map +1 -1
  11. package/dist/cli/commands/dev.js +64 -24
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.js +4 -4
  14. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  15. package/dist/cli/commands/scaffold.js +61 -5
  16. package/dist/cli/commands/scaffold.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 +375 -107
  24. package/dist/core/scaffold/functions-generator.js.map +1 -1
  25. package/dist/core/scaffold/model-parser.d.ts +7 -0
  26. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  27. package/dist/core/scaffold/model-parser.js +25 -0
  28. package/dist/core/scaffold/model-parser.js.map +1 -1
  29. package/package.json +3 -3
  30. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +694 -0
  31. package/src/__tests__/auth.test.ts +13 -13
  32. package/src/__tests__/fixtures.ts +1 -0
  33. package/src/__tests__/functions-generator.test.ts +136 -0
  34. package/src/__tests__/model-parser.test.ts +72 -0
  35. package/src/cli/commands/add-auth.ts +95 -6
  36. package/src/cli/commands/create-model.ts +1 -1
  37. package/src/cli/commands/dev-seeds.ts +5 -5
  38. package/src/cli/commands/dev.ts +67 -23
  39. package/src/cli/commands/init.ts +4 -4
  40. package/src/cli/commands/scaffold.ts +69 -10
  41. package/src/core/scaffold/auth-generator.ts +16 -19
  42. package/src/core/scaffold/functions-generator.ts +402 -108
  43. package/src/core/scaffold/model-parser.ts +28 -0
@@ -328,14 +328,18 @@ async function generateFunctionsCode(
328
328
  }
329
329
 
330
330
  if (backendLanguage === "csharp") {
331
- const functionFilePath = path.join(
332
- process.cwd(),
333
- functionsDir,
334
- "Crud",
335
- `${modelInfo.name}Functions.cs`
336
- );
337
- fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
338
- fs.writeFileSync(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo), "utf-8");
331
+ const crudDir = path.join(process.cwd(), functionsDir, "Crud");
332
+ const functionFilePath = path.join(crudDir, `${modelInfo.name}Functions.cs`);
333
+ fs.mkdirSync(crudDir, { recursive: true });
334
+
335
+ // Remove init-generated template (singular) to avoid route conflicts
336
+ const templatePath = path.join(crudDir, `${modelInfo.name}Function.cs`);
337
+ if (fs.existsSync(templatePath)) {
338
+ fs.unlinkSync(templatePath);
339
+ console.log(`๐Ÿ—‘๏ธ Removed template: ${templatePath}`);
340
+ }
341
+
342
+ fs.writeFileSync(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo, authPolicy), "utf-8");
339
343
  console.log(`โœ… Created: ${functionFilePath}`);
340
344
  return;
341
345
  }
@@ -344,7 +348,7 @@ async function generateFunctionsCode(
344
348
  const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
345
349
  fs.mkdirSync(blueprintsDir, { recursive: true });
346
350
 
347
- const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo);
351
+ const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo, authPolicy);
348
352
  fs.writeFileSync(blueprintPath, blueprint, "utf-8");
349
353
  updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration);
350
354
  console.log(`โœ… Created: ${blueprintPath}`);
@@ -767,6 +771,14 @@ function pruneGeneratedCSharpArtifacts(outputDir: string): void {
767
771
 
768
772
  const clientDir = path.join(outputDir, "src", "SwallowKitBackendModels", "Client");
769
773
  if (!fs.existsSync(clientDir)) {
774
+ // --global-property models ใงใฏClient/ใŒ็”Ÿๆˆใ•ใ‚Œใชใ„ใŸใ‚ใ€
775
+ // ใƒขใƒ‡ใƒซใŒไพๅญ˜ใ™ใ‚‹ๆœ€ๅฐ้™ใฎ Option<T> ใ‚’่‡ชๅ‰ใงไฝœๆˆใ™ใ‚‹
776
+ fs.mkdirSync(clientDir, { recursive: true });
777
+ fs.writeFileSync(
778
+ path.join(clientDir, "Option.cs"),
779
+ generateMinimalOptionCs(),
780
+ "utf-8"
781
+ );
770
782
  return;
771
783
  }
772
784
 
@@ -779,12 +791,59 @@ function pruneGeneratedCSharpArtifacts(outputDir: string): void {
779
791
  }
780
792
  }
781
793
 
794
+ /**
795
+ * OpenAPI Generator ใฎ csharp ใƒ†ใƒณใƒ—ใƒฌใƒผใƒˆใŒ็”Ÿๆˆใ™ใ‚‹ใƒขใƒ‡ใƒซใฏ Option<T> ใซไพๅญ˜ใ™ใ‚‹ใŒใ€
796
+ * supportingFiles ใ‚’้™คๅค–ใ—ใฆใ„ใ‚‹ใŸใ‚ Client/Option.cs ใŒ็”Ÿๆˆใ•ใ‚Œใชใ„ใ€‚
797
+ * Polly ็ญ‰ใฎไธ่ฆใชไพๅญ˜ใ‚’้ฟใ‘ใคใคใƒขใƒ‡ใƒซใ‚’ใ‚ณใƒณใƒ‘ใ‚คใƒซๅฏ่ƒฝใซใ™ใ‚‹ใŸใ‚ใ€ๆœ€ๅฐ้™ใฎ Option<T> ใ‚’ๆไพ›ใ™ใ‚‹ใ€‚
798
+ */
799
+ function generateMinimalOptionCs(): string {
800
+ return `// <auto-generated>
801
+ // Minimal Option<T> for OpenAPI Generator model compatibility.
802
+ // Full client supporting files are excluded to avoid Polly version conflicts.
803
+ // </auto-generated>
804
+
805
+ #nullable enable
806
+
807
+ namespace SwallowKitBackendModels.Client
808
+ {
809
+ /// <summary>
810
+ /// A wrapper for nullable/optional properties generated by OpenAPI Generator.
811
+ /// Tracks whether a value has been explicitly set (distinguishing null from absent).
812
+ /// </summary>
813
+ public readonly struct Option<TValue>
814
+ {
815
+ /// <summary>Whether this option has been explicitly set.</summary>
816
+ public bool IsSet { get; }
817
+
818
+ /// <summary>The contained value (may be default if not set).</summary>
819
+ public TValue Value { get; }
820
+
821
+ /// <summary>Create an Option with an explicit value.</summary>
822
+ public Option(TValue value)
823
+ {
824
+ IsSet = true;
825
+ Value = value;
826
+ }
827
+
828
+ /// <summary>Implicit conversion from Option to its inner value.</summary>
829
+ public static implicit operator TValue(Option<TValue> option) => option.Value;
830
+ }
831
+ }
832
+ `;
833
+ }
834
+
782
835
  function updatePythonFunctionRegistrations(functionAppPath: string, registration: string): void {
783
836
  if (!fs.existsSync(functionAppPath)) {
784
837
  throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
785
838
  }
786
839
 
787
840
  const content = fs.readFileSync(functionAppPath, "utf-8");
841
+
842
+ // Check if import line already exists (handles init-generated layout)
843
+ const importLine = registration.split("\n").find((l) => l.startsWith("from ") || l.startsWith("import "));
844
+ if (importLine && content.includes(importLine)) {
845
+ return;
846
+ }
788
847
  if (content.includes(registration)) {
789
848
  return;
790
849
  }
@@ -888,7 +947,7 @@ param databaseName string
888
947
  param containerName string = '${modelPascal}s'
889
948
 
890
949
  @description('Partition key path')
891
- param partitionKeyPath string = '/id'
950
+ param partitionKeyPath string = '${modelInfo.partitionKey}'
892
951
 
893
952
  @description('Throughput (RU/s) - only used for Free Tier')
894
953
  param throughput int = 400
@@ -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