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.
Files changed (39) 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 +62 -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/mock/connector-mock-server.d.ts +7 -1
  14. package/dist/core/mock/connector-mock-server.d.ts.map +1 -1
  15. package/dist/core/mock/connector-mock-server.js +23 -26
  16. package/dist/core/mock/connector-mock-server.js.map +1 -1
  17. package/dist/core/scaffold/auth-generator.d.ts +1 -1
  18. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  19. package/dist/core/scaffold/auth-generator.js +17 -20
  20. package/dist/core/scaffold/auth-generator.js.map +1 -1
  21. package/dist/core/scaffold/functions-generator.d.ts +2 -2
  22. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  23. package/dist/core/scaffold/functions-generator.js +34 -22
  24. package/dist/core/scaffold/functions-generator.js.map +1 -1
  25. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  26. package/dist/core/scaffold/model-parser.js +2 -0
  27. package/dist/core/scaffold/model-parser.js.map +1 -1
  28. package/package.json +5 -5
  29. package/src/__tests__/auth.test.ts +13 -13
  30. package/src/__tests__/connector-mock-server.test.ts +75 -37
  31. package/src/cli/commands/add-auth.ts +95 -6
  32. package/src/cli/commands/create-model.ts +1 -1
  33. package/src/cli/commands/dev-seeds.ts +1 -1
  34. package/src/cli/commands/dev.ts +66 -21
  35. package/src/cli/commands/scaffold.ts +68 -9
  36. package/src/core/mock/connector-mock-server.ts +27 -27
  37. package/src/core/scaffold/auth-generator.ts +16 -19
  38. package/src/core/scaffold/functions-generator.ts +37 -23
  39. 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
- it("handles POST /api/auth/login with default mock users", async () => {
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
- server = new ConnectorMockServer({
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
- server = new ConnectorMockServer({
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
- server = new ConnectorMockServer({
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
- server = new ConnectorMockServer({
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
- server = new ConnectorMockServer({
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
- generateMiddleware,
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 middleware
95
- console.log("\n Generating middleware...");
96
- const middlewarePath = path.join(cwd, "middleware.ts");
97
- fs.writeFileSync(middlewarePath, generateMiddleware(projectName), "utf-8");
98
- console.log(` Created: middleware.ts`);
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 {
@@ -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 を起動
@@ -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 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
  }
@@ -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 = this.authUsers.find(
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' を返す。