swallowkit 1.0.0-beta.15 → 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 (33) 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 +61 -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/scaffold/auth-generator.d.ts +1 -1
  14. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  15. package/dist/core/scaffold/auth-generator.js +17 -20
  16. package/dist/core/scaffold/auth-generator.js.map +1 -1
  17. package/dist/core/scaffold/functions-generator.d.ts +2 -2
  18. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  19. package/dist/core/scaffold/functions-generator.js +34 -22
  20. package/dist/core/scaffold/functions-generator.js.map +1 -1
  21. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  22. package/dist/core/scaffold/model-parser.js +2 -0
  23. package/dist/core/scaffold/model-parser.js.map +1 -1
  24. package/package.json +3 -3
  25. package/src/__tests__/auth.test.ts +13 -13
  26. package/src/cli/commands/add-auth.ts +95 -6
  27. package/src/cli/commands/create-model.ts +1 -1
  28. package/src/cli/commands/dev-seeds.ts +1 -1
  29. package/src/cli/commands/dev.ts +64 -20
  30. package/src/cli/commands/scaffold.ts +68 -9
  31. package/src/core/scaffold/auth-generator.ts +16 -19
  32. package/src/core/scaffold/functions-generator.ts +37 -23
  33. package/src/core/scaffold/model-parser.ts +2 -0
@@ -458,10 +458,28 @@ async function startDevEnvironment(options: DevOptions) {
458
458
  const processes: ChildProcess[] = [];
459
459
  let functionsEnv: NodeJS.ProcessEnv = process.env;
460
460
  let mockServer: ConnectorMockServer | null = null;
461
+ let envLocalPath = '';
462
+ let envLocalDefaultUrl = ''; // default Functions URL to restore on shutdown
461
463
 
462
464
  // Cleanup processes on Ctrl+C
463
465
  process.on('SIGINT', async () => {
464
466
  console.log('\nšŸ›‘ Stopping development servers...');
467
+ // Restore .env.local to default Functions port on shutdown
468
+ if (envLocalPath && envLocalDefaultUrl) {
469
+ try {
470
+ if (fs.existsSync(envLocalPath)) {
471
+ const content = fs.readFileSync(envLocalPath, 'utf-8');
472
+ if (content.includes('BACKEND_FUNCTIONS_BASE_URL=') &&
473
+ !content.includes(`BACKEND_FUNCTIONS_BASE_URL=${envLocalDefaultUrl}`)) {
474
+ const restored = content.replace(
475
+ /^BACKEND_FUNCTIONS_BASE_URL=.*/m,
476
+ `BACKEND_FUNCTIONS_BASE_URL=${envLocalDefaultUrl}`
477
+ );
478
+ fs.writeFileSync(envLocalPath, restored, 'utf-8');
479
+ }
480
+ }
481
+ } catch { /* ignore */ }
482
+ }
465
483
  if (mockServer) {
466
484
  await mockServer.stop();
467
485
  }
@@ -604,28 +622,34 @@ async function startDevEnvironment(options: DevOptions) {
604
622
  if (hasFunctions && !options.noFunctions) {
605
623
  // Build shared package before starting Functions
606
624
  const sharedDir = path.join(process.cwd(), 'shared');
607
- if (fs.existsSync(sharedDir) && fs.existsSync(path.join(sharedDir, 'package.json'))) {
608
- console.log('šŸ“¦ Building shared package...');
609
- const filterArgs = pm === 'pnpm'
610
- ? ['run', '--filter', 'shared', 'build']
611
- : ['run', '--workspace=shared', 'build'];
612
- const sharedBuild = spawn(pm, filterArgs, {
613
- cwd: process.cwd(),
614
- shell: true,
615
- stdio: 'inherit',
616
- });
625
+ const sharedPkgPath = path.join(sharedDir, 'package.json');
626
+ if (fs.existsSync(sharedDir) && fs.existsSync(sharedPkgPath)) {
627
+ const sharedPkg = JSON.parse(fs.readFileSync(sharedPkgPath, 'utf-8'));
628
+ if (sharedPkg.scripts?.build) {
629
+ console.log('šŸ“¦ Building shared package...');
630
+ const filterArgs = pm === 'pnpm'
631
+ ? ['run', '--filter', 'shared', 'build']
632
+ : ['run', '--workspace=shared', 'build'];
633
+ const sharedBuild = spawn(pm, filterArgs, {
634
+ cwd: process.cwd(),
635
+ shell: true,
636
+ stdio: 'inherit',
637
+ });
617
638
 
618
- await new Promise<void>((resolve, reject) => {
619
- sharedBuild.on('close', (code) => {
620
- if (code === 0) {
621
- console.log('āœ… Shared package built successfully');
622
- resolve();
623
- } else {
624
- reject(new Error(`Shared package build failed with code ${code}`));
625
- }
639
+ await new Promise<void>((resolve, reject) => {
640
+ sharedBuild.on('close', (code) => {
641
+ if (code === 0) {
642
+ console.log('āœ… Shared package built successfully');
643
+ resolve();
644
+ } else {
645
+ reject(new Error(`Shared package build failed with code ${code}`));
646
+ }
647
+ });
648
+ sharedBuild.on('error', reject);
626
649
  });
627
- sharedBuild.on('error', reject);
628
- });
650
+ } else {
651
+ console.log('āš ļø Shared package has no build script — skipping build. Run "swallowkit add-auth" to fix.');
652
+ }
629
653
  }
630
654
 
631
655
  // Azure Functions ć‚’čµ·å‹•
@@ -783,6 +807,26 @@ async function startDevEnvironment(options: DevOptions) {
783
807
  }, 3000);
784
808
  }
785
809
 
810
+ // Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
811
+ // When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
812
+ // Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
813
+ envLocalPath = path.join(process.cwd(), '.env.local');
814
+ envLocalDefaultUrl = `http://${options.host || 'localhost'}:${functionsPort}`;
815
+ const bffTargetUrl = `http://${options.host || 'localhost'}:${bffTargetPort}`;
816
+ try {
817
+ if (fs.existsSync(envLocalPath)) {
818
+ const envContent = fs.readFileSync(envLocalPath, 'utf-8');
819
+ if (envContent.includes('BACKEND_FUNCTIONS_BASE_URL=') &&
820
+ !envContent.includes(`BACKEND_FUNCTIONS_BASE_URL=${bffTargetUrl}`)) {
821
+ const updated = envContent.replace(
822
+ /^BACKEND_FUNCTIONS_BASE_URL=.*/m,
823
+ `BACKEND_FUNCTIONS_BASE_URL=${bffTargetUrl}`
824
+ );
825
+ fs.writeFileSync(envLocalPath, updated, 'utf-8');
826
+ }
827
+ }
828
+ } catch { /* ignore */ }
829
+
786
830
  const nextEnv: NodeJS.ProcessEnv = {
787
831
  ...process.env,
788
832
  BACKEND_FUNCTIONS_BASE_URL: `http://${options.host || 'localhost'}:${bffTargetPort}`,
@@ -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
  }
@@ -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' : '';