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.
- package/dist/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +1 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- 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 +5 -5
- package/dist/cli/commands/dev-seeds.js.map +1 -1
- package/dist/cli/commands/dev.js +64 -24
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.js +4 -4
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +61 -5
- 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 +375 -107
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +7 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +25 -0
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +694 -0
- package/src/__tests__/auth.test.ts +13 -13
- package/src/__tests__/fixtures.ts +1 -0
- package/src/__tests__/functions-generator.test.ts +136 -0
- package/src/__tests__/model-parser.test.ts +72 -0
- 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 +5 -5
- package/src/cli/commands/dev.ts +67 -23
- package/src/cli/commands/init.ts +4 -4
- package/src/cli/commands/scaffold.ts +69 -10
- package/src/core/scaffold/auth-generator.ts +16 -19
- package/src/core/scaffold/functions-generator.ts +402 -108
- 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
|
|
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
|
}
|
|
@@ -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 = '
|
|
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" })
|
|
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
|
|