@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,336 @@
1
+ import { MultiTenantOptions } from './multiTenant.js';
2
+ export { default as multiTenantPreset } from './multiTenant.js';
3
+ import { P as PresetResult, j as IRequestContext, m as IControllerResponse, n as PaginatedResult, d as AnyRecord, o as ResourceConfig } from '../index-DkAW8BXh.js';
4
+ import 'mongoose';
5
+ import 'fastify';
6
+ import '../types-B99TBmFV.js';
7
+
8
+ /**
9
+ * Soft Delete Preset
10
+ *
11
+ * Adds routes for listing deleted items and restoring them.
12
+ */
13
+
14
+ interface SoftDeleteOptions {
15
+ deletedField?: string;
16
+ }
17
+ declare function softDeletePreset(options?: SoftDeleteOptions): PresetResult;
18
+
19
+ /**
20
+ * Slug Lookup Preset
21
+ *
22
+ * Adds a route to get resource by slug.
23
+ */
24
+
25
+ interface SlugLookupOptions {
26
+ slugField?: string;
27
+ }
28
+ declare function slugLookupPreset(options?: SlugLookupOptions): PresetResult;
29
+
30
+ /**
31
+ * Owned By User Preset
32
+ *
33
+ * Adds ownership validation for update/delete operations.
34
+ *
35
+ * BEHAVIOR:
36
+ * - On update/remove, sets _ownershipCheck on request
37
+ * - BaseController enforces ownership before mutation
38
+ * - Users can only modify resources where ownerField matches their ID
39
+ *
40
+ * BYPASS:
41
+ * - Users with bypassRoles (default: ['admin', 'superadmin']) skip check
42
+ * - Resources without the ownerField are not checked
43
+ *
44
+ * @example
45
+ * defineResource({
46
+ * name: 'post',
47
+ * presets: [{ name: 'ownedByUser', ownerField: 'authorId' }],
48
+ * });
49
+ *
50
+ * // User A cannot update/delete User B's posts
51
+ * // Admins can modify any post
52
+ */
53
+
54
+ interface OwnedByUserOptions {
55
+ ownerField?: string;
56
+ bypassRoles?: string[];
57
+ }
58
+ declare function ownedByUserPreset(options?: OwnedByUserOptions): PresetResult;
59
+
60
+ /**
61
+ * Tree Preset
62
+ *
63
+ * Adds routes for hierarchical tree structures.
64
+ */
65
+
66
+ interface TreeOptions {
67
+ parentField?: string;
68
+ }
69
+ declare function treePreset(options?: TreeOptions): PresetResult;
70
+
71
+ /**
72
+ * Audited Preset
73
+ *
74
+ * Adds createdBy/updatedBy tracking to resources.
75
+ * Works with the audit plugin for full change tracking.
76
+ *
77
+ * @example
78
+ * defineResource({
79
+ * name: 'product',
80
+ * presets: ['audited'],
81
+ * // Fields createdBy, updatedBy auto-populated from user context
82
+ * });
83
+ */
84
+
85
+ interface AuditedPresetOptions {
86
+ /** Field name for creator (default: 'createdBy') */
87
+ createdByField?: string;
88
+ /** Field name for updater (default: 'updatedBy') */
89
+ updatedByField?: string;
90
+ }
91
+ /**
92
+ * Audited preset - adds createdBy/updatedBy tracking
93
+ */
94
+ declare function auditedPreset(options?: AuditedPresetOptions): PresetResult;
95
+
96
+ /**
97
+ * Preset Type Interfaces
98
+ *
99
+ * TypeScript interfaces that document the controller methods required by each preset.
100
+ * These interfaces help with type safety when using presets.
101
+ *
102
+ * @example Using with custom controllers
103
+ * ```typescript
104
+ * import { BaseController } from '@classytic/arc';
105
+ * import type { ISoftDeleteController } from '@classytic/arc/presets';
106
+ *
107
+ * class ProductController extends BaseController<Product> implements ISoftDeleteController {
108
+ * // TypeScript now ensures you have getDeleted() and restore() methods
109
+ * }
110
+ * ```
111
+ */
112
+
113
+ /**
114
+ * Soft Delete Preset Interface
115
+ *
116
+ * Required when using the `softDelete` preset.
117
+ * BaseController provides default implementations that delegate to repository methods.
118
+ *
119
+ * **Routes Added:**
120
+ * - `GET /deleted` → `getDeleted()`
121
+ * - `POST /:id/restore` → `restore()`
122
+ *
123
+ * **Repository Requirements:**
124
+ * Your repository must implement:
125
+ * - `getDeleted(options): Promise<PaginatedResult<T> | T[]>`
126
+ * - `restore(id): Promise<T | null>`
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * defineResource({
131
+ * name: 'product',
132
+ * presets: ['softDelete'],
133
+ * adapter: createMongooseAdapter({
134
+ * model: ProductModel,
135
+ * repository: productRepository, // Must implement getDeleted/restore
136
+ * }),
137
+ * });
138
+ * ```
139
+ */
140
+ interface ISoftDeleteController<TDoc = unknown> {
141
+ /**
142
+ * Get all soft-deleted items
143
+ * Called by: GET /deleted
144
+ */
145
+ getDeleted(context: IRequestContext): Promise<IControllerResponse<PaginatedResult<TDoc>>>;
146
+ /**
147
+ * Restore a soft-deleted item by ID
148
+ * Called by: POST /:id/restore
149
+ */
150
+ restore(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
151
+ }
152
+ /**
153
+ * Slug Lookup Preset Interface
154
+ *
155
+ * Required when using the `slugLookup` preset.
156
+ * BaseController provides default implementation that delegates to repository.
157
+ *
158
+ * **Routes Added:**
159
+ * - `GET /slug/:slug` → `getBySlug()`
160
+ *
161
+ * **Repository Requirements:**
162
+ * Your repository must implement:
163
+ * - `getBySlug(slug, options): Promise<T | null>`
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * defineResource({
168
+ * name: 'product',
169
+ * presets: ['slugLookup'],
170
+ * adapter: createMongooseAdapter({
171
+ * model: ProductModel,
172
+ * repository: productRepository, // Must implement getBySlug
173
+ * }),
174
+ * });
175
+ * ```
176
+ */
177
+ interface ISlugLookupController<TDoc = unknown> {
178
+ /**
179
+ * Get a resource by its slug
180
+ * Called by: GET /slug/:slug
181
+ */
182
+ getBySlug(context: IRequestContext): Promise<IControllerResponse<TDoc>>;
183
+ }
184
+ /**
185
+ * Tree Preset Interface
186
+ *
187
+ * Required when using the `tree` preset for hierarchical data structures.
188
+ * BaseController provides default implementations that delegate to repository.
189
+ *
190
+ * **Routes Added:**
191
+ * - `GET /tree` → `getTree()`
192
+ * - `GET /:parent/children` → `getChildren()`
193
+ *
194
+ * **Repository Requirements:**
195
+ * Your repository must implement:
196
+ * - `getTree(options): Promise<T[]>`
197
+ * - `getChildren(parentId, options): Promise<T[]>`
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * defineResource({
202
+ * name: 'category',
203
+ * presets: [{ name: 'tree', parentField: 'parentId' }],
204
+ * adapter: createMongooseAdapter({
205
+ * model: CategoryModel,
206
+ * repository: categoryRepository, // Must implement getTree/getChildren
207
+ * }),
208
+ * });
209
+ * ```
210
+ */
211
+ interface ITreeController<TDoc = unknown> {
212
+ /**
213
+ * Get the full hierarchical tree
214
+ * Called by: GET /tree
215
+ */
216
+ getTree(context: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
217
+ /**
218
+ * Get direct children of a parent node
219
+ * Called by: GET /:parent/children
220
+ */
221
+ getChildren(context: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
222
+ }
223
+ /**
224
+ * Owned By User Preset
225
+ *
226
+ * This preset does NOT require controller methods - it adds middleware only.
227
+ * Middleware automatically enforces ownership checks on update/delete operations.
228
+ *
229
+ * **Behavior:**
230
+ * - Users can only update/delete resources where `ownerField` matches their user ID
231
+ * - Admins (configurable via `bypassRoles`) can modify any resource
232
+ *
233
+ * **No controller interface needed** - ownership is enforced via middleware.
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * defineResource({
238
+ * name: 'post',
239
+ * presets: [{ name: 'ownedByUser', ownerField: 'authorId' }],
240
+ * });
241
+ * ```
242
+ */
243
+ type IOwnedByUserPreset = never;
244
+ /**
245
+ * Multi-Tenant Preset
246
+ *
247
+ * This preset does NOT require controller methods - it adds middleware only.
248
+ * Middleware automatically filters resources by organization/tenant ID.
249
+ *
250
+ * **Behavior:**
251
+ * - All list/get operations are automatically filtered by `tenantField`
252
+ * - Create operations automatically inject the tenant ID
253
+ * - Superadmins (configurable via `bypassRoles`) can access all tenants
254
+ *
255
+ * **No controller interface needed** - tenant isolation is enforced via middleware.
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * defineResource({
260
+ * name: 'invoice',
261
+ * presets: [{ name: 'multiTenant', tenantField: 'organizationId' }],
262
+ * });
263
+ * ```
264
+ */
265
+ type IMultiTenantPreset = never;
266
+ /**
267
+ * Audited Preset
268
+ *
269
+ * This preset does NOT require controller methods - it adds middleware only.
270
+ * Middleware automatically populates `createdBy`/`updatedBy` fields from authenticated user.
271
+ *
272
+ * **Behavior:**
273
+ * - On create: Sets both `createdBy` and `updatedBy` to current user ID
274
+ * - On update: Sets `updatedBy` to current user ID
275
+ * - Fields are marked as `systemManaged` in schemas (excluded from user input)
276
+ *
277
+ * **No controller interface needed** - audit fields are managed via middleware.
278
+ *
279
+ * @example
280
+ * ```typescript
281
+ * defineResource({
282
+ * name: 'product',
283
+ * presets: ['audited'], // Uses default fields: createdBy, updatedBy
284
+ * });
285
+ * ```
286
+ */
287
+ type IAuditedPreset = never;
288
+ /**
289
+ * Combined type for controllers using multiple presets
290
+ *
291
+ * @example
292
+ * ```typescript
293
+ * import type { IPresetController } from '@classytic/arc/presets';
294
+ *
295
+ * class ProductController
296
+ * extends BaseController<Product>
297
+ * implements IPresetController<Product, 'softDelete' | 'slugLookup' | 'tree'>
298
+ * {
299
+ * // TypeScript ensures all required methods are implemented
300
+ * }
301
+ * ```
302
+ */
303
+ type IPresetController<TDoc = unknown, TPresets extends 'softDelete' | 'slugLookup' | 'tree' | never = never> = TPresets extends 'softDelete' ? ISoftDeleteController<TDoc> : TPresets extends 'slugLookup' ? ISlugLookupController<TDoc> : TPresets extends 'tree' ? ITreeController<TDoc> : unknown;
304
+
305
+ /**
306
+ * Convenience alias for multiTenantPreset with public list/get routes
307
+ * Equivalent to: multiTenantPreset({ allowPublic: ['list', 'get'] })
308
+ */
309
+ declare const flexibleMultiTenantPreset: (options?: Omit<MultiTenantOptions, "allowPublic">) => PresetResult;
310
+
311
+ type PresetFactory = (options?: AnyRecord) => PresetResult;
312
+ /**
313
+ * Get preset by name with options
314
+ */
315
+ declare function getPreset(nameOrConfig: string | {
316
+ name: string;
317
+ [key: string]: unknown;
318
+ }): PresetResult;
319
+ /**
320
+ * Register a custom preset
321
+ */
322
+ declare function registerPreset(name: string, factory: PresetFactory): void;
323
+ /**
324
+ * Get all available preset names
325
+ */
326
+ declare function getAvailablePresets(): string[];
327
+ type PresetInput = string | PresetResult | {
328
+ name: string;
329
+ [key: string]: unknown;
330
+ };
331
+ /**
332
+ * Apply presets to resource config
333
+ */
334
+ declare function applyPresets<TDoc = AnyRecord>(config: ResourceConfig<TDoc>, presets?: PresetInput[]): ResourceConfig<TDoc>;
335
+
336
+ export { type AuditedPresetOptions, type IAuditedPreset, type IMultiTenantPreset, type IOwnedByUserPreset, type IPresetController, type ISlugLookupController, type ISoftDeleteController, type ITreeController, MultiTenantOptions, type OwnedByUserOptions, type SlugLookupOptions, type SoftDeleteOptions, type TreeOptions, applyPresets, auditedPreset, flexibleMultiTenantPreset, getAvailablePresets, getPreset, ownedByUserPreset, registerPreset, slugLookupPreset, softDeletePreset, treePreset };
@@ -0,0 +1,382 @@
1
+ // src/permissions/index.ts
2
+ function allowPublic() {
3
+ const check = () => true;
4
+ check._isPublic = true;
5
+ return check;
6
+ }
7
+ function requireRoles(roles, options) {
8
+ return (ctx) => {
9
+ if (!ctx.user) {
10
+ return { granted: false, reason: "Authentication required" };
11
+ }
12
+ const userRoles = ctx.user.roles ?? [];
13
+ if (roles.some((r) => userRoles.includes(r))) {
14
+ return true;
15
+ }
16
+ return {
17
+ granted: false,
18
+ reason: `Required roles: ${roles.join(", ")}`
19
+ };
20
+ };
21
+ }
22
+
23
+ // src/presets/softDelete.ts
24
+ function softDeletePreset(options = {}) {
25
+ const { deletedField: _deletedField = "deletedAt" } = options;
26
+ return {
27
+ name: "softDelete",
28
+ additionalRoutes: (permissions) => [
29
+ {
30
+ method: "GET",
31
+ path: "/deleted",
32
+ handler: "getDeleted",
33
+ summary: "Get soft-deleted items",
34
+ permissions: permissions.list ?? requireRoles(["admin"]),
35
+ wrapHandler: true
36
+ },
37
+ {
38
+ method: "POST",
39
+ path: "/:id/restore",
40
+ handler: "restore",
41
+ summary: "Restore soft-deleted item",
42
+ permissions: permissions.update ?? requireRoles(["admin"]),
43
+ wrapHandler: true
44
+ }
45
+ ]
46
+ };
47
+ }
48
+
49
+ // src/presets/slugLookup.ts
50
+ function slugLookupPreset(options = {}) {
51
+ const { slugField = "slug" } = options;
52
+ return {
53
+ name: "slugLookup",
54
+ additionalRoutes: (permissions) => [
55
+ {
56
+ method: "GET",
57
+ path: `/slug/:${slugField}`,
58
+ handler: "getBySlug",
59
+ summary: "Get by slug",
60
+ permissions: permissions.get ?? allowPublic(),
61
+ wrapHandler: true
62
+ // Handler is a ControllerHandler
63
+ }
64
+ ],
65
+ // Pass to controller so it knows which param to read
66
+ controllerOptions: {
67
+ slugField
68
+ }
69
+ };
70
+ }
71
+
72
+ // src/presets/ownedByUser.ts
73
+ function createOwnershipCheck(ownerField, bypassRoles) {
74
+ return async (request, _reply) => {
75
+ const user = request.user;
76
+ if (!user) return;
77
+ const userWithRoles = user;
78
+ if (userWithRoles.roles && bypassRoles.some((r) => userWithRoles.roles.includes(r))) return;
79
+ const userWithId = user;
80
+ const userId = userWithId._id ?? userWithId.id;
81
+ if (userId) {
82
+ request._ownershipCheck = {
83
+ field: ownerField,
84
+ userId
85
+ };
86
+ }
87
+ };
88
+ }
89
+ function ownedByUserPreset(options = {}) {
90
+ const {
91
+ ownerField = "userId",
92
+ bypassRoles = ["admin", "superadmin"]
93
+ } = options;
94
+ const ownershipMiddleware = createOwnershipCheck(ownerField, bypassRoles);
95
+ return {
96
+ name: "ownedByUser",
97
+ middlewares: {
98
+ update: [ownershipMiddleware],
99
+ delete: [ownershipMiddleware]
100
+ }
101
+ };
102
+ }
103
+
104
+ // src/presets/multiTenant.ts
105
+ function defaultExtractOrganizationId(request) {
106
+ const context = request.context;
107
+ if (context?.organizationId) {
108
+ return context.organizationId;
109
+ }
110
+ const user = request.user;
111
+ if (user?.organizationId) {
112
+ return user.organizationId;
113
+ }
114
+ if (user?.organization) {
115
+ const org = user.organization;
116
+ return org._id || org.id || org;
117
+ }
118
+ return null;
119
+ }
120
+ function createTenantFilter(tenantField, bypassRoles, extractOrganizationId) {
121
+ return async (request, reply) => {
122
+ const user = request.user;
123
+ if (!user) {
124
+ reply.code(401).send({
125
+ success: false,
126
+ error: "Unauthorized",
127
+ message: "Authentication required for multi-tenant resources"
128
+ });
129
+ return;
130
+ }
131
+ const userWithRoles = user;
132
+ if (userWithRoles.roles && bypassRoles.some((r) => userWithRoles.roles.includes(r))) return;
133
+ const orgId = extractOrganizationId(request);
134
+ if (!orgId) {
135
+ reply.code(403).send({
136
+ success: false,
137
+ error: "Forbidden",
138
+ message: "Organization context required for this operation"
139
+ });
140
+ return;
141
+ }
142
+ request.query = request.query ?? {};
143
+ request.query._policyFilters = {
144
+ ...request.query._policyFilters ?? {},
145
+ [tenantField]: orgId
146
+ };
147
+ };
148
+ }
149
+ function createFlexibleTenantFilter(tenantField, bypassRoles, extractOrganizationId) {
150
+ return async (request, reply) => {
151
+ const user = request.user;
152
+ const orgId = extractOrganizationId(request);
153
+ if (!orgId) {
154
+ return;
155
+ }
156
+ if (!user) {
157
+ reply.code(401).send({
158
+ success: false,
159
+ error: "Unauthorized",
160
+ message: "Authentication required for organization-scoped data"
161
+ });
162
+ return;
163
+ }
164
+ const userWithRoles = user;
165
+ if (userWithRoles.roles && bypassRoles.some((r) => userWithRoles.roles.includes(r))) {
166
+ return;
167
+ }
168
+ request.query = request.query ?? {};
169
+ request.query._policyFilters = {
170
+ ...request.query._policyFilters ?? {},
171
+ [tenantField]: orgId
172
+ };
173
+ };
174
+ }
175
+ function createTenantInjection(tenantField, extractOrganizationId) {
176
+ return async (request, reply) => {
177
+ const orgId = extractOrganizationId(request);
178
+ if (!orgId) {
179
+ reply.code(403).send({
180
+ success: false,
181
+ error: "Forbidden",
182
+ message: "Organization context required to create resources"
183
+ });
184
+ return;
185
+ }
186
+ if (request.body) {
187
+ request.body[tenantField] = orgId;
188
+ }
189
+ };
190
+ }
191
+ function multiTenantPreset(options = {}) {
192
+ const {
193
+ tenantField = "organizationId",
194
+ bypassRoles = ["superadmin"],
195
+ extractOrganizationId = defaultExtractOrganizationId,
196
+ allowPublic: allowPublic2 = []
197
+ } = options;
198
+ const strictTenantFilter = createTenantFilter(tenantField, bypassRoles, extractOrganizationId);
199
+ const flexibleTenantFilter = createFlexibleTenantFilter(tenantField, bypassRoles, extractOrganizationId);
200
+ const tenantInjection = createTenantInjection(tenantField, extractOrganizationId);
201
+ const getFilter = (route) => allowPublic2.includes(route) ? flexibleTenantFilter : strictTenantFilter;
202
+ return {
203
+ name: "multiTenant",
204
+ middlewares: {
205
+ list: [getFilter("list")],
206
+ get: [getFilter("get")],
207
+ create: [tenantInjection],
208
+ update: [getFilter("update")],
209
+ delete: [getFilter("delete")]
210
+ }
211
+ };
212
+ }
213
+
214
+ // src/presets/tree.ts
215
+ function treePreset(options = {}) {
216
+ const { parentField = "parent" } = options;
217
+ return {
218
+ name: "tree",
219
+ additionalRoutes: (permissions) => [
220
+ {
221
+ method: "GET",
222
+ path: "/tree",
223
+ handler: "getTree",
224
+ summary: "Get hierarchical tree",
225
+ permissions: permissions.list ?? allowPublic(),
226
+ wrapHandler: true
227
+ },
228
+ {
229
+ method: "GET",
230
+ path: `/:${parentField}/children`,
231
+ handler: "getChildren",
232
+ summary: "Get children of parent",
233
+ permissions: permissions.list ?? allowPublic(),
234
+ wrapHandler: true
235
+ }
236
+ ],
237
+ // Pass to controller so it knows which param to read
238
+ controllerOptions: {
239
+ parentField
240
+ }
241
+ };
242
+ }
243
+
244
+ // src/presets/audited.ts
245
+ function auditedPreset(options = {}) {
246
+ const { createdByField = "createdBy", updatedByField = "updatedBy" } = options;
247
+ const injectCreatedBy = async (request, _reply) => {
248
+ const userWithId = request.user;
249
+ if (userWithId?._id || userWithId?.id) {
250
+ const userId = userWithId._id ?? userWithId.id;
251
+ request.body[createdByField] = userId;
252
+ request.body[updatedByField] = userId;
253
+ }
254
+ return void 0;
255
+ };
256
+ const injectUpdatedBy = async (request, _reply) => {
257
+ const userWithId = request.user;
258
+ if (userWithId?._id || userWithId?.id) {
259
+ request.body[updatedByField] = userWithId._id ?? userWithId.id;
260
+ }
261
+ return void 0;
262
+ };
263
+ return {
264
+ name: "audited",
265
+ schemaOptions: {
266
+ fieldRules: {
267
+ [createdByField]: { systemManaged: true },
268
+ [updatedByField]: { systemManaged: true },
269
+ createdAt: { systemManaged: true },
270
+ updatedAt: { systemManaged: true }
271
+ }
272
+ },
273
+ middlewares: {
274
+ create: [injectCreatedBy],
275
+ update: [injectUpdatedBy]
276
+ }
277
+ };
278
+ }
279
+
280
+ // src/presets/index.ts
281
+ var flexibleMultiTenantPreset = (options = {}) => multiTenantPreset({ ...options, allowPublic: ["list", "get"] });
282
+ var presetRegistry = {
283
+ softDelete: softDeletePreset,
284
+ slugLookup: slugLookupPreset,
285
+ ownedByUser: ownedByUserPreset,
286
+ multiTenant: multiTenantPreset,
287
+ tree: treePreset,
288
+ audited: auditedPreset
289
+ };
290
+ function getPreset(nameOrConfig) {
291
+ if (typeof nameOrConfig === "object" && nameOrConfig.name) {
292
+ const { name, ...options } = nameOrConfig;
293
+ return resolvePreset(name, options);
294
+ }
295
+ return resolvePreset(nameOrConfig);
296
+ }
297
+ function resolvePreset(name, options = {}) {
298
+ const factory = presetRegistry[name];
299
+ if (!factory) {
300
+ const available = Object.keys(presetRegistry).join(", ");
301
+ throw new Error(
302
+ `Unknown preset: '${name}'
303
+ Available presets: ${available}
304
+ Docs: https://github.com/classytic/arc#presets`
305
+ );
306
+ }
307
+ return factory(options);
308
+ }
309
+ function registerPreset(name, factory) {
310
+ if (presetRegistry[name]) {
311
+ throw new Error(`Preset '${name}' already exists`);
312
+ }
313
+ presetRegistry[name] = factory;
314
+ }
315
+ function getAvailablePresets() {
316
+ return Object.keys(presetRegistry);
317
+ }
318
+ function applyPresets(config, presets = []) {
319
+ let result = { ...config };
320
+ for (const preset of presets) {
321
+ const resolved = resolvePresetInput(preset);
322
+ result = mergePreset(result, resolved);
323
+ }
324
+ return result;
325
+ }
326
+ function resolvePresetInput(preset) {
327
+ if (typeof preset === "object" && ("middlewares" in preset || "additionalRoutes" in preset)) {
328
+ return preset;
329
+ }
330
+ if (typeof preset === "object" && "name" in preset) {
331
+ const { name, ...options } = preset;
332
+ return resolvePreset(name, options);
333
+ }
334
+ return resolvePreset(preset);
335
+ }
336
+ function mergePreset(config, preset) {
337
+ const result = { ...config };
338
+ if (preset.additionalRoutes) {
339
+ const routes = typeof preset.additionalRoutes === "function" ? preset.additionalRoutes(config.permissions ?? {}) : preset.additionalRoutes;
340
+ result.additionalRoutes = [
341
+ ...result.additionalRoutes ?? [],
342
+ ...routes
343
+ ];
344
+ }
345
+ if (preset.middlewares) {
346
+ result.middlewares = result.middlewares ?? {};
347
+ for (const [op, mws] of Object.entries(preset.middlewares)) {
348
+ const key = op;
349
+ result.middlewares[key] = [
350
+ ...result.middlewares[key] ?? [],
351
+ ...mws ?? []
352
+ ];
353
+ }
354
+ }
355
+ if (preset.schemaOptions) {
356
+ result.schemaOptions = {
357
+ ...result.schemaOptions,
358
+ ...preset.schemaOptions
359
+ };
360
+ }
361
+ if (preset.controllerOptions) {
362
+ result._controllerOptions = {
363
+ ...result._controllerOptions,
364
+ ...preset.controllerOptions
365
+ };
366
+ }
367
+ if (preset.hooks && preset.hooks.length > 0) {
368
+ result._hooks = result._hooks ?? [];
369
+ for (const hook of preset.hooks) {
370
+ result._hooks.push({
371
+ presetName: preset.name,
372
+ operation: hook.operation,
373
+ phase: hook.phase,
374
+ handler: hook.handler,
375
+ priority: hook.priority
376
+ });
377
+ }
378
+ }
379
+ return result;
380
+ }
381
+
382
+ export { applyPresets, auditedPreset, flexibleMultiTenantPreset, getAvailablePresets, getPreset, multiTenantPreset, ownedByUserPreset, registerPreset, slugLookupPreset, softDeletePreset, treePreset };