@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
package/README.md CHANGED
@@ -281,7 +281,14 @@ export default defineResource({
281
281
  path: '/featured',
282
282
  handler: 'getFeatured', // Controller method name
283
283
  permissions: allowPublic(), // Permission function
284
- wrapHandler: true, // Required: true=controller, false=fastify
284
+ wrapHandler: true, // Arc context pattern (IRequestContext)
285
+ },
286
+ {
287
+ method: 'GET',
288
+ path: '/:id/download',
289
+ handler: 'downloadFile', // Fastify native handler
290
+ permissions: requireAuth(),
291
+ wrapHandler: false, // Native Fastify (request, reply)
285
292
  },
286
293
  ],
287
294
  });
@@ -293,6 +300,7 @@ Extend BaseController for built-in security and CRUD:
293
300
 
294
301
  ```typescript
295
302
  import { BaseController } from '@classytic/arc';
303
+ import type { IRequestContext, IControllerResponse } from '@classytic/arc';
296
304
  import type { ISoftDeleteController, ISlugLookupController } from '@classytic/arc/presets';
297
305
 
298
306
  // Type-safe controller with preset interfaces
@@ -302,21 +310,29 @@ class ProductController
302
310
  {
303
311
  constructor() {
304
312
  super(productRepository);
305
-
306
- // TypeScript ensures these methods exist (required by presets)
307
- this.getBySlug = this.getBySlug.bind(this);
308
- this.getDeleted = this.getDeleted.bind(this);
309
- this.restore = this.restore.bind(this);
310
313
  }
311
314
 
312
- // Custom method
313
- async getFeatured(req, reply) {
314
- // Security checks applied automatically
315
+ // Custom method - Arc context pattern
316
+ async getFeatured(req: IRequestContext): Promise<IControllerResponse> {
317
+ const { organizationId } = req;
318
+
315
319
  const products = await this.repository.findAll({
316
- filter: { isFeatured: true },
317
- ...this._applyFilters(req),
320
+ filter: { isFeatured: true, organizationId },
318
321
  });
319
- return reply.send({ success: true, data: products });
322
+
323
+ return { success: true, data: products };
324
+ }
325
+
326
+ // Preset methods
327
+ async getBySlug(req: IRequestContext): Promise<IControllerResponse> {
328
+ const { slug } = req.params;
329
+ const product = await this.repository.getBySlug(slug);
330
+
331
+ if (!product) {
332
+ return { success: false, error: 'Product not found', status: 404 };
333
+ }
334
+
335
+ return { success: true, data: product };
320
336
  }
321
337
  }
322
338
  ```
@@ -329,17 +345,36 @@ class ProductController
329
345
 
330
346
  **Note:** Presets like `multiTenant`, `ownedByUser`, and `audited` don't require controller methods—they work via middleware.
331
347
 
348
+ ### Request Context API
349
+
350
+ Controller methods receive `req: IRequestContext`:
351
+
352
+ ```typescript
353
+ interface IRequestContext {
354
+ params: Record<string, string>; // Route params: /users/:id
355
+ query: Record<string, unknown>; // Query string: ?page=1
356
+ body: unknown; // Request body
357
+ user: UserBase | null; // Authenticated user
358
+ headers: Record<string, string | undefined>; // Request headers
359
+ organizationId?: string; // Multi-tenant org ID
360
+ metadata?: Record<string, unknown>; // Custom data, _policyFilters, middleware context
361
+ }
362
+ ```
363
+
364
+ **Key Fields:**
365
+ - `req.metadata` - Custom data from hooks, policies, or middleware
366
+ - `req.organizationId` - Set by `multiTenant` preset or org scope plugin
367
+ - `req.user` - Set by auth plugin, preserves original auth structure
368
+
332
369
  ### TypeScript Strict Mode
333
370
 
334
- For maximum type safety, use strict controller typing:
371
+ For maximum type safety:
335
372
 
336
373
  ```typescript
337
- import { BaseController } from '@classytic/arc';
338
- import type { Document } from 'mongoose';
374
+ import { BaseController, IRequestContext, IControllerResponse } from '@classytic/arc';
339
375
  import type { ISoftDeleteController, ISlugLookupController } from '@classytic/arc/presets';
340
376
 
341
- // Define your document type
342
- interface ProductDocument extends Document {
377
+ interface Product {
343
378
  _id: string;
344
379
  name: string;
345
380
  slug: string;
@@ -347,43 +382,38 @@ interface ProductDocument extends Document {
347
382
  deletedAt?: Date;
348
383
  }
349
384
 
350
- // Strict controller with generics
351
385
  class ProductController
352
- extends BaseController<ProductDocument>
353
- implements
354
- ISoftDeleteController<ProductDocument>,
355
- ISlugLookupController<ProductDocument>
386
+ extends BaseController<Product>
387
+ implements ISoftDeleteController<Product>, ISlugLookupController<Product>
356
388
  {
357
- // TypeScript enforces these method signatures
358
- async getBySlug(req, reply): Promise<void> {
389
+ async getBySlug(req: IRequestContext): Promise<IControllerResponse<Product>> {
359
390
  const { slug } = req.params;
360
391
  const product = await this.repository.getBySlug(slug);
361
392
 
362
393
  if (!product) {
363
- return reply.code(404).send({ error: 'Product not found' });
394
+ return { success: false, error: 'Product not found', status: 404 };
364
395
  }
365
396
 
366
- return reply.send({ success: true, data: product });
397
+ return { success: true, data: product };
367
398
  }
368
399
 
369
- async getDeleted(req, reply): Promise<void> {
400
+ async getDeleted(req: IRequestContext): Promise<IControllerResponse<Product[]>> {
370
401
  const products = await this.repository.findDeleted();
371
- return reply.send({ success: true, data: products });
402
+ return { success: true, data: products };
372
403
  }
373
404
 
374
- async restore(req, reply): Promise<void> {
405
+ async restore(req: IRequestContext): Promise<IControllerResponse<Product>> {
375
406
  const { id } = req.params;
376
407
  const product = await this.repository.restore(id);
377
- return reply.send({ success: true, data: product });
408
+ return { success: true, data: product };
378
409
  }
379
410
  }
380
411
  ```
381
412
 
382
- **Benefits of strict typing:**
383
- - Compile-time checks for preset requirements
384
- - IntelliSense autocomplete for controller methods
385
- - Catch type mismatches before runtime
386
- - Refactoring safety across large codebases
413
+ **Benefits:**
414
+ - Compile-time type checking
415
+ - IntelliSense autocomplete
416
+ - Safe refactoring
387
417
 
388
418
  ### Repositories
389
419
 
package/bin/arc.js CHANGED
@@ -4,15 +4,18 @@
4
4
  * Arc CLI - Smart Backend Framework
5
5
  *
6
6
  * Commands:
7
- * arc generate resource <name> [options] Generate a new resource
8
- * arc generate controller <name> Generate a controller only
9
- * arc generate model <name> Generate a model only
10
- * arc introspect Show all registered resources
11
- * arc docs [output-path] Export OpenAPI specification
7
+ * arc init [name] Initialize a new Arc project
8
+ * arc generate resource <name> Generate a new resource
9
+ * arc generate controller <name> Generate a controller only
10
+ * arc generate model <name> Generate a model only
11
+ * arc introspect Show all registered resources
12
+ * arc docs [output-path] Export OpenAPI specification
12
13
  *
13
14
  * Examples:
14
- * arc generate resource product --module catalog
15
- * arc generate resource invoice --presets softDelete,multiTenant
15
+ * arc init my-api
16
+ * arc init my-api --mongokit --single --ts
17
+ * arc generate resource product
18
+ * arc g r invoice
16
19
  * arc introspect
17
20
  * arc docs ./openapi.json
18
21
  */
@@ -60,6 +63,11 @@ const [command, subcommand, ...rest] = args;
60
63
  async function main() {
61
64
  try {
62
65
  switch (command) {
66
+ case 'init':
67
+ case 'new':
68
+ await handleInit(subcommand ? [subcommand, ...rest] : rest);
69
+ break;
70
+
63
71
  case 'generate':
64
72
  case 'g':
65
73
  await handleGenerate(subcommand, rest);
@@ -76,12 +84,12 @@ async function main() {
76
84
  break;
77
85
 
78
86
  default:
79
- console.error(`❌ Unknown command: ${command}`);
87
+ console.error(`Unknown command: ${command}`);
80
88
  console.error('Run "arc --help" for usage');
81
89
  process.exit(1);
82
90
  }
83
91
  } catch (err) {
84
- console.error(`❌ Error: ${err.message}`);
92
+ console.error(`Error: ${err.message}`);
85
93
  if (process.env.DEBUG) {
86
94
  console.error(err.stack);
87
95
  }
@@ -93,13 +101,19 @@ async function main() {
93
101
  // Command Handlers
94
102
  // ============================================================================
95
103
 
104
+ async function handleInit(args) {
105
+ const options = parseInitOptions(args);
106
+ const { init } = await import('../dist/cli/commands/init.js');
107
+ await init(options);
108
+ }
109
+
96
110
  async function handleGenerate(type, args) {
97
111
  if (!type) {
98
- console.error('Missing type argument');
99
- console.log('\nUsage: arc generate <resource|controller|model> <name> [options]');
112
+ console.error('Missing type argument');
113
+ console.log('\nUsage: arc generate <resource|controller|model|repository|schemas> <name>');
100
114
  console.log('\nExamples:');
101
- console.log(' arc generate resource product --module catalog');
102
- console.log(' arc g r invoice --presets softDelete,multiTenant');
115
+ console.log(' arc generate resource product');
116
+ console.log(' arc g r invoice');
103
117
  process.exit(1);
104
118
  }
105
119
 
@@ -108,30 +122,32 @@ async function handleGenerate(type, args) {
108
122
  r: 'resource',
109
123
  c: 'controller',
110
124
  m: 'model',
125
+ repo: 'repository',
126
+ s: 'schemas',
111
127
  resource: 'resource',
112
128
  controller: 'controller',
113
129
  model: 'model',
130
+ repository: 'repository',
131
+ schemas: 'schemas',
114
132
  };
115
133
 
116
134
  const normalizedType = typeMap[type.toLowerCase()];
117
135
  if (!normalizedType) {
118
- console.error(`❌ Unknown type: ${type}`);
119
- console.log('Available types: resource (r), controller (c), model (m)');
136
+ console.error(`Unknown type: ${type}`);
137
+ console.log('Available types: resource (r), controller (c), model (m), repository (repo), schemas (s)');
120
138
  process.exit(1);
121
139
  }
122
140
 
123
141
  const name = args[0];
124
142
  if (!name) {
125
- console.error('Missing name argument');
126
- console.log(`\nUsage: arc generate ${normalizedType} <name> [options]`);
143
+ console.error('Missing name argument');
144
+ console.log(`\nUsage: arc generate ${normalizedType} <name>`);
127
145
  process.exit(1);
128
146
  }
129
147
 
130
- const options = parseGenerateOptions(args.slice(1));
131
-
132
148
  // Import and run
133
- const { generate } = await import('../dist/cli/index.js');
134
- await generate(normalizedType, name, options);
149
+ const { generate } = await import('../dist/cli/commands/generate.js');
150
+ await generate(normalizedType, args);
135
151
  }
136
152
 
137
153
  async function handleIntrospect(args) {
@@ -143,11 +159,11 @@ async function handleIntrospect(args) {
143
159
  const absolutePath = resolve(process.cwd(), entryPath);
144
160
  const fileUrl = pathToFileURL(absolutePath).href;
145
161
 
146
- console.log(`📦 Loading resources from: ${entryPath}\n`);
162
+ console.log(`Loading resources from: ${entryPath}\n`);
147
163
  try {
148
164
  await import(fileUrl);
149
165
  } catch (err) {
150
- console.error(`❌ Failed to load entry file: ${err.message}`);
166
+ console.error(`Failed to load entry file: ${err.message}`);
151
167
  if (process.env.DEBUG) {
152
168
  console.error(err.stack);
153
169
  }
@@ -168,11 +184,11 @@ async function handleDocs(args) {
168
184
  const absolutePath = resolve(process.cwd(), entryPath);
169
185
  const fileUrl = pathToFileURL(absolutePath).href;
170
186
 
171
- console.log(`📦 Loading resources from: ${entryPath}\n`);
187
+ console.log(`Loading resources from: ${entryPath}\n`);
172
188
  try {
173
189
  await import(fileUrl);
174
190
  } catch (err) {
175
- console.error(`❌ Failed to load entry file: ${err.message}`);
191
+ console.error(`Failed to load entry file: ${err.message}`);
176
192
  if (process.env.DEBUG) {
177
193
  console.error(err.stack);
178
194
  }
@@ -190,57 +206,54 @@ async function handleDocs(args) {
190
206
  // Option Parsing
191
207
  // ============================================================================
192
208
 
193
- function parseGenerateOptions(args) {
209
+ function parseInitOptions(args) {
194
210
  const opts = {
195
- module: undefined,
196
- presets: [],
197
- parentField: 'parent',
198
- withTests: true,
199
- dryRun: false,
211
+ name: undefined,
212
+ adapter: undefined,
213
+ tenant: undefined,
214
+ typescript: undefined,
215
+ skipInstall: false,
200
216
  force: false,
201
- typescript: true, // Default to TypeScript
202
- outputDir: process.cwd(),
203
217
  };
204
218
 
205
219
  for (let i = 0; i < args.length; i++) {
206
220
  const arg = args[i];
207
221
  const next = args[i + 1];
208
222
 
209
- switch (arg) {
210
- case '--module':
211
- case '-m':
212
- opts.module = next;
213
- i++;
214
- break;
223
+ // First non-flag argument is the project name
224
+ if (!arg.startsWith('-') && !opts.name) {
225
+ opts.name = arg;
226
+ continue;
227
+ }
215
228
 
216
- case '--presets':
217
- case '-p':
218
- opts.presets = next?.split(',').map((p) => p.trim()).filter(Boolean) || [];
229
+ switch (arg) {
230
+ case '--name':
231
+ case '-n':
232
+ opts.name = next;
219
233
  i++;
220
234
  break;
221
235
 
222
- case '--parent-field':
223
- opts.parentField = next;
224
- i++;
236
+ case '--mongokit':
237
+ opts.adapter = 'mongokit';
225
238
  break;
226
239
 
227
- case '--output':
228
- case '-o':
229
- opts.outputDir = next;
230
- i++;
240
+ case '--custom':
241
+ opts.adapter = 'custom';
231
242
  break;
232
243
 
233
- case '--no-tests':
234
- opts.withTests = false;
244
+ case '--multi-tenant':
245
+ case '--multi':
246
+ opts.tenant = 'multi';
235
247
  break;
236
248
 
237
- case '--dry-run':
238
- opts.dryRun = true;
249
+ case '--single-tenant':
250
+ case '--single':
251
+ opts.tenant = 'single';
239
252
  break;
240
253
 
241
- case '--force':
242
- case '-f':
243
- opts.force = true;
254
+ case '--ts':
255
+ case '--typescript':
256
+ opts.typescript = true;
244
257
  break;
245
258
 
246
259
  case '--js':
@@ -248,9 +261,13 @@ function parseGenerateOptions(args) {
248
261
  opts.typescript = false;
249
262
  break;
250
263
 
251
- case '--ts':
252
- case '--typescript':
253
- opts.typescript = true;
264
+ case '--skip-install':
265
+ opts.skipInstall = true;
266
+ break;
267
+
268
+ case '--force':
269
+ case '-f':
270
+ opts.force = true;
254
271
  break;
255
272
  }
256
273
  }
@@ -264,15 +281,14 @@ function parseGenerateOptions(args) {
264
281
 
265
282
  function printHelp() {
266
283
  console.log(`
267
- ╔═══════════════════════════════════════════════════════════════╗
268
- ║ 🔥 Arc CLI v${VERSION} ║
269
- ║ Resource-Oriented Backend Framework ║
270
- ╚═══════════════════════════════════════════════════════════════╝
284
+ Arc CLI v${VERSION}
285
+ Resource-Oriented Backend Framework
271
286
 
272
287
  USAGE
273
288
  arc <command> [options]
274
289
 
275
290
  COMMANDS
291
+ init, new Initialize a new Arc project
276
292
  generate, g Generate resources, controllers, or models
277
293
  introspect, i Show all registered resources
278
294
  docs, d Export OpenAPI specification
@@ -283,56 +299,55 @@ GLOBAL OPTIONS
283
299
  --version, -v Show version
284
300
  --help, -h Show this help
285
301
 
302
+ INIT OPTIONS
303
+ --mongokit Use MongoKit adapter (default, recommended)
304
+ --custom Use custom adapter (empty template)
305
+ --multi-tenant, --multi Multi-tenant mode (adds org scoping)
306
+ --single-tenant, --single Single-tenant mode (default)
307
+ --ts, --typescript Generate TypeScript (default)
308
+ --js, --javascript Generate JavaScript
309
+ --force, -f Overwrite existing directory
310
+ --skip-install Skip npm install after scaffolding
311
+
286
312
  GENERATE SUBCOMMANDS
287
- resource, r Generate full resource (model, repo, controller, routes)
288
- controller, c Generate controller only
289
- model, m Generate model only
290
-
291
- GENERATE OPTIONS
292
- --module, -m <name> Parent module (e.g., catalog, sales)
293
- --presets, -p <list> Comma-separated presets:
294
- softDelete - Soft delete with restore
295
- • slugLookup - GET by slug endpoint
296
- • ownedByUser - User ownership checks
297
- • multiTenant - Organization scoping
298
- • tree - Hierarchical data support
299
- • audited - Audit logging
300
- --parent-field <name> Custom parent field for tree preset
301
- --output, -o <path> Output directory (default: cwd)
302
- --no-tests Skip test file generation
303
- --dry-run Preview without creating files
304
- --force, -f Overwrite existing files
305
- --js, --javascript Generate JavaScript (default: TypeScript)
313
+ resource, r Generate full resource (model, repo, controller, schemas, resource)
314
+ controller, c Generate controller only
315
+ model, m Generate model only
316
+ repository, repo Generate repository only
317
+ schemas, s Generate schemas only
318
+
319
+ GENERATE NOTES
320
+ - Auto-detects TypeScript/JavaScript from tsconfig.json
321
+ - Files are created in src/resources/<name>/ directory
322
+ - Uses prefixed filenames: <name>.model.ts, <name>.repository.ts, etc.
306
323
 
307
324
  EXAMPLES
308
- # Generate a product resource in catalog module
309
- arc generate resource product --module catalog
325
+ # Initialize a new project (interactive prompts)
326
+ arc init my-api
310
327
 
311
- # Generate with presets (shorthand)
312
- arc g r invoice -m finance -p softDelete,multiTenant
328
+ # Initialize with all options (non-interactive)
329
+ arc init my-api --mongokit --single --ts
313
330
 
314
- # Generate controller only
315
- arc g controller auth
331
+ # Initialize a JavaScript single-tenant app
332
+ arc init my-api --mongokit --single --js
316
333
 
317
- # Preview what would be generated
318
- arc g r order --dry-run
334
+ # Generate a product resource
335
+ arc generate resource product
319
336
 
320
- # Export OpenAPI spec (load resources first)
321
- arc docs ./docs/openapi.json --entry ./index.js
337
+ # Shorthand for generating a resource
338
+ arc g r invoice
339
+
340
+ # Generate only a controller
341
+ arc g controller auth
322
342
 
323
- # Show registered resources (load resources first)
324
- arc introspect --entry ./index.js
343
+ # Generate only a model
344
+ arc g model order
325
345
 
326
- # Quick introspect (if resources already loaded)
327
- arc introspect
346
+ # Export OpenAPI spec (load resources first)
347
+ arc docs ./docs/openapi.json --entry ./dist/index.js
328
348
 
329
- PRESETS EXPLAINED
330
- softDelete Adds: deletedAt field, GET /deleted, POST /:id/restore
331
- slugLookup Adds: slug field, GET /slug/:slug endpoint
332
- ownedByUser Adds: createdBy field, ownership validation
333
- multiTenant Adds: organizationId field, org scoping middleware
334
- tree Adds: parent field, GET /tree, GET /:id/children
335
- audited Adds: audit log entries for all mutations
349
+ # Show registered resources
350
+ arc introspect --entry ./dist/index.js
336
351
 
337
352
  MORE INFO
338
353
  Documentation: https://github.com/classytic/arc