@classytic/arc 1.0.0 → 1.0.5

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.
@@ -0,0 +1,2410 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import * as readline from 'readline';
4
+ import { execSync, spawn } from 'child_process';
5
+
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+ async function init(options = {}) {
13
+ console.log(`
14
+ ╔═══════════════════════════════════════════════════════════════╗
15
+ ║ 🔥 Arc Project Setup ║
16
+ ║ Resource-Oriented Backend Framework ║
17
+ ╚═══════════════════════════════════════════════════════════════╝
18
+ `);
19
+ const config = await gatherConfig(options);
20
+ console.log(`
21
+ 📦 Creating project: ${config.name}`);
22
+ console.log(` Adapter: ${config.adapter === "mongokit" ? "MongoKit (MongoDB)" : "Custom"}`);
23
+ console.log(` Tenant: ${config.tenant === "multi" ? "Multi-tenant" : "Single-tenant"}`);
24
+ console.log(` Language: ${config.typescript ? "TypeScript" : "JavaScript"}
25
+ `);
26
+ const projectPath = path.join(process.cwd(), config.name);
27
+ try {
28
+ await fs.access(projectPath);
29
+ if (!options.force) {
30
+ console.error(`❌ Directory "${config.name}" already exists. Use --force to overwrite.`);
31
+ process.exit(1);
32
+ }
33
+ } catch {
34
+ }
35
+ const packageManager = detectPackageManager();
36
+ console.log(`📦 Using package manager: ${packageManager}
37
+ `);
38
+ await createProjectStructure(projectPath, config);
39
+ if (!options.skipInstall) {
40
+ console.log("\n📥 Installing dependencies...\n");
41
+ await installDependencies(projectPath, config, packageManager);
42
+ }
43
+ printSuccessMessage(config, options.skipInstall);
44
+ }
45
+ function detectPackageManager() {
46
+ try {
47
+ const cwd = process.cwd();
48
+ if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
49
+ if (existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
50
+ if (existsSync(path.join(cwd, "bun.lockb"))) return "bun";
51
+ if (existsSync(path.join(cwd, "package-lock.json"))) return "npm";
52
+ } catch {
53
+ }
54
+ if (isCommandAvailable("pnpm")) return "pnpm";
55
+ if (isCommandAvailable("yarn")) return "yarn";
56
+ if (isCommandAvailable("bun")) return "bun";
57
+ return "npm";
58
+ }
59
+ function isCommandAvailable(command) {
60
+ try {
61
+ execSync(`${command} --version`, { stdio: "ignore" });
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ function existsSync(filePath) {
68
+ try {
69
+ __require("fs").accessSync(filePath);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ async function installDependencies(projectPath, config, pm) {
76
+ const deps = [
77
+ "@classytic/arc@latest",
78
+ "fastify@latest",
79
+ "@fastify/cors@latest",
80
+ "@fastify/helmet@latest",
81
+ "@fastify/jwt@latest",
82
+ "@fastify/rate-limit@latest",
83
+ "@fastify/sensible@latest",
84
+ "@fastify/under-pressure@latest",
85
+ "bcryptjs@latest",
86
+ "dotenv@latest",
87
+ "jsonwebtoken@latest"
88
+ ];
89
+ if (config.adapter === "mongokit") {
90
+ deps.push("@classytic/mongokit@latest", "mongoose@latest");
91
+ }
92
+ const devDeps = [
93
+ "vitest@latest",
94
+ "pino-pretty@latest"
95
+ ];
96
+ if (config.typescript) {
97
+ devDeps.push(
98
+ "typescript@latest",
99
+ "@types/node@latest",
100
+ "@types/bcryptjs@latest",
101
+ "@types/jsonwebtoken@latest",
102
+ "tsx@latest"
103
+ );
104
+ }
105
+ const installCmd = getInstallCommand(pm, deps, false);
106
+ const installDevCmd = getInstallCommand(pm, devDeps, true);
107
+ console.log(` Installing dependencies...`);
108
+ await runCommand(installCmd, projectPath);
109
+ console.log(` Installing dev dependencies...`);
110
+ await runCommand(installDevCmd, projectPath);
111
+ console.log(`
112
+ ✅ Dependencies installed successfully!`);
113
+ }
114
+ function getInstallCommand(pm, packages, isDev) {
115
+ const pkgList = packages.join(" ");
116
+ switch (pm) {
117
+ case "pnpm":
118
+ return `pnpm add ${isDev ? "-D" : ""} ${pkgList}`;
119
+ case "yarn":
120
+ return `yarn add ${isDev ? "-D" : ""} ${pkgList}`;
121
+ case "bun":
122
+ return `bun add ${isDev ? "-d" : ""} ${pkgList}`;
123
+ case "npm":
124
+ default:
125
+ return `npm install ${isDev ? "--save-dev" : ""} ${pkgList}`;
126
+ }
127
+ }
128
+ function runCommand(command, cwd) {
129
+ return new Promise((resolve, reject) => {
130
+ const isWindows = process.platform === "win32";
131
+ const shell = isWindows ? "cmd" : "/bin/sh";
132
+ const shellFlag = isWindows ? "/c" : "-c";
133
+ const child = spawn(shell, [shellFlag, command], {
134
+ cwd,
135
+ stdio: "inherit",
136
+ env: { ...process.env, FORCE_COLOR: "1" }
137
+ });
138
+ child.on("close", (code) => {
139
+ if (code === 0) {
140
+ resolve();
141
+ } else {
142
+ reject(new Error(`Command failed with exit code ${code}`));
143
+ }
144
+ });
145
+ child.on("error", reject);
146
+ });
147
+ }
148
+ async function gatherConfig(options) {
149
+ const rl = readline.createInterface({
150
+ input: process.stdin,
151
+ output: process.stdout
152
+ });
153
+ const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
154
+ try {
155
+ const name = options.name || await question("📁 Project name: ") || "my-arc-app";
156
+ let adapter = options.adapter || "mongokit";
157
+ if (!options.adapter) {
158
+ const adapterChoice = await question("🗄️ Database adapter [1=MongoKit (recommended), 2=Custom]: ");
159
+ adapter = adapterChoice === "2" ? "custom" : "mongokit";
160
+ }
161
+ let tenant = options.tenant || "single";
162
+ if (!options.tenant) {
163
+ const tenantChoice = await question("🏢 Tenant mode [1=Single-tenant, 2=Multi-tenant]: ");
164
+ tenant = tenantChoice === "2" ? "multi" : "single";
165
+ }
166
+ let typescript = options.typescript ?? true;
167
+ if (options.typescript === void 0) {
168
+ const tsChoice = await question("�� Language [1=TypeScript (recommended), 2=JavaScript]: ");
169
+ typescript = tsChoice !== "2";
170
+ }
171
+ return { name, adapter, tenant, typescript };
172
+ } finally {
173
+ rl.close();
174
+ }
175
+ }
176
+ async function createProjectStructure(projectPath, config) {
177
+ const ext = config.typescript ? "ts" : "js";
178
+ const dirs = [
179
+ "",
180
+ "src",
181
+ "src/config",
182
+ // Config & env loading (import first!)
183
+ "src/shared",
184
+ // Shared utilities (adapters, presets, permissions)
185
+ "src/shared/presets",
186
+ // Preset definitions
187
+ "src/plugins",
188
+ // App-specific plugins
189
+ "src/resources",
190
+ // Resource definitions
191
+ "src/resources/user",
192
+ // User resource (user.model, user.repository, etc.)
193
+ "src/resources/auth",
194
+ // Auth resource (auth.resource, auth.handlers, etc.)
195
+ "src/resources/example",
196
+ // Example resource
197
+ "tests"
198
+ ];
199
+ for (const dir of dirs) {
200
+ await fs.mkdir(path.join(projectPath, dir), { recursive: true });
201
+ console.log(` 📁 Created: ${dir || "/"}`);
202
+ }
203
+ const files = {
204
+ "package.json": packageJsonTemplate(config),
205
+ ".gitignore": gitignoreTemplate(),
206
+ ".env.example": envExampleTemplate(config),
207
+ ".env.dev": envDevTemplate(config),
208
+ "README.md": readmeTemplate(config)
209
+ };
210
+ if (config.typescript) {
211
+ files["tsconfig.json"] = tsconfigTemplate();
212
+ }
213
+ files["vitest.config.ts"] = vitestConfigTemplate(config);
214
+ files[`src/config/env.${ext}`] = envLoaderTemplate(config);
215
+ files[`src/config/index.${ext}`] = configTemplate(config);
216
+ files[`src/app.${ext}`] = appTemplate(config);
217
+ files[`src/index.${ext}`] = indexTemplate(config);
218
+ files[`src/shared/index.${ext}`] = sharedIndexTemplate(config);
219
+ files[`src/shared/adapter.${ext}`] = config.adapter === "mongokit" ? createAdapterTemplate(config) : customAdapterTemplate(config);
220
+ files[`src/shared/permissions.${ext}`] = permissionsTemplate(config);
221
+ if (config.tenant === "multi") {
222
+ files[`src/shared/presets/index.${ext}`] = presetsMultiTenantTemplate(config);
223
+ files[`src/shared/presets/flexible-multi-tenant.${ext}`] = flexibleMultiTenantPresetTemplate(config);
224
+ } else {
225
+ files[`src/shared/presets/index.${ext}`] = presetsSingleTenantTemplate(config);
226
+ }
227
+ files[`src/plugins/index.${ext}`] = pluginsIndexTemplate(config);
228
+ files[`src/resources/index.${ext}`] = resourcesIndexTemplate(config);
229
+ files[`src/resources/user/user.model.${ext}`] = userModelTemplate(config);
230
+ files[`src/resources/user/user.repository.${ext}`] = userRepositoryTemplate(config);
231
+ files[`src/resources/user/user.controller.${ext}`] = userControllerTemplate(config);
232
+ files[`src/resources/auth/auth.resource.${ext}`] = authResourceTemplate(config);
233
+ files[`src/resources/auth/auth.handlers.${ext}`] = authHandlersTemplate(config);
234
+ files[`src/resources/auth/auth.schemas.${ext}`] = authSchemasTemplate();
235
+ files[`src/resources/example/example.model.${ext}`] = exampleModelTemplate(config);
236
+ files[`src/resources/example/example.repository.${ext}`] = exampleRepositoryTemplate(config);
237
+ files[`src/resources/example/example.resource.${ext}`] = exampleResourceTemplate(config);
238
+ files[`src/resources/example/example.controller.${ext}`] = exampleControllerTemplate(config);
239
+ files[`src/resources/example/example.schemas.${ext}`] = exampleSchemasTemplate(config);
240
+ files[`tests/example.test.${ext}`] = exampleTestTemplate(config);
241
+ files[`tests/auth.test.${ext}`] = authTestTemplate(config);
242
+ for (const [filePath, content] of Object.entries(files)) {
243
+ const fullPath = path.join(projectPath, filePath);
244
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
245
+ await fs.writeFile(fullPath, content);
246
+ console.log(` ✅ Created: ${filePath}`);
247
+ }
248
+ }
249
+ function packageJsonTemplate(config) {
250
+ const scripts = config.typescript ? {
251
+ dev: "tsx watch src/index.ts",
252
+ build: "tsc",
253
+ start: "node dist/index.js",
254
+ test: "vitest run",
255
+ "test:watch": "vitest"
256
+ } : {
257
+ dev: "node --watch src/index.js",
258
+ start: "node src/index.js",
259
+ test: "vitest run",
260
+ "test:watch": "vitest"
261
+ };
262
+ const imports = config.typescript ? {
263
+ "#config/*": "./dist/config/*",
264
+ "#shared/*": "./dist/shared/*",
265
+ "#resources/*": "./dist/resources/*",
266
+ "#plugins/*": "./dist/plugins/*"
267
+ } : {
268
+ "#config/*": "./src/config/*",
269
+ "#shared/*": "./src/shared/*",
270
+ "#resources/*": "./src/resources/*",
271
+ "#plugins/*": "./src/plugins/*"
272
+ };
273
+ return JSON.stringify(
274
+ {
275
+ name: config.name,
276
+ version: "1.0.0",
277
+ type: "module",
278
+ main: config.typescript ? "dist/index.js" : "src/index.js",
279
+ imports,
280
+ scripts,
281
+ engines: {
282
+ node: ">=20"
283
+ }
284
+ },
285
+ null,
286
+ 2
287
+ );
288
+ }
289
+ function tsconfigTemplate() {
290
+ return JSON.stringify(
291
+ {
292
+ compilerOptions: {
293
+ target: "ES2022",
294
+ module: "NodeNext",
295
+ moduleResolution: "NodeNext",
296
+ lib: ["ES2022"],
297
+ outDir: "./dist",
298
+ rootDir: "./src",
299
+ strict: true,
300
+ esModuleInterop: true,
301
+ skipLibCheck: true,
302
+ forceConsistentCasingInFileNames: true,
303
+ declaration: true,
304
+ declarationMap: true,
305
+ sourceMap: true,
306
+ resolveJsonModule: true,
307
+ paths: {
308
+ "#shared/*": ["./src/shared/*"],
309
+ "#resources/*": ["./src/resources/*"],
310
+ "#config/*": ["./src/config/*"],
311
+ "#plugins/*": ["./src/plugins/*"]
312
+ }
313
+ },
314
+ include: ["src/**/*"],
315
+ exclude: ["node_modules", "dist"]
316
+ },
317
+ null,
318
+ 2
319
+ );
320
+ }
321
+ function vitestConfigTemplate(config) {
322
+ const srcDir = config.typescript ? "./src" : "./src";
323
+ return `import { defineConfig } from 'vitest/config';
324
+ import { resolve } from 'path';
325
+
326
+ export default defineConfig({
327
+ test: {
328
+ globals: true,
329
+ environment: 'node',
330
+ },
331
+ resolve: {
332
+ alias: {
333
+ '#config': resolve(__dirname, '${srcDir}/config'),
334
+ '#shared': resolve(__dirname, '${srcDir}/shared'),
335
+ '#resources': resolve(__dirname, '${srcDir}/resources'),
336
+ '#plugins': resolve(__dirname, '${srcDir}/plugins'),
337
+ },
338
+ },
339
+ });
340
+ `;
341
+ }
342
+ function gitignoreTemplate() {
343
+ return `# Dependencies
344
+ node_modules/
345
+
346
+ # Build
347
+ dist/
348
+ *.js.map
349
+
350
+ # Environment
351
+ .env
352
+ .env.local
353
+ .env.*.local
354
+
355
+ # IDE
356
+ .vscode/
357
+ .idea/
358
+ *.swp
359
+ *.swo
360
+
361
+ # OS
362
+ .DS_Store
363
+ Thumbs.db
364
+
365
+ # Logs
366
+ *.log
367
+ npm-debug.log*
368
+
369
+ # Test coverage
370
+ coverage/
371
+ `;
372
+ }
373
+ function envExampleTemplate(config) {
374
+ let content = `# Server
375
+ PORT=8040
376
+ HOST=0.0.0.0
377
+ NODE_ENV=development
378
+
379
+ # JWT
380
+ JWT_SECRET=your-32-character-minimum-secret-here
381
+ `;
382
+ if (config.adapter === "mongokit") {
383
+ content += `
384
+ # MongoDB
385
+ MONGODB_URI=mongodb://localhost:27017/${config.name}
386
+ `;
387
+ }
388
+ if (config.tenant === "multi") {
389
+ content += `
390
+ # Multi-tenant
391
+ DEFAULT_ORG_ID=
392
+ `;
393
+ }
394
+ return content;
395
+ }
396
+ function readmeTemplate(config) {
397
+ const ext = config.typescript ? "ts" : "js";
398
+ return `# ${config.name}
399
+
400
+ Built with [Arc](https://github.com/classytic/arc) - Resource-Oriented Backend Framework
401
+
402
+ ## Quick Start
403
+
404
+ \`\`\`bash
405
+ # Install dependencies
406
+ npm install
407
+
408
+ # Start development server (uses .env.dev)
409
+ npm run dev
410
+
411
+ # Run tests
412
+ npm test
413
+ \`\`\`
414
+
415
+ ## Project Structure
416
+
417
+ \`\`\`
418
+ src/
419
+ ├── config/ # Configuration (loaded first)
420
+ │ ├── env.${ext} # Env loader (import first!)
421
+ │ └── index.${ext} # App config
422
+ ├── shared/ # Shared utilities
423
+ │ ├── adapter.${ext} # ${config.adapter === "mongokit" ? "MongoKit adapter factory" : "Custom adapter"}
424
+ │ ├── permissions.${ext} # Permission helpers
425
+ │ └── presets/ # ${config.tenant === "multi" ? "Multi-tenant presets" : "Standard presets"}
426
+ ├── plugins/ # App-specific plugins
427
+ │ └── index.${ext} # Plugin registry
428
+ ├── resources/ # API Resources
429
+ │ ├── index.${ext} # Resource registry
430
+ │ └── example/ # Example resource
431
+ │ ├── index.${ext} # Resource definition
432
+ │ ├── model.${ext} # Mongoose schema
433
+ │ └── repository.${ext} # MongoKit repository
434
+ ├── app.${ext} # App factory (reusable)
435
+ └── index.${ext} # Server entry point
436
+ tests/
437
+ └── example.test.${ext} # Example tests
438
+ \`\`\`
439
+
440
+ ## Architecture
441
+
442
+ ### Entry Points
443
+
444
+ - **\`src/index.${ext}\`** - HTTP server entry point
445
+ - **\`src/app.${ext}\`** - App factory (import for workers/tests)
446
+
447
+ \`\`\`${config.typescript ? "typescript" : "javascript"}
448
+ // For workers or custom entry points:
449
+ import { createAppInstance } from './app.js';
450
+
451
+ const app = await createAppInstance();
452
+ // Use app for your worker logic
453
+ \`\`\`
454
+
455
+ ### Adding Resources
456
+
457
+ 1. Create a new folder in \`src/resources/\`:
458
+
459
+ \`\`\`
460
+ src/resources/product/
461
+ ├── index.${ext} # Resource definition
462
+ ├── model.${ext} # Mongoose schema
463
+ └── repository.${ext} # MongoKit repository
464
+ \`\`\`
465
+
466
+ 2. Register in \`src/resources/index.${ext}\`:
467
+
468
+ \`\`\`${config.typescript ? "typescript" : "javascript"}
469
+ import productResource from './product/index.js';
470
+
471
+ export const resources = [
472
+ exampleResource,
473
+ productResource, // Add here
474
+ ];
475
+ \`\`\`
476
+
477
+ ### Adding Plugins
478
+
479
+ Add custom plugins in \`src/plugins/index.${ext}\`:
480
+
481
+ \`\`\`${config.typescript ? "typescript" : "javascript"}
482
+ export async function registerPlugins(app, deps) {
483
+ const { config } = deps; // Explicit dependency injection
484
+
485
+ await app.register(myCustomPlugin, { ...options });
486
+ }
487
+ \`\`\`
488
+
489
+ ## CLI Commands
490
+
491
+ \`\`\`bash
492
+ # Generate a new resource
493
+ arc generate resource product
494
+
495
+ # Introspect existing schema
496
+ arc introspect
497
+
498
+ # Generate API docs
499
+ arc docs
500
+ \`\`\`
501
+
502
+ ## Environment Files
503
+
504
+ - \`.env.dev\` - Development (default)
505
+ - \`.env.test\` - Testing
506
+ - \`.env.prod\` - Production
507
+ - \`.env\` - Fallback
508
+
509
+ ## API Documentation
510
+
511
+ API documentation is available via Scalar UI:
512
+
513
+ - **Interactive UI**: [http://localhost:8040/docs](http://localhost:8040/docs)
514
+ - **OpenAPI Spec**: [http://localhost:8040/_docs/openapi.json](http://localhost:8040/_docs/openapi.json)
515
+
516
+ ## API Endpoints
517
+
518
+ | Method | Endpoint | Description |
519
+ |--------|----------|-------------|
520
+ | GET | /docs | API documentation (Scalar UI) |
521
+ | GET | /_docs/openapi.json | OpenAPI 3.0 spec |
522
+ | GET | /examples | List all |
523
+ | GET | /examples/:id | Get by ID |
524
+ | POST | /examples | Create |
525
+ | PATCH | /examples/:id | Update |
526
+ | DELETE | /examples/:id | Delete |
527
+ `;
528
+ }
529
+ function indexTemplate(config) {
530
+ const ts = config.typescript;
531
+ return `/**
532
+ * ${config.name} - Server Entry Point
533
+ * Generated by Arc CLI
534
+ *
535
+ * This file starts the HTTP server.
536
+ * For workers or other entry points, import createAppInstance from './app.js'
537
+ */
538
+
539
+ // Load environment FIRST (before any other imports)
540
+ import '#config/env.js';
541
+
542
+ import config from '#config/index.js';
543
+ ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';" : ""}
544
+ import { createAppInstance } from './app.js';
545
+
546
+ async function main()${ts ? ": Promise<void>" : ""} {
547
+ console.log(\`🔧 Environment: \${config.env}\`);
548
+ ${config.adapter === "mongokit" ? `
549
+ // Connect to MongoDB
550
+ await mongoose.connect(config.database.uri);
551
+ console.log('📦 Connected to MongoDB');
552
+ ` : ""}
553
+ // Create and configure app
554
+ const app = await createAppInstance();
555
+
556
+ // Start server
557
+ await app.listen({ port: config.server.port, host: config.server.host });
558
+ console.log(\`🚀 Server running at http://\${config.server.host}:\${config.server.port}\`);
559
+ }
560
+
561
+ main().catch((err) => {
562
+ console.error('❌ Failed to start server:', err);
563
+ process.exit(1);
564
+ });
565
+ `;
566
+ }
567
+ function appTemplate(config) {
568
+ const ts = config.typescript;
569
+ const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
570
+ return `/**
571
+ * ${config.name} - App Factory
572
+ * Generated by Arc CLI
573
+ *
574
+ * Creates and configures the Fastify app instance.
575
+ * Can be imported by:
576
+ * - index.ts (HTTP server)
577
+ * - worker.ts (background workers)
578
+ * - tests (integration tests)
579
+ */
580
+
581
+ ${typeImport}import config from '#config/index.js';
582
+ import { createApp } from '@classytic/arc/factory';
583
+
584
+ // App-specific plugins
585
+ import { registerPlugins } from '#plugins/index.js';
586
+
587
+ // Resource registry
588
+ import { registerResources } from '#resources/index.js';
589
+
590
+ /**
591
+ * Create a fully configured app instance
592
+ *
593
+ * @returns Configured Fastify instance ready to use
594
+ */
595
+ export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : ""} {
596
+ // Create Arc app with base configuration
597
+ const app = await createApp({
598
+ preset: config.env === 'production' ? 'production' : 'development',
599
+ auth: {
600
+ jwt: { secret: config.jwt.secret },
601
+ },
602
+ cors: {
603
+ origin: config.cors.origins,
604
+ },
605
+ });
606
+
607
+ // Register app-specific plugins (explicit dependency injection)
608
+ await registerPlugins(app, { config });
609
+
610
+ // Register all resources
611
+ await registerResources(app);
612
+
613
+ return app;
614
+ }
615
+
616
+ export default createAppInstance;
617
+ `;
618
+ }
619
+ function envLoaderTemplate(config) {
620
+ const ts = config.typescript;
621
+ return `/**
622
+ * Environment Loader
623
+ *
624
+ * MUST be imported FIRST before any other imports.
625
+ * Loads .env files based on NODE_ENV.
626
+ *
627
+ * Usage:
628
+ * import './config/env.js'; // First line of entry point
629
+ */
630
+
631
+ import dotenv from 'dotenv';
632
+ import { existsSync } from 'node:fs';
633
+ import { resolve } from 'node:path';
634
+
635
+ /**
636
+ * Normalize environment string to short form
637
+ */
638
+ function normalizeEnv(env${ts ? ": string | undefined" : ""})${ts ? ": string" : ""} {
639
+ const normalized = (env || '').toLowerCase();
640
+ if (normalized === 'production' || normalized === 'prod') return 'prod';
641
+ if (normalized === 'test' || normalized === 'qa') return 'test';
642
+ return 'dev';
643
+ }
644
+
645
+ // Determine environment
646
+ const env = normalizeEnv(process.env.NODE_ENV);
647
+
648
+ // Load environment-specific .env file
649
+ const envFile = resolve(process.cwd(), \`.env.\${env}\`);
650
+ const defaultEnvFile = resolve(process.cwd(), '.env');
651
+
652
+ if (existsSync(envFile)) {
653
+ dotenv.config({ path: envFile });
654
+ console.log(\`📄 Loaded: .env.\${env}\`);
655
+ } else if (existsSync(defaultEnvFile)) {
656
+ dotenv.config({ path: defaultEnvFile });
657
+ console.log('📄 Loaded: .env');
658
+ } else {
659
+ console.warn('⚠️ No .env file found');
660
+ }
661
+
662
+ // Export for reference
663
+ export const ENV = env;
664
+ `;
665
+ }
666
+ function envDevTemplate(config) {
667
+ let content = `# Development Environment
668
+ NODE_ENV=development
669
+
670
+ # Server
671
+ PORT=8040
672
+ HOST=0.0.0.0
673
+
674
+ # JWT
675
+ JWT_SECRET=dev-secret-change-in-production-min-32-chars
676
+ JWT_EXPIRES_IN=7d
677
+
678
+ # CORS
679
+ CORS_ORIGINS=http://localhost:3000,http://localhost:5173
680
+ `;
681
+ if (config.adapter === "mongokit") {
682
+ content += `
683
+ # MongoDB
684
+ MONGODB_URI=mongodb://localhost:27017/${config.name}
685
+ `;
686
+ }
687
+ if (config.tenant === "multi") {
688
+ content += `
689
+ # Multi-tenant
690
+ ORG_HEADER=x-organization-id
691
+ `;
692
+ }
693
+ return content;
694
+ }
695
+ function pluginsIndexTemplate(config) {
696
+ const ts = config.typescript;
697
+ const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
698
+ const configType = ts ? ": { config: AppConfig }" : "";
699
+ const appType = ts ? ": FastifyInstance" : "";
700
+ let content = `/**
701
+ * App Plugins Registry
702
+ *
703
+ * Register your app-specific plugins here.
704
+ * Dependencies are passed explicitly (no shims, no magic).
705
+ */
706
+
707
+ ${typeImport}${ts ? "import type { AppConfig } from '../config/index.js';\n" : ""}import { openApiPlugin, scalarPlugin } from '@classytic/arc/docs';
708
+ `;
709
+ if (config.tenant === "multi") {
710
+ content += `import { orgScopePlugin } from '@classytic/arc/org';
711
+ `;
712
+ }
713
+ content += `
714
+ /**
715
+ * Register all app-specific plugins
716
+ *
717
+ * @param app - Fastify instance
718
+ * @param deps - Explicit dependencies (config, services, etc.)
719
+ */
720
+ export async function registerPlugins(
721
+ app${appType},
722
+ deps${configType}
723
+ )${ts ? ": Promise<void>" : ""} {
724
+ const { config } = deps;
725
+
726
+ // API Documentation (Scalar UI)
727
+ // OpenAPI spec: /_docs/openapi.json
728
+ // Scalar UI: /docs
729
+ await app.register(openApiPlugin, {
730
+ title: '${config.name} API',
731
+ version: '1.0.0',
732
+ description: 'API documentation for ${config.name}',
733
+ });
734
+ await app.register(scalarPlugin, {
735
+ routePrefix: '/docs',
736
+ theme: 'default',
737
+ });
738
+ `;
739
+ if (config.tenant === "multi") {
740
+ content += `
741
+ // Multi-tenant org scope
742
+ await app.register(orgScopePlugin, {
743
+ header: config.org?.header || 'x-organization-id',
744
+ bypassRoles: ['superadmin', 'admin'],
745
+ });
746
+ `;
747
+ }
748
+ content += `
749
+ // Add your custom plugins here:
750
+ // await app.register(myCustomPlugin, { ...options });
751
+ }
752
+ `;
753
+ return content;
754
+ }
755
+ function resourcesIndexTemplate(config) {
756
+ const ts = config.typescript;
757
+ const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
758
+ const appType = ts ? ": FastifyInstance" : "";
759
+ return `/**
760
+ * Resources Registry
761
+ *
762
+ * Central registry for all API resources.
763
+ * Flat structure - no barrels, direct imports.
764
+ */
765
+
766
+ ${typeImport}
767
+ // Auth resources (register, login, /users/me)
768
+ import { authResource, userProfileResource } from './auth/auth.resource.js';
769
+
770
+ // App resources
771
+ import exampleResource from './example/example.resource.js';
772
+
773
+ // Add more resources here:
774
+ // import productResource from './product/product.resource.js';
775
+
776
+ /**
777
+ * All registered resources
778
+ */
779
+ export const resources = [
780
+ authResource,
781
+ userProfileResource,
782
+ exampleResource,
783
+ ]${ts ? " as const" : ""};
784
+
785
+ /**
786
+ * Register all resources with the app
787
+ */
788
+ export async function registerResources(app${appType})${ts ? ": Promise<void>" : ""} {
789
+ for (const resource of resources) {
790
+ await app.register(resource.toPlugin());
791
+ }
792
+ }
793
+ `;
794
+ }
795
+ function sharedIndexTemplate(config) {
796
+ const ts = config.typescript;
797
+ return `/**
798
+ * Shared Utilities
799
+ *
800
+ * Central exports for resource definitions.
801
+ * Import from here for clean, consistent code.
802
+ */
803
+
804
+ // Adapter factory
805
+ export { createAdapter } from './adapter.js';
806
+
807
+ // Core Arc exports
808
+ export { createMongooseAdapter, defineResource } from '@classytic/arc';
809
+
810
+ // Permission helpers
811
+ export {
812
+ allowPublic,
813
+ requireAuth,
814
+ requireRoles,
815
+ requireOwnership,
816
+ allOf,
817
+ anyOf,
818
+ denyAll,
819
+ when,${ts ? "\n type PermissionCheck," : ""}
820
+ } from '@classytic/arc/permissions';
821
+
822
+ // Application permissions
823
+ export * from './permissions.js';
824
+
825
+ // Presets
826
+ export * from './presets/index.js';
827
+ `;
828
+ }
829
+ function createAdapterTemplate(config) {
830
+ const ts = config.typescript;
831
+ return `/**
832
+ * MongoKit Adapter Factory
833
+ *
834
+ * Creates Arc adapters using MongoKit repositories.
835
+ * The repository handles query parsing via MongoKit's built-in QueryParser.
836
+ */
837
+
838
+ import { createMongooseAdapter } from '@classytic/arc';
839
+ ${ts ? "import type { Model } from 'mongoose';\nimport type { Repository } from '@classytic/mongokit';" : ""}
840
+
841
+ /**
842
+ * Create a MongoKit-powered adapter for a resource
843
+ *
844
+ * Note: Query parsing is handled by MongoKit's Repository class.
845
+ * Just pass the model and repository - Arc handles the rest.
846
+ */
847
+ export function createAdapter${ts ? "<TDoc, TRepo extends Repository<TDoc>>" : ""}(
848
+ model${ts ? ": Model<TDoc>" : ""},
849
+ repository${ts ? ": TRepo" : ""}
850
+ )${ts ? ": ReturnType<typeof createMongooseAdapter>" : ""} {
851
+ return createMongooseAdapter({
852
+ model,
853
+ repository,
854
+ });
855
+ }
856
+ `;
857
+ }
858
+ function customAdapterTemplate(config) {
859
+ const ts = config.typescript;
860
+ return `/**
861
+ * Custom Adapter Factory
862
+ *
863
+ * Implement your own database adapter here.
864
+ */
865
+
866
+ import { createMongooseAdapter } from '@classytic/arc';
867
+ ${ts ? "import type { Model } from 'mongoose';" : ""}
868
+
869
+ /**
870
+ * Create a custom adapter for a resource
871
+ *
872
+ * Implement this based on your database choice:
873
+ * - Prisma: Use @classytic/prismakit (coming soon)
874
+ * - Drizzle: Create custom adapter
875
+ * - Raw SQL: Create custom adapter
876
+ */
877
+ export function createAdapter${ts ? "<TDoc>" : ""}(
878
+ model${ts ? ": Model<TDoc>" : ""},
879
+ repository${ts ? ": any" : ""}
880
+ )${ts ? ": ReturnType<typeof createMongooseAdapter>" : ""} {
881
+ // TODO: Implement your custom adapter
882
+ return createMongooseAdapter({
883
+ model,
884
+ repository,
885
+ });
886
+ }
887
+ `;
888
+ }
889
+ function presetsMultiTenantTemplate(config) {
890
+ const ts = config.typescript;
891
+ return `/**
892
+ * Arc Presets - Multi-Tenant Configuration
893
+ *
894
+ * Pre-configured presets for multi-tenant applications.
895
+ * Includes both strict and flexible tenant isolation options.
896
+ */
897
+
898
+ import {
899
+ multiTenantPreset,
900
+ ownedByUserPreset,
901
+ softDeletePreset,
902
+ slugLookupPreset,
903
+ } from '@classytic/arc/presets';
904
+
905
+ // Flexible preset for mixed public/private routes
906
+ export { flexibleMultiTenantPreset } from './flexible-multi-tenant.js';
907
+
908
+ /**
909
+ * Organization-scoped preset (STRICT)
910
+ * Always requires auth, always filters by organizationId.
911
+ * Use for admin-only resources.
912
+ */
913
+ export const orgScoped = multiTenantPreset({
914
+ tenantField: 'organizationId',
915
+ bypassRoles: ['superadmin', 'admin'],
916
+ });
917
+
918
+ /**
919
+ * Owned by creator preset
920
+ * Filters queries by createdBy field.
921
+ */
922
+ export const ownedByCreator = ownedByUserPreset({
923
+ ownerField: 'createdBy',
924
+ });
925
+
926
+ /**
927
+ * Owned by user preset
928
+ * For resources where userId references the owner.
929
+ */
930
+ export const ownedByUser = ownedByUserPreset({
931
+ ownerField: 'userId',
932
+ });
933
+
934
+ /**
935
+ * Soft delete preset
936
+ * Adds deletedAt filtering and restore endpoint.
937
+ */
938
+ export const softDelete = softDeletePreset();
939
+
940
+ /**
941
+ * Slug lookup preset
942
+ * Enables GET by slug in addition to ID.
943
+ */
944
+ export const slugLookup = slugLookupPreset();
945
+
946
+ // Export all presets
947
+ export const presets = {
948
+ orgScoped,
949
+ ownedByCreator,
950
+ ownedByUser,
951
+ softDelete,
952
+ slugLookup,
953
+ }${ts ? " as const" : ""};
954
+
955
+ export default presets;
956
+ `;
957
+ }
958
+ function presetsSingleTenantTemplate(config) {
959
+ const ts = config.typescript;
960
+ return `/**
961
+ * Arc Presets - Single-Tenant Configuration
962
+ *
963
+ * Pre-configured presets for single-tenant applications.
964
+ */
965
+
966
+ import {
967
+ ownedByUserPreset,
968
+ softDeletePreset,
969
+ slugLookupPreset,
970
+ } from '@classytic/arc/presets';
971
+
972
+ /**
973
+ * Owned by creator preset
974
+ * Filters queries by createdBy field.
975
+ */
976
+ export const ownedByCreator = ownedByUserPreset({
977
+ ownerField: 'createdBy',
978
+ });
979
+
980
+ /**
981
+ * Owned by user preset
982
+ * For resources where userId references the owner.
983
+ */
984
+ export const ownedByUser = ownedByUserPreset({
985
+ ownerField: 'userId',
986
+ });
987
+
988
+ /**
989
+ * Soft delete preset
990
+ * Adds deletedAt filtering and restore endpoint.
991
+ */
992
+ export const softDelete = softDeletePreset();
993
+
994
+ /**
995
+ * Slug lookup preset
996
+ * Enables GET by slug in addition to ID.
997
+ */
998
+ export const slugLookup = slugLookupPreset();
999
+
1000
+ // Export all presets
1001
+ export const presets = {
1002
+ ownedByCreator,
1003
+ ownedByUser,
1004
+ softDelete,
1005
+ slugLookup,
1006
+ }${ts ? " as const" : ""};
1007
+
1008
+ export default presets;
1009
+ `;
1010
+ }
1011
+ function flexibleMultiTenantPresetTemplate(config) {
1012
+ const ts = config.typescript;
1013
+ const typeAnnotations = ts ? `
1014
+ interface FlexibleMultiTenantOptions {
1015
+ tenantField?: string;
1016
+ bypassRoles?: string[];
1017
+ extractOrganizationId?: (request: any) => string | null;
1018
+ }
1019
+
1020
+ interface PresetMiddlewares {
1021
+ list: ((request: any, reply: any) => Promise<void>)[];
1022
+ get: ((request: any, reply: any) => Promise<void>)[];
1023
+ create: ((request: any, reply: any) => Promise<void>)[];
1024
+ update: ((request: any, reply: any) => Promise<void>)[];
1025
+ delete: ((request: any, reply: any) => Promise<void>)[];
1026
+ }
1027
+
1028
+ interface Preset {
1029
+ [key: string]: unknown;
1030
+ name: string;
1031
+ middlewares: PresetMiddlewares;
1032
+ }
1033
+ ` : "";
1034
+ return `/**
1035
+ * Flexible Multi-Tenant Preset
1036
+ *
1037
+ * Smarter tenant filtering that works with public + authenticated routes.
1038
+ *
1039
+ * Philosophy:
1040
+ * - No org header → No filtering (public data, all orgs)
1041
+ * - Org header present → Require auth, filter by org
1042
+ *
1043
+ * This differs from Arc's strict multiTenant which always requires auth.
1044
+ */
1045
+ ${typeAnnotations}
1046
+ /**
1047
+ * Default organization ID extractor
1048
+ * Tries multiple sources in order of priority
1049
+ */
1050
+ function defaultExtractOrganizationId(request${ts ? ": any" : ""})${ts ? ": string | null" : ""} {
1051
+ // Priority 1: Explicit context (set by org-scope plugin)
1052
+ if (request.context?.organizationId) {
1053
+ return String(request.context.organizationId);
1054
+ }
1055
+
1056
+ // Priority 2: User's organizationId field
1057
+ if (request.user?.organizationId) {
1058
+ return String(request.user.organizationId);
1059
+ }
1060
+
1061
+ // Priority 3: User's organization object (nested)
1062
+ if (request.user?.organization) {
1063
+ const org = request.user.organization;
1064
+ return String(org._id || org.id || org);
1065
+ }
1066
+
1067
+ return null;
1068
+ }
1069
+
1070
+ /**
1071
+ * Create flexible tenant filter middleware
1072
+ * Only filters when org context is present
1073
+ */
1074
+ function createFlexibleTenantFilter(
1075
+ tenantField${ts ? ": string" : ""},
1076
+ bypassRoles${ts ? ": string[]" : ""},
1077
+ extractOrganizationId${ts ? ": (request: any) => string | null" : ""}
1078
+ ) {
1079
+ return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
1080
+ const user = request.user;
1081
+ const orgId = extractOrganizationId(request);
1082
+
1083
+ // No org context - allow through (public data, no filtering)
1084
+ if (!orgId) {
1085
+ request.log?.debug?.({ msg: 'No org context - showing all data' });
1086
+ return;
1087
+ }
1088
+
1089
+ // Org context present - auth should already be handled by org-scope plugin
1090
+ // But double-check for safety
1091
+ if (!user) {
1092
+ request.log?.warn?.({ msg: 'Org context present but no user - should not happen' });
1093
+ return reply.code(401).send({
1094
+ success: false,
1095
+ error: 'Unauthorized',
1096
+ message: 'Authentication required for organization-scoped data',
1097
+ });
1098
+ }
1099
+
1100
+ // Bypass roles skip filter (superadmin sees all)
1101
+ const userRoles = Array.isArray(user.roles) ? user.roles : [];
1102
+ if (bypassRoles.some((r${ts ? ": string" : ""}) => userRoles.includes(r))) {
1103
+ request.log?.debug?.({ msg: 'Bypass role - no tenant filter' });
1104
+ return;
1105
+ }
1106
+
1107
+ // Apply tenant filter to query
1108
+ request.query = request.query ?? {};
1109
+ request.query._policyFilters = {
1110
+ ...(request.query._policyFilters ?? {}),
1111
+ [tenantField]: orgId,
1112
+ };
1113
+
1114
+ request.log?.debug?.({ msg: 'Tenant filter applied', orgId, tenantField });
1115
+ };
1116
+ }
1117
+
1118
+ /**
1119
+ * Create tenant injection middleware
1120
+ * Injects tenant ID into request body on create
1121
+ */
1122
+ function createTenantInjection(
1123
+ tenantField${ts ? ": string" : ""},
1124
+ extractOrganizationId${ts ? ": (request: any) => string | null" : ""}
1125
+ ) {
1126
+ return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
1127
+ const orgId = extractOrganizationId(request);
1128
+
1129
+ // Fail-closed: Require orgId for create operations
1130
+ if (!orgId) {
1131
+ return reply.code(403).send({
1132
+ success: false,
1133
+ error: 'Forbidden',
1134
+ message: 'Organization context required to create resources',
1135
+ });
1136
+ }
1137
+
1138
+ if (request.body) {
1139
+ request.body[tenantField] = orgId;
1140
+ }
1141
+ };
1142
+ }
1143
+
1144
+ /**
1145
+ * Flexible Multi-Tenant Preset
1146
+ *
1147
+ * @param options.tenantField - Field name in database (default: 'organizationId')
1148
+ * @param options.bypassRoles - Roles that bypass tenant isolation (default: ['superadmin'])
1149
+ * @param options.extractOrganizationId - Custom org ID extractor function
1150
+ */
1151
+ export function flexibleMultiTenantPreset(options${ts ? ": FlexibleMultiTenantOptions = {}" : " = {}"})${ts ? ": Preset" : ""} {
1152
+ const {
1153
+ tenantField = 'organizationId',
1154
+ bypassRoles = ['superadmin'],
1155
+ extractOrganizationId = defaultExtractOrganizationId,
1156
+ } = options;
1157
+
1158
+ const tenantFilter = createFlexibleTenantFilter(tenantField, bypassRoles, extractOrganizationId);
1159
+ const tenantInjection = createTenantInjection(tenantField, extractOrganizationId);
1160
+
1161
+ return {
1162
+ name: 'flexibleMultiTenant',
1163
+ middlewares: {
1164
+ list: [tenantFilter],
1165
+ get: [tenantFilter],
1166
+ create: [tenantInjection],
1167
+ update: [tenantFilter],
1168
+ delete: [tenantFilter],
1169
+ },
1170
+ };
1171
+ }
1172
+
1173
+ export default flexibleMultiTenantPreset;
1174
+ `;
1175
+ }
1176
+ function permissionsTemplate(config) {
1177
+ const ts = config.typescript;
1178
+ const typeImport = ts ? ",\n type PermissionCheck," : "";
1179
+ const returnType = ts ? ": PermissionCheck" : "";
1180
+ let content = `/**
1181
+ * Permission Helpers
1182
+ *
1183
+ * Clean, type-safe permission definitions for resources.
1184
+ */
1185
+
1186
+ import {
1187
+ requireAuth,
1188
+ requireRoles,
1189
+ requireOwnership,
1190
+ allowPublic,
1191
+ anyOf,
1192
+ allOf,
1193
+ denyAll,
1194
+ when${typeImport}
1195
+ } from '@classytic/arc/permissions';
1196
+
1197
+ // Re-export core helpers
1198
+ export {
1199
+ allowPublic,
1200
+ requireAuth,
1201
+ requireRoles,
1202
+ requireOwnership,
1203
+ allOf,
1204
+ anyOf,
1205
+ denyAll,
1206
+ when,
1207
+ };
1208
+
1209
+ // ============================================================================
1210
+ // Permission Helpers
1211
+ // ============================================================================
1212
+
1213
+ /**
1214
+ * Require any authenticated user
1215
+ */
1216
+ export const requireAuthenticated = ()${returnType} =>
1217
+ requireRoles(['user', 'admin', 'superadmin']);
1218
+
1219
+ /**
1220
+ * Require admin or superadmin
1221
+ */
1222
+ export const requireAdmin = ()${returnType} =>
1223
+ requireRoles(['admin', 'superadmin']);
1224
+
1225
+ /**
1226
+ * Require superadmin only
1227
+ */
1228
+ export const requireSuperadmin = ()${returnType} =>
1229
+ requireRoles(['superadmin']);
1230
+ `;
1231
+ if (config.tenant === "multi") {
1232
+ content += `
1233
+ /**
1234
+ * Require organization owner
1235
+ */
1236
+ export const requireOrgOwner = ()${returnType} =>
1237
+ requireRoles(['owner'], { bypassRoles: ['admin', 'superadmin'] });
1238
+
1239
+ /**
1240
+ * Require organization manager or higher
1241
+ */
1242
+ export const requireOrgManager = ()${returnType} =>
1243
+ requireRoles(['owner', 'manager'], { bypassRoles: ['admin', 'superadmin'] });
1244
+
1245
+ /**
1246
+ * Require organization staff (any org member)
1247
+ */
1248
+ export const requireOrgStaff = ()${returnType} =>
1249
+ requireRoles(['owner', 'manager', 'staff'], { bypassRoles: ['admin', 'superadmin'] });
1250
+ `;
1251
+ }
1252
+ content += `
1253
+ // ============================================================================
1254
+ // Standard Permission Sets
1255
+ // ============================================================================
1256
+
1257
+ /**
1258
+ * Public read, authenticated write (default for most resources)
1259
+ */
1260
+ export const publicReadPermissions = {
1261
+ list: allowPublic(),
1262
+ get: allowPublic(),
1263
+ create: requireAuthenticated(),
1264
+ update: requireAuthenticated(),
1265
+ delete: requireAuthenticated(),
1266
+ };
1267
+
1268
+ /**
1269
+ * All operations require authentication
1270
+ */
1271
+ export const authenticatedPermissions = {
1272
+ list: requireAuth(),
1273
+ get: requireAuth(),
1274
+ create: requireAuth(),
1275
+ update: requireAuth(),
1276
+ delete: requireAuth(),
1277
+ };
1278
+
1279
+ /**
1280
+ * Admin only permissions
1281
+ */
1282
+ export const adminPermissions = {
1283
+ list: requireAdmin(),
1284
+ get: requireAdmin(),
1285
+ create: requireSuperadmin(),
1286
+ update: requireSuperadmin(),
1287
+ delete: requireSuperadmin(),
1288
+ };
1289
+ `;
1290
+ if (config.tenant === "multi") {
1291
+ content += `
1292
+ /**
1293
+ * Organization staff permissions
1294
+ */
1295
+ export const orgStaffPermissions = {
1296
+ list: requireOrgStaff(),
1297
+ get: requireOrgStaff(),
1298
+ create: requireOrgManager(),
1299
+ update: requireOrgManager(),
1300
+ delete: requireOrgOwner(),
1301
+ };
1302
+ `;
1303
+ }
1304
+ return content;
1305
+ }
1306
+ function configTemplate(config) {
1307
+ const ts = config.typescript;
1308
+ let typeDefinition = "";
1309
+ if (ts) {
1310
+ typeDefinition = `
1311
+ export interface AppConfig {
1312
+ env: string;
1313
+ server: {
1314
+ port: number;
1315
+ host: string;
1316
+ };
1317
+ jwt: {
1318
+ secret: string;
1319
+ expiresIn: string;
1320
+ };
1321
+ cors: {
1322
+ origins: string[];
1323
+ };${config.adapter === "mongokit" ? `
1324
+ database: {
1325
+ uri: string;
1326
+ };` : ""}${config.tenant === "multi" ? `
1327
+ org?: {
1328
+ header: string;
1329
+ };` : ""}
1330
+ }
1331
+ `;
1332
+ }
1333
+ return `/**
1334
+ * Application Configuration
1335
+ *
1336
+ * All config is loaded from environment variables.
1337
+ * ENV file is loaded by config/env.ts (imported first in entry points).
1338
+ */
1339
+ ${typeDefinition}
1340
+ const config${ts ? ": AppConfig" : ""} = {
1341
+ env: process.env.NODE_ENV || 'development',
1342
+
1343
+ server: {
1344
+ port: parseInt(process.env.PORT || '8040', 10),
1345
+ host: process.env.HOST || '0.0.0.0',
1346
+ },
1347
+
1348
+ jwt: {
1349
+ secret: process.env.JWT_SECRET || 'dev-secret-change-in-production-min-32',
1350
+ expiresIn: process.env.JWT_EXPIRES_IN || '7d',
1351
+ },
1352
+
1353
+ cors: {
1354
+ origins: (process.env.CORS_ORIGINS || 'http://localhost:3000').split(','),
1355
+ },
1356
+ ${config.adapter === "mongokit" ? `
1357
+ database: {
1358
+ uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}',
1359
+ },
1360
+ ` : ""}${config.tenant === "multi" ? `
1361
+ org: {
1362
+ header: process.env.ORG_HEADER || 'x-organization-id',
1363
+ },
1364
+ ` : ""}};
1365
+
1366
+ export default config;
1367
+ `;
1368
+ }
1369
+ function exampleModelTemplate(config) {
1370
+ const ts = config.typescript;
1371
+ const typeExport = ts ? `
1372
+ export type ExampleDocument = mongoose.InferSchemaType<typeof exampleSchema>;
1373
+ export type ExampleModel = mongoose.Model<ExampleDocument>;
1374
+ ` : "";
1375
+ return `/**
1376
+ * Example Model
1377
+ * Generated by Arc CLI
1378
+ */
1379
+
1380
+ import mongoose from 'mongoose';
1381
+
1382
+ const exampleSchema = new mongoose.Schema(
1383
+ {
1384
+ name: { type: String, required: true, trim: true },
1385
+ description: { type: String, trim: true },
1386
+ isActive: { type: Boolean, default: true, index: true },
1387
+ ${config.tenant === "multi" ? " organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', required: true, index: true },\n" : ""} createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true },
1388
+ deletedAt: { type: Date, default: null, index: true },
1389
+ },
1390
+ {
1391
+ timestamps: true,
1392
+ toJSON: { virtuals: true },
1393
+ toObject: { virtuals: true },
1394
+ }
1395
+ );
1396
+
1397
+ // Indexes for common queries
1398
+ exampleSchema.index({ name: 1 });
1399
+ exampleSchema.index({ deletedAt: 1, isActive: 1 });
1400
+ ${config.tenant === "multi" ? "exampleSchema.index({ organizationId: 1, deletedAt: 1 });\n" : ""}${typeExport}
1401
+ const Example = mongoose.model${ts ? "<ExampleDocument>" : ""}('Example', exampleSchema);
1402
+
1403
+ export default Example;
1404
+ `;
1405
+ }
1406
+ function exampleRepositoryTemplate(config) {
1407
+ const ts = config.typescript;
1408
+ const typeImport = ts ? "import type { ExampleDocument } from './model.js';\n" : "";
1409
+ const generic = ts ? "<ExampleDocument>" : "";
1410
+ return `/**
1411
+ * Example Repository
1412
+ * Generated by Arc CLI
1413
+ *
1414
+ * MongoKit repository with plugins for:
1415
+ * - Soft delete (deletedAt filtering)
1416
+ * - Custom business logic methods
1417
+ */
1418
+
1419
+ import {
1420
+ Repository,
1421
+ softDeletePlugin,
1422
+ methodRegistryPlugin,
1423
+ } from '@classytic/mongokit';
1424
+ ${typeImport}import Example from './example.model.js';
1425
+
1426
+ class ExampleRepository extends Repository${generic} {
1427
+ constructor() {
1428
+ super(Example, [
1429
+ methodRegistryPlugin(), // Required for plugin method registration
1430
+ softDeletePlugin(), // Soft delete support
1431
+ ]);
1432
+ }
1433
+
1434
+ /**
1435
+ * Find all active (non-deleted) records
1436
+ */
1437
+ async findActive() {
1438
+ return this.Model.find({ isActive: true, deletedAt: null }).lean();
1439
+ }
1440
+ ${config.tenant === "multi" ? `
1441
+ /**
1442
+ * Find active records for an organization
1443
+ */
1444
+ async findActiveByOrg(organizationId${ts ? ": string" : ""}) {
1445
+ return this.Model.find({
1446
+ organizationId,
1447
+ isActive: true,
1448
+ deletedAt: null,
1449
+ }).lean();
1450
+ }
1451
+ ` : ""}
1452
+ // Note: softDeletePlugin provides restore() and getDeleted() methods automatically
1453
+ }
1454
+
1455
+ const exampleRepository = new ExampleRepository();
1456
+
1457
+ export default exampleRepository;
1458
+ export { ExampleRepository };
1459
+ `;
1460
+ }
1461
+ function exampleResourceTemplate(config) {
1462
+ config.typescript;
1463
+ config.tenant === "multi" ? "['softDelete', 'flexibleMultiTenant']" : "['softDelete']";
1464
+ return `/**
1465
+ * Example Resource
1466
+ * Generated by Arc CLI
1467
+ *
1468
+ * A complete resource with:
1469
+ * - Model (Mongoose schema)
1470
+ * - Repository (MongoKit with plugins)
1471
+ * - Permissions (role-based access)
1472
+ * - Presets (soft delete${config.tenant === "multi" ? ", multi-tenant" : ""})
1473
+ */
1474
+
1475
+ import { defineResource } from '@classytic/arc';
1476
+ import { createAdapter } from '#shared/adapter.js';
1477
+ import { publicReadPermissions } from '#shared/permissions.js';
1478
+ ${config.tenant === "multi" ? "import { flexibleMultiTenantPreset } from '#shared/presets/flexible-multi-tenant.js';\n" : ""}import Example from './example.model.js';
1479
+ import exampleRepository from './example.repository.js';
1480
+ import exampleController from './example.controller.js';
1481
+
1482
+ const exampleResource = defineResource({
1483
+ name: 'example',
1484
+ displayName: 'Examples',
1485
+ prefix: '/examples',
1486
+
1487
+ adapter: createAdapter(Example, exampleRepository),
1488
+ controller: exampleController,
1489
+
1490
+ presets: [
1491
+ 'softDelete',${config.tenant === "multi" ? `
1492
+ flexibleMultiTenantPreset({ tenantField: 'organizationId' }),` : ""}
1493
+ ],
1494
+
1495
+ permissions: publicReadPermissions,
1496
+
1497
+ // Add custom routes here:
1498
+ // additionalRoutes: [
1499
+ // {
1500
+ // method: 'GET',
1501
+ // path: '/custom',
1502
+ // summary: 'Custom endpoint',
1503
+ // handler: async (request, reply) => { ... },
1504
+ // },
1505
+ // ],
1506
+ });
1507
+
1508
+ export default exampleResource;
1509
+ `;
1510
+ }
1511
+ function exampleControllerTemplate(config) {
1512
+ const ts = config.typescript;
1513
+ return `/**
1514
+ * Example Controller
1515
+ * Generated by Arc CLI
1516
+ *
1517
+ * BaseController provides CRUD operations with:
1518
+ * - Automatic pagination
1519
+ * - Query parsing
1520
+ * - Validation
1521
+ */
1522
+
1523
+ import { BaseController } from '@classytic/arc';
1524
+ import exampleRepository from './example.repository.js';
1525
+ import { exampleSchemaOptions } from './example.schemas.js';
1526
+
1527
+ class ExampleController extends BaseController {
1528
+ constructor() {
1529
+ super(exampleRepository${ts ? " as any" : ""}, { schemaOptions: exampleSchemaOptions });
1530
+ }
1531
+
1532
+ // Add custom controller methods here:
1533
+ // async customAction(request, reply) {
1534
+ // // Custom logic
1535
+ // }
1536
+ }
1537
+
1538
+ const exampleController = new ExampleController();
1539
+ export default exampleController;
1540
+ `;
1541
+ }
1542
+ function exampleSchemasTemplate(config) {
1543
+ const ts = config.typescript;
1544
+ const multiTenantFields = config.tenant === "multi";
1545
+ return `/**
1546
+ * Example Schemas
1547
+ * Generated by Arc CLI
1548
+ *
1549
+ * Schema options for controller validation and query parsing
1550
+ */
1551
+
1552
+ import Example from './example.model.js';
1553
+ import { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';
1554
+
1555
+ /**
1556
+ * CRUD Schemas with Field Rules
1557
+ * Auto-generated from Mongoose model
1558
+ */
1559
+ const crudSchemas = buildCrudSchemasFromModel(Example, {
1560
+ strictAdditionalProperties: true,
1561
+ fieldRules: {
1562
+ // Mark fields as system-managed (excluded from create/update)
1563
+ // deletedAt: { systemManaged: true },
1564
+ },
1565
+ query: {
1566
+ filterableFields: {
1567
+ isActive: 'boolean',${multiTenantFields ? `
1568
+ organizationId: 'ObjectId',` : ""}
1569
+ createdAt: 'date',
1570
+ },
1571
+ },
1572
+ });
1573
+
1574
+ // Schema options for controller
1575
+ export const exampleSchemaOptions${ts ? ": any" : ""} = {
1576
+ query: {${multiTenantFields ? `
1577
+ allowedPopulate: ['organizationId'],` : ""}
1578
+ filterableFields: {
1579
+ isActive: 'boolean',${multiTenantFields ? `
1580
+ organizationId: 'ObjectId',` : ""}
1581
+ createdAt: 'date',
1582
+ },
1583
+ },
1584
+ };
1585
+
1586
+ export default crudSchemas;
1587
+ `;
1588
+ }
1589
+ function exampleTestTemplate(config) {
1590
+ const ts = config.typescript;
1591
+ return `/**
1592
+ * Example Resource Tests
1593
+ * Generated by Arc CLI
1594
+ *
1595
+ * Run tests: npm test
1596
+ * Watch mode: npm run test:watch
1597
+ */
1598
+
1599
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
1600
+ ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';\n" : ""}import { createAppInstance } from '../src/app.js';
1601
+ ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}
1602
+ describe('Example Resource', () => {
1603
+ let app${ts ? ": FastifyInstance" : ""};
1604
+
1605
+ beforeAll(async () => {
1606
+ ${config.adapter === "mongokit" ? ` // Connect to test database
1607
+ const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}-test';
1608
+ await mongoose.connect(testDbUri);
1609
+ ` : ""}
1610
+ // Create app instance
1611
+ app = await createAppInstance();
1612
+ await app.ready();
1613
+ });
1614
+
1615
+ afterAll(async () => {
1616
+ await app.close();
1617
+ ${config.adapter === "mongokit" ? " await mongoose.connection.close();" : ""}
1618
+ });
1619
+
1620
+ describe('GET /examples', () => {
1621
+ it('should return a list of examples', async () => {
1622
+ const response = await app.inject({
1623
+ method: 'GET',
1624
+ url: '/examples',
1625
+ });
1626
+
1627
+ expect(response.statusCode).toBe(200);
1628
+ const body = JSON.parse(response.body);
1629
+ expect(body).toHaveProperty('docs');
1630
+ expect(Array.isArray(body.docs)).toBe(true);
1631
+ });
1632
+ });
1633
+
1634
+ describe('POST /examples', () => {
1635
+ it('should require authentication', async () => {
1636
+ const response = await app.inject({
1637
+ method: 'POST',
1638
+ url: '/examples',
1639
+ payload: { name: 'Test Example' },
1640
+ });
1641
+
1642
+ // Should fail without auth token
1643
+ expect(response.statusCode).toBe(401);
1644
+ });
1645
+ });
1646
+
1647
+ // Add more tests as needed:
1648
+ // - GET /examples/:id
1649
+ // - PATCH /examples/:id
1650
+ // - DELETE /examples/:id
1651
+ // - Custom endpoints
1652
+ });
1653
+ `;
1654
+ }
1655
+ function userModelTemplate(config) {
1656
+ const ts = config.typescript;
1657
+ const orgRoles = config.tenant === "multi" ? `
1658
+ // Organization roles (for multi-tenant)
1659
+ const ORG_ROLES = ['owner', 'manager', 'hr', 'staff', 'contractor'] as const;
1660
+ type OrgRole = typeof ORG_ROLES[number];
1661
+ ` : "";
1662
+ const orgInterface = config.tenant === "multi" ? `
1663
+ type UserOrganization = {
1664
+ organizationId: Types.ObjectId;
1665
+ organizationName: string;
1666
+ roles: OrgRole[];
1667
+ joinedAt: Date;
1668
+ };
1669
+ ` : "";
1670
+ const orgSchema = config.tenant === "multi" ? `
1671
+ // Multi-org support
1672
+ organizations: [{
1673
+ organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', required: true },
1674
+ organizationName: { type: String, required: true },
1675
+ roles: { type: [String], enum: ORG_ROLES, default: [] },
1676
+ joinedAt: { type: Date, default: () => new Date() },
1677
+ }],
1678
+ ` : "";
1679
+ const orgMethods = config.tenant === "multi" ? `
1680
+ // Organization methods
1681
+ userSchema.methods.getOrgRoles = function(orgId${ts ? ": Types.ObjectId | string" : ""}) {
1682
+ const org = this.organizations.find(o => o.organizationId.toString() === orgId.toString());
1683
+ return org?.roles || [];
1684
+ };
1685
+
1686
+ userSchema.methods.hasOrgAccess = function(orgId${ts ? ": Types.ObjectId | string" : ""}) {
1687
+ return this.organizations.some(o => o.organizationId.toString() === orgId.toString());
1688
+ };
1689
+
1690
+ userSchema.methods.addOrganization = function(
1691
+ organizationId${ts ? ": Types.ObjectId" : ""},
1692
+ organizationName${ts ? ": string" : ""},
1693
+ roles${ts ? ": OrgRole[]" : ""} = []
1694
+ ) {
1695
+ const existing = this.organizations.find(o => o.organizationId.toString() === organizationId.toString());
1696
+ if (existing) {
1697
+ existing.organizationName = organizationName;
1698
+ existing.roles = [...new Set([...existing.roles, ...roles])];
1699
+ } else {
1700
+ this.organizations.push({ organizationId, organizationName, roles, joinedAt: new Date() });
1701
+ }
1702
+ return this;
1703
+ };
1704
+
1705
+ userSchema.methods.removeOrganization = function(organizationId${ts ? ": Types.ObjectId" : ""}) {
1706
+ this.organizations = this.organizations.filter(o => o.organizationId.toString() !== organizationId.toString());
1707
+ return this;
1708
+ };
1709
+
1710
+ // Index for org queries
1711
+ userSchema.index({ 'organizations.organizationId': 1 });
1712
+ ` : "";
1713
+ const userType = ts ? `
1714
+ type PlatformRole = 'user' | 'admin' | 'superadmin';
1715
+
1716
+ type User = {
1717
+ name: string;
1718
+ email: string;
1719
+ password: string;
1720
+ roles: PlatformRole[];${config.tenant === "multi" ? `
1721
+ organizations: UserOrganization[];` : ""}
1722
+ resetPasswordToken?: string;
1723
+ resetPasswordExpires?: Date;
1724
+ };
1725
+
1726
+ type UserMethods = {
1727
+ matchPassword: (enteredPassword: string) => Promise<boolean>;${config.tenant === "multi" ? `
1728
+ getOrgRoles: (orgId: Types.ObjectId | string) => OrgRole[];
1729
+ hasOrgAccess: (orgId: Types.ObjectId | string) => boolean;
1730
+ addOrganization: (orgId: Types.ObjectId, name: string, roles?: OrgRole[]) => UserDocument;
1731
+ removeOrganization: (orgId: Types.ObjectId) => UserDocument;` : ""}
1732
+ };
1733
+
1734
+ export type UserDocument = HydratedDocument<User, UserMethods>;
1735
+ export type UserModel = Model<User, {}, UserMethods>;
1736
+ ` : "";
1737
+ return `/**
1738
+ * User Model
1739
+ * Generated by Arc CLI
1740
+ */
1741
+
1742
+ import bcrypt from 'bcryptjs';
1743
+ import mongoose${ts ? ", { type HydratedDocument, type Model, type Types }" : ""} from 'mongoose';
1744
+ ${orgRoles}
1745
+ const { Schema } = mongoose;
1746
+ ${orgInterface}${userType}
1747
+ const userSchema = new Schema${ts ? "<User, UserModel, UserMethods>" : ""}(
1748
+ {
1749
+ name: { type: String, required: true, trim: true },
1750
+ email: {
1751
+ type: String,
1752
+ required: true,
1753
+ unique: true,
1754
+ lowercase: true,
1755
+ trim: true,
1756
+ },
1757
+ password: { type: String, required: true },
1758
+
1759
+ // Platform roles
1760
+ roles: {
1761
+ type: [String],
1762
+ enum: ['user', 'admin', 'superadmin'],
1763
+ default: ['user'],
1764
+ },
1765
+ ${orgSchema}
1766
+ // Password reset
1767
+ resetPasswordToken: String,
1768
+ resetPasswordExpires: Date,
1769
+ },
1770
+ { timestamps: true }
1771
+ );
1772
+
1773
+ // Password hashing
1774
+ userSchema.pre('save', async function() {
1775
+ if (!this.isModified('password')) return;
1776
+ const salt = await bcrypt.genSalt(10);
1777
+ this.password = await bcrypt.hash(this.password, salt);
1778
+ });
1779
+
1780
+ // Password comparison
1781
+ userSchema.methods.matchPassword = async function(enteredPassword${ts ? ": string" : ""}) {
1782
+ return bcrypt.compare(enteredPassword, this.password);
1783
+ };
1784
+ ${orgMethods}
1785
+ // Exclude password in JSON
1786
+ userSchema.set('toJSON', {
1787
+ transform: (_doc, ret${ts ? ": any" : ""}) => {
1788
+ delete ret.password;
1789
+ delete ret.resetPasswordToken;
1790
+ delete ret.resetPasswordExpires;
1791
+ return ret;
1792
+ },
1793
+ });
1794
+
1795
+ const User = mongoose.models.User${ts ? " as UserModel" : ""} || mongoose.model${ts ? "<User, UserModel>" : ""}('User', userSchema);
1796
+ export default User;
1797
+ `;
1798
+ }
1799
+ function userRepositoryTemplate(config) {
1800
+ const ts = config.typescript;
1801
+ const typeImport = ts ? "import type { UserDocument } from './model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : "";
1802
+ return `/**
1803
+ * User Repository
1804
+ * Generated by Arc CLI
1805
+ *
1806
+ * MongoKit repository with plugins for common operations
1807
+ */
1808
+
1809
+ import {
1810
+ Repository,
1811
+ methodRegistryPlugin,
1812
+ mongoOperationsPlugin,
1813
+ } from '@classytic/mongokit';
1814
+ ${typeImport}import User from './user.model.js';
1815
+
1816
+ ${ts ? "type ID = string | Types.ObjectId;\n" : ""}
1817
+ class UserRepository extends Repository${ts ? "<UserDocument>" : ""} {
1818
+ constructor() {
1819
+ super(User${ts ? " as any" : ""}, [
1820
+ methodRegistryPlugin(),
1821
+ mongoOperationsPlugin(),
1822
+ ]);
1823
+ }
1824
+
1825
+ /**
1826
+ * Find user by email
1827
+ */
1828
+ async findByEmail(email${ts ? ": string" : ""}) {
1829
+ return this.Model.findOne({ email: email.toLowerCase().trim() });
1830
+ }
1831
+
1832
+ /**
1833
+ * Find user by reset token
1834
+ */
1835
+ async findByResetToken(token${ts ? ": string" : ""}) {
1836
+ return this.Model.findOne({
1837
+ resetPasswordToken: token,
1838
+ resetPasswordExpires: { $gt: Date.now() },
1839
+ });
1840
+ }
1841
+
1842
+ /**
1843
+ * Check if email exists
1844
+ */
1845
+ async emailExists(email${ts ? ": string" : ""})${ts ? ": Promise<boolean>" : ""} {
1846
+ const result = await this.Model.exists({ email: email.toLowerCase().trim() });
1847
+ return !!result;
1848
+ }
1849
+
1850
+ /**
1851
+ * Update user password (triggers hash middleware)
1852
+ */
1853
+ async updatePassword(userId${ts ? ": ID" : ""}, newPassword${ts ? ": string" : ""}, options${ts ? ": { session?: ClientSession }" : ""} = {}) {
1854
+ const user = await this.Model.findById(userId).session(options.session ?? null);
1855
+ if (!user) throw new Error('User not found');
1856
+
1857
+ user.password = newPassword;
1858
+ user.resetPasswordToken = undefined;
1859
+ user.resetPasswordExpires = undefined;
1860
+ await user.save({ session: options.session ?? undefined });
1861
+ return user;
1862
+ }
1863
+
1864
+ /**
1865
+ * Set reset token
1866
+ */
1867
+ async setResetToken(userId${ts ? ": ID" : ""}, token${ts ? ": string" : ""}, expiresAt${ts ? ": Date" : ""}) {
1868
+ return this.Model.findByIdAndUpdate(
1869
+ userId,
1870
+ { resetPasswordToken: token, resetPasswordExpires: expiresAt },
1871
+ { new: true }
1872
+ );
1873
+ }
1874
+ ${config.tenant === "multi" ? `
1875
+ /**
1876
+ * Find users by organization
1877
+ */
1878
+ async findByOrganization(organizationId${ts ? ": ID" : ""}) {
1879
+ return this.Model.find({ 'organizations.organizationId': organizationId })
1880
+ .select('-password -resetPasswordToken -resetPasswordExpires')
1881
+ .lean();
1882
+ }
1883
+ ` : ""}
1884
+ }
1885
+
1886
+ const userRepository = new UserRepository();
1887
+ export default userRepository;
1888
+ export { UserRepository };
1889
+ `;
1890
+ }
1891
+ function userControllerTemplate(config) {
1892
+ const ts = config.typescript;
1893
+ return `/**
1894
+ * User Controller
1895
+ * Generated by Arc CLI
1896
+ *
1897
+ * BaseController for user management operations.
1898
+ * Used by auth resource for /users/me endpoints.
1899
+ */
1900
+
1901
+ import { BaseController } from '@classytic/arc';
1902
+ import userRepository from './user.repository.js';
1903
+
1904
+ class UserController extends BaseController {
1905
+ constructor() {
1906
+ super(userRepository${ts ? " as any" : ""});
1907
+ }
1908
+
1909
+ // Custom user operations can be added here
1910
+ }
1911
+
1912
+ const userController = new UserController();
1913
+ export default userController;
1914
+ `;
1915
+ }
1916
+ function authResourceTemplate(config) {
1917
+ const ts = config.typescript;
1918
+ return `/**
1919
+ * Auth Resource
1920
+ * Generated by Arc CLI
1921
+ *
1922
+ * Combined auth + user profile endpoints:
1923
+ * - POST /auth/register
1924
+ * - POST /auth/login
1925
+ * - POST /auth/refresh
1926
+ * - POST /auth/forgot-password
1927
+ * - POST /auth/reset-password
1928
+ * - GET /users/me
1929
+ * - PATCH /users/me
1930
+ */
1931
+
1932
+ import { defineResource } from '@classytic/arc';
1933
+ import { allowPublic, requireAuth } from '@classytic/arc/permissions';
1934
+ import { createAdapter } from '#shared/adapter.js';
1935
+ import User from '../user/user.model.js';
1936
+ import userRepository from '../user/user.repository.js';
1937
+ import * as handlers from './auth.handlers.js';
1938
+ import * as schemas from './auth.schemas.js';
1939
+
1940
+ /**
1941
+ * Auth Resource - handles authentication
1942
+ */
1943
+ export const authResource = defineResource({
1944
+ name: 'auth',
1945
+ displayName: 'Authentication',
1946
+ tag: 'Authentication',
1947
+ prefix: '/auth',
1948
+
1949
+ adapter: createAdapter(User${ts ? " as any" : ""}, userRepository${ts ? " as any" : ""}),
1950
+ disableDefaultRoutes: true,
1951
+
1952
+ additionalRoutes: [
1953
+ {
1954
+ method: 'POST',
1955
+ path: '/register',
1956
+ summary: 'Register new user',
1957
+ permissions: allowPublic(),
1958
+ handler: handlers.register,
1959
+ wrapHandler: false,
1960
+ schema: { body: schemas.registerBody },
1961
+ },
1962
+ {
1963
+ method: 'POST',
1964
+ path: '/login',
1965
+ summary: 'User login',
1966
+ permissions: allowPublic(),
1967
+ handler: handlers.login,
1968
+ wrapHandler: false,
1969
+ schema: { body: schemas.loginBody },
1970
+ },
1971
+ {
1972
+ method: 'POST',
1973
+ path: '/refresh',
1974
+ summary: 'Refresh access token',
1975
+ permissions: allowPublic(),
1976
+ handler: handlers.refreshToken,
1977
+ wrapHandler: false,
1978
+ schema: { body: schemas.refreshBody },
1979
+ },
1980
+ {
1981
+ method: 'POST',
1982
+ path: '/forgot-password',
1983
+ summary: 'Request password reset',
1984
+ permissions: allowPublic(),
1985
+ handler: handlers.forgotPassword,
1986
+ wrapHandler: false,
1987
+ schema: { body: schemas.forgotBody },
1988
+ },
1989
+ {
1990
+ method: 'POST',
1991
+ path: '/reset-password',
1992
+ summary: 'Reset password with token',
1993
+ permissions: allowPublic(),
1994
+ handler: handlers.resetPassword,
1995
+ wrapHandler: false,
1996
+ schema: { body: schemas.resetBody },
1997
+ },
1998
+ ],
1999
+ });
2000
+
2001
+ /**
2002
+ * User Profile Resource - handles /users/me
2003
+ */
2004
+ export const userProfileResource = defineResource({
2005
+ name: 'user-profile',
2006
+ displayName: 'User Profile',
2007
+ tag: 'User Profile',
2008
+ prefix: '/users',
2009
+
2010
+ adapter: createAdapter(User${ts ? " as any" : ""}, userRepository${ts ? " as any" : ""}),
2011
+ disableDefaultRoutes: true,
2012
+
2013
+ additionalRoutes: [
2014
+ {
2015
+ method: 'GET',
2016
+ path: '/me',
2017
+ summary: 'Get current user profile',
2018
+ permissions: requireAuth(),
2019
+ handler: handlers.getUserProfile,
2020
+ wrapHandler: false,
2021
+ },
2022
+ {
2023
+ method: 'PATCH',
2024
+ path: '/me',
2025
+ summary: 'Update current user profile',
2026
+ permissions: requireAuth(),
2027
+ handler: handlers.updateUserProfile,
2028
+ wrapHandler: false,
2029
+ schema: { body: schemas.updateUserBody },
2030
+ },
2031
+ ],
2032
+ });
2033
+
2034
+ export default authResource;
2035
+ `;
2036
+ }
2037
+ function authHandlersTemplate(config) {
2038
+ const ts = config.typescript;
2039
+ const typeAnnotations = ts ? `
2040
+ import type { FastifyRequest, FastifyReply } from 'fastify';
2041
+ ` : "";
2042
+ return `/**
2043
+ * Auth Handlers
2044
+ * Generated by Arc CLI
2045
+ */
2046
+
2047
+ import jwt from 'jsonwebtoken';
2048
+ import config from '#config/index.js';
2049
+ import userRepository from '../user/user.repository.js';
2050
+ ${typeAnnotations}
2051
+ // Token helpers
2052
+ function generateTokens(userId${ts ? ": string" : ""}) {
2053
+ const accessToken = jwt.sign({ id: userId }, config.jwt.secret, { expiresIn: '15m' });
2054
+ const refreshToken = jwt.sign({ id: userId }, config.jwt.secret, { expiresIn: '7d' });
2055
+ return { accessToken, refreshToken };
2056
+ }
2057
+
2058
+ /**
2059
+ * Register new user
2060
+ */
2061
+ export async function register(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2062
+ try {
2063
+ const { name, email, password } = request.body${ts ? " as any" : ""};
2064
+
2065
+ // Check if email exists
2066
+ if (await userRepository.emailExists(email)) {
2067
+ return reply.code(400).send({ success: false, message: 'Email already registered' });
2068
+ }
2069
+
2070
+ // Create user
2071
+ await userRepository.create({ name, email, password, roles: ['user'] });
2072
+
2073
+ return reply.code(201).send({ success: true, message: 'User registered successfully' });
2074
+ } catch (error) {
2075
+ request.log.error({ err: error }, 'Register error');
2076
+ return reply.code(500).send({ success: false, message: 'Registration failed' });
2077
+ }
2078
+ }
2079
+
2080
+ /**
2081
+ * Login user
2082
+ */
2083
+ export async function login(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2084
+ try {
2085
+ const { email, password } = request.body${ts ? " as any" : ""};
2086
+
2087
+ const user = await userRepository.findByEmail(email);
2088
+ if (!user || !(await user.matchPassword(password))) {
2089
+ return reply.code(401).send({ success: false, message: 'Invalid credentials' });
2090
+ }
2091
+
2092
+ const tokens = generateTokens(user._id.toString());
2093
+
2094
+ return reply.send({
2095
+ success: true,
2096
+ user: { id: user._id, name: user.name, email: user.email, roles: user.roles },
2097
+ ...tokens,
2098
+ });
2099
+ } catch (error) {
2100
+ request.log.error({ err: error }, 'Login error');
2101
+ return reply.code(500).send({ success: false, message: 'Login failed' });
2102
+ }
2103
+ }
2104
+
2105
+ /**
2106
+ * Refresh access token
2107
+ */
2108
+ export async function refreshToken(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2109
+ try {
2110
+ const { token } = request.body${ts ? " as any" : ""};
2111
+ if (!token) {
2112
+ return reply.code(401).send({ success: false, message: 'Refresh token required' });
2113
+ }
2114
+
2115
+ const decoded = jwt.verify(token, config.jwt.secret)${ts ? " as { id: string }" : ""};
2116
+ const tokens = generateTokens(decoded.id);
2117
+
2118
+ return reply.send({ success: true, ...tokens });
2119
+ } catch {
2120
+ return reply.code(401).send({ success: false, message: 'Invalid refresh token' });
2121
+ }
2122
+ }
2123
+
2124
+ /**
2125
+ * Forgot password
2126
+ */
2127
+ export async function forgotPassword(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2128
+ try {
2129
+ const { email } = request.body${ts ? " as any" : ""};
2130
+ const user = await userRepository.findByEmail(email);
2131
+
2132
+ if (user) {
2133
+ const token = Math.random().toString(36).slice(2) + Date.now().toString(36);
2134
+ const expires = new Date(Date.now() + 3600000); // 1 hour
2135
+ await userRepository.setResetToken(user._id, token, expires);
2136
+ // TODO: Send email with reset link
2137
+ request.log.info(\`Password reset token for \${email}: \${token}\`);
2138
+ }
2139
+
2140
+ // Always return success to prevent email enumeration
2141
+ return reply.send({ success: true, message: 'If email exists, reset link sent' });
2142
+ } catch (error) {
2143
+ request.log.error({ err: error }, 'Forgot password error');
2144
+ return reply.code(500).send({ success: false, message: 'Failed to process request' });
2145
+ }
2146
+ }
2147
+
2148
+ /**
2149
+ * Reset password
2150
+ */
2151
+ export async function resetPassword(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2152
+ try {
2153
+ const { token, newPassword } = request.body${ts ? " as any" : ""};
2154
+ const user = await userRepository.findByResetToken(token);
2155
+
2156
+ if (!user) {
2157
+ return reply.code(400).send({ success: false, message: 'Invalid or expired token' });
2158
+ }
2159
+
2160
+ await userRepository.updatePassword(user._id, newPassword);
2161
+ return reply.send({ success: true, message: 'Password has been reset' });
2162
+ } catch (error) {
2163
+ request.log.error({ err: error }, 'Reset password error');
2164
+ return reply.code(500).send({ success: false, message: 'Failed to reset password' });
2165
+ }
2166
+ }
2167
+
2168
+ /**
2169
+ * Get current user profile
2170
+ */
2171
+ export async function getUserProfile(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2172
+ try {
2173
+ const userId = (request${ts ? " as any" : ""}).user?._id || (request${ts ? " as any" : ""}).user?.id;
2174
+ const user = await userRepository.getById(userId);
2175
+
2176
+ if (!user) {
2177
+ return reply.code(404).send({ success: false, message: 'User not found' });
2178
+ }
2179
+
2180
+ return reply.send({ success: true, data: user });
2181
+ } catch (error) {
2182
+ request.log.error({ err: error }, 'Get profile error');
2183
+ return reply.code(500).send({ success: false, message: 'Failed to get profile' });
2184
+ }
2185
+ }
2186
+
2187
+ /**
2188
+ * Update current user profile
2189
+ */
2190
+ export async function updateUserProfile(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2191
+ try {
2192
+ const userId = (request${ts ? " as any" : ""}).user?._id || (request${ts ? " as any" : ""}).user?.id;
2193
+ const updates = { ...request.body${ts ? " as any" : ""} };
2194
+
2195
+ // Prevent updating protected fields
2196
+ if ('password' in updates) delete updates.password;
2197
+ if ('roles' in updates) delete updates.roles;
2198
+ if ('organizations' in updates) delete updates.organizations;
2199
+
2200
+ const user = await userRepository.Model.findByIdAndUpdate(userId, updates, { new: true });
2201
+ return reply.send({ success: true, data: user });
2202
+ } catch (error) {
2203
+ request.log.error({ err: error }, 'Update profile error');
2204
+ return reply.code(500).send({ success: false, message: 'Failed to update profile' });
2205
+ }
2206
+ }
2207
+ `;
2208
+ }
2209
+ function authSchemasTemplate(config) {
2210
+ return `/**
2211
+ * Auth Schemas
2212
+ * Generated by Arc CLI
2213
+ */
2214
+
2215
+ export const registerBody = {
2216
+ type: 'object',
2217
+ required: ['name', 'email', 'password'],
2218
+ properties: {
2219
+ name: { type: 'string', minLength: 2 },
2220
+ email: { type: 'string', format: 'email' },
2221
+ password: { type: 'string', minLength: 6 },
2222
+ },
2223
+ };
2224
+
2225
+ export const loginBody = {
2226
+ type: 'object',
2227
+ required: ['email', 'password'],
2228
+ properties: {
2229
+ email: { type: 'string', format: 'email' },
2230
+ password: { type: 'string' },
2231
+ },
2232
+ };
2233
+
2234
+ export const refreshBody = {
2235
+ type: 'object',
2236
+ required: ['token'],
2237
+ properties: {
2238
+ token: { type: 'string' },
2239
+ },
2240
+ };
2241
+
2242
+ export const forgotBody = {
2243
+ type: 'object',
2244
+ required: ['email'],
2245
+ properties: {
2246
+ email: { type: 'string', format: 'email' },
2247
+ },
2248
+ };
2249
+
2250
+ export const resetBody = {
2251
+ type: 'object',
2252
+ required: ['token', 'newPassword'],
2253
+ properties: {
2254
+ token: { type: 'string' },
2255
+ newPassword: { type: 'string', minLength: 6 },
2256
+ },
2257
+ };
2258
+
2259
+ export const updateUserBody = {
2260
+ type: 'object',
2261
+ properties: {
2262
+ name: { type: 'string', minLength: 2 },
2263
+ email: { type: 'string', format: 'email' },
2264
+ },
2265
+ };
2266
+ `;
2267
+ }
2268
+ function authTestTemplate(config) {
2269
+ const ts = config.typescript;
2270
+ return `/**
2271
+ * Auth Tests
2272
+ * Generated by Arc CLI
2273
+ */
2274
+
2275
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2276
+ ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';\n" : ""}import { createAppInstance } from '../src/app.js';
2277
+ ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}
2278
+ describe('Auth', () => {
2279
+ let app${ts ? ": FastifyInstance" : ""};
2280
+ const testUser = {
2281
+ name: 'Test User',
2282
+ email: 'test@example.com',
2283
+ password: 'password123',
2284
+ };
2285
+
2286
+ beforeAll(async () => {
2287
+ ${config.adapter === "mongokit" ? ` const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}-test';
2288
+ await mongoose.connect(testDbUri);
2289
+ // Clean up test data
2290
+ await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
2291
+ ` : ""}
2292
+ app = await createAppInstance();
2293
+ await app.ready();
2294
+ });
2295
+
2296
+ afterAll(async () => {
2297
+ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
2298
+ await mongoose.connection.close();
2299
+ ` : ""} await app.close();
2300
+ });
2301
+
2302
+ describe('POST /auth/register', () => {
2303
+ it('should register a new user', async () => {
2304
+ const response = await app.inject({
2305
+ method: 'POST',
2306
+ url: '/auth/register',
2307
+ payload: testUser,
2308
+ });
2309
+
2310
+ expect(response.statusCode).toBe(201);
2311
+ const body = JSON.parse(response.body);
2312
+ expect(body.success).toBe(true);
2313
+ });
2314
+
2315
+ it('should reject duplicate email', async () => {
2316
+ const response = await app.inject({
2317
+ method: 'POST',
2318
+ url: '/auth/register',
2319
+ payload: testUser,
2320
+ });
2321
+
2322
+ expect(response.statusCode).toBe(400);
2323
+ });
2324
+ });
2325
+
2326
+ describe('POST /auth/login', () => {
2327
+ it('should login with valid credentials', async () => {
2328
+ const response = await app.inject({
2329
+ method: 'POST',
2330
+ url: '/auth/login',
2331
+ payload: { email: testUser.email, password: testUser.password },
2332
+ });
2333
+
2334
+ expect(response.statusCode).toBe(200);
2335
+ const body = JSON.parse(response.body);
2336
+ expect(body.success).toBe(true);
2337
+ expect(body.accessToken).toBeDefined();
2338
+ expect(body.refreshToken).toBeDefined();
2339
+ });
2340
+
2341
+ it('should reject invalid credentials', async () => {
2342
+ const response = await app.inject({
2343
+ method: 'POST',
2344
+ url: '/auth/login',
2345
+ payload: { email: testUser.email, password: 'wrongpassword' },
2346
+ });
2347
+
2348
+ expect(response.statusCode).toBe(401);
2349
+ });
2350
+ });
2351
+
2352
+ describe('GET /users/me', () => {
2353
+ it('should require authentication', async () => {
2354
+ const response = await app.inject({
2355
+ method: 'GET',
2356
+ url: '/users/me',
2357
+ });
2358
+
2359
+ expect(response.statusCode).toBe(401);
2360
+ });
2361
+ });
2362
+ });
2363
+ `;
2364
+ }
2365
+ function printSuccessMessage(config, skipInstall) {
2366
+ const installStep = skipInstall ? ` npm install
2367
+ ` : "";
2368
+ console.log(`
2369
+ ╔═══════════════════════════════════════════════════════════════╗
2370
+ ║ ✅ Project Created! ║
2371
+ ╚═══════════════════════════════════════════════════════════════╝
2372
+
2373
+ Next steps:
2374
+
2375
+ cd ${config.name}
2376
+ ${installStep} npm run dev # Uses .env.dev automatically
2377
+
2378
+ API Documentation:
2379
+
2380
+ http://localhost:8040/docs # Scalar UI
2381
+ http://localhost:8040/_docs/openapi.json # OpenAPI spec
2382
+
2383
+ Run tests:
2384
+
2385
+ npm test # Run once
2386
+ npm run test:watch # Watch mode
2387
+
2388
+ Add resources:
2389
+
2390
+ 1. Create folder: src/resources/product/
2391
+ 2. Add: index.${config.typescript ? "ts" : "js"}, model.${config.typescript ? "ts" : "js"}, repository.${config.typescript ? "ts" : "js"}
2392
+ 3. Register in src/resources/index.${config.typescript ? "ts" : "js"}
2393
+
2394
+ Project structure:
2395
+
2396
+ src/
2397
+ ├── app.${config.typescript ? "ts" : "js"} # App factory (for workers/tests)
2398
+ ├── index.${config.typescript ? "ts" : "js"} # Server entry
2399
+ ├── config/ # Configuration
2400
+ ├── shared/ # Adapters, presets, permissions
2401
+ ├── plugins/ # App plugins (DI pattern)
2402
+ └── resources/ # API resources
2403
+
2404
+ Documentation:
2405
+ https://github.com/classytic/arc
2406
+ `);
2407
+ }
2408
+ var init_default = init;
2409
+
2410
+ export { init_default as default, init };