@classytic/arc 1.0.0 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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 };