@classytic/arc 1.0.5 → 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.
- package/README.md +930 -0
- package/dist/BaseController-nNRS3vpA.d.ts +233 -0
- package/dist/adapters/index.d.ts +2 -2
- package/dist/{arcCorePlugin-Cqi7j5-_.d.ts → arcCorePlugin-CAjBQtZB.d.ts} +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/cli/commands/init.js +21 -6
- package/dist/cli/index.js +21 -6
- package/dist/core/index.d.ts +220 -0
- package/dist/core/index.js +2764 -0
- package/dist/{createApp-9q_I1la4.d.ts → createApp-CjN9zZSL.d.ts} +1 -1
- package/dist/factory/index.d.ts +4 -4
- package/dist/hooks/index.d.ts +1 -1
- package/dist/{index-WBEvhmWM.d.ts → index-D5QTob1X.d.ts} +1 -1
- package/dist/index.d.ts +7 -236
- package/dist/index.js +5 -2
- package/dist/org/index.d.ts +1 -1
- package/dist/permissions/index.js +5 -2
- package/dist/plugins/index.d.ts +2 -2
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +3 -1
- package/dist/presets/multiTenant.d.ts +1 -1
- package/dist/registry/index.d.ts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/{types-EBZBXrg-.d.ts → types-zpN48n6B.d.ts} +1 -1
- package/dist/utils/index.d.ts +28 -4
- package/dist/utils/index.js +17 -8
- package/package.json +1 -1
|
@@ -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 };
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DataAdapter, y as CrudRepository, o as RepositoryLike, m as SchemaMetadata,
|
|
2
|
-
export { ag as AdapterFactory, F as FieldMetadata, n as RelationMetadata } from '../index-
|
|
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';
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -97,7 +97,6 @@ async function installDependencies(projectPath, config, pm) {
|
|
|
97
97
|
devDeps.push(
|
|
98
98
|
"typescript@latest",
|
|
99
99
|
"@types/node@latest",
|
|
100
|
-
"@types/bcryptjs@latest",
|
|
101
100
|
"@types/jsonwebtoken@latest",
|
|
102
101
|
"tsx@latest"
|
|
103
102
|
);
|
|
@@ -601,6 +600,9 @@ export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : "
|
|
|
601
600
|
},
|
|
602
601
|
cors: {
|
|
603
602
|
origin: config.cors.origins,
|
|
603
|
+
methods: config.cors.methods,
|
|
604
|
+
allowedHeaders: config.cors.allowedHeaders,
|
|
605
|
+
credentials: config.cors.credentials,
|
|
604
606
|
},
|
|
605
607
|
});
|
|
606
608
|
|
|
@@ -675,7 +677,10 @@ HOST=0.0.0.0
|
|
|
675
677
|
JWT_SECRET=dev-secret-change-in-production-min-32-chars
|
|
676
678
|
JWT_EXPIRES_IN=7d
|
|
677
679
|
|
|
678
|
-
# CORS
|
|
680
|
+
# CORS - Allowed origins
|
|
681
|
+
# Options:
|
|
682
|
+
# * = allow all origins (not recommended for production)
|
|
683
|
+
# Comma-separated list = specific origins only
|
|
679
684
|
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
|
680
685
|
`;
|
|
681
686
|
if (config.adapter === "mongokit") {
|
|
@@ -1319,7 +1324,10 @@ export interface AppConfig {
|
|
|
1319
1324
|
expiresIn: string;
|
|
1320
1325
|
};
|
|
1321
1326
|
cors: {
|
|
1322
|
-
origins: string[];
|
|
1327
|
+
origins: string[] | boolean; // true = allow all ('*')
|
|
1328
|
+
methods: string[];
|
|
1329
|
+
allowedHeaders: string[];
|
|
1330
|
+
credentials: boolean;
|
|
1323
1331
|
};${config.adapter === "mongokit" ? `
|
|
1324
1332
|
database: {
|
|
1325
1333
|
uri: string;
|
|
@@ -1351,7 +1359,14 @@ const config${ts ? ": AppConfig" : ""} = {
|
|
|
1351
1359
|
},
|
|
1352
1360
|
|
|
1353
1361
|
cors: {
|
|
1354
|
-
|
|
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,
|
|
1355
1370
|
},
|
|
1356
1371
|
${config.adapter === "mongokit" ? `
|
|
1357
1372
|
database: {
|
|
@@ -1405,7 +1420,7 @@ export default Example;
|
|
|
1405
1420
|
}
|
|
1406
1421
|
function exampleRepositoryTemplate(config) {
|
|
1407
1422
|
const ts = config.typescript;
|
|
1408
|
-
const typeImport = ts ? "import type { ExampleDocument } from './model.js';\n" : "";
|
|
1423
|
+
const typeImport = ts ? "import type { ExampleDocument } from './example.model.js';\n" : "";
|
|
1409
1424
|
const generic = ts ? "<ExampleDocument>" : "";
|
|
1410
1425
|
return `/**
|
|
1411
1426
|
* Example Repository
|
|
@@ -1798,7 +1813,7 @@ export default User;
|
|
|
1798
1813
|
}
|
|
1799
1814
|
function userRepositoryTemplate(config) {
|
|
1800
1815
|
const ts = config.typescript;
|
|
1801
|
-
const typeImport = ts ? "import type { UserDocument } from './model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : "";
|
|
1816
|
+
const typeImport = ts ? "import type { UserDocument } from './user.model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : "";
|
|
1802
1817
|
return `/**
|
|
1803
1818
|
* User Repository
|
|
1804
1819
|
* Generated by Arc CLI
|
package/dist/cli/index.js
CHANGED
|
@@ -712,7 +712,6 @@ async function installDependencies(projectPath, config, pm) {
|
|
|
712
712
|
devDeps.push(
|
|
713
713
|
"typescript@latest",
|
|
714
714
|
"@types/node@latest",
|
|
715
|
-
"@types/bcryptjs@latest",
|
|
716
715
|
"@types/jsonwebtoken@latest",
|
|
717
716
|
"tsx@latest"
|
|
718
717
|
);
|
|
@@ -1216,6 +1215,9 @@ export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : "
|
|
|
1216
1215
|
},
|
|
1217
1216
|
cors: {
|
|
1218
1217
|
origin: config.cors.origins,
|
|
1218
|
+
methods: config.cors.methods,
|
|
1219
|
+
allowedHeaders: config.cors.allowedHeaders,
|
|
1220
|
+
credentials: config.cors.credentials,
|
|
1219
1221
|
},
|
|
1220
1222
|
});
|
|
1221
1223
|
|
|
@@ -1290,7 +1292,10 @@ HOST=0.0.0.0
|
|
|
1290
1292
|
JWT_SECRET=dev-secret-change-in-production-min-32-chars
|
|
1291
1293
|
JWT_EXPIRES_IN=7d
|
|
1292
1294
|
|
|
1293
|
-
# CORS
|
|
1295
|
+
# CORS - Allowed origins
|
|
1296
|
+
# Options:
|
|
1297
|
+
# * = allow all origins (not recommended for production)
|
|
1298
|
+
# Comma-separated list = specific origins only
|
|
1294
1299
|
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
|
1295
1300
|
`;
|
|
1296
1301
|
if (config.adapter === "mongokit") {
|
|
@@ -1934,7 +1939,10 @@ export interface AppConfig {
|
|
|
1934
1939
|
expiresIn: string;
|
|
1935
1940
|
};
|
|
1936
1941
|
cors: {
|
|
1937
|
-
origins: string[];
|
|
1942
|
+
origins: string[] | boolean; // true = allow all ('*')
|
|
1943
|
+
methods: string[];
|
|
1944
|
+
allowedHeaders: string[];
|
|
1945
|
+
credentials: boolean;
|
|
1938
1946
|
};${config.adapter === "mongokit" ? `
|
|
1939
1947
|
database: {
|
|
1940
1948
|
uri: string;
|
|
@@ -1966,7 +1974,14 @@ const config${ts ? ": AppConfig" : ""} = {
|
|
|
1966
1974
|
},
|
|
1967
1975
|
|
|
1968
1976
|
cors: {
|
|
1969
|
-
|
|
1977
|
+
// '*' = allow all origins (true), otherwise comma-separated list
|
|
1978
|
+
origins:
|
|
1979
|
+
process.env.CORS_ORIGINS === '*'
|
|
1980
|
+
? true
|
|
1981
|
+
: (process.env.CORS_ORIGINS || 'http://localhost:3000').split(','),
|
|
1982
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
1983
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id', 'x-request-id'],
|
|
1984
|
+
credentials: true,
|
|
1970
1985
|
},
|
|
1971
1986
|
${config.adapter === "mongokit" ? `
|
|
1972
1987
|
database: {
|
|
@@ -2020,7 +2035,7 @@ export default Example;
|
|
|
2020
2035
|
}
|
|
2021
2036
|
function exampleRepositoryTemplate(config) {
|
|
2022
2037
|
const ts = config.typescript;
|
|
2023
|
-
const typeImport = ts ? "import type { ExampleDocument } from './model.js';\n" : "";
|
|
2038
|
+
const typeImport = ts ? "import type { ExampleDocument } from './example.model.js';\n" : "";
|
|
2024
2039
|
const generic = ts ? "<ExampleDocument>" : "";
|
|
2025
2040
|
return `/**
|
|
2026
2041
|
* Example Repository
|
|
@@ -2413,7 +2428,7 @@ export default User;
|
|
|
2413
2428
|
}
|
|
2414
2429
|
function userRepositoryTemplate(config) {
|
|
2415
2430
|
const ts = config.typescript;
|
|
2416
|
-
const typeImport = ts ? "import type { UserDocument } from './model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : "";
|
|
2431
|
+
const typeImport = ts ? "import type { UserDocument } from './user.model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : "";
|
|
2417
2432
|
return `/**
|
|
2418
2433
|
* User Repository
|
|
2419
2434
|
* Generated by Arc CLI
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
export { B as BaseController, a as BaseControllerOptions } from '../BaseController-nNRS3vpA.js';
|
|
2
|
+
import { RouteHandlerMethod, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
3
|
+
import { w as FastifyWithDecorators, B as CrudController, _ as CrudRouterOptions, d as RequestWithExtras, a as IRequestContext, c as IControllerResponse, I as IController } from '../index-D5QTob1X.js';
|
|
4
|
+
export { q as ResourceDefinition, p as defineResource } from '../index-D5QTob1X.js';
|
|
5
|
+
import { P as PermissionCheck } from '../types-B99TBmFV.js';
|
|
6
|
+
import 'mongoose';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CRUD Router Factory
|
|
10
|
+
*
|
|
11
|
+
* Creates standard REST routes with permission-based access control.
|
|
12
|
+
* Full TypeScript support with proper Fastify types.
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Permission-based access control via PermissionCheck functions
|
|
16
|
+
* - Organization scoping for multi-tenant routes
|
|
17
|
+
* - Consistent route patterns
|
|
18
|
+
* - Framework-agnostic controllers via adapter pattern
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create CRUD routes for a controller
|
|
23
|
+
*
|
|
24
|
+
* @param fastify - Fastify instance with Arc decorators
|
|
25
|
+
* @param controller - CRUD controller with handler methods
|
|
26
|
+
* @param options - Router configuration
|
|
27
|
+
*/
|
|
28
|
+
declare function createCrudRouter<TDoc = unknown>(fastify: FastifyWithDecorators, controller: CrudController<TDoc> | undefined, options?: CrudRouterOptions): void;
|
|
29
|
+
/**
|
|
30
|
+
* Helper to create org scoped middleware
|
|
31
|
+
*/
|
|
32
|
+
declare function createOrgScopedMiddleware(instance: FastifyWithDecorators): RouteHandlerMethod[];
|
|
33
|
+
/**
|
|
34
|
+
* Create permission middleware from PermissionCheck
|
|
35
|
+
* Useful for custom route registration
|
|
36
|
+
*/
|
|
37
|
+
declare function createPermissionMiddleware(permission: PermissionCheck, resourceName: string, action: string): RouteHandlerMethod | null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Action Router Factory (Stripe Pattern)
|
|
41
|
+
*
|
|
42
|
+
* Consolidates multiple state-transition endpoints into a single unified action endpoint.
|
|
43
|
+
* Instead of separate endpoints for each action (approve, dispatch, receive, cancel),
|
|
44
|
+
* this creates one endpoint: POST /:id/action
|
|
45
|
+
*
|
|
46
|
+
* Benefits:
|
|
47
|
+
* - 40% fewer endpoints
|
|
48
|
+
* - Consistent permission checking
|
|
49
|
+
* - Self-documenting via action enum
|
|
50
|
+
* - Type-safe action validation
|
|
51
|
+
* - Single audit point for all state transitions
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* import { createActionRouter } from '@classytic/arc';
|
|
55
|
+
* import { requireRoles } from '@classytic/arc/permissions';
|
|
56
|
+
*
|
|
57
|
+
* createActionRouter(fastify, {
|
|
58
|
+
* tag: 'Inventory - Transfers',
|
|
59
|
+
* actions: {
|
|
60
|
+
* approve: async (id, data, req) => transferService.approve(id, req.user),
|
|
61
|
+
* dispatch: async (id, data, req) => transferService.dispatch(id, data.transport, req.user),
|
|
62
|
+
* receive: async (id, data, req) => transferService.receive(id, data, req.user),
|
|
63
|
+
* cancel: async (id, data, req) => transferService.cancel(id, data.reason, req.user),
|
|
64
|
+
* },
|
|
65
|
+
* actionPermissions: {
|
|
66
|
+
* approve: requireRoles(['admin', 'warehouse-manager']),
|
|
67
|
+
* dispatch: requireRoles(['admin', 'warehouse-staff']),
|
|
68
|
+
* receive: requireRoles(['admin', 'store-manager']),
|
|
69
|
+
* cancel: requireRoles(['admin']),
|
|
70
|
+
* },
|
|
71
|
+
* actionSchemas: {
|
|
72
|
+
* dispatch: {
|
|
73
|
+
* transport: { type: 'object', properties: { driver: { type: 'string' } } }
|
|
74
|
+
* },
|
|
75
|
+
* cancel: {
|
|
76
|
+
* reason: { type: 'string', minLength: 10 }
|
|
77
|
+
* },
|
|
78
|
+
* }
|
|
79
|
+
* });
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Action handler function
|
|
84
|
+
* @param id - Resource ID
|
|
85
|
+
* @param data - Action-specific data from request body
|
|
86
|
+
* @param req - Full Fastify request object
|
|
87
|
+
* @returns Action result (will be wrapped in success response)
|
|
88
|
+
*/
|
|
89
|
+
type ActionHandler<TData = any, TResult = any> = (id: string, data: TData, req: RequestWithExtras) => Promise<TResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Action router configuration
|
|
92
|
+
*/
|
|
93
|
+
interface ActionRouterConfig {
|
|
94
|
+
/**
|
|
95
|
+
* OpenAPI tag for grouping routes
|
|
96
|
+
*/
|
|
97
|
+
tag?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Action handlers map
|
|
100
|
+
* @example { approve: (id, data, req) => service.approve(id), ... }
|
|
101
|
+
*/
|
|
102
|
+
actions: Record<string, ActionHandler>;
|
|
103
|
+
/**
|
|
104
|
+
* Per-action permission checks (PermissionCheck functions)
|
|
105
|
+
* @example { approve: requireRoles(['admin', 'manager']), cancel: requireRoles(['admin']) }
|
|
106
|
+
*/
|
|
107
|
+
actionPermissions?: Record<string, PermissionCheck>;
|
|
108
|
+
/**
|
|
109
|
+
* Per-action JSON schema for body validation
|
|
110
|
+
* @example { dispatch: { transport: { type: 'object' } } }
|
|
111
|
+
*/
|
|
112
|
+
actionSchemas?: Record<string, Record<string, any>>;
|
|
113
|
+
/**
|
|
114
|
+
* Global permission check applied to all actions (if action-specific not defined)
|
|
115
|
+
*/
|
|
116
|
+
globalAuth?: PermissionCheck;
|
|
117
|
+
/**
|
|
118
|
+
* Optional idempotency service
|
|
119
|
+
* If provided, will handle idempotency-key header
|
|
120
|
+
*/
|
|
121
|
+
idempotencyService?: IdempotencyService;
|
|
122
|
+
/**
|
|
123
|
+
* Custom error handler for action execution failures
|
|
124
|
+
* @param error - The error thrown by action handler
|
|
125
|
+
* @param action - The action that failed
|
|
126
|
+
* @param id - The resource ID
|
|
127
|
+
* @returns Status code and error response
|
|
128
|
+
*/
|
|
129
|
+
onError?: (error: Error, action: string, id: string) => {
|
|
130
|
+
statusCode: number;
|
|
131
|
+
error: string;
|
|
132
|
+
code?: string;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Idempotency service interface
|
|
137
|
+
* Apps can provide their own implementation
|
|
138
|
+
*/
|
|
139
|
+
interface IdempotencyService {
|
|
140
|
+
check(key: string, payload: any): Promise<{
|
|
141
|
+
isNew: boolean;
|
|
142
|
+
existingResult?: any;
|
|
143
|
+
}>;
|
|
144
|
+
complete(key: string | undefined, result: any): Promise<void>;
|
|
145
|
+
fail(key: string | undefined, error: Error): Promise<void>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Create action-based state transition endpoint
|
|
149
|
+
*
|
|
150
|
+
* Registers: POST /:id/action
|
|
151
|
+
* Body: { action: string, ...actionData }
|
|
152
|
+
*
|
|
153
|
+
* @param fastify - Fastify instance
|
|
154
|
+
* @param config - Action router configuration
|
|
155
|
+
*/
|
|
156
|
+
declare function createActionRouter(fastify: FastifyInstance, config: ActionRouterConfig): void;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Fastify Adapter for IController
|
|
160
|
+
*
|
|
161
|
+
* Converts between Fastify's request/reply and framework-agnostic IRequestContext/IControllerResponse.
|
|
162
|
+
* This allows controllers implementing IController to work seamlessly with Fastify.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create IRequestContext from Fastify request
|
|
167
|
+
*
|
|
168
|
+
* Extracts framework-agnostic context from Fastify-specific request object
|
|
169
|
+
*/
|
|
170
|
+
declare function createRequestContext(req: FastifyRequest): IRequestContext;
|
|
171
|
+
/**
|
|
172
|
+
* Send IControllerResponse via Fastify reply
|
|
173
|
+
*
|
|
174
|
+
* Converts framework-agnostic response to Fastify response
|
|
175
|
+
* Applies field masking if specified in request
|
|
176
|
+
*/
|
|
177
|
+
declare function sendControllerResponse<T>(reply: FastifyReply, response: IControllerResponse<T>, request?: FastifyRequest): void;
|
|
178
|
+
/**
|
|
179
|
+
* Create Fastify route handler from IController method
|
|
180
|
+
*
|
|
181
|
+
* Wraps framework-agnostic controller method in Fastify-specific handler
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* const controller = new BaseController(repository);
|
|
186
|
+
*
|
|
187
|
+
* // Create Fastify handler
|
|
188
|
+
* const listHandler = createFastifyHandler(controller.list.bind(controller));
|
|
189
|
+
*
|
|
190
|
+
* // Register route
|
|
191
|
+
* fastify.get('/products', listHandler);
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
declare function createFastifyHandler<T>(controllerMethod: (req: IRequestContext) => Promise<IControllerResponse<T>>): (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
195
|
+
/**
|
|
196
|
+
* Create Fastify adapters for all CRUD methods of an IController
|
|
197
|
+
*
|
|
198
|
+
* Returns Fastify-compatible handlers for each CRUD operation
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const controller = new BaseController(repository);
|
|
203
|
+
* const handlers = createCrudHandlers(controller);
|
|
204
|
+
*
|
|
205
|
+
* fastify.get('/', handlers.list);
|
|
206
|
+
* fastify.get('/:id', handlers.get);
|
|
207
|
+
* fastify.post('/', handlers.create);
|
|
208
|
+
* fastify.patch('/:id', handlers.update);
|
|
209
|
+
* fastify.delete('/:id', handlers.delete);
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
declare function createCrudHandlers<TDoc>(controller: IController<TDoc>): {
|
|
213
|
+
list: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
214
|
+
get: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
215
|
+
create: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
216
|
+
update: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
217
|
+
delete: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export { type ActionHandler, type ActionRouterConfig, type IdempotencyService, createActionRouter, createCrudHandlers, createCrudRouter, createFastifyHandler, createOrgScopedMiddleware, createPermissionMiddleware, createRequestContext, sendControllerResponse };
|