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.
- 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 +62 -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/mock/connector-mock-server.d.ts +7 -1
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -1
- package/dist/core/mock/connector-mock-server.js +23 -26
- package/dist/core/mock/connector-mock-server.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 +5 -5
- package/src/__tests__/auth.test.ts +13 -13
- package/src/__tests__/connector-mock-server.test.ts +75 -37
- 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 +66 -21
- package/src/cli/commands/scaffold.ts +68 -9
- package/src/core/mock/connector-mock-server.ts +27 -27
- 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
|
@@ -260,23 +260,65 @@ describe("ConnectorMockServer - Auth Endpoints", () => {
|
|
|
260
260
|
const AUTH_PORT = 19877;
|
|
261
261
|
const JWT_SECRET = "test-jwt-secret-for-mock-auth-tests";
|
|
262
262
|
|
|
263
|
+
// Auth-compatible User model (has loginId, password, roles fields)
|
|
264
|
+
const authUserModel = createRdbConnectorModelInfo({
|
|
265
|
+
name: "User",
|
|
266
|
+
displayName: "User",
|
|
267
|
+
schemaName: "userSchema",
|
|
268
|
+
filePath: "/models/user.ts",
|
|
269
|
+
fields: [
|
|
270
|
+
{ name: "id", type: "string", isOptional: false, isArray: false },
|
|
271
|
+
{ name: "loginId", type: "string", isOptional: false, isArray: false },
|
|
272
|
+
{ name: "password", type: "string", isOptional: false, isArray: false },
|
|
273
|
+
{ name: "name", type: "string", isOptional: false, isArray: false },
|
|
274
|
+
{ name: "email", type: "string", isOptional: false, isArray: false },
|
|
275
|
+
{ name: "roles", type: "string", isOptional: false, isArray: true },
|
|
276
|
+
],
|
|
277
|
+
connectorConfig: {
|
|
278
|
+
connector: "mysql",
|
|
279
|
+
operations: ["getAll", "getById"],
|
|
280
|
+
table: "users",
|
|
281
|
+
idColumn: "id",
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const testUsers = [
|
|
286
|
+
{ id: "1", loginId: "admin", password: "password123", name: "Admin User", email: "admin@example.com", roles: ["admin"] },
|
|
287
|
+
{ id: "2", loginId: "user", password: "password123", name: "Test User", email: "user@example.com", roles: ["user"] },
|
|
288
|
+
];
|
|
289
|
+
|
|
263
290
|
afterEach(async () => {
|
|
264
291
|
if (server) {
|
|
265
292
|
await server.stop();
|
|
266
293
|
}
|
|
267
294
|
});
|
|
268
295
|
|
|
269
|
-
|
|
296
|
+
/** Start mock server with auth-compatible User model and seed data */
|
|
297
|
+
async function startAuthServer() {
|
|
270
298
|
server = new ConnectorMockServer({
|
|
271
299
|
port: AUTH_PORT,
|
|
272
300
|
functionsTarget: "localhost:7071",
|
|
273
|
-
connectorModels: [],
|
|
301
|
+
connectorModels: [authUserModel],
|
|
302
|
+
mockCount: 0,
|
|
274
303
|
authConfig: {
|
|
275
304
|
jwtSecret: JWT_SECRET,
|
|
276
305
|
tokenExpiry: "1h",
|
|
306
|
+
customJwt: {
|
|
307
|
+
userTable: "users",
|
|
308
|
+
loginIdColumn: "loginId",
|
|
309
|
+
passwordHashColumn: "password",
|
|
310
|
+
rolesColumn: "roles",
|
|
311
|
+
},
|
|
277
312
|
},
|
|
278
313
|
});
|
|
279
314
|
await server.start();
|
|
315
|
+
// Populate user store with known test data
|
|
316
|
+
const store = server.getStore("User");
|
|
317
|
+
store.push(...testUsers);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
it("handles POST /api/auth/login with users from RDB mock store", async () => {
|
|
321
|
+
await startAuthServer();
|
|
280
322
|
|
|
281
323
|
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
282
324
|
loginId: "admin",
|
|
@@ -294,13 +336,7 @@ describe("ConnectorMockServer - Auth Endpoints", () => {
|
|
|
294
336
|
});
|
|
295
337
|
|
|
296
338
|
it("returns 401 for invalid credentials", async () => {
|
|
297
|
-
|
|
298
|
-
port: AUTH_PORT,
|
|
299
|
-
functionsTarget: "localhost:7071",
|
|
300
|
-
connectorModels: [],
|
|
301
|
-
authConfig: { jwtSecret: JWT_SECRET },
|
|
302
|
-
});
|
|
303
|
-
await server.start();
|
|
339
|
+
await startAuthServer();
|
|
304
340
|
|
|
305
341
|
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
306
342
|
loginId: "admin",
|
|
@@ -311,13 +347,7 @@ describe("ConnectorMockServer - Auth Endpoints", () => {
|
|
|
311
347
|
});
|
|
312
348
|
|
|
313
349
|
it("returns 401 for non-existent user", async () => {
|
|
314
|
-
|
|
315
|
-
port: AUTH_PORT,
|
|
316
|
-
functionsTarget: "localhost:7071",
|
|
317
|
-
connectorModels: [],
|
|
318
|
-
authConfig: { jwtSecret: JWT_SECRET },
|
|
319
|
-
});
|
|
320
|
-
await server.start();
|
|
350
|
+
await startAuthServer();
|
|
321
351
|
|
|
322
352
|
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
323
353
|
loginId: "nobody",
|
|
@@ -328,13 +358,7 @@ describe("ConnectorMockServer - Auth Endpoints", () => {
|
|
|
328
358
|
});
|
|
329
359
|
|
|
330
360
|
it("handles GET /api/auth/me with valid JWT", async () => {
|
|
331
|
-
|
|
332
|
-
port: AUTH_PORT,
|
|
333
|
-
functionsTarget: "localhost:7071",
|
|
334
|
-
connectorModels: [],
|
|
335
|
-
authConfig: { jwtSecret: JWT_SECRET },
|
|
336
|
-
});
|
|
337
|
-
await server.start();
|
|
361
|
+
await startAuthServer();
|
|
338
362
|
|
|
339
363
|
// Login first
|
|
340
364
|
const loginRes = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
@@ -355,26 +379,14 @@ describe("ConnectorMockServer - Auth Endpoints", () => {
|
|
|
355
379
|
});
|
|
356
380
|
|
|
357
381
|
it("returns 401 for /api/auth/me without token", async () => {
|
|
358
|
-
|
|
359
|
-
port: AUTH_PORT,
|
|
360
|
-
functionsTarget: "localhost:7071",
|
|
361
|
-
connectorModels: [],
|
|
362
|
-
authConfig: { jwtSecret: JWT_SECRET },
|
|
363
|
-
});
|
|
364
|
-
await server.start();
|
|
382
|
+
await startAuthServer();
|
|
365
383
|
|
|
366
384
|
const res = await httpRequest(AUTH_PORT, "GET", "/api/auth/me");
|
|
367
385
|
expect(res.status).toBe(401);
|
|
368
386
|
});
|
|
369
387
|
|
|
370
388
|
it("handles POST /api/auth/logout", async () => {
|
|
371
|
-
|
|
372
|
-
port: AUTH_PORT,
|
|
373
|
-
functionsTarget: "localhost:7071",
|
|
374
|
-
connectorModels: [],
|
|
375
|
-
authConfig: { jwtSecret: JWT_SECRET },
|
|
376
|
-
});
|
|
377
|
-
await server.start();
|
|
389
|
+
await startAuthServer();
|
|
378
390
|
|
|
379
391
|
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/logout");
|
|
380
392
|
expect(res.status).toBe(200);
|
|
@@ -398,4 +410,30 @@ describe("ConnectorMockServer - Auth Endpoints", () => {
|
|
|
398
410
|
// Should get 502 (proxy error) since auth is not handled by mock
|
|
399
411
|
expect(res.status).toBe(502);
|
|
400
412
|
});
|
|
413
|
+
|
|
414
|
+
it("returns 500 when no user model matches the configured userTable", async () => {
|
|
415
|
+
server = new ConnectorMockServer({
|
|
416
|
+
port: AUTH_PORT,
|
|
417
|
+
functionsTarget: "localhost:7071",
|
|
418
|
+
connectorModels: [], // no models at all
|
|
419
|
+
authConfig: {
|
|
420
|
+
jwtSecret: JWT_SECRET,
|
|
421
|
+
customJwt: {
|
|
422
|
+
userTable: "users",
|
|
423
|
+
loginIdColumn: "loginId",
|
|
424
|
+
passwordHashColumn: "password",
|
|
425
|
+
rolesColumn: "roles",
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
await server.start();
|
|
430
|
+
|
|
431
|
+
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
432
|
+
loginId: "admin",
|
|
433
|
+
password: "password123",
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(res.status).toBe(500);
|
|
437
|
+
expect((res.body as any).error).toContain("No user model found");
|
|
438
|
+
});
|
|
401
439
|
});
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
generateBFFAuthLoginRoute,
|
|
19
19
|
generateBFFAuthLogoutRoute,
|
|
20
20
|
generateBFFAuthMeRoute,
|
|
21
|
-
|
|
21
|
+
generateProxy,
|
|
22
22
|
generateLoginPage,
|
|
23
23
|
generateAuthContext,
|
|
24
24
|
generateBFFCallFunctionWithAuth,
|
|
@@ -76,6 +76,9 @@ export async function addAuthCommand(options: AddAuthOptions) {
|
|
|
76
76
|
fs.writeFileSync(authModelPath, generateAuthModels(), "utf-8");
|
|
77
77
|
console.log(` Created: shared/models/auth.ts`);
|
|
78
78
|
|
|
79
|
+
// Ensure shared package has build infrastructure (tsconfig, build script)
|
|
80
|
+
ensureSharedBuildInfrastructure(cwd);
|
|
81
|
+
|
|
79
82
|
// Update shared/index.ts to re-export auth
|
|
80
83
|
updateSharedIndex(cwd);
|
|
81
84
|
|
|
@@ -91,11 +94,11 @@ export async function addAuthCommand(options: AddAuthOptions) {
|
|
|
91
94
|
console.log("\n Generating BFF auth routes...");
|
|
92
95
|
generateBFFAuth(cwd, projectName, sharedPackageName);
|
|
93
96
|
|
|
94
|
-
// 4. Generate
|
|
95
|
-
console.log("\n Generating
|
|
96
|
-
const
|
|
97
|
-
fs.writeFileSync(
|
|
98
|
-
console.log(` Created:
|
|
97
|
+
// 4. Generate proxy
|
|
98
|
+
console.log("\n Generating proxy...");
|
|
99
|
+
const proxyPath = path.join(cwd, "proxy.ts");
|
|
100
|
+
fs.writeFileSync(proxyPath, generateProxy(projectName), "utf-8");
|
|
101
|
+
console.log(` Created: proxy.ts`);
|
|
99
102
|
|
|
100
103
|
// 5. Generate login page
|
|
101
104
|
console.log("\n Generating login page...");
|
|
@@ -143,6 +146,73 @@ export async function addAuthCommand(options: AddAuthOptions) {
|
|
|
143
146
|
console.log(` 5. Run scaffold to regenerate functions with auth guards`);
|
|
144
147
|
}
|
|
145
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Ensure the shared package has proper build infrastructure
|
|
151
|
+
* (tsconfig.json, build script, typescript devDependency).
|
|
152
|
+
* Required for `dev` command which runs `npm run --workspace=shared build`.
|
|
153
|
+
*/
|
|
154
|
+
function ensureSharedBuildInfrastructure(cwd: string): void {
|
|
155
|
+
const sharedDir = path.join(cwd, "shared");
|
|
156
|
+
const pkgPath = path.join(sharedDir, "package.json");
|
|
157
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
158
|
+
|
|
159
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
160
|
+
let updated = false;
|
|
161
|
+
|
|
162
|
+
// Ensure scripts.build exists
|
|
163
|
+
if (!pkg.scripts?.build) {
|
|
164
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
165
|
+
pkg.scripts.build = "tsc";
|
|
166
|
+
pkg.scripts.watch = "tsc --watch";
|
|
167
|
+
updated = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Ensure main points to compiled output
|
|
171
|
+
if (!pkg.main || pkg.main === "index.ts") {
|
|
172
|
+
pkg.main = "dist/index.js";
|
|
173
|
+
pkg.types = "dist/index.d.ts";
|
|
174
|
+
updated = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Ensure typescript devDependency
|
|
178
|
+
if (!pkg.devDependencies?.typescript) {
|
|
179
|
+
if (!pkg.devDependencies) pkg.devDependencies = {};
|
|
180
|
+
pkg.devDependencies.typescript = "^5.0.0";
|
|
181
|
+
updated = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (updated) {
|
|
185
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), "utf-8");
|
|
186
|
+
console.log(` Updated: shared/package.json (added build infrastructure)`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Ensure tsconfig.json exists
|
|
190
|
+
const tsconfigPath = path.join(sharedDir, "tsconfig.json");
|
|
191
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
192
|
+
const tsconfig = {
|
|
193
|
+
compilerOptions: {
|
|
194
|
+
target: "ES2020",
|
|
195
|
+
module: "commonjs",
|
|
196
|
+
moduleResolution: "node",
|
|
197
|
+
lib: ["ES2020"],
|
|
198
|
+
outDir: "dist",
|
|
199
|
+
rootDir: ".",
|
|
200
|
+
declaration: true,
|
|
201
|
+
declarationMap: true,
|
|
202
|
+
sourceMap: true,
|
|
203
|
+
strict: true,
|
|
204
|
+
esModuleInterop: true,
|
|
205
|
+
skipLibCheck: true,
|
|
206
|
+
forceConsistentCasingInFileNames: true,
|
|
207
|
+
},
|
|
208
|
+
include: ["index.ts", "models/**/*"],
|
|
209
|
+
exclude: ["node_modules", "dist"],
|
|
210
|
+
};
|
|
211
|
+
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2), "utf-8");
|
|
212
|
+
console.log(` Created: shared/tsconfig.json`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
146
216
|
function updateSharedIndex(cwd: string): void {
|
|
147
217
|
const indexPath = path.join(cwd, "shared", "index.ts");
|
|
148
218
|
if (fs.existsSync(indexPath)) {
|
|
@@ -226,6 +296,25 @@ function generateFunctionsAuth(
|
|
|
226
296
|
|
|
227
297
|
// __init__.py
|
|
228
298
|
fs.writeFileSync(path.join(authDir, "__init__.py"), "", "utf-8");
|
|
299
|
+
|
|
300
|
+
// Register auth blueprint in function_app.py
|
|
301
|
+
const functionAppPath = path.join(functionsDir, "function_app.py");
|
|
302
|
+
if (fs.existsSync(functionAppPath)) {
|
|
303
|
+
const content = fs.readFileSync(functionAppPath, "utf-8");
|
|
304
|
+
const authImport = "from blueprints.auth import bp as auth_bp";
|
|
305
|
+
const authRegister = "app.register_blueprint(auth_bp)";
|
|
306
|
+
if (!content.includes(authImport)) {
|
|
307
|
+
const marker = "# SwallowKit scaffold registrations";
|
|
308
|
+
if (content.includes(marker)) {
|
|
309
|
+
const updated = content.replace(
|
|
310
|
+
marker,
|
|
311
|
+
`${authImport}\n${authRegister}\n${marker}`
|
|
312
|
+
);
|
|
313
|
+
fs.writeFileSync(functionAppPath, updated, "utf-8");
|
|
314
|
+
console.log(` Updated: functions/function_app.py (registered auth blueprint)`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
229
318
|
}
|
|
230
319
|
}
|
|
231
320
|
|
|
@@ -45,7 +45,7 @@ export const displayName = '${pascalName}';
|
|
|
45
45
|
function generateConnectorModelTemplate(modelName: string, connectorName: string, connectorType: 'rdb' | 'api'): string {
|
|
46
46
|
const pascalName = toPascalCase(modelName);
|
|
47
47
|
const kebabName = modelName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
48
|
-
const pluralName = kebabName + 's';
|
|
48
|
+
const pluralName = kebabName.endsWith('s') ? kebabName : kebabName + 's';
|
|
49
49
|
|
|
50
50
|
const schema = `import { z } from 'zod/v4';
|
|
51
51
|
|
|
@@ -30,7 +30,7 @@ interface LoadedSeedFile {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function getContainerNameForModel(model: Pick<ModelInfo, "name">): string {
|
|
33
|
-
return `${model.name}s`;
|
|
33
|
+
return model.name.endsWith('s') ? model.name : `${model.name}s`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function getSeedEnvironmentDir(environment: string, seedsDir: string = "dev-seeds"): string {
|
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 を起動
|
|
@@ -711,7 +735,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
711
735
|
|
|
712
736
|
// Resolve auth config — auth functions use RDB connectors, mocked alongside other models
|
|
713
737
|
const authConfig = getAuthConfig();
|
|
714
|
-
let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
|
|
738
|
+
let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { userTable: string; loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
|
|
715
739
|
if (authConfig?.provider === 'custom-jwt' && authConfig.customJwt) {
|
|
716
740
|
const fullConfig = getFullConfig();
|
|
717
741
|
// Read JWT_SECRET from functions/local.settings.json if available
|
|
@@ -727,6 +751,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
727
751
|
jwtSecret,
|
|
728
752
|
tokenExpiry: authConfig.customJwt.tokenExpiry,
|
|
729
753
|
customJwt: {
|
|
754
|
+
userTable: authConfig.customJwt.userTable,
|
|
730
755
|
loginIdColumn: authConfig.customJwt.loginIdColumn,
|
|
731
756
|
passwordHashColumn: authConfig.customJwt.passwordHashColumn,
|
|
732
757
|
rolesColumn: authConfig.customJwt.rolesColumn,
|
|
@@ -782,6 +807,26 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
782
807
|
}, 3000);
|
|
783
808
|
}
|
|
784
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
|
+
|
|
785
830
|
const nextEnv: NodeJS.ProcessEnv = {
|
|
786
831
|
...process.env,
|
|
787
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
|
}
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
* - その他のリクエスト → 実Azure Functions へプロキシ
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import * as fs from "fs";
|
|
8
7
|
import * as http from "http";
|
|
9
|
-
import * as path from "path";
|
|
10
8
|
import { ModelInfo, toCamelCase } from "../scaffold/model-parser";
|
|
11
9
|
import { generateMockDocuments } from "./zod-mock-generator";
|
|
12
10
|
import { loadDevSeedFiles } from "../../cli/commands/dev-seeds";
|
|
@@ -36,6 +34,8 @@ export interface ConnectorMockServerOptions {
|
|
|
36
34
|
tokenExpiry?: string;
|
|
37
35
|
/** Custom JWT config from swallowkit.config.js */
|
|
38
36
|
customJwt?: {
|
|
37
|
+
/** RDB table name that holds user records (e.g., "users") */
|
|
38
|
+
userTable: string;
|
|
39
39
|
loginIdColumn: string;
|
|
40
40
|
passwordHashColumn: string;
|
|
41
41
|
rolesColumn: string;
|
|
@@ -54,7 +54,6 @@ export class ConnectorMockServer {
|
|
|
54
54
|
private server: http.Server | null = null;
|
|
55
55
|
private stores = new Map<string, MockDocument[]>();
|
|
56
56
|
private routeMap = new Map<string, ModelInfo>(); // route → model
|
|
57
|
-
private authUsers: MockDocument[] = [];
|
|
58
57
|
private options: ConnectorMockServerOptions;
|
|
59
58
|
|
|
60
59
|
constructor(options: ConnectorMockServerOptions) {
|
|
@@ -141,29 +140,6 @@ export class ConnectorMockServer {
|
|
|
141
140
|
console.warn(`⚠️ Failed to load dev-seeds for connectors: ${(err as Error).message}`);
|
|
142
141
|
}
|
|
143
142
|
}
|
|
144
|
-
|
|
145
|
-
// Load auth users from dev-seeds/_auth-users.json (auth uses RDB connector, same mock approach)
|
|
146
|
-
if (this.options.seedEnv && this.options.authConfig) {
|
|
147
|
-
const seedsDir = this.options.seedsDir || path.join(process.cwd(), "dev-seeds");
|
|
148
|
-
const authUsersPath = path.join(seedsDir, this.options.seedEnv, "_auth-users.json");
|
|
149
|
-
if (fs.existsSync(authUsersPath)) {
|
|
150
|
-
try {
|
|
151
|
-
this.authUsers = JSON.parse(fs.readFileSync(authUsersPath, "utf-8"));
|
|
152
|
-
console.log(` 📂 Loaded ${this.authUsers.length} auth user seed(s) from _auth-users.json`);
|
|
153
|
-
} catch (err) {
|
|
154
|
-
console.warn(`⚠️ Failed to load _auth-users.json: ${(err as Error).message}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Fallback: default auth user seeds when no seed file exists
|
|
160
|
-
if (this.options.authConfig && this.authUsers.length === 0) {
|
|
161
|
-
this.authUsers = [
|
|
162
|
-
{ id: "1", loginId: "admin", password: "password123", name: "Admin User", email: "admin@example.com", roles: ["admin"] },
|
|
163
|
-
{ id: "2", loginId: "user", password: "password123", name: "Test User", email: "user@example.com", roles: ["user"] },
|
|
164
|
-
];
|
|
165
|
-
console.log(" 📂 Using default auth user seeds (admin/password123, user/password123)");
|
|
166
|
-
}
|
|
167
143
|
}
|
|
168
144
|
|
|
169
145
|
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
@@ -353,11 +329,18 @@ export class ConnectorMockServer {
|
|
|
353
329
|
return this.sendJson(res, 400, { error: "loginId and password are required" });
|
|
354
330
|
}
|
|
355
331
|
|
|
332
|
+
const users = this.resolveUserStore();
|
|
333
|
+
if (!users) {
|
|
334
|
+
return this.sendJson(res, 500, {
|
|
335
|
+
error: "No user model found — ensure a connector model with the configured userTable exists",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
356
339
|
const loginField = this.options.authConfig?.customJwt?.loginIdColumn || "loginId";
|
|
357
340
|
const passwordField = this.options.authConfig?.customJwt?.passwordHashColumn || "password";
|
|
358
341
|
const rolesField = this.options.authConfig?.customJwt?.rolesColumn || "roles";
|
|
359
342
|
|
|
360
|
-
const user =
|
|
343
|
+
const user = users.find(
|
|
361
344
|
(u) => (u[loginField] || u.loginId) === loginId
|
|
362
345
|
);
|
|
363
346
|
|
|
@@ -442,6 +425,23 @@ export class ConnectorMockServer {
|
|
|
442
425
|
|
|
443
426
|
// ─── Utilities ────────────────────────────────────────────
|
|
444
427
|
|
|
428
|
+
/**
|
|
429
|
+
* authConfig.customJwt.userTable に対応するモデルのストアを返す。
|
|
430
|
+
* ユーザーテーブルが見つからない場合は null を返す。
|
|
431
|
+
*/
|
|
432
|
+
private resolveUserStore(): MockDocument[] | null {
|
|
433
|
+
const userTable = this.options.authConfig?.customJwt?.userTable;
|
|
434
|
+
if (!userTable) return null;
|
|
435
|
+
|
|
436
|
+
for (const model of this.options.connectorModels) {
|
|
437
|
+
const cfg = model.connectorConfig;
|
|
438
|
+
if (cfg && "table" in cfg && cfg.table === userTable) {
|
|
439
|
+
return this.stores.get(toCamelCase(model.name)) || null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
445
|
/**
|
|
446
446
|
* JWT を検証し、ペイロード(roles 含む)を返す。
|
|
447
447
|
* 認証不要な場合は null を返す。401/403 の場合はレスポンスを送信して 'error' を返す。
|