@classytic/arc 2.4.2 → 2.6.1

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 (104) hide show
  1. package/README.md +22 -6
  2. package/dist/{BaseController-CkM5dUh_.mjs → BaseController-AbbRx3e0.mjs} +5 -2
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-DTC4Ug66.mjs → adapters-CTn28N4y.mjs} +72 -11
  6. package/dist/audit/index.d.mts +1 -1
  7. package/dist/audit/index.mjs +11 -1
  8. package/dist/audit/mongodb.d.mts +1 -1
  9. package/dist/auth/index.d.mts +1 -1
  10. package/dist/auth/index.mjs +2 -2
  11. package/dist/cache/index.mjs +2 -2
  12. package/dist/cli/commands/describe.d.mts +1 -1
  13. package/dist/cli/commands/describe.mjs +1 -1
  14. package/dist/cli/commands/generate.d.mts +1 -1
  15. package/dist/cli/commands/generate.mjs +1 -1
  16. package/dist/cli/commands/init.d.mts +1 -1
  17. package/dist/cli/commands/init.mjs +13 -10
  18. package/dist/cli/commands/introspect.d.mts +1 -1
  19. package/dist/cli/commands/introspect.mjs +1 -1
  20. package/dist/cli/index.d.mts +4 -4
  21. package/dist/cli/index.mjs +4 -4
  22. package/dist/core/index.d.mts +2 -2
  23. package/dist/core/index.mjs +2 -2
  24. package/dist/{createApp-ByWNRsZj.mjs → createApp-Bol7DLUf.mjs} +404 -279
  25. package/dist/{defineResource-D9aY5Cy6.mjs → defineResource-bVKHjQzE.mjs} +116 -19
  26. package/dist/discovery/index.d.mts +1 -1
  27. package/dist/discovery/index.mjs +2 -2
  28. package/dist/docs/index.d.mts +1 -1
  29. package/dist/dynamic/index.d.mts +1 -1
  30. package/dist/dynamic/index.mjs +2 -2
  31. package/dist/{elevation-Ca_yveIO.d.mts → elevation-C_taLQrM.d.mts} +27 -1
  32. package/dist/{errorHandler--zp54tGc.mjs → errorHandler-r2595m8T.mjs} +5 -5
  33. package/dist/{errors-CPpvPHT0.d.mts → errors-CcVbl1-T.d.mts} +17 -1
  34. package/dist/{errors-rxhfP7Hf.mjs → errors-NoQKsbAT.mjs} +23 -1
  35. package/dist/{eventPlugin-iGrSEmwJ.d.mts → eventPlugin-DW45v4V5.d.mts} +30 -2
  36. package/dist/events/index.d.mts +2 -2
  37. package/dist/events/index.mjs +40 -10
  38. package/dist/events/transports/redis.d.mts +1 -1
  39. package/dist/events/transports/redis.mjs +1 -1
  40. package/dist/factory/index.d.mts +44 -23
  41. package/dist/factory/index.mjs +136 -2
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/idempotency/index.d.mts +3 -3
  44. package/dist/idempotency/mongodb.d.mts +1 -1
  45. package/dist/idempotency/redis.d.mts +1 -1
  46. package/dist/{index-yhxyjqNb.d.mts → index-BIsZ_su5.d.mts} +4 -8
  47. package/dist/{index-BL8CaQih.d.mts → index-Cb3gtbg7.d.mts} +2 -2
  48. package/dist/{index-Diqcm14c.d.mts → index-NGZksqM5.d.mts} +30 -1
  49. package/dist/index.d.mts +6 -6
  50. package/dist/index.mjs +8 -7
  51. package/dist/integrations/event-gateway.d.mts +1 -1
  52. package/dist/integrations/event-gateway.mjs +2 -2
  53. package/dist/integrations/index.d.mts +1 -1
  54. package/dist/integrations/jobs.d.mts +1 -1
  55. package/dist/integrations/jobs.mjs +1 -1
  56. package/dist/integrations/mcp/index.d.mts +4 -2
  57. package/dist/integrations/mcp/index.mjs +1 -1
  58. package/dist/integrations/mcp/testing.d.mts +1 -1
  59. package/dist/integrations/mcp/testing.mjs +1 -1
  60. package/dist/integrations/streamline.d.mts +1 -1
  61. package/dist/integrations/streamline.mjs +1 -1
  62. package/dist/integrations/websocket-redis.d.mts +1 -1
  63. package/dist/integrations/websocket-redis.mjs +1 -1
  64. package/dist/integrations/websocket.d.mts +1 -1
  65. package/dist/integrations/websocket.mjs +1 -1
  66. package/dist/{interface-DGmPxakH.d.mts → interface-DDW43OmS.d.mts} +194 -13
  67. package/dist/{memory-Cb_7iy9e.mjs → memory-BFAYkf8H.mjs} +1 -4
  68. package/dist/{mongodb-CUpYfxfD.d.mts → mongodb-kltrBPa1.d.mts} +10 -0
  69. package/dist/{mongodb-bga9AbkD.d.mts → mongodb-pMvOlR5_.d.mts} +1 -1
  70. package/dist/org/index.d.mts +1 -1
  71. package/dist/org/index.mjs +1 -1
  72. package/dist/permissions/index.d.mts +2 -2
  73. package/dist/permissions/index.mjs +2 -2
  74. package/dist/{permissions-CA5zg0yK.mjs → permissions-C8ImI8gC.mjs} +45 -3
  75. package/dist/plugins/index.d.mts +1 -1
  76. package/dist/plugins/index.mjs +3 -3
  77. package/dist/plugins/response-cache.d.mts +1 -1
  78. package/dist/plugins/response-cache.mjs +1 -1
  79. package/dist/plugins/tracing-entry.mjs +1 -1
  80. package/dist/presets/index.d.mts +2 -2
  81. package/dist/presets/index.mjs +2 -2
  82. package/dist/presets/multiTenant.d.mts +2 -2
  83. package/dist/presets/multiTenant.mjs +2 -2
  84. package/dist/{presets-C9QXJV1u.mjs → presets-BMfdy34e.mjs} +3 -3
  85. package/dist/{queryCachePlugin-ClosZdNS.mjs → queryCachePlugin-XtFplYO9.mjs} +1 -1
  86. package/dist/{redis-CQ5YxMC5.d.mts → redis-D0Qc-9EW.d.mts} +1 -1
  87. package/dist/registry/index.d.mts +1 -1
  88. package/dist/{resourceToTools-PMFE8HIv.mjs → resourceToTools-DH3c3e-T.mjs} +81 -7
  89. package/dist/scope/index.d.mts +2 -2
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/{sse-BkViJPlT.mjs → sse-BF7GR7IB.mjs} +1 -1
  92. package/dist/testing/index.d.mts +2 -2
  93. package/dist/testing/index.mjs +1 -1
  94. package/dist/types/index.d.mts +3 -3
  95. package/dist/types/index.mjs +23 -2
  96. package/dist/{types-C6TQjtdi.mjs → types-BhtYdxZU.mjs} +26 -1
  97. package/dist/{types-Dt0-AI6E.d.mts → types-D5hJ-k_3.d.mts} +189 -6
  98. package/dist/{types-BJmgxNbF.d.mts → types-D5rjsS_i.d.mts} +1 -1
  99. package/dist/utils/index.d.mts +2 -2
  100. package/dist/utils/index.mjs +1 -1
  101. package/package.json +7 -5
  102. package/skills/arc/SKILL.md +115 -8
  103. package/skills/arc/references/mcp.md +160 -2
  104. /package/dist/{interface-B4awm1RJ.d.mts → interface-gr-7qo9j.d.mts} +0 -0
@@ -1,4 +1,4 @@
1
- import { t as BaseController } from "./BaseController-CkM5dUh_.mjs";
1
+ import { t as BaseController } from "./BaseController-AbbRx3e0.mjs";
2
2
  import { t as pluralize } from "./pluralize-CcT6qF0a.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/integrations/mcp/createMcpServer.ts
@@ -148,16 +148,55 @@ function typeToZod(type) {
148
148
  default: return z.string();
149
149
  }
150
150
  }
151
- /** Build list/query shape with filterable fields + pagination */
151
+ /** Operators that apply to numeric/date fields */
152
+ const COMPARISON_OPS = new Set([
153
+ "gt",
154
+ "gte",
155
+ "lt",
156
+ "lte"
157
+ ]);
158
+ /** Map operator to a human-readable description suffix */
159
+ function opDescription(op, fieldName) {
160
+ switch (op) {
161
+ case "gt": return `${fieldName} greater than`;
162
+ case "gte": return `${fieldName} greater than or equal`;
163
+ case "lt": return `${fieldName} less than`;
164
+ case "lte": return `${fieldName} less than or equal`;
165
+ case "ne": return `${fieldName} not equal to`;
166
+ case "in": return `${fieldName} in comma-separated list`;
167
+ case "nin": return `${fieldName} not in comma-separated list`;
168
+ case "exists": return `${fieldName} exists (true/false)`;
169
+ default: return `${fieldName} ${op}`;
170
+ }
171
+ }
172
+ /** Build list/query shape with filterable fields, operators, and pagination */
152
173
  function buildListShape(fieldRules, options) {
153
- const { filterableFields = [], hiddenFields = [], extraHideFields = [] } = options;
174
+ const { filterableFields = [], hiddenFields = [], extraHideFields = [], allowedOperators } = options;
154
175
  const allHidden = new Set([...hiddenFields, ...extraHideFields]);
155
176
  const shape = { ...PAGINATION_SHAPE };
156
- if (fieldRules) for (const name of filterableFields) {
177
+ if (!fieldRules) return shape;
178
+ for (const name of filterableFields) {
157
179
  if (allHidden.has(name)) continue;
158
180
  const rule = fieldRules[name];
159
181
  if (!rule) continue;
160
- shape[name] = buildFieldSchema(rule).optional();
182
+ const filterField = buildFieldSchema(rule);
183
+ shape[name] = filterField.optional();
184
+ if (allowedOperators?.length) {
185
+ const isNumericOrDate = rule.type === "number" || rule.type === "date";
186
+ for (const op of allowedOperators) {
187
+ if (COMPARISON_OPS.has(op) && !isNumericOrDate) continue;
188
+ if (op === "eq") continue;
189
+ if (op === "exists") {
190
+ shape[`${name}_${op}`] = z.boolean().optional().describe(opDescription(op, name));
191
+ continue;
192
+ }
193
+ if (op === "in" || op === "nin") {
194
+ shape[`${name}_${op}`] = z.string().optional().describe(opDescription(op, name));
195
+ continue;
196
+ }
197
+ shape[`${name}_${op}`] = filterField.optional().describe(opDescription(op, name));
198
+ }
199
+ }
161
200
  }
162
201
  return shape;
163
202
  }
@@ -193,7 +232,7 @@ function buildRequestContext(input, auth, operation, policyFilters) {
193
232
  case "list": return {
194
233
  ...base,
195
234
  params: {},
196
- query: { ...input },
235
+ query: expandOperatorKeys(input),
197
236
  body: void 0
198
237
  };
199
238
  case "get": return {
@@ -225,6 +264,40 @@ function buildRequestContext(input, auth, operation, policyFilters) {
225
264
  };
226
265
  }
227
266
  }
267
+ /** Convert MCP operator keys (`price_gt`) to MongoKit bracket notation (`price[gt]`). */
268
+ const OPERATOR_SUFFIXES = new Set([
269
+ "eq",
270
+ "ne",
271
+ "gt",
272
+ "gte",
273
+ "lt",
274
+ "lte",
275
+ "in",
276
+ "nin",
277
+ "exists"
278
+ ]);
279
+ function expandOperatorKeys(input) {
280
+ const out = {};
281
+ for (const [key, value] of Object.entries(input)) {
282
+ const lastUnderscore = key.lastIndexOf("_");
283
+ if (lastUnderscore > 0) {
284
+ const op = key.slice(lastUnderscore + 1);
285
+ if (OPERATOR_SUFFIXES.has(op)) {
286
+ const field = key.slice(0, lastUnderscore);
287
+ const existing = out[field];
288
+ if (existing && typeof existing === "object" && existing !== null) existing[op] = value;
289
+ else if (existing === void 0) out[field] = { [op]: value };
290
+ else out[field] = {
291
+ eq: existing,
292
+ [op]: value
293
+ };
294
+ continue;
295
+ }
296
+ }
297
+ out[key] = value;
298
+ }
299
+ return out;
300
+ }
228
301
  function buildScope(auth) {
229
302
  if (!auth) return { kind: "public" };
230
303
  if (auth.organizationId) return {
@@ -314,7 +387,8 @@ function resourceToTools(resource, config = {}) {
314
387
  hiddenFields,
315
388
  readonlyFields,
316
389
  extraHideFields: config.hideFields,
317
- filterableFields
390
+ filterableFields,
391
+ allowedOperators
318
392
  }),
319
393
  handler: createHandler(op, controller, resource.name, resource.permissions)
320
394
  });
@@ -1,4 +1,4 @@
1
- import { a as AUTHENTICATED_SCOPE, c as getOrgId, d as getUserId, f as getUserRoles, g as isMember, h as isElevated, i as elevationPlugin, l as getOrgRoles, m as isAuthenticated, n as ElevationOptions, o as PUBLIC_SCOPE, p as hasOrgAccess, r as _default, s as RequestScope, t as ElevationEvent, u as getTeamId } from "../elevation-Ca_yveIO.mjs";
1
+ import { _ as isMember, a as AUTHENTICATED_SCOPE, c as getOrgContext, d as getTeamId, f as getUserId, g as isElevated, h as isAuthenticated, i as elevationPlugin, l as getOrgId, m as hasOrgAccess, n as ElevationOptions, o as PUBLIC_SCOPE, p as getUserRoles, r as _default, s as RequestScope, t as ElevationEvent, u as getOrgRoles } from "../elevation-C_taLQrM.mjs";
2
2
  import { FastifyReply, FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/scope/rateLimitKey.d.ts
@@ -29,4 +29,4 @@ interface ResolveOrgFromHeaderOptions {
29
29
  */
30
30
  declare function resolveOrgFromHeader(options: ResolveOrgFromHeaderOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
31
31
  //#endregion
32
- export { AUTHENTICATED_SCOPE, type ElevationEvent, type ElevationOptions, PUBLIC_SCOPE, type RateLimitKeyContext, type RequestScope, type ResolveOrgFromHeaderOptions, type TenantKeyGeneratorOptions, createTenantKeyGenerator, _default as elevationPlugin, elevationPlugin as elevationPluginFn, getOrgId, getOrgRoles, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, resolveOrgFromHeader };
32
+ export { AUTHENTICATED_SCOPE, type ElevationEvent, type ElevationOptions, PUBLIC_SCOPE, type RateLimitKeyContext, type RequestScope, type ResolveOrgFromHeaderOptions, type TenantKeyGeneratorOptions, createTenantKeyGenerator, _default as elevationPlugin, elevationPlugin as elevationPluginFn, getOrgContext, getOrgId, getOrgRoles, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, resolveOrgFromHeader };
@@ -1,4 +1,4 @@
1
- import { a as getTeamId, c as hasOrgAccess, d as isMember, i as getOrgRoles, l as isAuthenticated, n as PUBLIC_SCOPE, o as getUserId, r as getOrgId, s as getUserRoles, t as AUTHENTICATED_SCOPE, u as isElevated } from "../types-C6TQjtdi.mjs";
1
+ import { a as getOrgRoles, c as getUserRoles, d as isElevated, f as isMember, i as getOrgId, l as hasOrgAccess, n as PUBLIC_SCOPE, o as getTeamId, r as getOrgContext, s as getUserId, t as AUTHENTICATED_SCOPE, u as isAuthenticated } from "../types-BhtYdxZU.mjs";
2
2
  import { n as normalizeRoles } from "../types-ZUu_h0jp.mjs";
3
3
  import { n as elevation_default, t as elevationPlugin } from "../elevation-BEdACOLB.mjs";
4
4
  //#region src/scope/rateLimitKey.ts
@@ -75,4 +75,4 @@ function resolveOrgFromHeader(options) {
75
75
  };
76
76
  }
77
77
  //#endregion
78
- export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, createTenantKeyGenerator, elevation_default as elevationPlugin, elevationPlugin as elevationPluginFn, getOrgId, getOrgRoles, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, resolveOrgFromHeader };
78
+ export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, createTenantKeyGenerator, elevation_default as elevationPlugin, elevationPlugin as elevationPluginFn, getOrgContext, getOrgId, getOrgRoles, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, resolveOrgFromHeader };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { n as PUBLIC_SCOPE, r as getOrgId } from "./types-C6TQjtdi.mjs";
2
+ import { i as getOrgId, n as PUBLIC_SCOPE } from "./types-BhtYdxZU.mjs";
3
3
  import { t as arcLog } from "./logger-Dz3j1ItV.mjs";
4
4
  import fp from "fastify-plugin";
5
5
  //#region src/plugins/sse.ts
@@ -1,5 +1,5 @@
1
- import { Lt as ResourceDefinition, l as AnyRecord, zt as CrudRepository } from "../interface-DGmPxakH.mjs";
2
- import { r as CreateAppOptions } from "../types-Dt0-AI6E.mjs";
1
+ import { Bt as ResourceDefinition, Ht as CrudRepository, l as AnyRecord } from "../interface-DDW43OmS.mjs";
2
+ import { r as CreateAppOptions } from "../types-D5hJ-k_3.mjs";
3
3
  import Fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
4
4
  import { Connection } from "mongoose";
5
5
  import { Mock } from "vitest";
@@ -1752,7 +1752,7 @@ function runEventTests(resourceName, displayName, events) {
1752
1752
  * ```
1753
1753
  */
1754
1754
  async function createTestApp(options = {}) {
1755
- const { createApp } = await import("../createApp-ByWNRsZj.mjs").then((n) => n.r);
1755
+ const { createApp } = await import("../createApp-Bol7DLUf.mjs").then((n) => n.r);
1756
1756
  const { useInMemoryDb = true, mongoUri: providedMongoUri, ...appOptions } = options;
1757
1757
  const defaultAuth = {
1758
1758
  type: "jwt",
@@ -1,4 +1,4 @@
1
- import { a as AUTHENTICATED_SCOPE, c as getOrgId, g as isMember, h as isElevated, l as getOrgRoles, m as isAuthenticated, n as ElevationOptions, o as PUBLIC_SCOPE, p as hasOrgAccess, s as RequestScope, t as ElevationEvent, u as getTeamId } from "../elevation-Ca_yveIO.mjs";
2
- import { $ as RegistryStats, A as HealthCheck, At as FastifyHandler, B as MiddlewareConfig, Bt as InferDoc, C as EventDefinition, D as FastifyWithDecorators, E as FastifyWithAuth, F as IntrospectionData, G as ParsedQuery, H as ObjectId, Ht as PaginationParams, I as IntrospectionPluginOptions, J as PresetHook, K as PopulateOption, L as JWTPayload, M as InferAdapterDoc, Mt as IControllerResponse, N as InferDocType, Nt as IRequestContext, O as FieldRule, Ot as ControllerHandler, P as InferResourceDoc, Pt as RouteHandler, Q as RegistryEntry, R as JwtContext, S as CrudSchemas, T as FastifyRequestExtras, U as OpenApiSchemas, Ut as QueryOptions, V as MiddlewareHandler, Vt as PaginatedResult, W as OwnershipCheck, X as QueryParserInterface, Y as PresetResult, Z as RateLimitConfig, _ as ConfigError, _t as ValidateOptions, at as ResourceHooks, b as CrudRouteKey, c as AdditionalRoute, ct as RouteHandlerMethod, d as ArcDecorator, dt as TokenPair, et as RequestContext, f as ArcInternalMetadata, ft as TypedController, g as AuthenticatorContext, gt as UserOrganization, h as Authenticator, ht as UserLike, it as ResourceConfig, j as HealthOptions, jt as IController, k as GracefulShutdownOptions, kt as ControllerLike, l as AnyRecord, lt as RouteSchemaOptions, m as AuthPluginOptions, mt as TypedResourceConfig, nt as RequestWithExtras, ot as ResourceMetadata, p as AuthHelpers, pt as TypedRepository, q as PresetFunction, rt as ResourceCacheConfig, st as ResourcePermissions, tt as RequestIdOptions, u as ApiResponse, ut as ServiceContext, v as ControllerQueryOptions, vt as ValidationResult, w as EventsDecorator, x as CrudRouterOptions, xt as BaseControllerOptions, y as CrudController, yt as getUserId, z as LookupOption, zt as CrudRepository } from "../interface-DGmPxakH.mjs";
1
+ import { _ as isMember, a as AUTHENTICATED_SCOPE, d as getTeamId, g as isElevated, h as isAuthenticated, l as getOrgId, m as hasOrgAccess, n as ElevationOptions, o as PUBLIC_SCOPE, s as RequestScope, t as ElevationEvent, u as getOrgRoles } from "../elevation-C_taLQrM.mjs";
2
+ import { $ as RegistryEntry, A as GracefulShutdownOptions, B as LookupOption, C as CrudSchemas, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, Gt as PaginationParams, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, K as ParsedQuery, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, Nt as FastifyHandler, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, R as JWTPayload, S as CrudRouterOptions, St as getUserId, T as EventsDecorator, U as ObjectId, Ut as InferDoc, V as MiddlewareConfig, W as OpenApiSchemas, Wt as PaginatedResult, X as PresetResult, Y as PresetHook, Z as QueryParserInterface, _ as AuthenticatorContext, _t as UserLike, at as ResourceConfig, b as CrudController, bt as ValidationResult, c as AdditionalRoute, ct as ResourceMetadata, d as ArcDecorator, dt as RouteSchemaOptions, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, g as Authenticator, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, it as ResourceCacheConfig, j as HealthCheck, jt as ControllerHandler, k as FieldRule, l as AnyRecord, lt as ResourcePermissions, m as AuthHelpers, mt as TypedController, nt as RequestIdOptions, ot as ResourceHookContext, p as ArcRequest, pt as TokenPair, q as PopulateOption, rt as RequestWithExtras, st as ResourceHooks, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, y as ControllerQueryOptions, yt as ValidateOptions, z as JwtContext } from "../interface-DDW43OmS.mjs";
3
3
  import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-BNUccdcf.mjs";
4
- export { AUTHENTICATED_SCOPE, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteHandler, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
4
+ export { AUTHENTICATED_SCOPE, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteHandler, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
@@ -1,6 +1,27 @@
1
- import { a as getTeamId, c as hasOrgAccess, d as isMember, i as getOrgRoles, l as isAuthenticated, n as PUBLIC_SCOPE, r as getOrgId, t as AUTHENTICATED_SCOPE, u as isElevated } from "../types-C6TQjtdi.mjs";
1
+ import { a as getOrgRoles, d as isElevated, f as isMember, i as getOrgId, l as hasOrgAccess, n as PUBLIC_SCOPE, o as getTeamId, t as AUTHENTICATED_SCOPE, u as isAuthenticated } from "../types-BhtYdxZU.mjs";
2
2
  //#region src/types/index.ts
3
3
  /**
4
+ * Response envelope helper — wraps data in Arc's standard `{ success, data }` format.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { envelope } from '@classytic/arc';
9
+ *
10
+ * handler: async (req, reply) => {
11
+ * const data = await getResults();
12
+ * return envelope(data);
13
+ * // → { success: true, data }
14
+ * }
15
+ * ```
16
+ */
17
+ function envelope(data, meta) {
18
+ return {
19
+ success: true,
20
+ data,
21
+ ...meta
22
+ };
23
+ }
24
+ /**
4
25
  * Extract user ID from a user object (supports both id and _id)
5
26
  */
6
27
  function getUserId(user) {
@@ -9,4 +30,4 @@ function getUserId(user) {
9
30
  return id ? String(id) : void 0;
10
31
  }
11
32
  //#endregion
12
- export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
33
+ export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
@@ -58,9 +58,34 @@ function getUserRoles(scope) {
58
58
  if (scope.kind === "member") return scope.userRoles;
59
59
  return [];
60
60
  }
61
+ /**
62
+ * Org context — canonical extraction from a Fastify request.
63
+ *
64
+ * Works regardless of auth type (JWT, Better Auth, custom) by reading
65
+ * `request.scope` and `request.user`. Eliminates the need for each resource
66
+ * to re-invent org extraction from headers/user/scope.
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * import { getOrgContext } from '@classytic/arc/scope';
71
+ *
72
+ * handler: async (request, reply) => {
73
+ * const { userId, organizationId, roles, orgRoles } = getOrgContext(request);
74
+ * }
75
+ * ```
76
+ */
77
+ function getOrgContext(request) {
78
+ const scope = request.scope ?? { kind: "public" };
79
+ return {
80
+ userId: getUserId(scope) ?? request.user?.id ?? request.user?._id,
81
+ organizationId: getOrgId(scope) ?? request.user?.organizationId ?? request.headers?.["x-organization-id"],
82
+ roles: getUserRoles(scope),
83
+ orgRoles: getOrgRoles(scope)
84
+ };
85
+ }
61
86
  /** Default public scope — used as initial decoration value */
62
87
  const PUBLIC_SCOPE = Object.freeze({ kind: "public" });
63
88
  /** Default authenticated scope — used when user is logged in but no org */
64
89
  const AUTHENTICATED_SCOPE = Object.freeze({ kind: "authenticated" });
65
90
  //#endregion
66
- export { getTeamId as a, hasOrgAccess as c, isMember as d, getOrgRoles as i, isAuthenticated as l, PUBLIC_SCOPE as n, getUserId as o, getOrgId as r, getUserRoles as s, AUTHENTICATED_SCOPE as t, isElevated as u };
91
+ export { getOrgRoles as a, getUserRoles as c, isElevated as d, isMember as f, getOrgId as i, hasOrgAccess as l, PUBLIC_SCOPE as n, getTeamId as o, getOrgContext as r, getUserId as s, AUTHENTICATED_SCOPE as t, isAuthenticated as u };
@@ -1,14 +1,133 @@
1
- import { n as ElevationOptions } from "./elevation-Ca_yveIO.mjs";
2
- import { h as Authenticator } from "./interface-DGmPxakH.mjs";
1
+ import { n as ElevationOptions } from "./elevation-C_taLQrM.mjs";
2
+ import { g as Authenticator } from "./interface-DDW43OmS.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "./externalPaths-DpO-s7r8.mjs";
4
4
  import { i as CacheStore } from "./interface-D_BWALyZ.mjs";
5
5
  import { r as QueryCachePluginOptions } from "./queryCachePlugin-DcmETvcB.mjs";
6
6
  import { i as EventTransport } from "./EventTransport-wc5hSLik.mjs";
7
- import { t as EventPluginOptions } from "./eventPlugin-iGrSEmwJ.mjs";
7
+ import { t as EventPluginOptions } from "./eventPlugin-DW45v4V5.mjs";
8
8
  import { c as MetricsOptions, d as SSEOptions, m as CachingOptions, r as VersioningOptions, t as ErrorHandlerOptions } from "./errorHandler-Do4vVQ1f.mjs";
9
- import { r as IdempotencyStore } from "./interface-B4awm1RJ.mjs";
9
+ import { r as IdempotencyStore } from "./interface-gr-7qo9j.mjs";
10
10
  import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, FastifyServerOptions } from "fastify";
11
11
 
12
+ //#region src/factory/loadResources.d.ts
13
+ /**
14
+ * loadResources — Auto-discover resource files from a directory.
15
+ *
16
+ * Scans for `*.resource.{ts,js,mts,mjs}` files, imports each,
17
+ * and collects their default exports. No barrel file needed.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { createApp, loadResources } from '@classytic/arc/factory';
22
+ *
23
+ * // Recommended: import.meta.url — works in both src/ (dev) and dist/ (prod)
24
+ * const app = await createApp({
25
+ * resources: await loadResources(import.meta.url),
26
+ * auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
27
+ * });
28
+ *
29
+ * // Or explicit path (must match runtime layout)
30
+ * const app2 = await createApp({
31
+ * resources: await loadResources('./src/resources'),
32
+ * });
33
+ * ```
34
+ *
35
+ * File convention:
36
+ * ```
37
+ * src/resources/
38
+ * product/product.resource.ts → export default defineResource({ name: 'product', ... })
39
+ * order/order.resource.ts → export default defineResource({ name: 'order', ... })
40
+ * ```
41
+ */
42
+ /**
43
+ * Resource interface — the contract between `loadResources`/`createApp` and resource definitions.
44
+ *
45
+ * Matches the shape of `ResourceDefinition` from `defineResource()` without requiring
46
+ * the import. All properties except `toPlugin` are optional so plain objects work too:
47
+ *
48
+ * ```typescript
49
+ * // Full resource (from defineResource)
50
+ * const product = defineResource({ name: 'product', ... }); // satisfies ResourceLike
51
+ *
52
+ * // Minimal resource (plain object)
53
+ * const simple: ResourceLike = { name: 'ping', toPlugin: () => () => {} };
54
+ * ```
55
+ */
56
+ interface ResourceLike {
57
+ /** Plugin factory — called by createApp to register routes */
58
+ toPlugin: () => unknown;
59
+ /** Resource name (used for route generation, logging, duplicate detection) */
60
+ name?: string;
61
+ /** Route prefix (default: `/${name}s`) */
62
+ prefix?: string;
63
+ /** Skip the global `resourcePrefix` from createApp — register at root */
64
+ skipGlobalPrefix?: boolean;
65
+ /** Display name for docs/OpenAPI */
66
+ displayName?: string;
67
+ /** Applied preset names */
68
+ _appliedPresets?: string[];
69
+ }
70
+ interface LoadResourcesOptions {
71
+ /** File pattern suffix (default: '.resource'). Matches `*.resource.{ts,js,mts,mjs}`. */
72
+ suffix?: string;
73
+ /** Recurse into subdirectories (default: true) */
74
+ recursive?: boolean;
75
+ /**
76
+ * Resource names to exclude. Matched against the resource's `.name` property
77
+ * after import, so you use the resource name (not the filename).
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * await loadResources('./src/resources', { exclude: ['debug', 'legacy-report'] })
82
+ * ```
83
+ */
84
+ exclude?: string[];
85
+ /**
86
+ * Resource names to include. When set, only matching resources are returned.
87
+ * Takes priority over `exclude`.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * // Only load these two resources (useful for testing or microservice splits)
92
+ * await loadResources('./src/resources', { include: ['product', 'order'] })
93
+ * ```
94
+ */
95
+ include?: string[];
96
+ /**
97
+ * Suppress warning logs for skipped/failed files.
98
+ * Useful when your resources directory contains factory files or helpers
99
+ * that don't export a resource (e.g., `account.resource.ts` exporting a factory).
100
+ *
101
+ * @default false
102
+ */
103
+ silent?: boolean;
104
+ }
105
+ /**
106
+ * Scan a directory for resource files and import their default exports.
107
+ *
108
+ * Accepts a directory path OR `import.meta.url` (file:// URL).
109
+ * When given a URL, resolves to the directory containing that file —
110
+ * so `loadResources(import.meta.url)` works in both dev (`src/`) and
111
+ * production (`dist/`) without path gymnastics.
112
+ *
113
+ * @param dir - Directory path, or `import.meta.url` (file:// URL resolved to its dirname)
114
+ * @param options - Pattern and recursion options
115
+ * @returns Array of resource definitions (anything with `.toPlugin()`)
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * // Works from both src/ and dist/ — resolves relative to the calling file
120
+ * await loadResources(import.meta.url);
121
+ *
122
+ * // Subdirectory relative to the calling file
123
+ * await loadResources(import.meta.url, { suffix: '.resource' });
124
+ *
125
+ * // Explicit path (must match runtime layout)
126
+ * await loadResources('./src/resources');
127
+ * ```
128
+ */
129
+ declare function loadResources(dir: string, options?: LoadResourcesOptions): Promise<ResourceLike[]>;
130
+ //#endregion
12
131
  //#region src/factory/types.d.ts
13
132
  type CorsOptions = Record<string, unknown> & {
14
133
  origin?: unknown;
@@ -473,8 +592,72 @@ interface CreateAppOptions {
473
592
  ajv?: {
474
593
  keywords?: string[];
475
594
  };
476
- /** Custom plugin registration function */
595
+ /**
596
+ * Resources to register automatically.
597
+ * Each resource's `.toPlugin()` is called and registered for you.
598
+ *
599
+ * @example
600
+ * ```ts
601
+ * const app = await createApp({
602
+ * resources: [productResource, orderResource, userResource],
603
+ * auth: { type: 'jwt', jwt: { secret: 'xxx' } },
604
+ * });
605
+ * ```
606
+ */
607
+ resources?: Array<ResourceLike>;
608
+ /**
609
+ * URL prefix for all auto-registered resources.
610
+ * Applied only to resources in the `resources` array — not to `plugins()`.
611
+ *
612
+ * @example
613
+ * ```ts
614
+ * const app = await createApp({
615
+ * resourcePrefix: '/api/v1',
616
+ * resources: await loadResources(import.meta.url),
617
+ * });
618
+ * // product → /api/v1/products, order → /api/v1/orders
619
+ * ```
620
+ */
621
+ resourcePrefix?: string;
622
+ /**
623
+ * Custom plugin registration — runs after Arc core (security, auth, events)
624
+ * but before `bootstrap` and `resources`.
625
+ *
626
+ * Use this for infrastructure setup: database connections, OpenAPI docs,
627
+ * webhook plugins, SSE wiring, etc.
628
+ */
477
629
  plugins?: (fastify: FastifyInstance) => Promise<void>;
630
+ /**
631
+ * Bootstrap functions — run after `plugins()` but before `resources`.
632
+ *
633
+ * Use this for domain initialization that needs infrastructure ready
634
+ * (DB connected, events wired, Redis available) but must complete
635
+ * before resources register (e.g., engine singletons, event handlers,
636
+ * seed data, connection verification).
637
+ *
638
+ * Boot order:
639
+ * ```
640
+ * 1. Arc core (security, auth, events)
641
+ * 2. plugins() ← infra (DB, SSE, docs)
642
+ * 3. bootstrap[] ← domain init (singletons, event handlers)
643
+ * 4. resources[] ← auto-discovered routes
644
+ * ```
645
+ *
646
+ * @example
647
+ * ```ts
648
+ * const app = await createApp({
649
+ * plugins: async (f) => { await connectDB(); await f.register(docsPlugin); },
650
+ * bootstrap: [inventoryInit, accountingInit, loyaltyInit],
651
+ * resources: await loadResources(import.meta.url),
652
+ * });
653
+ * ```
654
+ */
655
+ bootstrap?: Array<(fastify: FastifyInstance) => void | Promise<void>>;
656
+ /**
657
+ * Hook called after resources are registered but before the app is ready.
658
+ * Use for post-registration wiring (e.g., cross-resource event subscriptions).
659
+ */
660
+ afterResources?: (fastify: FastifyInstance) => void | Promise<void>;
478
661
  /** Hook called after all plugins are loaded and the app is ready */
479
662
  onReady?: (fastify: FastifyInstance) => void | Promise<void>;
480
663
  /** Hook called when the app is shutting down */
@@ -507,4 +690,4 @@ interface RawBodyOptions {
507
690
  runFirst?: boolean;
508
691
  }
509
692
  //#endregion
510
- export { CustomPluginAuthOption as a, RawBodyOptions as c, CustomAuthenticatorOption as i, UnderPressureOptions as l, BetterAuthOption as n, JwtAuthOption as o, CreateAppOptions as r, MultipartOptions as s, AuthOption as t };
693
+ export { CustomPluginAuthOption as a, RawBodyOptions as c, ResourceLike as d, loadResources as f, CustomAuthenticatorOption as i, UnderPressureOptions as l, BetterAuthOption as n, JwtAuthOption as o, CreateAppOptions as r, MultipartOptions as s, AuthOption as t, LoadResourcesOptions as u };
@@ -1,4 +1,4 @@
1
- import { Lt as ResourceDefinition } from "./interface-DGmPxakH.mjs";
1
+ import { Bt as ResourceDefinition } from "./interface-DDW43OmS.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/integrations/mcp/types.d.ts
@@ -1,5 +1,5 @@
1
- import { G as ParsedQuery, U as OpenApiSchemas, X as QueryParserInterface, l as AnyRecord } from "../interface-DGmPxakH.mjs";
2
- import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createError, i as ForbiddenError, l as ServiceUnavailableError, n as ConflictError, o as OrgAccessDeniedError, p as isArcError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-CPpvPHT0.mjs";
1
+ import { K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, l as AnyRecord } from "../interface-DDW43OmS.mjs";
2
+ import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-CcVbl1-T.mjs";
3
3
  import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-JP2GdJ4b.mjs";
4
4
  import { FastifyInstance } from "fastify";
5
5
 
@@ -1,7 +1,7 @@
1
1
  import { n as createQueryParser, t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
2
2
  import { a as createCircuitBreaker, i as CircuitState, n as CircuitBreakerError, o as createCircuitBreakerRegistry, r as CircuitBreakerRegistry, t as CircuitBreaker } from "../circuitBreaker-BOBOpN2w.mjs";
3
3
  import { _ as defineCompensation, a as getListQueryParams, c as listResponse, d as paginateWrapper, f as paginationSchema, g as wrapResponse, h as successResponseSchema, i as getDefaultCrudSchemas, l as messageWrapper, m as responses, n as deleteResponse, o as itemResponse, p as queryParams, r as errorResponseSchema, s as itemWrapper, t as createStateMachine, u as mutationResponse, v as withCompensation } from "../utils-Dc0WhlIl.mjs";
4
- import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createError, f as isArcError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-rxhfP7Hf.mjs";
4
+ import { a as OrgAccessDeniedError, c as ServiceUnavailableError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-NoQKsbAT.mjs";
5
5
  import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-DjzHpFam.mjs";
6
6
  import { t as hasEvents } from "../typeGuards-Cj5Rgvlg.mjs";
7
7
  export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createError, createQueryParser, createStateMachine, defineCompensation, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, itemWrapper, listResponse, messageWrapper, mutationResponse, paginateWrapper, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.4.2",
3
+ "version": "2.6.1",
4
4
  "description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -220,7 +220,7 @@
220
220
  "node": ">=22"
221
221
  },
222
222
  "peerDependencies": {
223
- "@classytic/mongokit": ">=3.4.3",
223
+ "@classytic/mongokit": ">=3.5.0",
224
224
  "@classytic/streamline": ">=2.0.0",
225
225
  "@fastify/cors": "^11.0.0",
226
226
  "@fastify/helmet": "^13.0.0",
@@ -244,7 +244,7 @@
244
244
  "fastify-raw-body": "^5.0.0",
245
245
  "ioredis": "^5.0.0",
246
246
  "mongodb": "^6.0.0 || ^7.0.0",
247
- "mongoose": "^8.0.0 || ^9.0.0",
247
+ "mongoose": ">=9.0.0",
248
248
  "pino-pretty": "^13.0.0",
249
249
  "zod": "^4.0.0"
250
250
  },
@@ -333,11 +333,12 @@
333
333
  },
334
334
  "dependencies": {
335
335
  "fastify-plugin": "^5.0.1",
336
- "qs": "^6.14.1"
336
+ "qs": "^6.14.1",
337
+ "secure-json-parse": "^4.1.0"
337
338
  },
338
339
  "devDependencies": {
339
340
  "@biomejs/biome": "^2.4.10",
340
- "@classytic/mongokit": "^3.4.3",
341
+ "@classytic/mongokit": "^3.5.2",
341
342
  "@fastify/jwt": "^10.0.0",
342
343
  "@fastify/multipart": "^9.0.0",
343
344
  "@fastify/type-provider-typebox": "^6.0.0",
@@ -349,6 +350,7 @@
349
350
  "@vitest/coverage-v8": "^3.2.4",
350
351
  "fastify-raw-body": "^5.0.0",
351
352
  "jsonwebtoken": "^9.0.0",
353
+ "knip": "^6.3.0",
352
354
  "mongodb": "^7.1.0",
353
355
  "mongodb-memory-server": "^11.0.1",
354
356
  "tsdown": "^0.21.7",