@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,233 @@
1
+ import { A as AnyRecord, I as IController, R as RouteSchemaOptions, Q as QueryParserInterface, a as IRequestContext, S as ServiceContext, C as ControllerQueryOptions, b as RequestContext, H as HookSystem, c as IControllerResponse, P as PaginatedResult } from './index-D5QTob1X.js';
2
+
3
+ /**
4
+ * Base Controller - Framework-Agnostic CRUD Operations
5
+ *
6
+ * Implements IController interface for framework portability.
7
+ * Works with Fastify, Express, Next.js, or any framework via adapter pattern.
8
+ *
9
+ * @example
10
+ * import { BaseController } from '@classytic/arc';
11
+ *
12
+ * // Use Arc's default query parser (works out of the box)
13
+ * class ProductController extends BaseController {
14
+ * constructor(repository: CrudRepository) {
15
+ * super(repository);
16
+ * }
17
+ * }
18
+ *
19
+ * // Or use MongoKit's parser for advanced MongoDB features ($lookup, aggregations)
20
+ * import { QueryParser } from '@classytic/mongokit';
21
+ * defineResource({
22
+ * name: 'product',
23
+ * queryParser: new QueryParser(),
24
+ * // ...
25
+ * });
26
+ *
27
+ * // Or use a custom parser for SQL databases
28
+ * defineResource({
29
+ * name: 'user',
30
+ * queryParser: new PgQueryParser(),
31
+ * // ...
32
+ * });
33
+ */
34
+
35
+ /**
36
+ * Flexible repository interface that accepts any repository shape
37
+ * Core CRUD methods use flexible signatures to work with any implementation
38
+ * Custom methods can be added via the index signature
39
+ *
40
+ * @example
41
+ * // MongoKit repository with custom methods
42
+ * interface MyRepository extends FlexibleRepository {
43
+ * findByEmail(email: string): Promise<User>;
44
+ * customMethod(): Promise<void>;
45
+ * }
46
+ */
47
+ interface FlexibleRepository {
48
+ getAll(...args: any[]): Promise<any>;
49
+ getById(...args: any[]): Promise<any>;
50
+ create(...args: any[]): Promise<any>;
51
+ update(...args: any[]): Promise<any>;
52
+ delete(...args: any[]): Promise<any>;
53
+ [key: string]: any;
54
+ }
55
+ interface BaseControllerOptions {
56
+ /** Schema options for field sanitization */
57
+ schemaOptions?: RouteSchemaOptions;
58
+ /**
59
+ * Query parser instance
60
+ * Default: Arc built-in query parser (adapter-agnostic).
61
+ * You can swap in MongoKit QueryParser, pgkit parser, etc.
62
+ */
63
+ queryParser?: QueryParserInterface;
64
+ /** Maximum limit for pagination (default: 100) */
65
+ maxLimit?: number;
66
+ /** Default limit for pagination (default: 20) */
67
+ defaultLimit?: number;
68
+ /** Default sort field (default: '-createdAt') */
69
+ defaultSort?: string;
70
+ /** Resource name for hook execution (e.g., 'product' → 'product.created') */
71
+ resourceName?: string;
72
+ /** Disable automatic event emission (default: false) */
73
+ disableEvents?: boolean;
74
+ }
75
+ /**
76
+ * Framework-agnostic base controller implementing MongoKit's IController
77
+ *
78
+ * @template TDoc - The document type
79
+ * @template TRepository - The repository type (defaults to CrudRepository<TDoc>, preserves custom methods when specified)
80
+ *
81
+ * Use with Fastify adapter for Fastify integration (see createFastifyAdapter in createCrudRouter)
82
+ *
83
+ * @example
84
+ * // Without custom repository type (backward compatible)
85
+ * class SimpleController extends BaseController<Product> {
86
+ * constructor(repository: CrudRepository<Product>) {
87
+ * super(repository);
88
+ * }
89
+ * }
90
+ *
91
+ * @example
92
+ * // With custom repository type (type-safe access to custom methods)
93
+ * class ProductController extends BaseController<Product, ProductRepository> {
94
+ * constructor(repository: ProductRepository) {
95
+ * super(repository);
96
+ * }
97
+ *
98
+ * async customMethod(context: IRequestContext) {
99
+ * // TypeScript knows about ProductRepository's custom methods
100
+ * return await this.repository.findByCategory(...);
101
+ * }
102
+ * }
103
+ */
104
+ declare class BaseController<TDoc = AnyRecord, TRepository extends FlexibleRepository = FlexibleRepository> implements IController<TDoc> {
105
+ protected repository: TRepository;
106
+ protected schemaOptions: RouteSchemaOptions;
107
+ protected queryParser: QueryParserInterface;
108
+ protected maxLimit: number;
109
+ protected defaultLimit: number;
110
+ protected defaultSort: string;
111
+ protected resourceName?: string;
112
+ protected disableEvents: boolean;
113
+ /** Preset field names for dynamic param reading */
114
+ protected _presetFields: {
115
+ slugField?: string;
116
+ parentField?: string;
117
+ };
118
+ constructor(repository: TRepository, options?: BaseControllerOptions);
119
+ /**
120
+ * Inject resource options from defineResource
121
+ */
122
+ _setResourceOptions(options: {
123
+ schemaOptions?: RouteSchemaOptions;
124
+ presetFields?: {
125
+ slugField?: string;
126
+ parentField?: string;
127
+ };
128
+ resourceName?: string;
129
+ queryParser?: QueryParserInterface;
130
+ }): void;
131
+ /**
132
+ * Build service context from IRequestContext
133
+ */
134
+ protected _buildContext(req: IRequestContext): ServiceContext;
135
+ /**
136
+ * Parse query into QueryOptions using queryParser
137
+ */
138
+ protected _parseQueryOptions(req: IRequestContext): ControllerQueryOptions;
139
+ /**
140
+ * Apply org and policy filters
141
+ */
142
+ protected _applyFilters(options: ControllerQueryOptions, req: IRequestContext): ControllerQueryOptions;
143
+ /**
144
+ * Build filter for single-item operations (get/update/delete)
145
+ * Combines ID filter with policy/org filters for proper security enforcement
146
+ */
147
+ protected _buildIdFilter(id: string, req: IRequestContext): AnyRecord;
148
+ /**
149
+ * Check if a value matches a MongoDB query operator
150
+ */
151
+ protected _matchesOperator(itemValue: unknown, operator: string, filterValue: unknown): boolean;
152
+ /**
153
+ * Forbidden paths that could lead to prototype pollution
154
+ */
155
+ private static readonly FORBIDDEN_PATHS;
156
+ /**
157
+ * Get nested value from object using dot notation (e.g., "owner.id")
158
+ * Security: Validates path against forbidden patterns to prevent prototype pollution
159
+ */
160
+ protected _getNestedValue(obj: AnyRecord, path: string): unknown;
161
+ /**
162
+ * Check if item matches a single filter condition
163
+ * Supports nested paths (e.g., "owner.id", "metadata.status")
164
+ */
165
+ protected _matchesFilter(item: AnyRecord, key: string, filterValue: unknown): boolean;
166
+ /**
167
+ * Check if item matches policy filters (for get/update/delete operations)
168
+ * Validates that fetched item satisfies all policy constraints
169
+ * Supports MongoDB query operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $regex, $and, $or
170
+ */
171
+ protected _checkPolicyFilters(item: AnyRecord, req: IRequestContext): boolean;
172
+ /** Parse lean option (default: true for performance) */
173
+ protected _parseLean(leanValue: unknown): boolean;
174
+ /** Get blocked fields from schema options */
175
+ protected _getBlockedFields(schemaOptions: RouteSchemaOptions): string[];
176
+ /**
177
+ * Convert parsed select object to string format
178
+ * Converts { name: 1, email: 1, password: 0 } → 'name email -password'
179
+ */
180
+ protected _selectToString(select: string | string[] | Record<string, 0 | 1> | undefined): string | undefined;
181
+ /** Sanitize select fields */
182
+ protected _sanitizeSelect(select: string | undefined, schemaOptions: RouteSchemaOptions): string | undefined;
183
+ /** Sanitize populate fields */
184
+ protected _sanitizePopulate(populate: unknown, schemaOptions: RouteSchemaOptions): string[] | undefined;
185
+ /** Check org scope for a document */
186
+ protected _checkOrgScope(item: AnyRecord | null, arcContext: RequestContext | undefined): boolean;
187
+ /** Check ownership for update/delete (ownedByUser preset) */
188
+ protected _checkOwnership(item: AnyRecord | null, req: IRequestContext): boolean;
189
+ /**
190
+ * Get hook system from context (instance-scoped) or fall back to global singleton
191
+ * This allows proper isolation when running multiple app instances (e.g., in tests)
192
+ */
193
+ protected _getHooks(req: IRequestContext): HookSystem;
194
+ /**
195
+ * List resources with filtering, pagination, sorting
196
+ * Implements IController.list()
197
+ */
198
+ list(req: IRequestContext): Promise<IControllerResponse<PaginatedResult<TDoc>>>;
199
+ /**
200
+ * Get single resource by ID
201
+ * Implements IController.get()
202
+ */
203
+ get(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
204
+ /**
205
+ * Create new resource
206
+ * Implements IController.create()
207
+ */
208
+ create(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
209
+ /**
210
+ * Update existing resource
211
+ * Implements IController.update()
212
+ */
213
+ update(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
214
+ /**
215
+ * Delete resource
216
+ * Implements IController.delete()
217
+ */
218
+ delete(req: IRequestContext): Promise<IControllerResponse<{
219
+ message: string;
220
+ }>>;
221
+ /** Get resource by slug (slugLookup preset) */
222
+ getBySlug(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
223
+ /** Get soft-deleted resources (softDelete preset) */
224
+ getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginatedResult<TDoc>>>;
225
+ /** Restore soft-deleted resource (softDelete preset) */
226
+ restore(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
227
+ /** Get hierarchical tree (tree preset) */
228
+ getTree(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
229
+ /** Get children of parent (tree preset) */
230
+ getChildren(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
231
+ }
232
+
233
+ export { BaseController as B, type BaseControllerOptions as a };
@@ -1,5 +1,5 @@
1
- import { D as DataAdapter, f as CrudRepository, i as RepositoryLike, p as SchemaMetadata, g as RouteSchemaOptions, af as OpenApiSchemas, h as QueryParserInterface, ae as ParsedQuery, V as ValidationResult } from '../index-DkAW8BXh.js';
2
- export { ag as AdapterFactory, F as FieldMetadata, q as RelationMetadata } from '../index-DkAW8BXh.js';
1
+ import { D as DataAdapter, y as CrudRepository, o as RepositoryLike, m as SchemaMetadata, R as RouteSchemaOptions, af as OpenApiSchemas, Q as QueryParserInterface, ae as ParsedQuery, V as ValidationResult } from '../index-D5QTob1X.js';
2
+ export { ag as AdapterFactory, F as FieldMetadata, n as RelationMetadata } from '../index-D5QTob1X.js';
3
3
  import { Model } from 'mongoose';
4
4
  import 'fastify';
5
5
  import '../types-B99TBmFV.js';
@@ -1,5 +1,5 @@
1
1
  import { FastifyPluginAsync, FastifyInstance, FastifyRequest } from 'fastify';
2
- import { H as HookSystem, b as ResourceRegistry } from './index-DkAW8BXh.js';
2
+ import { H as HookSystem, j as ResourceRegistry } from './index-D5QTob1X.js';
3
3
 
4
4
  /**
5
5
  * Request ID Plugin
@@ -1,5 +1,5 @@
1
1
  import { FastifyPluginAsync } from 'fastify';
2
- import { A as AuthHelpers, a as AuthPluginOptions } from '../index-DkAW8BXh.js';
2
+ import { g as AuthHelpers, h as AuthPluginOptions } from '../index-D5QTob1X.js';
3
3
  import 'mongoose';
4
4
  import '../types-B99TBmFV.js';
5
5
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Arc CLI - Generate Command
3
+ *
4
+ * Scaffolds resources with consistent naming:
5
+ * - src/resources/product/product.model.ts
6
+ * - src/resources/product/product.repository.ts
7
+ * - src/resources/product/product.resource.ts
8
+ * - src/resources/product/product.controller.ts
9
+ * - src/resources/product/product.schemas.ts
10
+ */
11
+ /**
12
+ * Generate command handler
13
+ */
14
+ declare function generate(type: string | undefined, args: string[]): Promise<void>;
15
+
16
+ export { generate as default, generate };
@@ -0,0 +1,334 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ // src/cli/commands/generate.ts
5
+ function isTypeScriptProject() {
6
+ return existsSync(join(process.cwd(), "tsconfig.json"));
7
+ }
8
+ function getTemplates(ts) {
9
+ return {
10
+ model: (name) => `/**
11
+ * ${name} Model
12
+ * Generated by Arc CLI
13
+ */
14
+
15
+ import mongoose${ts ? ", { type HydratedDocument }" : ""} from 'mongoose';
16
+
17
+ const { Schema } = mongoose;
18
+ ${ts ? `
19
+ type ${name} = {
20
+ name: string;
21
+ description?: string;
22
+ isActive: boolean;
23
+ };
24
+
25
+ export type ${name}Document = HydratedDocument<${name}>;
26
+ ` : ""}
27
+ const ${name.toLowerCase()}Schema = new Schema${ts ? `<${name}>` : ""}(
28
+ {
29
+ name: { type: String, required: true, trim: true },
30
+ description: { type: String, trim: true },
31
+ isActive: { type: Boolean, default: true },
32
+ },
33
+ { timestamps: true }
34
+ );
35
+
36
+ // Indexes
37
+ ${name.toLowerCase()}Schema.index({ name: 1 });
38
+ ${name.toLowerCase()}Schema.index({ isActive: 1 });
39
+
40
+ const ${name} = mongoose.models.${name}${ts ? ` as mongoose.Model<${name}>` : ""} || mongoose.model${ts ? `<${name}>` : ""}('${name}', ${name.toLowerCase()}Schema);
41
+ export default ${name};
42
+ `,
43
+ repository: (name) => `/**
44
+ * ${name} Repository
45
+ * Generated by Arc CLI
46
+ */
47
+
48
+ import {
49
+ Repository,
50
+ methodRegistryPlugin,
51
+ softDeletePlugin,
52
+ mongoOperationsPlugin,
53
+ } from '@classytic/mongokit';
54
+ ${ts ? `import type { ${name}Document } from './${name.toLowerCase()}.model.js';` : ""}
55
+ import ${name} from './${name.toLowerCase()}.model.js';
56
+
57
+ class ${name}Repository extends Repository${ts ? `<${name}Document>` : ""} {
58
+ constructor() {
59
+ super(${name}${ts ? " as any" : ""}, [
60
+ methodRegistryPlugin(),
61
+ softDeletePlugin(),
62
+ mongoOperationsPlugin(),
63
+ ]);
64
+ }
65
+
66
+ /**
67
+ * Find all active records
68
+ */
69
+ async findActive() {
70
+ return this.Model.find({ isActive: true, deletedAt: null }).lean();
71
+ }
72
+
73
+ // Add custom repository methods here
74
+ }
75
+
76
+ const ${name.toLowerCase()}Repository = new ${name}Repository();
77
+ export default ${name.toLowerCase()}Repository;
78
+ export { ${name}Repository };
79
+ `,
80
+ controller: (name) => `/**
81
+ * ${name} Controller
82
+ * Generated by Arc CLI
83
+ */
84
+
85
+ import { BaseController } from '@classytic/arc';
86
+ import ${name.toLowerCase()}Repository from './${name.toLowerCase()}.repository.js';
87
+ import { ${name.toLowerCase()}SchemaOptions } from './${name.toLowerCase()}.schemas.js';
88
+
89
+ class ${name}Controller extends BaseController {
90
+ constructor() {
91
+ super(${name.toLowerCase()}Repository${ts ? " as any" : ""}, { schemaOptions: ${name.toLowerCase()}SchemaOptions });
92
+ }
93
+
94
+ // Add custom controller methods here
95
+ }
96
+
97
+ const ${name.toLowerCase()}Controller = new ${name}Controller();
98
+ export default ${name.toLowerCase()}Controller;
99
+ `,
100
+ schemas: (name) => `/**
101
+ * ${name} Schemas
102
+ * Generated by Arc CLI
103
+ */
104
+
105
+ import ${name} from './${name.toLowerCase()}.model.js';
106
+ import { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';
107
+
108
+ /**
109
+ * CRUD Schemas with Field Rules
110
+ */
111
+ const crudSchemas = buildCrudSchemasFromModel(${name}, {
112
+ strictAdditionalProperties: true,
113
+ fieldRules: {
114
+ // Mark fields as system-managed (excluded from create/update)
115
+ // deletedAt: { systemManaged: true },
116
+ },
117
+ query: {
118
+ filterableFields: {
119
+ isActive: 'boolean',
120
+ createdAt: 'date',
121
+ },
122
+ },
123
+ });
124
+
125
+ // Schema options for controller
126
+ export const ${name.toLowerCase()}SchemaOptions${ts ? ": any" : ""} = {
127
+ query: {
128
+ filterableFields: {
129
+ isActive: 'boolean',
130
+ createdAt: 'date',
131
+ },
132
+ },
133
+ };
134
+
135
+ export default crudSchemas;
136
+ `,
137
+ resource: (name) => `/**
138
+ * ${name} Resource
139
+ * Generated by Arc CLI
140
+ */
141
+
142
+ import { defineResource } from '@classytic/arc';
143
+ import { createAdapter } from '#shared/adapter.js';
144
+ import { publicReadPermissions } from '#shared/permissions.js';
145
+ import ${name} from './${name.toLowerCase()}.model.js';
146
+ import ${name.toLowerCase()}Repository from './${name.toLowerCase()}.repository.js';
147
+ import ${name.toLowerCase()}Controller from './${name.toLowerCase()}.controller.js';
148
+
149
+ const ${name.toLowerCase()}Resource = defineResource({
150
+ name: '${name.toLowerCase()}',
151
+ displayName: '${name}s',
152
+ prefix: '/${name.toLowerCase()}s',
153
+
154
+ adapter: createAdapter(${name}, ${name.toLowerCase()}Repository),
155
+ controller: ${name.toLowerCase()}Controller,
156
+
157
+ presets: ['softDelete'],
158
+
159
+ permissions: publicReadPermissions,
160
+
161
+ // Add custom routes here:
162
+ // additionalRoutes: [
163
+ // {
164
+ // method: 'GET',
165
+ // path: '/custom',
166
+ // summary: 'Custom endpoint',
167
+ // handler: async (request, reply) => { ... },
168
+ // },
169
+ // ],
170
+ });
171
+
172
+ export default ${name.toLowerCase()}Resource;
173
+ `,
174
+ test: (name) => `/**
175
+ * ${name} Tests
176
+ * Generated by Arc CLI
177
+ */
178
+
179
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
180
+ import mongoose from 'mongoose';
181
+ import { createAppInstance } from '../src/app.js';
182
+ ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}
183
+ describe('${name} Resource', () => {
184
+ let app${ts ? ": FastifyInstance" : ""};
185
+
186
+ beforeAll(async () => {
187
+ const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/test-${name.toLowerCase()}';
188
+ await mongoose.connect(testDbUri);
189
+ app = await createAppInstance();
190
+ await app.ready();
191
+ });
192
+
193
+ afterAll(async () => {
194
+ await app.close();
195
+ await mongoose.connection.close();
196
+ });
197
+
198
+ describe('GET /${name.toLowerCase()}s', () => {
199
+ it('should return a list', async () => {
200
+ const response = await app.inject({
201
+ method: 'GET',
202
+ url: '/${name.toLowerCase()}s',
203
+ });
204
+
205
+ expect(response.statusCode).toBe(200);
206
+ const body = JSON.parse(response.body);
207
+ expect(body).toHaveProperty('docs');
208
+ });
209
+ });
210
+ });
211
+ `
212
+ };
213
+ }
214
+ async function generate(type, args) {
215
+ if (!type) {
216
+ console.error("Error: Missing type argument");
217
+ console.log("Usage: arc generate <resource|controller|model|repository|schemas> <name>");
218
+ process.exit(1);
219
+ }
220
+ const [name] = args;
221
+ if (!name) {
222
+ console.error("Error: Missing name argument");
223
+ console.log("Usage: arc generate <type> <name>");
224
+ console.log("Example: arc generate resource product");
225
+ process.exit(1);
226
+ }
227
+ const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
228
+ const lowerName = name.toLowerCase();
229
+ const ts = isTypeScriptProject();
230
+ const ext = ts ? "ts" : "js";
231
+ const templates = getTemplates(ts);
232
+ const resourcePath = join(process.cwd(), "src", "resources", lowerName);
233
+ switch (type) {
234
+ case "resource":
235
+ case "r":
236
+ await generateResource(capitalizedName, lowerName, resourcePath, templates, ext);
237
+ break;
238
+ case "controller":
239
+ case "c":
240
+ await generateFile(capitalizedName, lowerName, resourcePath, "controller", templates.controller, ext);
241
+ break;
242
+ case "model":
243
+ case "m":
244
+ await generateFile(capitalizedName, lowerName, resourcePath, "model", templates.model, ext);
245
+ break;
246
+ case "repository":
247
+ case "repo":
248
+ await generateFile(capitalizedName, lowerName, resourcePath, "repository", templates.repository, ext);
249
+ break;
250
+ case "schemas":
251
+ case "s":
252
+ await generateFile(capitalizedName, lowerName, resourcePath, "schemas", templates.schemas, ext);
253
+ break;
254
+ default:
255
+ console.error(`Unknown type: ${type}`);
256
+ console.log("Available types: resource, controller, model, repository, schemas");
257
+ process.exit(1);
258
+ }
259
+ }
260
+ async function generateResource(name, lowerName, resourcePath, templates, ext) {
261
+ console.log(`
262
+ 📦 Generating resource: ${name}...
263
+ `);
264
+ if (!existsSync(resourcePath)) {
265
+ mkdirSync(resourcePath, { recursive: true });
266
+ console.log(` 📁 Created: src/resources/${lowerName}/`);
267
+ }
268
+ const files = {
269
+ [`${lowerName}.model.${ext}`]: templates.model(name),
270
+ [`${lowerName}.repository.${ext}`]: templates.repository(name),
271
+ [`${lowerName}.controller.${ext}`]: templates.controller(name),
272
+ [`${lowerName}.schemas.${ext}`]: templates.schemas(name),
273
+ [`${lowerName}.resource.${ext}`]: templates.resource(name)
274
+ };
275
+ for (const [filename, content] of Object.entries(files)) {
276
+ const filepath = join(resourcePath, filename);
277
+ if (existsSync(filepath)) {
278
+ console.warn(` ⚠ Skipped: ${filename} (already exists)`);
279
+ } else {
280
+ writeFileSync(filepath, content);
281
+ console.log(` ✅ Created: ${filename}`);
282
+ }
283
+ }
284
+ const testsDir = join(process.cwd(), "tests");
285
+ if (!existsSync(testsDir)) {
286
+ mkdirSync(testsDir, { recursive: true });
287
+ }
288
+ const testPath = join(testsDir, `${lowerName}.test.${ext}`);
289
+ if (!existsSync(testPath)) {
290
+ writeFileSync(testPath, templates.test(name));
291
+ console.log(` ✅ Created: tests/${lowerName}.test.${ext}`);
292
+ }
293
+ console.log(`
294
+ ╔═══════════════════════════════════════════════════════════════╗
295
+ ║ ✅ Resource Generated! ║
296
+ ╚═══════════════════════════════════════════════════════════════╝
297
+
298
+ Next steps:
299
+
300
+ 1. Register in src/resources/index.${ext}:
301
+ import ${lowerName}Resource from './${lowerName}/${lowerName}.resource.js';
302
+
303
+ export const resources = [
304
+ // ... existing resources
305
+ ${lowerName}Resource,
306
+ ];
307
+
308
+ 2. Customize the model schema in:
309
+ src/resources/${lowerName}/${lowerName}.model.${ext}
310
+
311
+ 3. Run tests:
312
+ npm test
313
+ `);
314
+ }
315
+ async function generateFile(name, lowerName, resourcePath, fileType, template, ext) {
316
+ console.log(`
317
+ 📦 Generating ${fileType}: ${name}...
318
+ `);
319
+ if (!existsSync(resourcePath)) {
320
+ mkdirSync(resourcePath, { recursive: true });
321
+ console.log(` 📁 Created: src/resources/${lowerName}/`);
322
+ }
323
+ const filename = `${lowerName}.${fileType}.${ext}`;
324
+ const filepath = join(resourcePath, filename);
325
+ if (existsSync(filepath)) {
326
+ console.error(` ❌ Error: ${filename} already exists`);
327
+ process.exit(1);
328
+ }
329
+ writeFileSync(filepath, template(name));
330
+ console.log(` ✅ Created: ${filename}`);
331
+ }
332
+ var generate_default = generate;
333
+
334
+ export { generate_default as default, generate };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Arc CLI - Init Command
3
+ *
4
+ * Scaffolds a new Arc project with clean architecture:
5
+ * - MongoKit or Custom adapter
6
+ * - Multi-tenant or Single-tenant
7
+ * - TypeScript or JavaScript
8
+ *
9
+ * Automatically installs dependencies using detected package manager.
10
+ */
11
+ interface InitOptions {
12
+ name?: string;
13
+ adapter?: 'mongokit' | 'custom';
14
+ tenant?: 'multi' | 'single';
15
+ typescript?: boolean;
16
+ skipInstall?: boolean;
17
+ force?: boolean;
18
+ }
19
+ /**
20
+ * Initialize a new Arc project
21
+ */
22
+ declare function init(options?: InitOptions): Promise<void>;
23
+
24
+ export { type InitOptions, init as default, init };