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.
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +85 -5
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/create-model.js +1 -1
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.js +1 -1
- package/dist/cli/commands/dev-seeds.js.map +1 -1
- package/dist/cli/commands/dev.js +61 -21
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +60 -4
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +17 -20
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts +2 -2
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +34 -22
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +2 -0
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/auth.test.ts +13 -13
- package/src/cli/commands/add-auth.ts +95 -6
- package/src/cli/commands/create-model.ts +1 -1
- package/src/cli/commands/dev-seeds.ts +1 -1
- package/src/cli/commands/dev.ts +64 -20
- package/src/cli/commands/scaffold.ts +68 -9
- package/src/core/scaffold/auth-generator.ts +16 -19
- package/src/core/scaffold/functions-generator.ts +37 -23
- package/src/core/scaffold/model-parser.ts +2 -0
package/src/cli/commands/dev.ts
CHANGED
|
@@ -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
|
-
|
|
608
|
-
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
);
|
|
337
|
-
fs.
|
|
338
|
-
|
|
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" })
|
|
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" })
|
|
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
|
|
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
|
|
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
|
|
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 ćććć¼ć®č»¢éļ¼
|
|
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
|
|
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' : '';
|