@classytic/arc 2.8.0 → 2.8.3

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 (117) hide show
  1. package/README.md +10 -1
  2. package/dist/{BaseController-CpMfCXdn.mjs → BaseController-DAGGc5Xn.mjs} +76 -25
  3. package/dist/{EventTransport-n1KBxC_N.d.mts → EventTransport-CinyO7zQ.d.mts} +37 -1
  4. package/dist/{ResourceRegistry-BOtJuRCs.mjs → ResourceRegistry-Dq3_zBQP.mjs} +17 -5
  5. package/dist/adapters/index.d.mts +2 -2
  6. package/dist/adapters/index.mjs +1 -1
  7. package/dist/{adapters-BxGgSHjj.mjs → adapters-BBqAVvPK.mjs} +11 -0
  8. package/dist/audit/index.d.mts +1 -1
  9. package/dist/audit/index.mjs +1 -1
  10. package/dist/audit/mongodb.d.mts +1 -1
  11. package/dist/audit/mongodb.mjs +1 -1
  12. package/dist/auth/index.d.mts +4 -4
  13. package/dist/auth/index.mjs +3 -3
  14. package/dist/auth/redis-session.d.mts +1 -1
  15. package/dist/{betterAuthOpenApi-CHCIuA-p.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
  16. package/dist/cache/index.d.mts +2 -2
  17. package/dist/cli/commands/describe.mjs +1 -1
  18. package/dist/cli/commands/docs.mjs +2 -2
  19. package/dist/cli/commands/generate.mjs +1 -1
  20. package/dist/cli/commands/init.mjs +10 -10
  21. package/dist/cli/commands/introspect.mjs +3 -3
  22. package/dist/core/index.d.mts +3 -3
  23. package/dist/core/index.mjs +5 -5
  24. package/dist/{core-BfrfxNqO.mjs → core-DKSwNSXf.mjs} +1 -1
  25. package/dist/{createActionRouter-CbkIAaGh.mjs → createActionRouter-Df1BuawX.mjs} +87 -21
  26. package/dist/{createApp-Cy8eUNKQ.mjs → createApp-BOYjBgdI.mjs} +16 -7
  27. package/dist/{defineResource-CovBXvTB.mjs → defineResource-Bb_Bdhtw.mjs} +60 -33
  28. package/dist/docs/index.d.mts +2 -2
  29. package/dist/docs/index.mjs +1 -1
  30. package/dist/dynamic/index.d.mts +2 -2
  31. package/dist/dynamic/index.mjs +1 -1
  32. package/dist/{errorHandler-BeN-ERN7.d.mts → errorHandler-CdZDavNH.d.mts} +2 -2
  33. package/dist/{errorHandler-BW08lEiy.mjs → errorHandler-mzqk4cGl.mjs} +1 -1
  34. package/dist/{eventPlugin-CAOWMQS8.d.mts → eventPlugin-CVxlE6De.d.mts} +1 -1
  35. package/dist/{eventPlugin-x4jo3sG0.mjs → eventPlugin-D91S2YF4.mjs} +19 -1
  36. package/dist/events/index.d.mts +399 -28
  37. package/dist/events/index.mjs +345 -29
  38. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  39. package/dist/events/transports/redis-stream-entry.mjs +3 -1
  40. package/dist/events/transports/redis.d.mts +1 -1
  41. package/dist/factory/index.d.mts +1 -1
  42. package/dist/factory/index.mjs +2 -152
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/idempotency/index.d.mts +3 -3
  45. package/dist/idempotency/mongodb.d.mts +1 -1
  46. package/dist/idempotency/mongodb.mjs +18 -6
  47. package/dist/idempotency/redis.d.mts +1 -1
  48. package/dist/idempotency/redis.mjs +10 -1
  49. package/dist/{index-BpMhrFgn.d.mts → index-BgmMdpm8.d.mts} +1 -1
  50. package/dist/{index-CBru2y5Y.d.mts → index-CSkeivBx.d.mts} +3 -3
  51. package/dist/{index-qct60lnl.d.mts → index-CpTSDqmD.d.mts} +60 -6
  52. package/dist/index.d.mts +8 -8
  53. package/dist/index.mjs +7 -7
  54. package/dist/integrations/event-gateway.d.mts +1 -1
  55. package/dist/integrations/event-gateway.mjs +1 -1
  56. package/dist/integrations/index.d.mts +1 -1
  57. package/dist/integrations/mcp/index.d.mts +2 -2
  58. package/dist/integrations/mcp/index.mjs +1 -1
  59. package/dist/integrations/mcp/testing.d.mts +1 -1
  60. package/dist/integrations/mcp/testing.mjs +1 -1
  61. package/dist/{interface-IJqN3pXK.d.mts → interface-BVuMfeVv.d.mts} +596 -125
  62. package/dist/loadResources-Bksk8ydA.mjs +154 -0
  63. package/dist/{mongodb-B1eVtFhw.d.mts → mongodb-B8U2xaLj.d.mts} +1 -1
  64. package/dist/{mongodb-NShVZDMr.d.mts → mongodb-X7LbEjTN.d.mts} +10 -1
  65. package/dist/{openapi-AYLVjqVe.mjs → openapi-CYCuekCn.mjs} +50 -3
  66. package/dist/org/index.d.mts +2 -2
  67. package/dist/permissions/index.d.mts +3 -3
  68. package/dist/plugins/index.d.mts +5 -5
  69. package/dist/plugins/index.mjs +8 -8
  70. package/dist/plugins/tracing-entry.d.mts +1 -1
  71. package/dist/plugins/tracing-entry.mjs +1 -1
  72. package/dist/policies/index.d.mts +1 -1
  73. package/dist/presets/index.d.mts +3 -3
  74. package/dist/presets/index.mjs +1 -1
  75. package/dist/presets/multiTenant.d.mts +1 -1
  76. package/dist/{presets-BFrGvvjL.mjs → presets-C2xgzW6x.mjs} +10 -18
  77. package/dist/{queryCachePlugin-BCFVXnxK.d.mts → queryCachePlugin-CnTZZTC5.d.mts} +1 -1
  78. package/dist/{redis-stream-CF1lrKVk.d.mts → redis-stream-D54N5oXs.d.mts} +1 -1
  79. package/dist/{redis-Bunu3qWg.d.mts → redis-z3sFr1UP.d.mts} +1 -1
  80. package/dist/registry/index.d.mts +1 -1
  81. package/dist/registry/index.mjs +1 -1
  82. package/dist/{resourceToTools-C_1SMiCz.mjs → resourceToTools-O_HwWXFa.mjs} +194 -64
  83. package/dist/rpc/index.d.mts +1 -1
  84. package/dist/rpc/index.mjs +1 -1
  85. package/dist/scope/index.d.mts +2 -2
  86. package/dist/testing/index.d.mts +2 -2
  87. package/dist/testing/index.mjs +1 -1
  88. package/dist/types/index.d.mts +5 -5
  89. package/dist/{types-gUxAIZHp.d.mts → types-Bg2X42_m.d.mts} +30 -9
  90. package/dist/{types-BoaZHr-2.d.mts → types-CVC4HOKi.d.mts} +1 -1
  91. package/dist/{types-Ct0PUUSp.d.mts → types-CcG4avic.d.mts} +1 -1
  92. package/dist/utils/index.d.mts +43 -17
  93. package/dist/utils/index.mjs +5 -5
  94. package/dist/{utils-B-l6410F.mjs → utils-yYT3HDXt.mjs} +65 -13
  95. package/package.json +10 -9
  96. package/skills/arc/SKILL.md +79 -6
  97. /package/dist/{caching-CHH-iHs3.mjs → caching-CjybdRwx.mjs} +0 -0
  98. /package/dist/{circuitBreaker-BGVoB1hD.d.mts → circuitBreaker-CvXkjfrW.d.mts} +0 -0
  99. /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
  100. /package/dist/{elevation-UJO3-NvX.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  101. /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
  102. /package/dist/{errors-BI8kEKsO.d.mts → errors-Bmn3eZT6.d.mts} +0 -0
  103. /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  104. /package/dist/{fields-DoeDgh2b.d.mts → fields-DC4So2M2.d.mts} +0 -0
  105. /package/dist/{interface-CkkWm5uR.d.mts → interface-B-pe8fhj.d.mts} +0 -0
  106. /package/dist/{interface-bpoLKKqx.d.mts → interface-DplgQO2e.d.mts} +0 -0
  107. /package/dist/{metrics-DuhiSEZI.mjs → metrics-TuOmguhi.mjs} +0 -0
  108. /package/dist/{mongodb-5Ff3w8jy.mjs → mongodb-B5O6xaW1.mjs} +0 -0
  109. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-A0tWEl1K.mjs} +0 -0
  110. /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
  111. /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
  112. /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
  113. /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  114. /package/dist/{sse-CD5Hghpu.mjs → sse-CJpt7LGI.mjs} +0 -0
  115. /package/dist/{tracing-xqXzWeaf.d.mts → tracing-DxjKk7eW.d.mts} +0 -0
  116. /package/dist/{types-CN6JvmYz.d.mts → types-C72d3NDn.d.mts} +0 -0
  117. /package/dist/{versioning-CPU_5Xfs.mjs → versioning-Cm8qoFDg.mjs} +0 -0
@@ -1,3 +1,4 @@
1
+ import { t as ArcError } from "./errors-BF2bIOIS.mjs";
1
2
  //#region src/utils/compensation.ts
2
3
  /**
3
4
  * Run steps in order with automatic compensation on failure.
@@ -69,6 +70,69 @@ function defineCompensation(name, steps) {
69
70
  };
70
71
  }
71
72
  //#endregion
73
+ //#region src/utils/defineGuard.ts
74
+ /** Hidden property key for guard context storage on the request object. */
75
+ const GUARD_STORE_KEY = "__arcGuardContext";
76
+ /**
77
+ * Create a typed guard. See module JSDoc for usage.
78
+ */
79
+ function defineGuard(config) {
80
+ const { name, resolve } = config;
81
+ const preHandler = async (req, reply) => {
82
+ const ctx = await resolve(req, reply);
83
+ if (!reply.sent) {
84
+ const store = req[GUARD_STORE_KEY] ?? {};
85
+ store[name] = ctx;
86
+ req[GUARD_STORE_KEY] = store;
87
+ }
88
+ };
89
+ return {
90
+ preHandler,
91
+ name,
92
+ from(req) {
93
+ const store = req[GUARD_STORE_KEY];
94
+ if (!store || !(name in store)) throw new Error(`Guard '${name}' not resolved on this request. Add it to routeGuards or the route's preHandler array.`);
95
+ return store[name];
96
+ }
97
+ };
98
+ }
99
+ //#endregion
100
+ //#region src/utils/handleRaw.ts
101
+ /**
102
+ * Wrap a raw Fastify handler with Arc's response envelope and error handling.
103
+ *
104
+ * @param handler - Async function that receives `(request, reply)` and returns data.
105
+ * The return value is sent as `{ success: true, data }`. If it returns
106
+ * `undefined` or `null`, `{ success: true }` is sent (no `data` field).
107
+ * @param statusCode - HTTP status code for successful responses (default: 200)
108
+ */
109
+ function handleRaw(handler, statusCode = 200) {
110
+ return async (request, reply) => {
111
+ try {
112
+ const result = await handler(request, reply);
113
+ if (reply.sent) return;
114
+ if (result === void 0 || result === null) reply.code(statusCode).send({ success: true });
115
+ else reply.code(statusCode).send({
116
+ success: true,
117
+ data: result
118
+ });
119
+ } catch (err) {
120
+ if (reply.sent) return;
121
+ if (err instanceof ArcError) {
122
+ reply.code(err.statusCode).send(err.toJSON());
123
+ return;
124
+ }
125
+ const error = err;
126
+ const code = error.statusCode ?? error.status ?? 500;
127
+ reply.code(code).send({
128
+ success: false,
129
+ error: error.message ?? "Internal server error",
130
+ ...error.code && { code: error.code }
131
+ });
132
+ }
133
+ };
134
+ }
135
+ //#endregion
72
136
  //#region src/utils/responseSchemas.ts
73
137
  /**
74
138
  * Base success response schema
@@ -216,10 +280,6 @@ function listResponse(itemSchema) {
216
280
  };
217
281
  }
218
282
  /**
219
- * Alias for listResponse - matches local responseSchemas.js naming
220
- */
221
- const paginateWrapper = listResponse;
222
- /**
223
283
  * Create a single item response schema
224
284
  *
225
285
  * Runtime format: { success, data: {...} }
@@ -228,10 +288,6 @@ function itemResponse(itemSchema) {
228
288
  return wrapResponse(itemSchema);
229
289
  }
230
290
  /**
231
- * Alias for itemResponse - matches local responseSchemas.js naming
232
- */
233
- const itemWrapper = itemResponse;
234
- /**
235
291
  * Create a create/update response schema
236
292
  */
237
293
  function mutationResponse(itemSchema) {
@@ -288,10 +344,6 @@ function deleteResponse() {
288
344
  additionalProperties: true
289
345
  };
290
346
  }
291
- /**
292
- * Alias for deleteResponse - matches local responseSchemas.js naming
293
- */
294
- const messageWrapper = deleteResponse;
295
347
  const responses = {
296
348
  200: (schema) => ({
297
349
  description: "Successful response",
@@ -591,4 +643,4 @@ function createStateMachine(name, transitions = {}, options = {}) {
591
643
  };
592
644
  }
593
645
  //#endregion
594
- export { defineCompensation as _, getListQueryParams as a, listResponse as c, paginateWrapper as d, paginationSchema as f, wrapResponse as g, successResponseSchema as h, getDefaultCrudSchemas as i, messageWrapper as l, responses as m, deleteResponse as n, itemResponse as o, queryParams as p, errorResponseSchema as r, itemWrapper as s, createStateMachine as t, mutationResponse as u, withCompensation as v };
646
+ export { withCompensation as _, getListQueryParams as a, mutationResponse as c, responses as d, successResponseSchema as f, defineCompensation as g, defineGuard as h, getDefaultCrudSchemas as i, paginationSchema as l, handleRaw as m, deleteResponse as n, itemResponse as o, wrapResponse as p, errorResponseSchema as r, listResponse as s, createStateMachine as t, queryParams as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.8.0",
3
+ "version": "2.8.3",
4
4
  "description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -227,7 +227,7 @@
227
227
  "node": ">=22"
228
228
  },
229
229
  "peerDependencies": {
230
- "@classytic/mongokit": ">=3.5.6",
230
+ "@classytic/mongokit": ">=3.6.0",
231
231
  "@classytic/streamline": ">=2.1.0",
232
232
  "@fastify/cors": ">=11.0.0",
233
233
  "@fastify/helmet": ">=13.0.0",
@@ -340,18 +340,18 @@
340
340
  },
341
341
  "dependencies": {
342
342
  "fastify-plugin": "^5.0.1",
343
- "qs": "^6.14.1",
343
+ "qs": "^6.15.1",
344
344
  "secure-json-parse": "^4.1.0"
345
345
  },
346
346
  "devDependencies": {
347
- "@better-auth/mongo-adapter": "^1.6.0",
348
- "@biomejs/biome": "^2.4.10",
349
- "@classytic/mongokit": "3.5.6",
347
+ "@better-auth/mongo-adapter": "^1.6.2",
348
+ "@biomejs/biome": "^2.4.11",
349
+ "@classytic/mongokit": "file:../../packages/mongokit/classytic-mongokit-3.6.0.tgz",
350
350
  "@classytic/streamline": "^2.1.0",
351
351
  "@fastify/cors": "^11.2.0",
352
352
  "@fastify/helmet": "^13.0.2",
353
353
  "@fastify/jwt": "^10.0.0",
354
- "@fastify/multipart": "^9.0.0",
354
+ "@fastify/multipart": "^10.0.0",
355
355
  "@fastify/rate-limit": "^10.3.0",
356
356
  "@fastify/sensible": "^6.0.4",
357
357
  "@fastify/type-provider-typebox": "^6.0.0",
@@ -362,10 +362,11 @@
362
362
  "@types/node": "^22.10.0",
363
363
  "@types/qs": "^6.14.0",
364
364
  "@vitest/coverage-v8": "^3.2.4",
365
- "better-auth": "^1.6.0",
365
+ "ajv": "^8.18.0",
366
+ "better-auth": "^1.6.2",
366
367
  "fastify-raw-body": "^5.0.0",
367
368
  "jsonwebtoken": "^9.0.0",
368
- "knip": "^6.3.0",
369
+ "knip": "^6.4.1",
369
370
  "mongodb": "^7.1.0",
370
371
  "mongodb-memory-server": "^11.0.1",
371
372
  "mongoose": "^9.4.1",
@@ -8,7 +8,7 @@ description: |
8
8
  Triggers: arc, fastify resource, defineResource, createApp, BaseController, arc preset,
9
9
  arc auth, arc events, arc jobs, arc websocket, arc mcp, arc plugin, arc testing, arc cli,
10
10
  arc permissions, arc hooks, arc pipeline, arc factory, arc cache, arc QueryCache.
11
- version: 2.8.0
11
+ version: 2.8.1
12
12
  license: MIT
13
13
  metadata:
14
14
  author: Classytic
@@ -88,17 +88,28 @@ const productResource = defineResource({
88
88
  delete: requireRoles(['admin']),
89
89
  },
90
90
  cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] },
91
- additionalRoutes: [
92
- { method: 'GET', path: '/featured', handler: 'getFeatured', permissions: allowPublic(), wrapHandler: true },
93
- ],
94
91
 
95
- // v2.8: routes (replaces additionalRoutes additionalRoutes still works but is deprecated)
92
+ // v2.8.1: routeGuards auto-apply to ALL routes (CRUD + custom + preset)
93
+ routeGuards: [modeGuard, orgGuard.preHandler],
94
+
95
+ // v2.8.1: fieldRules constraints → auto-map to OpenAPI + AJV validation
96
+ schemaOptions: {
97
+ fieldRules: {
98
+ name: { minLength: 2, maxLength: 200, description: 'Product name' },
99
+ price: { min: 0, max: 100000 },
100
+ sku: { pattern: '^[A-Z]{3}-\\d{3}$' },
101
+ status: { enum: ['draft', 'active', 'archived'] },
102
+ deletedAt: { systemManaged: true },
103
+ },
104
+ },
105
+
106
+ // Custom routes (compose with presets — softDelete adds /deleted, /:id/restore)
96
107
  routes: [
97
108
  { method: 'GET', path: '/stats', handler: 'getStats', permissions: auth() },
98
109
  { method: 'POST', path: '/webhook', handler: webhookFn, raw: true, permissions: auth() },
99
110
  ],
100
111
 
101
- // v2.8: actions (replaces onRegister + createActionRouter)
112
+ // Actions (replaces onRegister + createActionRouter)
102
113
  actions: {
103
114
  approve: async (id, data, req) => service.approve(id, req.user._id),
104
115
  cancel: {
@@ -112,8 +123,70 @@ const productResource = defineResource({
112
123
 
113
124
  await fastify.register(productResource.toPlugin());
114
125
  // Auto-generates: GET /, GET /:id, POST /, PATCH /:id, DELETE /:id
126
+ // + softDelete preset adds: GET /deleted, POST /:id/restore
127
+ ```
128
+
129
+ ## routeGuards + defineGuard (v2.8.1)
130
+
131
+ Resource-level guards that apply to **every** route (CRUD + custom + preset):
132
+
133
+ ```typescript
134
+ import { defineGuard } from '@classytic/arc/utils';
135
+ import type { RouteHandlerMethod } from '@classytic/arc';
136
+
137
+ // Simple guard — reject if condition fails
138
+ const modeGuard: RouteHandlerMethod = async (req, reply) => {
139
+ if (!req.headers['x-mode']) {
140
+ reply.code(403).send({ error: 'Mode header required' });
141
+ }
142
+ };
143
+
144
+ // Typed guard — resolve context once, extract anywhere
145
+ const orgGuard = defineGuard({
146
+ name: 'org',
147
+ resolve: (req) => {
148
+ const orgId = req.headers['x-org-id'] as string;
149
+ if (!orgId) throw new Error('Missing x-org-id');
150
+ return { orgId, actorId: req.user?.id ?? 'system' };
151
+ },
152
+ });
153
+
154
+ defineResource({
155
+ name: 'procurement',
156
+ routeGuards: [modeGuard, orgGuard.preHandler], // all routes protected
157
+ routes: [{
158
+ method: 'GET', path: '/summary', raw: true, permissions: auth(),
159
+ handler: async (req, reply) => {
160
+ const { orgId } = orgGuard.from(req); // typed, no re-computation
161
+ reply.send({ orgId, count: await Model.countDocuments() });
162
+ },
163
+ }],
164
+ // ...
165
+ });
115
166
  ```
116
167
 
168
+ **Execution order:** auth → permissions → cache/idempotency → `routeGuards` → per-route `preHandler`
169
+
170
+ ## fieldRules → OpenAPI + AJV (v2.8.1)
171
+
172
+ One definition, two outputs — constraints auto-map to OpenAPI schema + Fastify AJV validation:
173
+
174
+ ```typescript
175
+ schemaOptions: {
176
+ fieldRules: {
177
+ name: { minLength: 2, maxLength: 200, description: 'Product name' },
178
+ price: { min: 0, max: 100000 },
179
+ sku: { pattern: '^[A-Z]{3}-\\d{3}$' },
180
+ status: { enum: ['draft', 'active', 'archived'] },
181
+ password: { hidden: true }, // blocked from select + OpenAPI
182
+ deletedAt: { systemManaged: true }, // blocked from input schemas
183
+ slug: { immutable: true }, // excluded from update body
184
+ },
185
+ },
186
+ ```
187
+
188
+ Mongoose model-level constraints (`minlength`, `maxlength`, `min`, `max`, `enum`) take precedence. `fieldRules` supplements what the model doesn't declare.
189
+
117
190
  ## Authentication
118
191
 
119
192
  Auth uses a **discriminated union** with `type` field:
File without changes