@classytic/arc 2.10.8 → 2.11.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.
- package/dist/{BaseController-DVNKvoX4.mjs → BaseController-JNV08qOT.mjs} +480 -442
- package/dist/{queryCachePlugin-Dumka73q.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
- package/dist/audit/index.d.mts +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +5 -5
- package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
- package/dist/cache/index.d.mts +3 -2
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +37 -27
- package/dist/cli/commands/init.mjs +46 -33
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -3
- package/dist/core-DXdSSFW-.mjs +1037 -0
- package/dist/createActionRouter-BwaSM0No.mjs +166 -0
- package/dist/{createApp-BwnEAO2h.mjs → createApp-P1d6rjPy.mjs} +75 -27
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-Dci0AYLT.mjs → elevation-DOFoxoDs.mjs} +1 -1
- package/dist/{errorHandler-CSxe7KIM.mjs → errorHandler-BQm8ZxTK.mjs} +1 -1
- package/dist/{eventPlugin-ByU4Cv0e.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +2 -2
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-C_Noptz-.d.mts → index-BYCqHCVu.d.mts} +2 -2
- package/dist/{index-BGbpGVyM.d.mts → index-C_bgx9o4.d.mts} +712 -500
- package/dist/{index-BziRPS4H.d.mts → index-CvM1e09j.d.mts} +29 -10
- package/dist/{index-EqQN6p0W.d.mts → index-pUczGjO0.d.mts} +11 -8
- package/dist/index-smCAoA5W.d.mts +1179 -0
- package/dist/index.d.mts +6 -38
- package/dist/index.mjs +9 -9
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +46 -5
- package/dist/integrations/streamline.mjs +50 -21
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +2 -154
- package/dist/integrations/websocket.mjs +292 -224
- package/dist/{keys-nWQGUTu1.mjs → keys-CARyUjiR.mjs} +2 -0
- package/dist/{loadResources-Bksk8ydA.mjs → loadResources-CPpkyKfM.mjs} +32 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{openapi-DpNpqBmo.mjs → openapi-C0L9ar7m.mjs} +4 -4
- package/dist/org/index.d.mts +1 -1
- package/dist/permissions/index.d.mts +1 -1
- package/dist/permissions/index.mjs +2 -4
- package/dist/{permissions-wkqRwicB.mjs → permissions-B4vU9L0Q.mjs} +221 -3
- package/dist/{pipe-CGJxqDGx.mjs → pipe-DVoIheVC.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +42 -24
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +6 -0
- package/dist/presets/search.d.mts +1 -1
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-CrwOvuXI.mjs → presets-k604Lj99.mjs} +1 -1
- package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
- package/dist/{queryCachePlugin-ChLNZvFT.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
- package/dist/{redis-MXLp1oOf.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{resourceToTools-BhF3JV5p.mjs → resourceToTools--okX6QBr.mjs} +534 -420
- package/dist/routerShared-DeESFp4a.mjs +515 -0
- package/dist/schemaIR-BlG9bY7v.mjs +137 -0
- package/dist/scope/index.mjs +2 -2
- package/dist/testing/index.d.mts +367 -711
- package/dist/testing/index.mjs +637 -1434
- package/dist/{tracing-xqXzWeaf.d.mts → tracing-DokiEsuz.d.mts} +9 -4
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +1 -3
- package/dist/{types-CVdgPXBW.d.mts → types-BdA4uMBV.d.mts} +191 -28
- package/dist/{types-CVKBssX5.d.mts → types-Bh_gEJBi.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -968
- package/dist/utils/index.mjs +5 -6
- package/dist/utils-D3Yxnrwr.mjs +1639 -0
- package/dist/websocket-CyJ1VIFI.d.mts +186 -0
- package/package.json +7 -5
- package/skills/arc/SKILL.md +124 -39
- package/skills/arc/references/testing.md +212 -183
- package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
- package/dist/core-3MWJosCH.mjs +0 -1459
- package/dist/createActionRouter-C8UUB3Px.mjs +0 -249
- package/dist/errors-BI8kEKsO.d.mts +0 -140
- package/dist/fields-CTMWOUDt.mjs +0 -126
- package/dist/queryParser-NR__Qiju.mjs +0 -419
- package/dist/types-CDnTEpga.mjs +0 -27
- package/dist/utils-LMwVidKy.mjs +0 -947
- /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
- /package/dist/{ResourceRegistry-CcN2LVrc.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
- /package/dist/{actionPermissions-TUVR3uiZ.mjs → actionPermissions-C8YYU92K.mjs} +0 -0
- /package/dist/{caching-3h93rkJM.mjs → caching-CheW3m-S.mjs} +0 -0
- /package/dist/{errorHandler-2ii4RIYr.d.mts → errorHandler-Co3lnVmJ.d.mts} +0 -0
- /package/dist/{errors-BqdUDja_.mjs → errors-D5c-5BJL.mjs} +0 -0
- /package/dist/{eventPlugin-D1ThQ1Pp.d.mts → eventPlugin-CUNjYYRY.d.mts} +0 -0
- /package/dist/{interface-B-pe8fhj.d.mts → interface-CkkWm5uR.d.mts} +0 -0
- /package/dist/{interface-yhyb_pLY.d.mts → interface-Da0r7Lna.d.mts} +0 -0
- /package/dist/{memory-DqI-449b.mjs → memory-DikHSvWa.mjs} +0 -0
- /package/dist/{metrics-TuOmguhi.mjs → metrics-Csh4nsvv.mjs} +0 -0
- /package/dist/{multipartBody-CUQGVlM_.mjs → multipartBody-CvTR1Un6.mjs} +0 -0
- /package/dist/{pluralize-CWP6MB39.mjs → pluralize-BneOJkpi.mjs} +0 -0
- /package/dist/{redis-stream-bkO88VHx.d.mts → redis-stream-CM8TXTix.d.mts} +0 -0
- /package/dist/{registry-B0Wl7uVV.mjs → registry-D63ee7fl.mjs} +0 -0
- /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
- /package/dist/{requestContext-C38GskNt.mjs → requestContext-CfRkaxwf.mjs} +0 -0
- /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
- /package/dist/{sse-D8UeDwis.mjs → sse-V7aXc3bW.mjs} +0 -0
- /package/dist/{store-helpers-DYYUQbQN.mjs → store-helpers-BhrzxvyQ.mjs} +0 -0
- /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
- /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
- /package/dist/{versioning-B6mimogM.mjs → versioning-CGPjkqAg.mjs} +0 -0
- /package/dist/{versioning-CeUXHfjw.d.mts → versioning-M9lNLhO8.d.mts} +0 -0
package/dist/utils-LMwVidKy.mjs
DELETED
|
@@ -1,947 +0,0 @@
|
|
|
1
|
-
import { t as ArcError } from "./errors-BqdUDja_.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/defineErrorMapper.ts
|
|
357
|
-
/**
|
|
358
|
-
* Register an `ErrorMapper` with its domain-specific generic argument and
|
|
359
|
-
* have it assign cleanly into `ErrorMapper[]` (no `as unknown as ErrorMapper`).
|
|
360
|
-
*
|
|
361
|
-
* The returned mapper is identical at runtime — `type` and `toResponse` are
|
|
362
|
-
* passed through untouched. Only the declared type widens from
|
|
363
|
-
* `ErrorMapper<T>` to `ErrorMapper` so the array inference works.
|
|
364
|
-
*
|
|
365
|
-
* Safety: the `errorHandlerPlugin` dispatches via `error instanceof mapper.type`
|
|
366
|
-
* before invoking `toResponse`, so the widened callback signature is never
|
|
367
|
-
* called with a non-`T` error at runtime. This helper codifies that invariant
|
|
368
|
-
* in one place.
|
|
369
|
-
*/
|
|
370
|
-
function defineErrorMapper(mapper) {
|
|
371
|
-
return mapper;
|
|
372
|
-
}
|
|
373
|
-
//#endregion
|
|
374
|
-
//#region src/utils/defineGuard.ts
|
|
375
|
-
/** Hidden property key for guard context storage on the request object. */
|
|
376
|
-
const GUARD_STORE_KEY = "__arcGuardContext";
|
|
377
|
-
/**
|
|
378
|
-
* Create a typed guard. See module JSDoc for usage.
|
|
379
|
-
*/
|
|
380
|
-
function defineGuard(config) {
|
|
381
|
-
const { name, resolve } = config;
|
|
382
|
-
const preHandler = async (req, reply) => {
|
|
383
|
-
const ctx = await resolve(req, reply);
|
|
384
|
-
if (!reply.sent) {
|
|
385
|
-
const store = req[GUARD_STORE_KEY] ?? {};
|
|
386
|
-
store[name] = ctx;
|
|
387
|
-
req[GUARD_STORE_KEY] = store;
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
return {
|
|
391
|
-
preHandler,
|
|
392
|
-
name,
|
|
393
|
-
from(req) {
|
|
394
|
-
const store = req[GUARD_STORE_KEY];
|
|
395
|
-
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.`);
|
|
396
|
-
return store[name];
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
//#endregion
|
|
401
|
-
//#region src/utils/handleRaw.ts
|
|
402
|
-
/**
|
|
403
|
-
* Wrap a raw Fastify handler with Arc's response envelope and error handling.
|
|
404
|
-
*
|
|
405
|
-
* @param handler - Async function that receives `(request, reply)` and returns data.
|
|
406
|
-
* The return value is sent as `{ success: true, data }`. If it returns
|
|
407
|
-
* `undefined` or `null`, `{ success: true }` is sent (no `data` field).
|
|
408
|
-
* @param statusCode - HTTP status code for successful responses (default: 200)
|
|
409
|
-
*/
|
|
410
|
-
function handleRaw(handler, statusCode = 200) {
|
|
411
|
-
return async (request, reply) => {
|
|
412
|
-
try {
|
|
413
|
-
const result = await handler(request, reply);
|
|
414
|
-
if (reply.sent) return;
|
|
415
|
-
if (result === void 0 || result === null) reply.code(statusCode).send({ success: true });
|
|
416
|
-
else reply.code(statusCode).send({
|
|
417
|
-
success: true,
|
|
418
|
-
data: result
|
|
419
|
-
});
|
|
420
|
-
} catch (err) {
|
|
421
|
-
if (reply.sent) return;
|
|
422
|
-
if (err instanceof ArcError) {
|
|
423
|
-
reply.code(err.statusCode).send(err.toJSON());
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
const error = err;
|
|
427
|
-
const code = error.statusCode ?? error.status ?? 500;
|
|
428
|
-
reply.code(code).send({
|
|
429
|
-
success: false,
|
|
430
|
-
error: error.message ?? "Internal server error",
|
|
431
|
-
...error.code && { code: error.code }
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
//#endregion
|
|
437
|
-
//#region src/utils/responseSchemas.ts
|
|
438
|
-
/**
|
|
439
|
-
* Base success response schema
|
|
440
|
-
*/
|
|
441
|
-
const successResponseSchema = {
|
|
442
|
-
type: "object",
|
|
443
|
-
properties: { success: {
|
|
444
|
-
type: "boolean",
|
|
445
|
-
example: true
|
|
446
|
-
} },
|
|
447
|
-
required: ["success"]
|
|
448
|
-
};
|
|
449
|
-
/**
|
|
450
|
-
* Error response schema
|
|
451
|
-
*/
|
|
452
|
-
const errorResponseSchema = {
|
|
453
|
-
type: "object",
|
|
454
|
-
properties: {
|
|
455
|
-
success: {
|
|
456
|
-
type: "boolean",
|
|
457
|
-
example: false
|
|
458
|
-
},
|
|
459
|
-
error: {
|
|
460
|
-
type: "string",
|
|
461
|
-
description: "Error message"
|
|
462
|
-
},
|
|
463
|
-
code: {
|
|
464
|
-
type: "string",
|
|
465
|
-
description: "Error code"
|
|
466
|
-
},
|
|
467
|
-
message: {
|
|
468
|
-
type: "string",
|
|
469
|
-
description: "Detailed message"
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
required: ["success", "error"]
|
|
473
|
-
};
|
|
474
|
-
/**
|
|
475
|
-
* Pagination schema - matches MongoKit/Arc runtime format
|
|
476
|
-
*
|
|
477
|
-
* Runtime format (flat fields):
|
|
478
|
-
* { page, limit, total, pages, hasNext, hasPrev }
|
|
479
|
-
*/
|
|
480
|
-
const paginationSchema = {
|
|
481
|
-
type: "object",
|
|
482
|
-
properties: {
|
|
483
|
-
page: {
|
|
484
|
-
type: "integer",
|
|
485
|
-
example: 1
|
|
486
|
-
},
|
|
487
|
-
limit: {
|
|
488
|
-
type: "integer",
|
|
489
|
-
example: 20
|
|
490
|
-
},
|
|
491
|
-
total: {
|
|
492
|
-
type: "integer",
|
|
493
|
-
example: 100
|
|
494
|
-
},
|
|
495
|
-
pages: {
|
|
496
|
-
type: "integer",
|
|
497
|
-
example: 5
|
|
498
|
-
},
|
|
499
|
-
hasNext: {
|
|
500
|
-
type: "boolean",
|
|
501
|
-
example: true
|
|
502
|
-
},
|
|
503
|
-
hasPrev: {
|
|
504
|
-
type: "boolean",
|
|
505
|
-
example: false
|
|
506
|
-
}
|
|
507
|
-
},
|
|
508
|
-
required: [
|
|
509
|
-
"page",
|
|
510
|
-
"limit",
|
|
511
|
-
"total",
|
|
512
|
-
"pages",
|
|
513
|
-
"hasNext",
|
|
514
|
-
"hasPrev"
|
|
515
|
-
]
|
|
516
|
-
};
|
|
517
|
-
/**
|
|
518
|
-
* Wrap a data schema in a success response
|
|
519
|
-
*/
|
|
520
|
-
function wrapResponse(dataSchema) {
|
|
521
|
-
return {
|
|
522
|
-
type: "object",
|
|
523
|
-
properties: {
|
|
524
|
-
success: {
|
|
525
|
-
type: "boolean",
|
|
526
|
-
example: true
|
|
527
|
-
},
|
|
528
|
-
data: dataSchema
|
|
529
|
-
},
|
|
530
|
-
required: ["success", "data"],
|
|
531
|
-
additionalProperties: true
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Create a list response schema with pagination - matches MongoKit/Arc runtime format
|
|
536
|
-
*
|
|
537
|
-
* Runtime format:
|
|
538
|
-
* { success, docs: [...], page, limit, total, pages, hasNext, hasPrev }
|
|
539
|
-
*
|
|
540
|
-
* Note: Uses 'docs' array (not 'data') with flat pagination fields
|
|
541
|
-
*/
|
|
542
|
-
function listResponse(itemSchema) {
|
|
543
|
-
return {
|
|
544
|
-
type: "object",
|
|
545
|
-
properties: {
|
|
546
|
-
success: {
|
|
547
|
-
type: "boolean",
|
|
548
|
-
example: true
|
|
549
|
-
},
|
|
550
|
-
docs: {
|
|
551
|
-
type: "array",
|
|
552
|
-
items: itemSchema
|
|
553
|
-
},
|
|
554
|
-
page: {
|
|
555
|
-
type: "integer",
|
|
556
|
-
example: 1
|
|
557
|
-
},
|
|
558
|
-
limit: {
|
|
559
|
-
type: "integer",
|
|
560
|
-
example: 20
|
|
561
|
-
},
|
|
562
|
-
total: {
|
|
563
|
-
type: "integer",
|
|
564
|
-
example: 100
|
|
565
|
-
},
|
|
566
|
-
pages: {
|
|
567
|
-
type: "integer",
|
|
568
|
-
example: 5
|
|
569
|
-
},
|
|
570
|
-
hasNext: {
|
|
571
|
-
type: "boolean",
|
|
572
|
-
example: false
|
|
573
|
-
},
|
|
574
|
-
hasPrev: {
|
|
575
|
-
type: "boolean",
|
|
576
|
-
example: false
|
|
577
|
-
}
|
|
578
|
-
},
|
|
579
|
-
required: ["success", "docs"],
|
|
580
|
-
additionalProperties: true
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Create a single item response schema
|
|
585
|
-
*
|
|
586
|
-
* Runtime format: { success, data: {...} }
|
|
587
|
-
*/
|
|
588
|
-
function itemResponse(itemSchema) {
|
|
589
|
-
return wrapResponse(itemSchema);
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Create a create/update response schema
|
|
593
|
-
*/
|
|
594
|
-
function mutationResponse(itemSchema) {
|
|
595
|
-
return {
|
|
596
|
-
type: "object",
|
|
597
|
-
properties: {
|
|
598
|
-
success: {
|
|
599
|
-
type: "boolean",
|
|
600
|
-
example: true
|
|
601
|
-
},
|
|
602
|
-
data: itemSchema,
|
|
603
|
-
message: {
|
|
604
|
-
type: "string",
|
|
605
|
-
example: "Created successfully"
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
required: ["success", "data"],
|
|
609
|
-
additionalProperties: true
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Create a delete response schema
|
|
614
|
-
*
|
|
615
|
-
* Runtime format: { success, data: { message, id?, soft? } }
|
|
616
|
-
*/
|
|
617
|
-
function deleteResponse() {
|
|
618
|
-
return {
|
|
619
|
-
type: "object",
|
|
620
|
-
properties: {
|
|
621
|
-
success: {
|
|
622
|
-
type: "boolean",
|
|
623
|
-
example: true
|
|
624
|
-
},
|
|
625
|
-
data: {
|
|
626
|
-
type: "object",
|
|
627
|
-
properties: {
|
|
628
|
-
message: {
|
|
629
|
-
type: "string",
|
|
630
|
-
example: "Deleted successfully"
|
|
631
|
-
},
|
|
632
|
-
id: {
|
|
633
|
-
type: "string",
|
|
634
|
-
example: "507f1f77bcf86cd799439011"
|
|
635
|
-
},
|
|
636
|
-
soft: {
|
|
637
|
-
type: "boolean",
|
|
638
|
-
example: false
|
|
639
|
-
}
|
|
640
|
-
},
|
|
641
|
-
required: ["message"]
|
|
642
|
-
}
|
|
643
|
-
},
|
|
644
|
-
required: ["success"],
|
|
645
|
-
additionalProperties: true
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
const responses = {
|
|
649
|
-
200: (schema) => ({
|
|
650
|
-
description: "Successful response",
|
|
651
|
-
content: { "application/json": { schema } }
|
|
652
|
-
}),
|
|
653
|
-
201: (schema) => ({
|
|
654
|
-
description: "Created successfully",
|
|
655
|
-
content: { "application/json": { schema: mutationResponse(schema) } }
|
|
656
|
-
}),
|
|
657
|
-
400: {
|
|
658
|
-
description: "Bad Request",
|
|
659
|
-
content: { "application/json": { schema: {
|
|
660
|
-
...errorResponseSchema,
|
|
661
|
-
properties: {
|
|
662
|
-
...errorResponseSchema.properties,
|
|
663
|
-
code: {
|
|
664
|
-
type: "string",
|
|
665
|
-
example: "VALIDATION_ERROR"
|
|
666
|
-
},
|
|
667
|
-
details: {
|
|
668
|
-
type: "object",
|
|
669
|
-
properties: { errors: {
|
|
670
|
-
type: "array",
|
|
671
|
-
items: {
|
|
672
|
-
type: "object",
|
|
673
|
-
properties: {
|
|
674
|
-
field: { type: "string" },
|
|
675
|
-
message: { type: "string" }
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
} }
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
} } }
|
|
682
|
-
},
|
|
683
|
-
401: {
|
|
684
|
-
description: "Unauthorized",
|
|
685
|
-
content: { "application/json": { schema: {
|
|
686
|
-
...errorResponseSchema,
|
|
687
|
-
properties: {
|
|
688
|
-
...errorResponseSchema.properties,
|
|
689
|
-
code: {
|
|
690
|
-
type: "string",
|
|
691
|
-
example: "UNAUTHORIZED"
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
} } }
|
|
695
|
-
},
|
|
696
|
-
403: {
|
|
697
|
-
description: "Forbidden",
|
|
698
|
-
content: { "application/json": { schema: {
|
|
699
|
-
...errorResponseSchema,
|
|
700
|
-
properties: {
|
|
701
|
-
...errorResponseSchema.properties,
|
|
702
|
-
code: {
|
|
703
|
-
type: "string",
|
|
704
|
-
example: "FORBIDDEN"
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
} } }
|
|
708
|
-
},
|
|
709
|
-
404: {
|
|
710
|
-
description: "Not Found",
|
|
711
|
-
content: { "application/json": { schema: {
|
|
712
|
-
...errorResponseSchema,
|
|
713
|
-
properties: {
|
|
714
|
-
...errorResponseSchema.properties,
|
|
715
|
-
code: {
|
|
716
|
-
type: "string",
|
|
717
|
-
example: "NOT_FOUND"
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
} } }
|
|
721
|
-
},
|
|
722
|
-
409: {
|
|
723
|
-
description: "Conflict",
|
|
724
|
-
content: { "application/json": { schema: {
|
|
725
|
-
...errorResponseSchema,
|
|
726
|
-
properties: {
|
|
727
|
-
...errorResponseSchema.properties,
|
|
728
|
-
code: {
|
|
729
|
-
type: "string",
|
|
730
|
-
example: "CONFLICT"
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
} } }
|
|
734
|
-
},
|
|
735
|
-
500: {
|
|
736
|
-
description: "Internal Server Error",
|
|
737
|
-
content: { "application/json": { schema: {
|
|
738
|
-
...errorResponseSchema,
|
|
739
|
-
properties: {
|
|
740
|
-
...errorResponseSchema.properties,
|
|
741
|
-
code: {
|
|
742
|
-
type: "string",
|
|
743
|
-
example: "INTERNAL_ERROR"
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
} } }
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
|
-
const queryParams = {
|
|
750
|
-
pagination: {
|
|
751
|
-
page: {
|
|
752
|
-
type: "integer",
|
|
753
|
-
minimum: 1,
|
|
754
|
-
default: 1,
|
|
755
|
-
description: "Page number"
|
|
756
|
-
},
|
|
757
|
-
limit: {
|
|
758
|
-
type: "integer",
|
|
759
|
-
minimum: 1,
|
|
760
|
-
maximum: 100,
|
|
761
|
-
default: 20,
|
|
762
|
-
description: "Items per page"
|
|
763
|
-
}
|
|
764
|
-
},
|
|
765
|
-
sorting: { sort: {
|
|
766
|
-
type: "string",
|
|
767
|
-
description: "Sort field (prefix with - for descending)",
|
|
768
|
-
example: "-createdAt"
|
|
769
|
-
} },
|
|
770
|
-
filtering: {
|
|
771
|
-
select: {
|
|
772
|
-
description: "Fields to include (space-separated or object)",
|
|
773
|
-
example: "name email createdAt"
|
|
774
|
-
},
|
|
775
|
-
populate: {
|
|
776
|
-
description: "Relations to populate (comma-separated string or bracket-notation object)",
|
|
777
|
-
example: "author,category"
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
};
|
|
781
|
-
/**
|
|
782
|
-
* Get standard list query parameters schema
|
|
783
|
-
*/
|
|
784
|
-
function getListQueryParams() {
|
|
785
|
-
return {
|
|
786
|
-
type: "object",
|
|
787
|
-
properties: {
|
|
788
|
-
...queryParams.pagination,
|
|
789
|
-
...queryParams.sorting,
|
|
790
|
-
...queryParams.filtering
|
|
791
|
-
},
|
|
792
|
-
additionalProperties: true
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* Generic item schema that allows any properties.
|
|
797
|
-
* Used as default when no user schema is provided.
|
|
798
|
-
* Enables fast-json-stringify while still passing through all fields.
|
|
799
|
-
*/
|
|
800
|
-
const genericItemSchema = {
|
|
801
|
-
type: "object",
|
|
802
|
-
additionalProperties: true
|
|
803
|
-
};
|
|
804
|
-
/**
|
|
805
|
-
* Recursively strip `example` keys from a schema object.
|
|
806
|
-
* The `example` keyword is OpenAPI metadata — not standard JSON Schema —
|
|
807
|
-
* and triggers Ajv strict mode errors when used on routes without the
|
|
808
|
-
* `keywords: ['example']` AJV config (e.g., raw Fastify without createApp).
|
|
809
|
-
*/
|
|
810
|
-
function stripExamples(schema) {
|
|
811
|
-
if (schema === null || typeof schema !== "object") return schema;
|
|
812
|
-
if (Array.isArray(schema)) return schema.map(stripExamples);
|
|
813
|
-
const result = {};
|
|
814
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
815
|
-
if (key === "example") continue;
|
|
816
|
-
result[key] = stripExamples(value);
|
|
817
|
-
}
|
|
818
|
-
return result;
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Get default response schemas for all CRUD operations.
|
|
822
|
-
*
|
|
823
|
-
* When routes have response schemas, Fastify compiles them with
|
|
824
|
-
* fast-json-stringify for 2-3x faster serialization and prevents
|
|
825
|
-
* accidental field disclosure.
|
|
826
|
-
*
|
|
827
|
-
* These defaults use `additionalProperties: true` so all fields pass through.
|
|
828
|
-
* Override with specific schemas for full serialization performance + safety.
|
|
829
|
-
*
|
|
830
|
-
* Note: `example` properties are stripped from defaults so they work with
|
|
831
|
-
* any Fastify instance (not just createApp which adds `keywords: ['example']`).
|
|
832
|
-
*/
|
|
833
|
-
function getDefaultCrudSchemas() {
|
|
834
|
-
return stripExamples({
|
|
835
|
-
list: {
|
|
836
|
-
querystring: getListQueryParams(),
|
|
837
|
-
response: { 200: listResponse(genericItemSchema) }
|
|
838
|
-
},
|
|
839
|
-
get: { response: { 200: itemResponse(genericItemSchema) } },
|
|
840
|
-
create: { response: { 201: mutationResponse(genericItemSchema) } },
|
|
841
|
-
update: { response: { 200: itemResponse(genericItemSchema) } },
|
|
842
|
-
delete: { response: { 200: deleteResponse() } }
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
//#endregion
|
|
846
|
-
//#region src/utils/stateMachine.ts
|
|
847
|
-
/**
|
|
848
|
-
* Create a state machine for validating transitions
|
|
849
|
-
*
|
|
850
|
-
* @param name - Name of the state machine (used in error messages)
|
|
851
|
-
* @param transitions - Map of actions to allowed source statuses
|
|
852
|
-
* @param options - Additional options (history, guards, actions)
|
|
853
|
-
* @returns State machine with can() and assert() methods
|
|
854
|
-
*
|
|
855
|
-
* @example
|
|
856
|
-
* // Basic usage
|
|
857
|
-
* const transferState = createStateMachine('Transfer', {
|
|
858
|
-
* approve: ['draft'],
|
|
859
|
-
* dispatch: ['approved'],
|
|
860
|
-
* receive: ['dispatched', 'in_transit'],
|
|
861
|
-
* cancel: ['draft', 'approved'],
|
|
862
|
-
* });
|
|
863
|
-
*
|
|
864
|
-
* @example
|
|
865
|
-
* // With guards and actions
|
|
866
|
-
* const orderState = createStateMachine('Order', {
|
|
867
|
-
* approve: {
|
|
868
|
-
* from: ['pending'],
|
|
869
|
-
* to: 'approved',
|
|
870
|
-
* guard: ({ data }) => data.paymentConfirmed,
|
|
871
|
-
* before: ({ from, to }) => console.log(`Approving order from ${from} to ${to}`),
|
|
872
|
-
* after: ({ data }) => sendApprovalEmail(data.customerId),
|
|
873
|
-
* },
|
|
874
|
-
* }, { trackHistory: true });
|
|
875
|
-
*/
|
|
876
|
-
function createStateMachine(name, transitions = {}, options = {}) {
|
|
877
|
-
const normalized = /* @__PURE__ */ new Map();
|
|
878
|
-
const history = options.trackHistory ? [] : void 0;
|
|
879
|
-
Object.entries(transitions).forEach(([action, allowed]) => {
|
|
880
|
-
if (Array.isArray(allowed)) normalized.set(action, { from: new Set(allowed) });
|
|
881
|
-
else if (typeof allowed === "object" && "from" in allowed) normalized.set(action, {
|
|
882
|
-
from: new Set(Array.isArray(allowed.from) ? allowed.from : [allowed.from]),
|
|
883
|
-
to: allowed.to,
|
|
884
|
-
guard: allowed.guard,
|
|
885
|
-
before: allowed.before,
|
|
886
|
-
after: allowed.after
|
|
887
|
-
});
|
|
888
|
-
});
|
|
889
|
-
const can = (action, status) => {
|
|
890
|
-
const transition = normalized.get(action);
|
|
891
|
-
if (!transition || !status) return false;
|
|
892
|
-
return transition.from.has(status);
|
|
893
|
-
};
|
|
894
|
-
const canAsync = async (action, status, context) => {
|
|
895
|
-
const transition = normalized.get(action);
|
|
896
|
-
if (!transition || !status) return false;
|
|
897
|
-
if (!transition.from.has(status)) return false;
|
|
898
|
-
if (transition.guard) try {
|
|
899
|
-
return await transition.guard({
|
|
900
|
-
from: status,
|
|
901
|
-
to: transition.to || "",
|
|
902
|
-
action,
|
|
903
|
-
data: context
|
|
904
|
-
});
|
|
905
|
-
} catch {
|
|
906
|
-
return false;
|
|
907
|
-
}
|
|
908
|
-
return true;
|
|
909
|
-
};
|
|
910
|
-
const assert = (action, status, errorFactory, message) => {
|
|
911
|
-
if (can(action, status)) return;
|
|
912
|
-
const errorMessage = message || `${name} cannot '${action}' when status is '${status || "unknown"}'`;
|
|
913
|
-
if (typeof errorFactory === "function") throw errorFactory(errorMessage);
|
|
914
|
-
throw new Error(errorMessage);
|
|
915
|
-
};
|
|
916
|
-
const recordTransition = (from, to, action, metadata) => {
|
|
917
|
-
if (history) history.push({
|
|
918
|
-
from,
|
|
919
|
-
to,
|
|
920
|
-
action,
|
|
921
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
922
|
-
metadata
|
|
923
|
-
});
|
|
924
|
-
};
|
|
925
|
-
const getHistory = () => {
|
|
926
|
-
return history ? [...history] : [];
|
|
927
|
-
};
|
|
928
|
-
const clearHistory = () => {
|
|
929
|
-
if (history) history.length = 0;
|
|
930
|
-
};
|
|
931
|
-
const getAvailableActions = (status) => {
|
|
932
|
-
const actions = [];
|
|
933
|
-
for (const [action, transition] of normalized.entries()) if (transition.from.has(status)) actions.push(action);
|
|
934
|
-
return actions;
|
|
935
|
-
};
|
|
936
|
-
return {
|
|
937
|
-
can,
|
|
938
|
-
canAsync,
|
|
939
|
-
assert,
|
|
940
|
-
recordTransition,
|
|
941
|
-
getHistory,
|
|
942
|
-
clearHistory,
|
|
943
|
-
getAvailableActions
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
//#endregion
|
|
947
|
-
export { createCircuitBreaker as C, CircuitState as S, defineCompensation as _, getListQueryParams as a, CircuitBreakerError as b, mutationResponse as c, responses as d, successResponseSchema as f, defineErrorMapper 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, withCompensation as v, createCircuitBreakerRegistry as w, CircuitBreakerRegistry as x, CircuitBreaker as y };
|