@classytic/arc 1.0.0 → 1.0.8

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