@classytic/arc 2.10.3 → 2.11.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 (153) hide show
  1. package/README.md +1 -1
  2. package/dist/{BaseController-CbKKIflT.mjs → BaseController-JNV08qOT.mjs} +595 -537
  3. package/dist/{queryCachePlugin-BKbWjgDG.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  4. package/dist/actionPermissions-C8YYU92K.mjs +22 -0
  5. package/dist/adapters/index.d.mts +2 -2
  6. package/dist/adapters/index.mjs +1 -1
  7. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  8. package/dist/audit/index.d.mts +2 -2
  9. package/dist/audit/index.mjs +15 -17
  10. package/dist/auth/index.d.mts +4 -4
  11. package/dist/auth/index.mjs +3 -3
  12. package/dist/auth/redis-session.d.mts +1 -1
  13. package/dist/{betterAuthOpenApi-BBRVhjQN.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  14. package/dist/cache/index.d.mts +3 -2
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/cli/commands/docs.mjs +2 -2
  17. package/dist/cli/commands/generate.mjs +37 -27
  18. package/dist/cli/commands/init.mjs +47 -34
  19. package/dist/cli/commands/introspect.mjs +1 -1
  20. package/dist/context/index.d.mts +58 -0
  21. package/dist/context/index.mjs +2 -0
  22. package/dist/core/index.d.mts +3 -3
  23. package/dist/core/index.mjs +4 -3
  24. package/dist/core-DXdSSFW-.mjs +1037 -0
  25. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  26. package/dist/{createApp-BuvPma24.mjs → createApp-DvNYEhpb.mjs} +118 -36
  27. package/dist/docs/index.d.mts +2 -2
  28. package/dist/docs/index.mjs +1 -1
  29. package/dist/{elevation-C7hgL_aI.mjs → elevation-DOFoxoDs.mjs} +1 -1
  30. package/dist/errorHandler-Co3lnVmJ.d.mts +114 -0
  31. package/dist/{eventPlugin-DCUjuiQT.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  32. package/dist/{eventPlugin-CxWgpd6K.d.mts → eventPlugin-CUNjYYRY.d.mts} +1 -1
  33. package/dist/events/index.d.mts +4 -4
  34. package/dist/events/index.mjs +69 -51
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis.d.mts +1 -1
  37. package/dist/factory/index.d.mts +1 -1
  38. package/dist/factory/index.mjs +2 -2
  39. package/dist/{fields-Lo1VUDpt.d.mts → fields-C8Y0XLAu.d.mts} +1 -1
  40. package/dist/hooks/index.d.mts +1 -1
  41. package/dist/hooks/index.mjs +1 -1
  42. package/dist/idempotency/index.d.mts +3 -3
  43. package/dist/idempotency/index.mjs +38 -27
  44. package/dist/idempotency/redis.d.mts +1 -1
  45. package/dist/{index-ChIw3776.d.mts → index-BYCqHCVu.d.mts} +4 -4
  46. package/dist/{index-Cl0uoKd5.d.mts → index-Cm0vUrr_.d.mts} +2100 -1688
  47. package/dist/{index-DStwgFUK.d.mts → index-DAushRTt.d.mts} +29 -10
  48. package/dist/index-DsJ1MNfC.d.mts +1179 -0
  49. package/dist/{index-8qw4y6ff.d.mts → index-t8pLpPFW.d.mts} +13 -10
  50. package/dist/index.d.mts +7 -251
  51. package/dist/index.mjs +8 -128
  52. package/dist/integrations/event-gateway.d.mts +2 -2
  53. package/dist/integrations/event-gateway.mjs +1 -1
  54. package/dist/integrations/index.d.mts +2 -2
  55. package/dist/integrations/mcp/index.d.mts +2 -2
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -1
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/integrations/streamline.d.mts +46 -5
  60. package/dist/integrations/streamline.mjs +50 -21
  61. package/dist/integrations/websocket-redis.d.mts +1 -1
  62. package/dist/integrations/websocket.d.mts +2 -154
  63. package/dist/integrations/websocket.mjs +292 -224
  64. package/dist/{keys-qcD-TVJl.mjs → keys-CARyUjiR.mjs} +2 -0
  65. package/dist/{loadResources-BAzJItAJ.mjs → loadResources-YNwKHvRA.mjs} +3 -1
  66. package/dist/logger/index.d.mts +81 -0
  67. package/dist/{logger-DLg8-Ueg.mjs → logger/index.mjs} +1 -6
  68. package/dist/middleware/index.d.mts +109 -0
  69. package/dist/middleware/index.mjs +70 -0
  70. package/dist/multipartBody-CvTR1Un6.mjs +123 -0
  71. package/dist/{openapi-B5F8AddX.mjs → openapi-C0L9ar7m.mjs} +9 -7
  72. package/dist/org/index.d.mts +2 -2
  73. package/dist/permissions/index.d.mts +2 -2
  74. package/dist/permissions/index.mjs +1 -3
  75. package/dist/{permissions-Dk6mshja.mjs → permissions-B4vU9L0Q.mjs} +220 -2
  76. package/dist/pipe-DVoIheVC.mjs +62 -0
  77. package/dist/pipeline/index.d.mts +62 -0
  78. package/dist/pipeline/index.mjs +53 -0
  79. package/dist/plugins/index.d.mts +25 -5
  80. package/dist/plugins/index.mjs +10 -10
  81. package/dist/plugins/response-cache.mjs +1 -1
  82. package/dist/plugins/tracing-entry.d.mts +1 -1
  83. package/dist/plugins/tracing-entry.mjs +42 -24
  84. package/dist/presets/filesUpload.d.mts +4 -4
  85. package/dist/presets/filesUpload.mjs +255 -1
  86. package/dist/presets/index.d.mts +1 -1
  87. package/dist/presets/index.mjs +2 -2
  88. package/dist/presets/multiTenant.d.mts +1 -1
  89. package/dist/presets/multiTenant.mjs +48 -8
  90. package/dist/presets/search.d.mts +2 -2
  91. package/dist/presets/search.mjs +1 -1
  92. package/dist/{presets-fLJVXdVn.mjs → presets-k604Lj99.mjs} +1 -1
  93. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  94. package/dist/{queryCachePlugin-DQCEfJis.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  95. package/dist/{redis-DqyeggCa.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  96. package/dist/{redis-stream-CakIQmwR.d.mts → redis-stream-CM8TXTix.d.mts} +1 -1
  97. package/dist/registry/index.d.mts +1 -1
  98. package/dist/registry/index.mjs +2 -2
  99. package/dist/{requestContext-xHIKedG6.mjs → requestContext-CfRkaxwf.mjs} +1 -1
  100. package/dist/{resourceToTools-BElv3xPT.mjs → resourceToTools--okX6QBr.mjs} +534 -415
  101. package/dist/routerShared-DeESFp4a.mjs +515 -0
  102. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  103. package/dist/scope/index.d.mts +2 -2
  104. package/dist/scope/index.mjs +1 -1
  105. package/dist/{sse-yBCgOLGu.mjs → sse-V7aXc3bW.mjs} +1 -1
  106. package/dist/{store-helpers-ZCSMJJAX.mjs → store-helpers-BhrzxvyQ.mjs} +4 -0
  107. package/dist/testing/index.d.mts +367 -711
  108. package/dist/testing/index.mjs +646 -1434
  109. package/dist/testing/storageContract.d.mts +1 -1
  110. package/dist/{tracing-65B51Dw3.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  111. package/dist/types/index.d.mts +5 -5
  112. package/dist/types/index.mjs +1 -3
  113. package/dist/types/storage.d.mts +1 -1
  114. package/dist/{types-Co8k3NyS.d.mts → types-CgikqKAj.d.mts} +133 -21
  115. package/dist/{types-Btdda02s.d.mts → types-D9NqiYIw.d.mts} +1 -1
  116. package/dist/utils/index.d.mts +2 -898
  117. package/dist/utils/index.mjs +4 -5
  118. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  119. package/dist/versioning-M9lNLhO8.d.mts +117 -0
  120. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  121. package/package.json +26 -8
  122. package/skills/arc/SKILL.md +124 -39
  123. package/skills/arc/references/testing.md +212 -183
  124. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  125. package/dist/core-CcR01lup.mjs +0 -1411
  126. package/dist/createActionRouter-Bp_5c_2b.mjs +0 -249
  127. package/dist/errorHandler-DRQ3EqfL.d.mts +0 -218
  128. package/dist/errors-CCSsMpXE.d.mts +0 -140
  129. package/dist/fields-bxkeltzz.mjs +0 -126
  130. package/dist/filesUpload-t21LS-py.mjs +0 -377
  131. package/dist/queryParser-DBqBB6AC.mjs +0 -352
  132. package/dist/types-Csi3FLfq.mjs +0 -27
  133. package/dist/utils-B2fNOD_i.mjs +0 -929
  134. /package/dist/{EventTransport-CUw5NNWe.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
  135. /package/dist/{HookSystem-BNYKnrXF.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  136. /package/dist/{ResourceRegistry-BPd6NQDm.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  137. /package/dist/{caching-CBpK_SCM.mjs → caching-CheW3m-S.mjs} +0 -0
  138. /package/dist/{elevation-C5SwtkAn.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  139. /package/dist/{errorHandler-Bb49BvPD.mjs → errorHandler-BQm8ZxTK.mjs} +0 -0
  140. /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  141. /package/dist/{interface-CSbZdv_3.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  142. /package/dist/{interface-D218ikEo.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  143. /package/dist/{memory-B5Amv9A1.mjs → memory-DikHSvWa.mjs} +0 -0
  144. /package/dist/{metrics-DuhiSEZI.mjs → metrics-Csh4nsvv.mjs} +0 -0
  145. /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-BneOJkpi.mjs} +0 -0
  146. /package/dist/{registry-B3lRFBWo.mjs → registry-D63ee7fl.mjs} +0 -0
  147. /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  148. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  149. /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  150. /package/dist/{storage-CVk_SEn2.d.mts → storage-BwGQXUpd.d.mts} +0 -0
  151. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  152. /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
  153. /package/dist/{versioning-C2U_bLY0.mjs → versioning-CGPjkqAg.mjs} +0 -0
@@ -1,929 +0,0 @@
1
- import { t as ArcError } from "./errors-D5c-5BJL.mjs";
2
- //#region src/utils/circuitBreaker.ts
3
- /**
4
- * Circuit Breaker Pattern
5
- *
6
- * Wraps external service calls with failure protection.
7
- * Prevents cascading failures by "opening" the circuit when
8
- * a service is failing, allowing it time to recover.
9
- *
10
- * States:
11
- * - CLOSED: Normal operation, requests pass through
12
- * - OPEN: Too many failures, all requests fail fast
13
- * - HALF_OPEN: Testing if service recovered, limited requests
14
- *
15
- * @example
16
- * import { CircuitBreaker } from '@classytic/arc/utils';
17
- *
18
- * const paymentBreaker = new CircuitBreaker(async (amount) => {
19
- * return await stripe.charges.create({ amount });
20
- * }, {
21
- * failureThreshold: 5,
22
- * resetTimeout: 30000,
23
- * timeout: 5000,
24
- * });
25
- *
26
- * try {
27
- * const result = await paymentBreaker.call(100);
28
- * } catch (error) {
29
- * // Handle failure or circuit open
30
- * }
31
- */
32
- const CircuitState = {
33
- CLOSED: "CLOSED",
34
- OPEN: "OPEN",
35
- HALF_OPEN: "HALF_OPEN"
36
- };
37
- var CircuitBreakerError = class extends Error {
38
- state;
39
- constructor(message, state) {
40
- super(message);
41
- this.name = "CircuitBreakerError";
42
- this.state = state;
43
- }
44
- };
45
- var CircuitBreaker = class {
46
- state = CircuitState.CLOSED;
47
- failures = 0;
48
- successes = 0;
49
- totalCalls = 0;
50
- nextAttempt = 0;
51
- lastCallAt = null;
52
- openedAt = null;
53
- failureThreshold;
54
- resetTimeout;
55
- timeout;
56
- successThreshold;
57
- fallback;
58
- onStateChange;
59
- onError;
60
- name;
61
- fn;
62
- constructor(fn, options = {}) {
63
- this.fn = fn;
64
- this.failureThreshold = options.failureThreshold ?? 5;
65
- this.resetTimeout = options.resetTimeout ?? 6e4;
66
- this.timeout = options.timeout ?? 1e4;
67
- this.successThreshold = options.successThreshold ?? 1;
68
- this.fallback = options.fallback;
69
- this.onStateChange = options.onStateChange;
70
- this.onError = options.onError;
71
- this.name = options.name ?? "CircuitBreaker";
72
- }
73
- /**
74
- * Call the wrapped function with circuit breaker protection
75
- */
76
- async call(...args) {
77
- this.totalCalls++;
78
- this.lastCallAt = Date.now();
79
- if (this.state === CircuitState.OPEN) {
80
- if (Date.now() < this.nextAttempt) {
81
- const error = new CircuitBreakerError(`Circuit breaker is OPEN for ${this.name}`, CircuitState.OPEN);
82
- if (this.fallback) return this.fallback(...args);
83
- throw error;
84
- }
85
- this.setState(CircuitState.HALF_OPEN);
86
- }
87
- try {
88
- const result = await this.executeWithTimeout(args);
89
- this.onSuccess();
90
- return result;
91
- } catch (err) {
92
- this.onFailure(err instanceof Error ? err : new Error(String(err)));
93
- throw err;
94
- }
95
- }
96
- /**
97
- * Execute function with timeout
98
- */
99
- async executeWithTimeout(args) {
100
- return new Promise((resolve, reject) => {
101
- const timeoutId = setTimeout(() => {
102
- reject(/* @__PURE__ */ new Error(`Request timeout after ${this.timeout}ms`));
103
- }, this.timeout);
104
- this.fn(...args).then((result) => {
105
- clearTimeout(timeoutId);
106
- resolve(result);
107
- }).catch((error) => {
108
- clearTimeout(timeoutId);
109
- reject(error);
110
- });
111
- });
112
- }
113
- /**
114
- * Handle successful call
115
- */
116
- onSuccess() {
117
- this.failures = 0;
118
- this.successes++;
119
- if (this.state === CircuitState.HALF_OPEN) {
120
- if (this.successes >= this.successThreshold) {
121
- this.setState(CircuitState.CLOSED);
122
- this.successes = 0;
123
- }
124
- }
125
- }
126
- /**
127
- * Handle failed call
128
- */
129
- onFailure(error) {
130
- this.failures++;
131
- this.successes = 0;
132
- if (this.onError) this.onError(error);
133
- if (this.state === CircuitState.HALF_OPEN || this.failures >= this.failureThreshold) {
134
- this.setState(CircuitState.OPEN);
135
- this.nextAttempt = Date.now() + this.resetTimeout;
136
- this.openedAt = Date.now();
137
- }
138
- }
139
- /**
140
- * Change circuit state
141
- */
142
- setState(newState) {
143
- const oldState = this.state;
144
- if (oldState !== newState) {
145
- this.state = newState;
146
- if (this.onStateChange) this.onStateChange(oldState, newState);
147
- }
148
- }
149
- /**
150
- * Manually open the circuit
151
- */
152
- open() {
153
- this.setState(CircuitState.OPEN);
154
- this.nextAttempt = Date.now() + this.resetTimeout;
155
- this.openedAt = Date.now();
156
- }
157
- /**
158
- * Manually close the circuit
159
- */
160
- close() {
161
- this.failures = 0;
162
- this.successes = 0;
163
- this.setState(CircuitState.CLOSED);
164
- this.openedAt = null;
165
- }
166
- /**
167
- * Get current statistics
168
- */
169
- getStats() {
170
- return {
171
- name: this.name,
172
- state: this.state,
173
- failures: this.failures,
174
- successes: this.successes,
175
- totalCalls: this.totalCalls,
176
- openedAt: this.openedAt,
177
- lastCallAt: this.lastCallAt
178
- };
179
- }
180
- /**
181
- * Get current state
182
- */
183
- getState() {
184
- return this.state;
185
- }
186
- /**
187
- * Check if circuit is open
188
- */
189
- isOpen() {
190
- return this.state === CircuitState.OPEN;
191
- }
192
- /**
193
- * Check if circuit is closed
194
- */
195
- isClosed() {
196
- return this.state === CircuitState.CLOSED;
197
- }
198
- /**
199
- * Reset statistics
200
- */
201
- reset() {
202
- this.failures = 0;
203
- this.successes = 0;
204
- this.totalCalls = 0;
205
- this.lastCallAt = null;
206
- this.openedAt = null;
207
- this.setState(CircuitState.CLOSED);
208
- }
209
- };
210
- /**
211
- * Create a circuit breaker with sensible defaults
212
- *
213
- * @example
214
- * const emailBreaker = createCircuitBreaker(
215
- * async (to, subject, body) => sendEmail(to, subject, body),
216
- * { name: 'email-service' }
217
- * );
218
- */
219
- function createCircuitBreaker(fn, options) {
220
- return new CircuitBreaker(fn, options);
221
- }
222
- /**
223
- * Circuit breaker registry for managing multiple breakers
224
- */
225
- var CircuitBreakerRegistry = class {
226
- breakers = /* @__PURE__ */ new Map();
227
- /**
228
- * Register a circuit breaker
229
- */
230
- register(name, fn, options) {
231
- const breaker = new CircuitBreaker(fn, {
232
- ...options,
233
- name
234
- });
235
- this.breakers.set(name, breaker);
236
- return breaker;
237
- }
238
- /**
239
- * Get a circuit breaker by name
240
- */
241
- get(name) {
242
- return this.breakers.get(name);
243
- }
244
- /**
245
- * Get all breakers
246
- */
247
- getAll() {
248
- return this.breakers;
249
- }
250
- /**
251
- * Get statistics for all breakers
252
- */
253
- getAllStats() {
254
- const stats = {};
255
- for (const [name, breaker] of this.breakers.entries()) stats[name] = breaker.getStats();
256
- return stats;
257
- }
258
- /**
259
- * Reset all breakers
260
- */
261
- resetAll() {
262
- for (const breaker of this.breakers.values()) breaker.reset();
263
- }
264
- /**
265
- * Open all breakers
266
- */
267
- openAll() {
268
- for (const breaker of this.breakers.values()) breaker.open();
269
- }
270
- /**
271
- * Close all breakers
272
- */
273
- closeAll() {
274
- for (const breaker of this.breakers.values()) breaker.close();
275
- }
276
- };
277
- /**
278
- * Create a new CircuitBreakerRegistry instance.
279
- * Use this instead of a global singleton — attach to fastify.arc or pass explicitly.
280
- */
281
- function createCircuitBreakerRegistry() {
282
- return new CircuitBreakerRegistry();
283
- }
284
- //#endregion
285
- //#region src/utils/compensation.ts
286
- /**
287
- * Run steps in order with automatic compensation on failure.
288
- *
289
- * @typeParam TCtx - Context type shared across steps (defaults to Record<string, unknown>)
290
- */
291
- async function withCompensation(_name, steps, initialContext, hooks) {
292
- const ctx = { ...initialContext };
293
- const completedSteps = [];
294
- const results = {};
295
- const completed = [];
296
- for (const step of steps) {
297
- if (step.fireAndForget) {
298
- completedSteps.push(step.name);
299
- step.execute(ctx).then((result) => hooks?.onStepComplete?.(step.name, result), () => {});
300
- continue;
301
- }
302
- try {
303
- const result = await step.execute(ctx);
304
- completedSteps.push(step.name);
305
- results[step.name] = result;
306
- completed.push({
307
- step,
308
- result
309
- });
310
- hooks?.onStepComplete?.(step.name, result);
311
- } catch (err) {
312
- const error = err instanceof Error ? err : new Error(String(err));
313
- hooks?.onStepFailed?.(step.name, error);
314
- const compensationErrors = await rollback(ctx, completed, hooks);
315
- return {
316
- success: false,
317
- completedSteps,
318
- results,
319
- failedStep: step.name,
320
- error: error.message,
321
- ...compensationErrors.length > 0 ? { compensationErrors } : {}
322
- };
323
- }
324
- }
325
- return {
326
- success: true,
327
- completedSteps,
328
- results
329
- };
330
- }
331
- async function rollback(ctx, completed, hooks) {
332
- const errors = [];
333
- for (let i = completed.length - 1; i >= 0; i--) {
334
- const entry = completed[i];
335
- if (!entry?.step.compensate) continue;
336
- const compensateFn = entry.step.compensate;
337
- try {
338
- await compensateFn(ctx, entry.result);
339
- hooks?.onCompensate?.(entry.step.name);
340
- } catch (err) {
341
- errors.push({
342
- step: entry.step.name,
343
- error: err instanceof Error ? err.message : String(err)
344
- });
345
- }
346
- }
347
- return errors;
348
- }
349
- function defineCompensation(name, steps) {
350
- return {
351
- name,
352
- execute: (initialContext, hooks) => withCompensation(name, steps, initialContext, hooks)
353
- };
354
- }
355
- //#endregion
356
- //#region src/utils/defineGuard.ts
357
- /** Hidden property key for guard context storage on the request object. */
358
- const GUARD_STORE_KEY = "__arcGuardContext";
359
- /**
360
- * Create a typed guard. See module JSDoc for usage.
361
- */
362
- function defineGuard(config) {
363
- const { name, resolve } = config;
364
- const preHandler = async (req, reply) => {
365
- const ctx = await resolve(req, reply);
366
- if (!reply.sent) {
367
- const store = req[GUARD_STORE_KEY] ?? {};
368
- store[name] = ctx;
369
- req[GUARD_STORE_KEY] = store;
370
- }
371
- };
372
- return {
373
- preHandler,
374
- name,
375
- from(req) {
376
- const store = req[GUARD_STORE_KEY];
377
- 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.`);
378
- return store[name];
379
- }
380
- };
381
- }
382
- //#endregion
383
- //#region src/utils/handleRaw.ts
384
- /**
385
- * Wrap a raw Fastify handler with Arc's response envelope and error handling.
386
- *
387
- * @param handler - Async function that receives `(request, reply)` and returns data.
388
- * The return value is sent as `{ success: true, data }`. If it returns
389
- * `undefined` or `null`, `{ success: true }` is sent (no `data` field).
390
- * @param statusCode - HTTP status code for successful responses (default: 200)
391
- */
392
- function handleRaw(handler, statusCode = 200) {
393
- return async (request, reply) => {
394
- try {
395
- const result = await handler(request, reply);
396
- if (reply.sent) return;
397
- if (result === void 0 || result === null) reply.code(statusCode).send({ success: true });
398
- else reply.code(statusCode).send({
399
- success: true,
400
- data: result
401
- });
402
- } catch (err) {
403
- if (reply.sent) return;
404
- if (err instanceof ArcError) {
405
- reply.code(err.statusCode).send(err.toJSON());
406
- return;
407
- }
408
- const error = err;
409
- const code = error.statusCode ?? error.status ?? 500;
410
- reply.code(code).send({
411
- success: false,
412
- error: error.message ?? "Internal server error",
413
- ...error.code && { code: error.code }
414
- });
415
- }
416
- };
417
- }
418
- //#endregion
419
- //#region src/utils/responseSchemas.ts
420
- /**
421
- * Base success response schema
422
- */
423
- const successResponseSchema = {
424
- type: "object",
425
- properties: { success: {
426
- type: "boolean",
427
- example: true
428
- } },
429
- required: ["success"]
430
- };
431
- /**
432
- * Error response schema
433
- */
434
- const errorResponseSchema = {
435
- type: "object",
436
- properties: {
437
- success: {
438
- type: "boolean",
439
- example: false
440
- },
441
- error: {
442
- type: "string",
443
- description: "Error message"
444
- },
445
- code: {
446
- type: "string",
447
- description: "Error code"
448
- },
449
- message: {
450
- type: "string",
451
- description: "Detailed message"
452
- }
453
- },
454
- required: ["success", "error"]
455
- };
456
- /**
457
- * Pagination schema - matches MongoKit/Arc runtime format
458
- *
459
- * Runtime format (flat fields):
460
- * { page, limit, total, pages, hasNext, hasPrev }
461
- */
462
- const paginationSchema = {
463
- type: "object",
464
- properties: {
465
- page: {
466
- type: "integer",
467
- example: 1
468
- },
469
- limit: {
470
- type: "integer",
471
- example: 20
472
- },
473
- total: {
474
- type: "integer",
475
- example: 100
476
- },
477
- pages: {
478
- type: "integer",
479
- example: 5
480
- },
481
- hasNext: {
482
- type: "boolean",
483
- example: true
484
- },
485
- hasPrev: {
486
- type: "boolean",
487
- example: false
488
- }
489
- },
490
- required: [
491
- "page",
492
- "limit",
493
- "total",
494
- "pages",
495
- "hasNext",
496
- "hasPrev"
497
- ]
498
- };
499
- /**
500
- * Wrap a data schema in a success response
501
- */
502
- function wrapResponse(dataSchema) {
503
- return {
504
- type: "object",
505
- properties: {
506
- success: {
507
- type: "boolean",
508
- example: true
509
- },
510
- data: dataSchema
511
- },
512
- required: ["success", "data"],
513
- additionalProperties: true
514
- };
515
- }
516
- /**
517
- * Create a list response schema with pagination - matches MongoKit/Arc runtime format
518
- *
519
- * Runtime format:
520
- * { success, docs: [...], page, limit, total, pages, hasNext, hasPrev }
521
- *
522
- * Note: Uses 'docs' array (not 'data') with flat pagination fields
523
- */
524
- function listResponse(itemSchema) {
525
- return {
526
- type: "object",
527
- properties: {
528
- success: {
529
- type: "boolean",
530
- example: true
531
- },
532
- docs: {
533
- type: "array",
534
- items: itemSchema
535
- },
536
- page: {
537
- type: "integer",
538
- example: 1
539
- },
540
- limit: {
541
- type: "integer",
542
- example: 20
543
- },
544
- total: {
545
- type: "integer",
546
- example: 100
547
- },
548
- pages: {
549
- type: "integer",
550
- example: 5
551
- },
552
- hasNext: {
553
- type: "boolean",
554
- example: false
555
- },
556
- hasPrev: {
557
- type: "boolean",
558
- example: false
559
- }
560
- },
561
- required: ["success", "docs"],
562
- additionalProperties: true
563
- };
564
- }
565
- /**
566
- * Create a single item response schema
567
- *
568
- * Runtime format: { success, data: {...} }
569
- */
570
- function itemResponse(itemSchema) {
571
- return wrapResponse(itemSchema);
572
- }
573
- /**
574
- * Create a create/update response schema
575
- */
576
- function mutationResponse(itemSchema) {
577
- return {
578
- type: "object",
579
- properties: {
580
- success: {
581
- type: "boolean",
582
- example: true
583
- },
584
- data: itemSchema,
585
- message: {
586
- type: "string",
587
- example: "Created successfully"
588
- }
589
- },
590
- required: ["success", "data"],
591
- additionalProperties: true
592
- };
593
- }
594
- /**
595
- * Create a delete response schema
596
- *
597
- * Runtime format: { success, data: { message, id?, soft? } }
598
- */
599
- function deleteResponse() {
600
- return {
601
- type: "object",
602
- properties: {
603
- success: {
604
- type: "boolean",
605
- example: true
606
- },
607
- data: {
608
- type: "object",
609
- properties: {
610
- message: {
611
- type: "string",
612
- example: "Deleted successfully"
613
- },
614
- id: {
615
- type: "string",
616
- example: "507f1f77bcf86cd799439011"
617
- },
618
- soft: {
619
- type: "boolean",
620
- example: false
621
- }
622
- },
623
- required: ["message"]
624
- }
625
- },
626
- required: ["success"],
627
- additionalProperties: true
628
- };
629
- }
630
- const responses = {
631
- 200: (schema) => ({
632
- description: "Successful response",
633
- content: { "application/json": { schema } }
634
- }),
635
- 201: (schema) => ({
636
- description: "Created successfully",
637
- content: { "application/json": { schema: mutationResponse(schema) } }
638
- }),
639
- 400: {
640
- description: "Bad Request",
641
- content: { "application/json": { schema: {
642
- ...errorResponseSchema,
643
- properties: {
644
- ...errorResponseSchema.properties,
645
- code: {
646
- type: "string",
647
- example: "VALIDATION_ERROR"
648
- },
649
- details: {
650
- type: "object",
651
- properties: { errors: {
652
- type: "array",
653
- items: {
654
- type: "object",
655
- properties: {
656
- field: { type: "string" },
657
- message: { type: "string" }
658
- }
659
- }
660
- } }
661
- }
662
- }
663
- } } }
664
- },
665
- 401: {
666
- description: "Unauthorized",
667
- content: { "application/json": { schema: {
668
- ...errorResponseSchema,
669
- properties: {
670
- ...errorResponseSchema.properties,
671
- code: {
672
- type: "string",
673
- example: "UNAUTHORIZED"
674
- }
675
- }
676
- } } }
677
- },
678
- 403: {
679
- description: "Forbidden",
680
- content: { "application/json": { schema: {
681
- ...errorResponseSchema,
682
- properties: {
683
- ...errorResponseSchema.properties,
684
- code: {
685
- type: "string",
686
- example: "FORBIDDEN"
687
- }
688
- }
689
- } } }
690
- },
691
- 404: {
692
- description: "Not Found",
693
- content: { "application/json": { schema: {
694
- ...errorResponseSchema,
695
- properties: {
696
- ...errorResponseSchema.properties,
697
- code: {
698
- type: "string",
699
- example: "NOT_FOUND"
700
- }
701
- }
702
- } } }
703
- },
704
- 409: {
705
- description: "Conflict",
706
- content: { "application/json": { schema: {
707
- ...errorResponseSchema,
708
- properties: {
709
- ...errorResponseSchema.properties,
710
- code: {
711
- type: "string",
712
- example: "CONFLICT"
713
- }
714
- }
715
- } } }
716
- },
717
- 500: {
718
- description: "Internal Server Error",
719
- content: { "application/json": { schema: {
720
- ...errorResponseSchema,
721
- properties: {
722
- ...errorResponseSchema.properties,
723
- code: {
724
- type: "string",
725
- example: "INTERNAL_ERROR"
726
- }
727
- }
728
- } } }
729
- }
730
- };
731
- const queryParams = {
732
- pagination: {
733
- page: {
734
- type: "integer",
735
- minimum: 1,
736
- default: 1,
737
- description: "Page number"
738
- },
739
- limit: {
740
- type: "integer",
741
- minimum: 1,
742
- maximum: 100,
743
- default: 20,
744
- description: "Items per page"
745
- }
746
- },
747
- sorting: { sort: {
748
- type: "string",
749
- description: "Sort field (prefix with - for descending)",
750
- example: "-createdAt"
751
- } },
752
- filtering: {
753
- select: {
754
- description: "Fields to include (space-separated or object)",
755
- example: "name email createdAt"
756
- },
757
- populate: {
758
- description: "Relations to populate (comma-separated string or bracket-notation object)",
759
- example: "author,category"
760
- }
761
- }
762
- };
763
- /**
764
- * Get standard list query parameters schema
765
- */
766
- function getListQueryParams() {
767
- return {
768
- type: "object",
769
- properties: {
770
- ...queryParams.pagination,
771
- ...queryParams.sorting,
772
- ...queryParams.filtering
773
- },
774
- additionalProperties: true
775
- };
776
- }
777
- /**
778
- * Generic item schema that allows any properties.
779
- * Used as default when no user schema is provided.
780
- * Enables fast-json-stringify while still passing through all fields.
781
- */
782
- const genericItemSchema = {
783
- type: "object",
784
- additionalProperties: true
785
- };
786
- /**
787
- * Recursively strip `example` keys from a schema object.
788
- * The `example` keyword is OpenAPI metadata — not standard JSON Schema —
789
- * and triggers Ajv strict mode errors when used on routes without the
790
- * `keywords: ['example']` AJV config (e.g., raw Fastify without createApp).
791
- */
792
- function stripExamples(schema) {
793
- if (schema === null || typeof schema !== "object") return schema;
794
- if (Array.isArray(schema)) return schema.map(stripExamples);
795
- const result = {};
796
- for (const [key, value] of Object.entries(schema)) {
797
- if (key === "example") continue;
798
- result[key] = stripExamples(value);
799
- }
800
- return result;
801
- }
802
- /**
803
- * Get default response schemas for all CRUD operations.
804
- *
805
- * When routes have response schemas, Fastify compiles them with
806
- * fast-json-stringify for 2-3x faster serialization and prevents
807
- * accidental field disclosure.
808
- *
809
- * These defaults use `additionalProperties: true` so all fields pass through.
810
- * Override with specific schemas for full serialization performance + safety.
811
- *
812
- * Note: `example` properties are stripped from defaults so they work with
813
- * any Fastify instance (not just createApp which adds `keywords: ['example']`).
814
- */
815
- function getDefaultCrudSchemas() {
816
- return stripExamples({
817
- list: {
818
- querystring: getListQueryParams(),
819
- response: { 200: listResponse(genericItemSchema) }
820
- },
821
- get: { response: { 200: itemResponse(genericItemSchema) } },
822
- create: { response: { 201: mutationResponse(genericItemSchema) } },
823
- update: { response: { 200: itemResponse(genericItemSchema) } },
824
- delete: { response: { 200: deleteResponse() } }
825
- });
826
- }
827
- //#endregion
828
- //#region src/utils/stateMachine.ts
829
- /**
830
- * Create a state machine for validating transitions
831
- *
832
- * @param name - Name of the state machine (used in error messages)
833
- * @param transitions - Map of actions to allowed source statuses
834
- * @param options - Additional options (history, guards, actions)
835
- * @returns State machine with can() and assert() methods
836
- *
837
- * @example
838
- * // Basic usage
839
- * const transferState = createStateMachine('Transfer', {
840
- * approve: ['draft'],
841
- * dispatch: ['approved'],
842
- * receive: ['dispatched', 'in_transit'],
843
- * cancel: ['draft', 'approved'],
844
- * });
845
- *
846
- * @example
847
- * // With guards and actions
848
- * const orderState = createStateMachine('Order', {
849
- * approve: {
850
- * from: ['pending'],
851
- * to: 'approved',
852
- * guard: ({ data }) => data.paymentConfirmed,
853
- * before: ({ from, to }) => console.log(`Approving order from ${from} to ${to}`),
854
- * after: ({ data }) => sendApprovalEmail(data.customerId),
855
- * },
856
- * }, { trackHistory: true });
857
- */
858
- function createStateMachine(name, transitions = {}, options = {}) {
859
- const normalized = /* @__PURE__ */ new Map();
860
- const history = options.trackHistory ? [] : void 0;
861
- Object.entries(transitions).forEach(([action, allowed]) => {
862
- if (Array.isArray(allowed)) normalized.set(action, { from: new Set(allowed) });
863
- else if (typeof allowed === "object" && "from" in allowed) normalized.set(action, {
864
- from: new Set(Array.isArray(allowed.from) ? allowed.from : [allowed.from]),
865
- to: allowed.to,
866
- guard: allowed.guard,
867
- before: allowed.before,
868
- after: allowed.after
869
- });
870
- });
871
- const can = (action, status) => {
872
- const transition = normalized.get(action);
873
- if (!transition || !status) return false;
874
- return transition.from.has(status);
875
- };
876
- const canAsync = async (action, status, context) => {
877
- const transition = normalized.get(action);
878
- if (!transition || !status) return false;
879
- if (!transition.from.has(status)) return false;
880
- if (transition.guard) try {
881
- return await transition.guard({
882
- from: status,
883
- to: transition.to || "",
884
- action,
885
- data: context
886
- });
887
- } catch {
888
- return false;
889
- }
890
- return true;
891
- };
892
- const assert = (action, status, errorFactory, message) => {
893
- if (can(action, status)) return;
894
- const errorMessage = message || `${name} cannot '${action}' when status is '${status || "unknown"}'`;
895
- if (typeof errorFactory === "function") throw errorFactory(errorMessage);
896
- throw new Error(errorMessage);
897
- };
898
- const recordTransition = (from, to, action, metadata) => {
899
- if (history) history.push({
900
- from,
901
- to,
902
- action,
903
- timestamp: /* @__PURE__ */ new Date(),
904
- metadata
905
- });
906
- };
907
- const getHistory = () => {
908
- return history ? [...history] : [];
909
- };
910
- const clearHistory = () => {
911
- if (history) history.length = 0;
912
- };
913
- const getAvailableActions = (status) => {
914
- const actions = [];
915
- for (const [action, transition] of normalized.entries()) if (transition.from.has(status)) actions.push(action);
916
- return actions;
917
- };
918
- return {
919
- can,
920
- canAsync,
921
- assert,
922
- recordTransition,
923
- getHistory,
924
- clearHistory,
925
- getAvailableActions
926
- };
927
- }
928
- //#endregion
929
- export { createCircuitBreakerRegistry as C, createCircuitBreaker as S, withCompensation as _, getListQueryParams as a, CircuitBreakerRegistry as b, 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, CircuitBreaker as v, CircuitState as x, CircuitBreakerError as y };