@classytic/arc 1.0.0

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +900 -0
  3. package/bin/arc.js +344 -0
  4. package/dist/adapters/index.d.ts +237 -0
  5. package/dist/adapters/index.js +668 -0
  6. package/dist/arcCorePlugin-DTPWXcZN.d.ts +273 -0
  7. package/dist/audit/index.d.ts +195 -0
  8. package/dist/audit/index.js +319 -0
  9. package/dist/auth/index.d.ts +47 -0
  10. package/dist/auth/index.js +174 -0
  11. package/dist/cli/commands/docs.d.ts +11 -0
  12. package/dist/cli/commands/docs.js +474 -0
  13. package/dist/cli/commands/introspect.d.ts +8 -0
  14. package/dist/cli/commands/introspect.js +338 -0
  15. package/dist/cli/index.d.ts +43 -0
  16. package/dist/cli/index.js +520 -0
  17. package/dist/createApp-pzUAkzbz.d.ts +77 -0
  18. package/dist/docs/index.d.ts +166 -0
  19. package/dist/docs/index.js +650 -0
  20. package/dist/errors-8WIxGS_6.d.ts +122 -0
  21. package/dist/events/index.d.ts +117 -0
  22. package/dist/events/index.js +89 -0
  23. package/dist/factory/index.d.ts +38 -0
  24. package/dist/factory/index.js +1664 -0
  25. package/dist/hooks/index.d.ts +4 -0
  26. package/dist/hooks/index.js +199 -0
  27. package/dist/idempotency/index.d.ts +323 -0
  28. package/dist/idempotency/index.js +500 -0
  29. package/dist/index-DkAW8BXh.d.ts +1302 -0
  30. package/dist/index.d.ts +331 -0
  31. package/dist/index.js +4734 -0
  32. package/dist/migrations/index.d.ts +185 -0
  33. package/dist/migrations/index.js +274 -0
  34. package/dist/org/index.d.ts +129 -0
  35. package/dist/org/index.js +220 -0
  36. package/dist/permissions/index.d.ts +144 -0
  37. package/dist/permissions/index.js +100 -0
  38. package/dist/plugins/index.d.ts +46 -0
  39. package/dist/plugins/index.js +1069 -0
  40. package/dist/policies/index.d.ts +398 -0
  41. package/dist/policies/index.js +196 -0
  42. package/dist/presets/index.d.ts +336 -0
  43. package/dist/presets/index.js +382 -0
  44. package/dist/presets/multiTenant.d.ts +39 -0
  45. package/dist/presets/multiTenant.js +112 -0
  46. package/dist/registry/index.d.ts +16 -0
  47. package/dist/registry/index.js +253 -0
  48. package/dist/testing/index.d.ts +618 -0
  49. package/dist/testing/index.js +48032 -0
  50. package/dist/types/index.d.ts +4 -0
  51. package/dist/types/index.js +8 -0
  52. package/dist/types-0IPhH_NR.d.ts +143 -0
  53. package/dist/types-B99TBmFV.d.ts +76 -0
  54. package/dist/utils/index.d.ts +655 -0
  55. package/dist/utils/index.js +905 -0
  56. package/package.json +227 -0
@@ -0,0 +1,520 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+
4
+ // src/cli/index.ts
5
+ async function generate(type, name, options = {}) {
6
+ const validTypes = ["resource", "controller", "model"];
7
+ if (!validTypes.includes(type)) {
8
+ throw new Error(`Unknown generation type: ${type}. Valid types: ${validTypes.join(", ")}`);
9
+ }
10
+ const {
11
+ module: moduleName,
12
+ presets = [],
13
+ parentField = "parent",
14
+ withTests = true,
15
+ dryRun = false,
16
+ force = false,
17
+ outputDir = process.cwd(),
18
+ typescript = true
19
+ } = options;
20
+ const SAFE_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9-]*$/;
21
+ if (!SAFE_NAME_PATTERN.test(name)) {
22
+ throw new Error(`Invalid resource name: "${name}". Use only letters, numbers, and hyphens (must start with letter).`);
23
+ }
24
+ if (moduleName && !SAFE_NAME_PATTERN.test(moduleName)) {
25
+ throw new Error(`Invalid module name: "${moduleName}". Use only letters, numbers, and hyphens (must start with letter).`);
26
+ }
27
+ const ext = typescript ? "ts" : "js";
28
+ const kebab = kebabCase(name);
29
+ const dirPath = moduleName ? path.join(outputDir, "modules", moduleName, kebab) : path.join(outputDir, "modules", kebab);
30
+ let files;
31
+ switch (type) {
32
+ case "resource":
33
+ files = generateResourceFiles(name, { presets, parentField, module: moduleName, withTests, typescript });
34
+ break;
35
+ case "controller":
36
+ files = [{ name: `${kebab}.controller.${ext}`, content: controllerTemplate(name, { presets, typescript }) }];
37
+ break;
38
+ case "model":
39
+ files = [{ name: `${kebab}.model.${ext}`, content: modelTemplate(name, { presets, parentField, typescript }) }];
40
+ break;
41
+ default:
42
+ throw new Error(`Unknown type: ${type}`);
43
+ }
44
+ console.log(`
45
+ šŸ“¦ Generating ${type}: ${pascalCase(name)}`);
46
+ console.log(`šŸ“ Directory: ${dirPath}`);
47
+ console.log(`šŸ”§ Presets: ${presets.length ? presets.join(", ") : "none"}`);
48
+ console.log(`šŸ“ Language: ${typescript ? "TypeScript" : "JavaScript"}`);
49
+ if (dryRun) {
50
+ console.log("\nšŸƒ DRY RUN - No files created\n");
51
+ for (const file of files) {
52
+ console.log(` Would create: ${file.name}`);
53
+ }
54
+ return { files, dirPath };
55
+ }
56
+ await fs.mkdir(dirPath, { recursive: true });
57
+ let created = 0;
58
+ let skipped = 0;
59
+ for (const file of files) {
60
+ const filePath = path.join(dirPath, file.name);
61
+ try {
62
+ await fs.access(filePath);
63
+ if (!force) {
64
+ console.log(` ā­ļø Skipped: ${file.name} (exists)`);
65
+ skipped++;
66
+ continue;
67
+ }
68
+ } catch {
69
+ }
70
+ await fs.writeFile(filePath, file.content);
71
+ console.log(` āœ… Created: ${file.name}`);
72
+ created++;
73
+ }
74
+ console.log(`
75
+ šŸŽ‰ Done! Created ${created} file(s), skipped ${skipped}
76
+ `);
77
+ if (type === "resource") {
78
+ printNextSteps(name, moduleName);
79
+ }
80
+ return { files, dirPath };
81
+ }
82
+ function generateResourceFiles(name, options) {
83
+ const { presets, parentField, module: moduleName, withTests, typescript } = options;
84
+ const kebab = kebabCase(name);
85
+ const ext = typescript ? "ts" : "js";
86
+ const files = [
87
+ { name: `${kebab}.model.${ext}`, content: modelTemplate(name, { presets, parentField, typescript }) },
88
+ { name: `${kebab}.repository.${ext}`, content: repositoryTemplate(name, { presets, parentField, typescript }) },
89
+ { name: `${kebab}.controller.${ext}`, content: controllerTemplate(name, { presets, typescript }) },
90
+ { name: `${kebab}.resource.${ext}`, content: resourceTemplate(name, { presets, parentField, module: moduleName}) },
91
+ { name: `routes.${ext}`, content: routesTemplate(name) }
92
+ ];
93
+ if (withTests) {
94
+ files.push({ name: `${kebab}.test.${ext}`, content: testTemplate(name, { presets, typescript }) });
95
+ }
96
+ return files;
97
+ }
98
+ function modelTemplate(name, opts) {
99
+ const pascal = pascalCase(name);
100
+ const camel = camelCase(name);
101
+ const { presets = [], parentField = "parent", typescript = true } = opts;
102
+ const hasSlug = presets.includes("slugLookup");
103
+ const hasSoftDelete = presets.includes("softDelete");
104
+ const hasTree = presets.includes("tree");
105
+ const hasMultiTenant = presets.includes("multiTenant");
106
+ const hasOwned = presets.includes("ownedByUser");
107
+ const hasAudited = presets.includes("audited");
108
+ return `/**
109
+ * ${pascal} Model
110
+ * @generated by Arc CLI
111
+ */
112
+
113
+ import mongoose from 'mongoose';
114
+ ${hasSlug ? "import slugPlugin from '@classytic/mongoose-slug-plugin';\n" : ""}
115
+ const ${camel}Schema = new mongoose.Schema(
116
+ {
117
+ name: { type: String, required: true, trim: true },
118
+ ${hasSlug ? " slug: { type: String, unique: true, sparse: true, index: true },\n" : ""}${hasTree ? ` ${parentField}: { type: mongoose.Schema.Types.ObjectId, ref: '${pascal}', default: null, index: true },
119
+ displayOrder: { type: Number, default: 0 },
120
+ ` : ""}${hasMultiTenant ? " organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', required: true, index: true },\n" : ""}${hasOwned ? " createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true },\n" : ""} description: { type: String, trim: true },
121
+ isActive: { type: Boolean, default: true, index: true },
122
+ ${hasSoftDelete ? " deletedAt: { type: Date, default: null, index: true },\n" : ""}${hasAudited ? ` lastModifiedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
123
+ ` : ""} },
124
+ {
125
+ timestamps: true,
126
+ toJSON: { virtuals: true },
127
+ toObject: { virtuals: true },
128
+ }
129
+ );
130
+
131
+ // Indexes
132
+ ${camel}Schema.index({ name: 1 });
133
+ ${hasSoftDelete ? `${camel}Schema.index({ deletedAt: 1, isActive: 1 });
134
+ ` : ""}${hasSlug ? `
135
+ ${camel}Schema.plugin(slugPlugin, { sourceField: 'name' });
136
+ ` : ""}
137
+ ${typescript ? `export type ${pascal}Document = mongoose.InferSchemaType<typeof ${camel}Schema>;
138
+ ` : ""}
139
+ export const ${pascal} = mongoose.model${typescript ? `<${pascal}Document>` : ""}('${pascal}', ${camel}Schema);
140
+ export default ${pascal};
141
+ `;
142
+ }
143
+ function repositoryTemplate(name, opts) {
144
+ const pascal = pascalCase(name);
145
+ const camel = camelCase(name);
146
+ const kebab = kebabCase(name);
147
+ const { presets = [], parentField = "parent", typescript = true } = opts;
148
+ const hasSoftDelete = presets.includes("softDelete");
149
+ const hasSlug = presets.includes("slugLookup");
150
+ const hasTree = presets.includes("tree");
151
+ const typeImport = typescript ? `
152
+ import type { ${pascal}Document } from './${kebab}.model.js';` : "";
153
+ const repoGeneric = typescript ? `<${pascal}Document>` : "";
154
+ const returnType = (t) => typescript ? `: Promise<${t}>` : "";
155
+ return `/**
156
+ * ${pascal} Repository
157
+ * @generated by Arc CLI
158
+ */
159
+
160
+ import { Repository${hasSoftDelete ? ", softDeletePlugin" : ""} } from '@classytic/mongokit';
161
+ import { ${pascal} } from './${kebab}.model.js';${typeImport}
162
+
163
+ class ${pascal}Repository extends Repository${repoGeneric} {
164
+ constructor() {
165
+ super(${pascal}${hasSoftDelete ? ", [softDeletePlugin()]" : ""});
166
+ }
167
+
168
+ /** Find all active records */
169
+ async findActive()${returnType(`${pascal}Document[]`)} {
170
+ return this.Model.find({ isActive: true${hasSoftDelete ? ", deletedAt: null" : ""} }).lean();
171
+ }
172
+ ${hasSlug ? `
173
+ /** Find by slug */
174
+ async getBySlug(slug${typescript ? ": string" : ""})${returnType(`${pascal}Document | null`)} {
175
+ return this.Model.findOne({ slug: slug.toLowerCase()${hasSoftDelete ? ", deletedAt: null" : ""} }).lean();
176
+ }
177
+ ` : ""}${hasSoftDelete ? `
178
+ /** Get soft-deleted records */
179
+ async getDeleted()${returnType(`${pascal}Document[]`)} {
180
+ return this.Model.find({ deletedAt: { $ne: null } }).sort({ deletedAt: -1 }).lean();
181
+ }
182
+
183
+ /** Restore a soft-deleted record */
184
+ async restore(id${typescript ? ": string" : ""})${returnType(`${pascal}Document | null`)} {
185
+ return this.Model.findByIdAndUpdate(id, { deletedAt: null }, { new: true }).lean();
186
+ }
187
+ ` : ""}${hasTree ? `
188
+ /** Get hierarchical tree structure */
189
+ async getTree()${returnType(`${pascal}Document[]`)} {
190
+ const all = await this.Model.find({ isActive: true${hasSoftDelete ? ", deletedAt: null" : ""} })
191
+ .sort({ displayOrder: 1 })
192
+ .lean();
193
+
194
+ const map = new Map${typescript ? `<string, ${pascal}Document & { children: ${pascal}Document[] }>` : ""}();
195
+ const roots${typescript ? `: (${pascal}Document & { children: ${pascal}Document[] })[]` : ""} = [];
196
+
197
+ for (const item of all) {
198
+ map.set(item._id.toString(), { ...item, children: [] });
199
+ }
200
+
201
+ for (const item of all) {
202
+ const node = map.get(item._id.toString())!;
203
+ const parentId = (item${typescript ? " as any" : ""}).${parentField};
204
+ if (parentId && map.has(parentId.toString())) {
205
+ map.get(parentId.toString())!.children.push(node);
206
+ } else {
207
+ roots.push(node);
208
+ }
209
+ }
210
+
211
+ return roots;
212
+ }
213
+
214
+ /** Get direct children of a parent */
215
+ async getChildren(parentId${typescript ? ": string" : ""})${returnType(`${pascal}Document[]`)} {
216
+ return this.Model.find({
217
+ ${parentField}: parentId,
218
+ isActive: true${hasSoftDelete ? ",\n deletedAt: null" : ""},
219
+ }).sort({ displayOrder: 1 }).lean();
220
+ }
221
+ ` : ""}}
222
+
223
+ export const ${camel}Repository = new ${pascal}Repository();
224
+ export default ${camel}Repository;
225
+ `;
226
+ }
227
+ function controllerTemplate(name, opts) {
228
+ const pascal = pascalCase(name);
229
+ const camel = camelCase(name);
230
+ const kebab = kebabCase(name);
231
+ const { presets = [], typescript = true } = opts;
232
+ const hasSoftDelete = presets.includes("softDelete");
233
+ const hasSlug = presets.includes("slugLookup");
234
+ const hasTree = presets.includes("tree");
235
+ return `/**
236
+ * ${pascal} Controller
237
+ * @generated by Arc CLI
238
+ *
239
+ * Extends BaseController for built-in security:
240
+ * - Organization scoping (multi-tenant isolation)
241
+ * - Ownership checks (user data protection)
242
+ * - Policy-based filtering
243
+ */
244
+
245
+ import { BaseController } from '@classytic/arc';
246
+ ${typescript ? "import type { IRequestContext, IControllerResponse } from '@classytic/arc';\n" : ""}import { ${camel}Repository } from './${kebab}.repository.js';
247
+
248
+ class ${pascal}Controller extends BaseController {
249
+ constructor() {
250
+ super(${camel}Repository);
251
+
252
+ // Bind methods (required for route handler context)
253
+ ${hasSlug ? " this.getBySlug = this.getBySlug.bind(this);\n" : ""}${hasSoftDelete ? ` this.getDeleted = this.getDeleted.bind(this);
254
+ this.restore = this.restore.bind(this);
255
+ ` : ""}${hasTree ? ` this.getTree = this.getTree.bind(this);
256
+ this.getChildren = this.getChildren.bind(this);
257
+ ` : ""} }
258
+
259
+ // ========================================
260
+ // Custom Methods (add your own below)
261
+ // ========================================
262
+
263
+ // Example: Custom search endpoint
264
+ // async search(ctx${typescript ? ": IRequestContext" : ""})${typescript ? ": Promise<IControllerResponse>" : ""} {
265
+ // const { query } = ctx.query${typescript ? " as { query: string }" : ""};
266
+ // const results = await this.repository.Model.find({
267
+ // name: { $regex: query, $options: 'i' },
268
+ // }).lean();
269
+ // return { success: true, data: results, status: 200 };
270
+ // }
271
+ }
272
+
273
+ export const ${camel}Controller = new ${pascal}Controller();
274
+ export default ${camel}Controller;
275
+ `;
276
+ }
277
+ function resourceTemplate(name, opts) {
278
+ const pascal = pascalCase(name);
279
+ const camel = camelCase(name);
280
+ const kebab = kebabCase(name);
281
+ const { presets = [], parentField = "parent", module: moduleName} = opts;
282
+ const presetsStr = presets.length > 0 ? presets.map((p) => {
283
+ if (p === "tree" && parentField !== "parent") {
284
+ return `{ name: 'tree', parentField: '${parentField}' }`;
285
+ }
286
+ return `'${p}'`;
287
+ }).join(",\n ") : "";
288
+ return `/**
289
+ * ${pascal} Resource Definition
290
+ * @generated by Arc CLI
291
+ */
292
+
293
+ import { defineResource, createMongooseAdapter } from '@classytic/arc';
294
+ import { ${pascal} } from './${kebab}.model.js';
295
+ import { ${camel}Repository } from './${kebab}.repository.js';
296
+
297
+ export default defineResource({
298
+ name: '${kebab}',
299
+ displayName: '${pascal}s',
300
+ ${moduleName ? ` module: '${moduleName}',
301
+ ` : ""}
302
+ adapter: createMongooseAdapter({
303
+ model: ${pascal},
304
+ repository: ${camel}Repository,
305
+ }),
306
+ ${presetsStr ? `
307
+ presets: [
308
+ ${presetsStr},
309
+ ],
310
+ ` : ""}
311
+ permissions: {
312
+ list: [],
313
+ get: [],
314
+ create: ['admin'],
315
+ update: ['admin'],
316
+ delete: ['admin'],
317
+ },
318
+
319
+ additionalRoutes: [],
320
+
321
+ events: {
322
+ created: { description: '${pascal} created' },
323
+ updated: { description: '${pascal} updated' },
324
+ deleted: { description: '${pascal} deleted' },
325
+ },
326
+ });
327
+ `;
328
+ }
329
+ function routesTemplate(name, opts) {
330
+ const camel = camelCase(name);
331
+ const kebab = kebabCase(name);
332
+ return `/**
333
+ * ${pascalCase(name)} Routes
334
+ * @generated by Arc CLI
335
+ *
336
+ * Register this plugin in your app:
337
+ * await fastify.register(${camel}Routes);
338
+ */
339
+
340
+ import ${camel}Resource from './${kebab}.resource.js';
341
+
342
+ export default ${camel}Resource.toPlugin();
343
+ `;
344
+ }
345
+ function testTemplate(name, opts) {
346
+ const pascal = pascalCase(name);
347
+ const kebab = kebabCase(name);
348
+ const { presets = [], typescript = true } = opts;
349
+ const hasSoftDelete = presets.includes("softDelete");
350
+ const hasSlug = presets.includes("slugLookup");
351
+ return `/**
352
+ * ${pascal} Tests
353
+ * @generated by Arc CLI
354
+ */
355
+
356
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
357
+ ${typescript ? "import type { FastifyInstance } from 'fastify';\n" : ""}import { createTestApp } from '@classytic/arc/testing';
358
+
359
+ describe('${pascal} API', () => {
360
+ let app${typescript ? ": FastifyInstance" : ""};
361
+
362
+ beforeAll(async () => {
363
+ app = await createTestApp({
364
+ auth: { jwt: { secret: 'test-secret-32-chars-minimum-len' } },
365
+ });
366
+ });
367
+
368
+ afterAll(async () => {
369
+ await app?.close();
370
+ });
371
+
372
+ describe('CRUD Operations', () => {
373
+ let createdId${typescript ? ": string" : ""};
374
+
375
+ it('should create a ${kebab}', async () => {
376
+ const response = await app.inject({
377
+ method: 'POST',
378
+ url: '/${kebab}s',
379
+ payload: { name: 'Test ${pascal}' },
380
+ });
381
+
382
+ expect(response.statusCode).toBe(201);
383
+ const body = response.json();
384
+ expect(body.success).toBe(true);
385
+ expect(body.data.name).toBe('Test ${pascal}');
386
+ createdId = body.data._id;
387
+ });
388
+
389
+ it('should list ${kebab}s', async () => {
390
+ const response = await app.inject({
391
+ method: 'GET',
392
+ url: '/${kebab}s',
393
+ });
394
+
395
+ expect(response.statusCode).toBe(200);
396
+ const body = response.json();
397
+ expect(body.success).toBe(true);
398
+ expect(Array.isArray(body.docs || body.data)).toBe(true);
399
+ });
400
+
401
+ it('should get ${kebab} by id', async () => {
402
+ const response = await app.inject({
403
+ method: 'GET',
404
+ url: \`/${kebab}s/\${createdId}\`,
405
+ });
406
+
407
+ expect(response.statusCode).toBe(200);
408
+ expect(response.json().data._id).toBe(createdId);
409
+ });
410
+
411
+ it('should update ${kebab}', async () => {
412
+ const response = await app.inject({
413
+ method: 'PATCH',
414
+ url: \`/${kebab}s/\${createdId}\`,
415
+ payload: { name: 'Updated ${pascal}' },
416
+ });
417
+
418
+ expect(response.statusCode).toBe(200);
419
+ expect(response.json().data.name).toBe('Updated ${pascal}');
420
+ });
421
+
422
+ it('should delete ${kebab}', async () => {
423
+ const response = await app.inject({
424
+ method: 'DELETE',
425
+ url: \`/${kebab}s/\${createdId}\`,
426
+ });
427
+
428
+ expect(response.statusCode).toBe(200);
429
+ expect(response.json().success).toBe(true);
430
+ });
431
+ });
432
+ ${hasSlug ? `
433
+ describe('Slug Lookup', () => {
434
+ it('should get by slug', async () => {
435
+ // Create with auto-generated slug
436
+ const createRes = await app.inject({
437
+ method: 'POST',
438
+ url: '/${kebab}s',
439
+ payload: { name: 'Slug Test Item' },
440
+ });
441
+ const slug = createRes.json().data.slug;
442
+
443
+ const response = await app.inject({
444
+ method: 'GET',
445
+ url: \`/${kebab}s/slug/\${slug}\`,
446
+ });
447
+
448
+ expect(response.statusCode).toBe(200);
449
+ expect(response.json().data.slug).toBe(slug);
450
+ });
451
+ });
452
+ ` : ""}${hasSoftDelete ? `
453
+ describe('Soft Delete', () => {
454
+ it('should soft delete and restore', async () => {
455
+ // Create
456
+ const createRes = await app.inject({
457
+ method: 'POST',
458
+ url: '/${kebab}s',
459
+ payload: { name: 'Soft Delete Test' },
460
+ });
461
+ const id = createRes.json().data._id;
462
+
463
+ // Delete (soft)
464
+ await app.inject({
465
+ method: 'DELETE',
466
+ url: \`/${kebab}s/\${id}\`,
467
+ });
468
+
469
+ // Verify in deleted list
470
+ const deletedRes = await app.inject({
471
+ method: 'GET',
472
+ url: '/${kebab}s/deleted',
473
+ });
474
+ expect(deletedRes.json().data.some((d${typescript ? ": any" : ""}) => d._id === id)).toBe(true);
475
+
476
+ // Restore
477
+ const restoreRes = await app.inject({
478
+ method: 'POST',
479
+ url: \`/${kebab}s/\${id}/restore\`,
480
+ });
481
+ expect(restoreRes.statusCode).toBe(200);
482
+ });
483
+ });
484
+ ` : ""}});
485
+ `;
486
+ }
487
+ function printNextSteps(name, moduleName) {
488
+ const kebab = kebabCase(name);
489
+ const camel = camelCase(name);
490
+ const modulePath = moduleName ? `${moduleName}/${kebab}` : kebab;
491
+ console.log(`šŸ“‹ Next Steps:
492
+
493
+ 1. Register the route in your app:
494
+ ${`import ${camel}Routes from '#modules/${modulePath}/routes.js';
495
+ await fastify.register(${camel}Routes);`}
496
+
497
+ 2. Run tests:
498
+ npm test -- ${kebab}
499
+
500
+ 3. Access your API:
501
+ GET /${kebab}s List all
502
+ GET /${kebab}s/:id Get by ID
503
+ POST /${kebab}s Create
504
+ PATCH /${kebab}s/:id Update
505
+ DELETE /${kebab}s/:id Delete
506
+ `);
507
+ }
508
+ function pascalCase(str) {
509
+ return str.split(/[-_\s]+/).map((s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()).join("");
510
+ }
511
+ function camelCase(str) {
512
+ const pascal = pascalCase(str);
513
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
514
+ }
515
+ function kebabCase(str) {
516
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
517
+ }
518
+ var cli_default = { generate };
519
+
520
+ export { cli_default as default, generate };
@@ -0,0 +1,77 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ import { C as CreateAppOptions } from './types-0IPhH_NR.js';
3
+
4
+ /**
5
+ * ArcFactory - Production-ready Fastify application factory
6
+ *
7
+ * Enforces security best practices by making plugins opt-out instead of opt-in.
8
+ * A developer must explicitly disable security features rather than forget to enable them.
9
+ *
10
+ * Note: Arc is database-agnostic. Connect your database separately and provide
11
+ * adapters when defining resources. This allows multiple databases, custom
12
+ * connection pooling, and full control over your data layer.
13
+ *
14
+ * @example
15
+ * // 1. Connect your database(s) separately
16
+ * import mongoose from 'mongoose';
17
+ * await mongoose.connect(process.env.MONGO_URI);
18
+ *
19
+ * // 2. Create Arc app (no database config needed)
20
+ * const app = await createApp({
21
+ * preset: 'production',
22
+ * auth: { jwt: { secret: process.env.JWT_SECRET } },
23
+ * cors: { origin: ['https://example.com'] },
24
+ * });
25
+ *
26
+ * // 3. Register resources with your adapters
27
+ * await app.register(productResource.toPlugin());
28
+ *
29
+ * @example
30
+ * // Multiple databases example
31
+ * const primaryDb = await mongoose.connect(process.env.PRIMARY_DB);
32
+ * const analyticsDb = mongoose.createConnection(process.env.ANALYTICS_DB);
33
+ *
34
+ * const orderResource = defineResource({
35
+ * adapter: createMongooseAdapter({ model: OrderModel, repository: orderRepo }),
36
+ * });
37
+ *
38
+ * const analyticsResource = defineResource({
39
+ * adapter: createMongooseAdapter({ model: AnalyticsModel, repository: analyticsRepo }),
40
+ * });
41
+ */
42
+
43
+ /**
44
+ * Create a production-ready Fastify application with Arc framework
45
+ *
46
+ * Security plugins are enabled by default (opt-out):
47
+ * - helmet (security headers)
48
+ * - cors (cross-origin requests)
49
+ * - rateLimit (DDoS protection)
50
+ * - underPressure (health monitoring)
51
+ *
52
+ * Note: Compression is not included due to known Fastify 5 issues.
53
+ * Use a reverse proxy (Nginx, Caddy) or CDN for compression.
54
+ *
55
+ * @param options - Application configuration
56
+ * @returns Configured Fastify instance
57
+ */
58
+ declare function createApp(options: CreateAppOptions): Promise<FastifyInstance>;
59
+ /**
60
+ * Quick factory for common scenarios
61
+ */
62
+ declare const ArcFactory: {
63
+ /**
64
+ * Create production app with strict security
65
+ */
66
+ production(options: Omit<CreateAppOptions, "preset">): Promise<FastifyInstance>;
67
+ /**
68
+ * Create development app with relaxed security
69
+ */
70
+ development(options: Omit<CreateAppOptions, "preset">): Promise<FastifyInstance>;
71
+ /**
72
+ * Create testing app with minimal setup
73
+ */
74
+ testing(options: Omit<CreateAppOptions, "preset">): Promise<FastifyInstance>;
75
+ };
76
+
77
+ export { ArcFactory as A, createApp as c };