@classytic/arc 2.10.8 → 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 (136) hide show
  1. package/dist/{BaseController-DVNKvoX4.mjs → BaseController-JNV08qOT.mjs} +480 -442
  2. package/dist/{queryCachePlugin-Dumka73q.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  6. package/dist/audit/index.d.mts +1 -1
  7. package/dist/auth/index.d.mts +1 -1
  8. package/dist/auth/index.mjs +5 -5
  9. package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  10. package/dist/cache/index.d.mts +3 -2
  11. package/dist/cache/index.mjs +3 -3
  12. package/dist/cli/commands/docs.mjs +2 -2
  13. package/dist/cli/commands/generate.mjs +37 -27
  14. package/dist/cli/commands/init.mjs +46 -33
  15. package/dist/cli/commands/introspect.mjs +1 -1
  16. package/dist/context/index.mjs +1 -1
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -3
  19. package/dist/core-DXdSSFW-.mjs +1037 -0
  20. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  21. package/dist/{createApp-BwnEAO2h.mjs → createApp-DvNYEhpb.mjs} +75 -27
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-Dci0AYLT.mjs → elevation-DOFoxoDs.mjs} +1 -1
  25. package/dist/{errorHandler-CSxe7KIM.mjs → errorHandler-BQm8ZxTK.mjs} +1 -1
  26. package/dist/{eventPlugin-ByU4Cv0e.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  27. package/dist/events/index.d.mts +3 -3
  28. package/dist/events/index.mjs +2 -2
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/factory/index.d.mts +1 -1
  31. package/dist/factory/index.mjs +2 -2
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/hooks/index.mjs +1 -1
  34. package/dist/idempotency/index.d.mts +3 -3
  35. package/dist/idempotency/index.mjs +1 -1
  36. package/dist/idempotency/redis.d.mts +1 -1
  37. package/dist/{index-C_Noptz-.d.mts → index-BYCqHCVu.d.mts} +2 -2
  38. package/dist/{index-BGbpGVyM.d.mts → index-Cm0vUrr_.d.mts} +699 -494
  39. package/dist/{index-BziRPS4H.d.mts → index-DAushRTt.d.mts} +29 -10
  40. package/dist/index-DsJ1MNfC.d.mts +1179 -0
  41. package/dist/{index-EqQN6p0W.d.mts → index-t8pLpPFW.d.mts} +11 -8
  42. package/dist/index.d.mts +6 -38
  43. package/dist/index.mjs +9 -9
  44. package/dist/integrations/event-gateway.d.mts +1 -1
  45. package/dist/integrations/event-gateway.mjs +1 -1
  46. package/dist/integrations/index.d.mts +2 -2
  47. package/dist/integrations/mcp/index.d.mts +2 -2
  48. package/dist/integrations/mcp/index.mjs +1 -1
  49. package/dist/integrations/mcp/testing.d.mts +1 -1
  50. package/dist/integrations/mcp/testing.mjs +1 -1
  51. package/dist/integrations/streamline.d.mts +46 -5
  52. package/dist/integrations/streamline.mjs +50 -21
  53. package/dist/integrations/websocket-redis.d.mts +1 -1
  54. package/dist/integrations/websocket.d.mts +2 -154
  55. package/dist/integrations/websocket.mjs +292 -224
  56. package/dist/{keys-nWQGUTu1.mjs → keys-CARyUjiR.mjs} +2 -0
  57. package/dist/{loadResources-Bksk8ydA.mjs → loadResources-YNwKHvRA.mjs} +3 -1
  58. package/dist/middleware/index.d.mts +1 -1
  59. package/dist/middleware/index.mjs +1 -1
  60. package/dist/{openapi-DpNpqBmo.mjs → openapi-C0L9ar7m.mjs} +4 -4
  61. package/dist/org/index.d.mts +1 -1
  62. package/dist/permissions/index.d.mts +1 -1
  63. package/dist/permissions/index.mjs +2 -4
  64. package/dist/{permissions-wkqRwicB.mjs → permissions-B4vU9L0Q.mjs} +221 -3
  65. package/dist/{pipe-CGJxqDGx.mjs → pipe-DVoIheVC.mjs} +1 -1
  66. package/dist/pipeline/index.d.mts +1 -1
  67. package/dist/pipeline/index.mjs +1 -1
  68. package/dist/plugins/index.d.mts +4 -4
  69. package/dist/plugins/index.mjs +10 -10
  70. package/dist/plugins/response-cache.mjs +1 -1
  71. package/dist/plugins/tracing-entry.d.mts +1 -1
  72. package/dist/plugins/tracing-entry.mjs +42 -24
  73. package/dist/presets/filesUpload.d.mts +1 -1
  74. package/dist/presets/filesUpload.mjs +3 -3
  75. package/dist/presets/index.d.mts +1 -1
  76. package/dist/presets/index.mjs +1 -1
  77. package/dist/presets/multiTenant.d.mts +1 -1
  78. package/dist/presets/multiTenant.mjs +6 -0
  79. package/dist/presets/search.d.mts +1 -1
  80. package/dist/presets/search.mjs +1 -1
  81. package/dist/{presets-CrwOvuXI.mjs → presets-k604Lj99.mjs} +1 -1
  82. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  83. package/dist/{queryCachePlugin-ChLNZvFT.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  84. package/dist/{redis-MXLp1oOf.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  85. package/dist/registry/index.d.mts +1 -1
  86. package/dist/registry/index.mjs +2 -2
  87. package/dist/{resourceToTools-BhF3JV5p.mjs → resourceToTools--okX6QBr.mjs} +534 -420
  88. package/dist/routerShared-DeESFp4a.mjs +515 -0
  89. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/testing/index.d.mts +367 -711
  92. package/dist/testing/index.mjs +637 -1434
  93. package/dist/{tracing-xqXzWeaf.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  94. package/dist/types/index.d.mts +3 -3
  95. package/dist/types/index.mjs +1 -3
  96. package/dist/{types-CVdgPXBW.d.mts → types-CgikqKAj.d.mts} +118 -19
  97. package/dist/{types-CVKBssX5.d.mts → types-D9NqiYIw.d.mts} +1 -1
  98. package/dist/utils/index.d.mts +2 -968
  99. package/dist/utils/index.mjs +5 -6
  100. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  101. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  102. package/package.json +7 -5
  103. package/skills/arc/SKILL.md +123 -38
  104. package/skills/arc/references/testing.md +212 -183
  105. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  106. package/dist/core-3MWJosCH.mjs +0 -1459
  107. package/dist/createActionRouter-C8UUB3Px.mjs +0 -249
  108. package/dist/errors-BI8kEKsO.d.mts +0 -140
  109. package/dist/fields-CTMWOUDt.mjs +0 -126
  110. package/dist/queryParser-NR__Qiju.mjs +0 -419
  111. package/dist/types-CDnTEpga.mjs +0 -27
  112. package/dist/utils-LMwVidKy.mjs +0 -947
  113. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  114. /package/dist/{ResourceRegistry-CcN2LVrc.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  115. /package/dist/{actionPermissions-TUVR3uiZ.mjs → actionPermissions-C8YYU92K.mjs} +0 -0
  116. /package/dist/{caching-3h93rkJM.mjs → caching-CheW3m-S.mjs} +0 -0
  117. /package/dist/{errorHandler-2ii4RIYr.d.mts → errorHandler-Co3lnVmJ.d.mts} +0 -0
  118. /package/dist/{errors-BqdUDja_.mjs → errors-D5c-5BJL.mjs} +0 -0
  119. /package/dist/{eventPlugin-D1ThQ1Pp.d.mts → eventPlugin-CUNjYYRY.d.mts} +0 -0
  120. /package/dist/{interface-B-pe8fhj.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  121. /package/dist/{interface-yhyb_pLY.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  122. /package/dist/{memory-DqI-449b.mjs → memory-DikHSvWa.mjs} +0 -0
  123. /package/dist/{metrics-TuOmguhi.mjs → metrics-Csh4nsvv.mjs} +0 -0
  124. /package/dist/{multipartBody-CUQGVlM_.mjs → multipartBody-CvTR1Un6.mjs} +0 -0
  125. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-BneOJkpi.mjs} +0 -0
  126. /package/dist/{redis-stream-bkO88VHx.d.mts → redis-stream-CM8TXTix.d.mts} +0 -0
  127. /package/dist/{registry-B0Wl7uVV.mjs → registry-D63ee7fl.mjs} +0 -0
  128. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  129. /package/dist/{requestContext-C38GskNt.mjs → requestContext-CfRkaxwf.mjs} +0 -0
  130. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  131. /package/dist/{sse-D8UeDwis.mjs → sse-V7aXc3bW.mjs} +0 -0
  132. /package/dist/{store-helpers-DYYUQbQN.mjs → store-helpers-BhrzxvyQ.mjs} +0 -0
  133. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  134. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  135. /package/dist/{versioning-B6mimogM.mjs → versioning-CGPjkqAg.mjs} +0 -0
  136. /package/dist/{versioning-CeUXHfjw.d.mts → versioning-M9lNLhO8.d.mts} +0 -0
@@ -0,0 +1,1639 @@
1
+ import { m as RESERVED_QUERY_PARAMS, t as CRUD_OPERATIONS } from "./constants-BhY1OHoH.mjs";
2
+ import { arcLog } from "./logger/index.mjs";
3
+ import { t as ArcError } from "./errors-D5c-5BJL.mjs";
4
+ import { r as getAvailablePresets } from "./presets-k604Lj99.mjs";
5
+ //#region src/utils/simpleEqualityMatcher.ts
6
+ /**
7
+ * `simpleEqualityMatcher` — a minimal, dialect-agnostic flat-key equality
8
+ * matcher for `DataAdapter.matchesFilter` / `BaseController({ matchesFilter })`.
9
+ *
10
+ * **What it does:** for each `[key, expected]` in the filter, compares
11
+ * `item[key]` to `expected` via string coercion (so Mongo `ObjectId` values
12
+ * match their string representation) and returns `true` only if every
13
+ * filter entry matches. Array item values are matched implicitly (contains).
14
+ *
15
+ * **What it does NOT do:**
16
+ * - No `$eq` / `$ne` / `$in` / `$nin` / `$gt` / `$lt` / `$regex` / `$exists`
17
+ * - No `$and` / `$or`
18
+ * - No dot-path traversal (`"owner.id"`)
19
+ * - No schema-specific coercion
20
+ *
21
+ * **Why it exists:** 95%+ of arc's `_policyFilters` are produced by built-in
22
+ * permission helpers and are shaped like `{ ownerId: "u1" }` or
23
+ * `{ organizationId: "org_x" }` — flat equality. For that common shape,
24
+ * this helper is a safe, tested, 15-line defense-in-depth matcher that
25
+ * hosts using minimal repos (no `getOne(compoundFilter)` DB path) can opt
26
+ * into without arc shipping a full Mongo-syntax engine.
27
+ *
28
+ * **When to use:**
29
+ * - Your adapter/repo doesn't natively filter on `getOne(compoundFilter)`
30
+ * - Your `_policyFilters` are flat equality (from arc's built-in permission helpers)
31
+ * - You want defense-in-depth on `validateItemAccess` / `fetchDetailed`'s `getById` fallback
32
+ *
33
+ * **When NOT to use:**
34
+ * - Your `_policyFilters` use operators (`$in`, `$ne`, etc.) — supply a
35
+ * native matcher (mongokit's repo does the filter at the DB layer; for
36
+ * custom repos, wrap the kit's own predicate engine).
37
+ * - You're a mongokit / sqlitekit / Prisma user — the DB-level filter
38
+ * applied by `getOne(compoundFilter)` already covers this.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { simpleEqualityMatcher } from '@classytic/arc/utils';
43
+ *
44
+ * // On a custom adapter
45
+ * const adapter: DataAdapter = {
46
+ * repository,
47
+ * type: 'custom',
48
+ * name: 'in-memory',
49
+ * matchesFilter: simpleEqualityMatcher,
50
+ * };
51
+ *
52
+ * // Or directly on BaseController for ad-hoc controllers
53
+ * new BaseController(repo, { matchesFilter: simpleEqualityMatcher });
54
+ * ```
55
+ */
56
+ function simpleEqualityMatcher(item, filters) {
57
+ if (!item || typeof item !== "object") return false;
58
+ const obj = item;
59
+ for (const [key, expected] of Object.entries(filters)) {
60
+ if (expected && typeof expected === "object" && !Array.isArray(expected) && Object.getPrototypeOf(expected) === Object.prototype && Object.keys(expected).some((k) => k.startsWith("$"))) return false;
61
+ const actual = obj[key];
62
+ if (Array.isArray(actual)) {
63
+ const expectedStr = String(expected);
64
+ if (!actual.some((v) => String(v) === expectedStr)) return false;
65
+ continue;
66
+ }
67
+ if (String(actual) !== String(expected)) return false;
68
+ }
69
+ return true;
70
+ }
71
+ //#endregion
72
+ //#region src/utils/userHelpers.ts
73
+ /**
74
+ * Extract a user ID from a user object. Accepts `id` or `_id` — returns
75
+ * `undefined` when neither is present. Used by arc's controllers to
76
+ * populate `createdBy` / `updatedBy` fields and for cache scoping.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * import { getUserId } from '@classytic/arc/utils';
81
+ * const uid = getUserId(request.user);
82
+ * ```
83
+ */
84
+ function getUserId(user) {
85
+ if (!user) return void 0;
86
+ const id = user.id ?? user._id;
87
+ return id ? String(id) : void 0;
88
+ }
89
+ //#endregion
90
+ //#region src/utils/queryParser.ts
91
+ /**
92
+ * Arc Query Parser - Default URL-to-Query Parser
93
+ *
94
+ * Framework-agnostic query parser that converts URL parameters to query options.
95
+ * This is Arc's built-in parser; users can swap in MongoKit's QueryParser,
96
+ * pgkit's parser, or any custom parser implementing QueryParserInterface.
97
+ *
98
+ * @example
99
+ * // Use Arc default parser (auto-applied if no queryParser option)
100
+ * defineResource({ name: 'product', adapter: ... });
101
+ *
102
+ * // Use MongoKit's QueryParser (recommended for MongoDB - has $lookup, aggregations, etc.)
103
+ * import { QueryParser } from '@classytic/mongokit';
104
+ * defineResource({
105
+ * name: 'product',
106
+ * adapter: ...,
107
+ * queryParser: new QueryParser(),
108
+ * });
109
+ *
110
+ * // Use custom parser for SQL databases
111
+ * defineResource({
112
+ * name: 'user',
113
+ * adapter: ...,
114
+ * queryParser: new PgQueryParser(),
115
+ * });
116
+ */
117
+ const log = arcLog("queryParser");
118
+ /**
119
+ * Regex patterns that can cause catastrophic backtracking (ReDoS attacks)
120
+ * Detects:
121
+ * - Quantifiers: {n,m}
122
+ * - Possessive quantifiers: *+, ++, ?+
123
+ * - Nested quantifiers: (a+)+, (a*)*
124
+ * - Backreferences: \1, \2, etc.
125
+ */
126
+ const DANGEROUS_REGEX_PATTERNS = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\(.+\))\+|\(\?:|\\[0-9]|(\[.+\]).+(\[.+\]))/;
127
+ /**
128
+ * Arc's default query parser
129
+ *
130
+ * Converts URL query parameters to a structured query format:
131
+ * - Pagination: ?page=1&limit=20
132
+ * - Sorting: ?sort=-createdAt,name (- prefix = descending)
133
+ * - Filtering: ?status=active&price[gte]=100&price[lte]=500
134
+ * - Search: ?search=keyword
135
+ * - Populate: ?populate=author,category
136
+ * - Field selection: ?select=name,price,status
137
+ * - Keyset pagination: ?after=cursor_value
138
+ *
139
+ * For advanced MongoDB features ($lookup, aggregations), use MongoKit's QueryParser.
140
+ */
141
+ var ArcQueryParser = class {
142
+ maxLimit;
143
+ defaultLimit;
144
+ maxRegexLength;
145
+ maxSearchLength;
146
+ maxFilterDepth;
147
+ _allowedFilterFields;
148
+ _allowedSortFields;
149
+ _allowedOperators;
150
+ /** Allowed filter fields (used by MCP for auto-derive) */
151
+ allowedFilterFields;
152
+ /** Allowed sort fields (used by MCP for sort descriptions) */
153
+ allowedSortFields;
154
+ /** Allowed operators (used by MCP for operator descriptions) */
155
+ allowedOperators;
156
+ /** Supported filter operators */
157
+ operators = {
158
+ eq: "$eq",
159
+ ne: "$ne",
160
+ gt: "$gt",
161
+ gte: "$gte",
162
+ lt: "$lt",
163
+ lte: "$lte",
164
+ in: "$in",
165
+ nin: "$nin",
166
+ like: "$regex",
167
+ contains: "$regex",
168
+ regex: "$regex",
169
+ exists: "$exists"
170
+ };
171
+ constructor(options = {}) {
172
+ this.maxLimit = options.maxLimit ?? 1e3;
173
+ this.defaultLimit = options.defaultLimit ?? 20;
174
+ this.maxRegexLength = options.maxRegexLength ?? 200;
175
+ this.maxSearchLength = options.maxSearchLength ?? 200;
176
+ this.maxFilterDepth = options.maxFilterDepth ?? 10;
177
+ if (options.allowedFilterFields) {
178
+ this._allowedFilterFields = new Set(options.allowedFilterFields);
179
+ this.allowedFilterFields = options.allowedFilterFields;
180
+ }
181
+ if (options.allowedSortFields) {
182
+ this._allowedSortFields = new Set(options.allowedSortFields);
183
+ this.allowedSortFields = options.allowedSortFields;
184
+ }
185
+ if (options.allowedOperators) {
186
+ this._allowedOperators = new Set(options.allowedOperators);
187
+ this.allowedOperators = options.allowedOperators;
188
+ }
189
+ }
190
+ /**
191
+ * Parse URL query parameters into structured query options
192
+ */
193
+ parse(query) {
194
+ const q = query ?? {};
195
+ const page = this.parseNumber(q.page, 1);
196
+ const limit = Math.min(this.parseNumber(q.limit, this.defaultLimit), this.maxLimit);
197
+ const after = this.parseString(q.after ?? q.cursor);
198
+ const sort = this.parseSort(q.sort);
199
+ const { populate, populateOptions } = this.parsePopulate(q.populate);
200
+ const search = this.parseSearch(q.search);
201
+ const select = this.parseSelect(q.select);
202
+ return {
203
+ filters: this.parseFilters(q),
204
+ limit,
205
+ sort,
206
+ populate,
207
+ populateOptions,
208
+ search,
209
+ page: after ? void 0 : page,
210
+ after,
211
+ select
212
+ };
213
+ }
214
+ parseNumber(value, defaultValue) {
215
+ if (value === void 0 || value === null) return defaultValue;
216
+ const num = parseInt(String(value), 10);
217
+ return Number.isNaN(num) ? defaultValue : Math.max(1, num);
218
+ }
219
+ parseString(value) {
220
+ if (value === void 0 || value === null) return void 0;
221
+ const str = String(value).trim();
222
+ return str.length > 0 ? str : void 0;
223
+ }
224
+ /**
225
+ * Parse populate parameter — handles both simple string and bracket notation.
226
+ *
227
+ * Simple: ?populate=author,category → { populate: 'author,category' }
228
+ * Bracket: ?populate[author][select]=name,email → { populateOptions: [{ path: 'author', select: 'name email' }] }
229
+ */
230
+ parsePopulate(value) {
231
+ if (value === void 0 || value === null) return {};
232
+ if (typeof value === "string") {
233
+ const trimmed = value.trim();
234
+ return trimmed.length > 0 ? { populate: trimmed } : {};
235
+ }
236
+ if (typeof value === "object" && !Array.isArray(value)) {
237
+ const obj = value;
238
+ const keys = Object.keys(obj);
239
+ if (keys.length === 0) return {};
240
+ const options = [];
241
+ for (const path of keys) {
242
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(path)) continue;
243
+ const config = obj[path];
244
+ if (typeof config === "object" && config !== null && !Array.isArray(config)) {
245
+ const cfg = config;
246
+ const option = { path };
247
+ if (typeof cfg.select === "string") option.select = cfg.select.split(",").map((s) => s.trim()).filter(Boolean).join(" ");
248
+ if (typeof cfg.match === "object" && cfg.match !== null) option.match = cfg.match;
249
+ options.push(option);
250
+ } else options.push({ path });
251
+ }
252
+ return options.length > 0 ? { populateOptions: options } : {};
253
+ }
254
+ return {};
255
+ }
256
+ parseSort(value) {
257
+ if (!value) return void 0;
258
+ const sortStr = String(value);
259
+ const result = {};
260
+ for (const field of sortStr.split(",")) {
261
+ const trimmed = field.trim();
262
+ if (!trimmed) continue;
263
+ if (!/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
264
+ const fieldName = trimmed.startsWith("-") ? trimmed.slice(1) : trimmed;
265
+ if (this._allowedSortFields && !this._allowedSortFields.has(fieldName)) continue;
266
+ if (trimmed.startsWith("-")) result[fieldName] = -1;
267
+ else result[fieldName] = 1;
268
+ }
269
+ return Object.keys(result).length > 0 ? result : void 0;
270
+ }
271
+ parseSearch(value) {
272
+ if (!value) return void 0;
273
+ const search = String(value).trim();
274
+ if (search.length === 0) return void 0;
275
+ if (search.length > this.maxSearchLength) return search.slice(0, this.maxSearchLength);
276
+ return search;
277
+ }
278
+ parseSelect(value) {
279
+ if (!value) return void 0;
280
+ const selectStr = String(value);
281
+ const result = {};
282
+ for (const field of selectStr.split(",")) {
283
+ const trimmed = field.trim();
284
+ if (!trimmed) continue;
285
+ if (!/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
286
+ if (trimmed.startsWith("-")) result[trimmed.slice(1)] = 0;
287
+ else result[trimmed] = 1;
288
+ }
289
+ return Object.keys(result).length > 0 ? result : void 0;
290
+ }
291
+ /**
292
+ * Check if a value exceeds the maximum nesting depth.
293
+ * Prevents filter bombs where deeply nested objects consume excessive memory/CPU.
294
+ */
295
+ exceedsDepth(obj, currentDepth = 0) {
296
+ if (currentDepth > this.maxFilterDepth) return true;
297
+ if (obj === null || obj === void 0) return false;
298
+ if (Array.isArray(obj)) return obj.some((v) => this.exceedsDepth(v, currentDepth));
299
+ if (typeof obj !== "object") return false;
300
+ return Object.values(obj).some((v) => this.exceedsDepth(v, currentDepth + 1));
301
+ }
302
+ parseFilters(query) {
303
+ const filters = {};
304
+ for (const [key, value] of Object.entries(query)) {
305
+ if (RESERVED_QUERY_PARAMS.has(key)) continue;
306
+ if (value === void 0 || value === null) continue;
307
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key)) continue;
308
+ if (this._allowedFilterFields && !this._allowedFilterFields.has(key)) continue;
309
+ if (this.exceedsDepth(value)) continue;
310
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
311
+ const operatorObj = value;
312
+ const operatorKeys = Object.keys(operatorObj);
313
+ const allOperators = operatorKeys.every((op) => this.operators[op] && (!this._allowedOperators || this._allowedOperators.has(op)));
314
+ const allKnownOperators = operatorKeys.every((op) => this.operators[op]);
315
+ if (allOperators && operatorKeys.length > 0) {
316
+ const mongoFilters = {};
317
+ let needsCaseInsensitive = false;
318
+ for (const [op, opValue] of Object.entries(operatorObj)) {
319
+ const mongoOp = this.operators[op];
320
+ if (mongoOp) {
321
+ mongoFilters[mongoOp] = this.parseFilterValue(opValue, op);
322
+ if (op === "contains" || op === "like") needsCaseInsensitive = true;
323
+ }
324
+ }
325
+ if (needsCaseInsensitive) mongoFilters.$options = "i";
326
+ filters[key] = mongoFilters;
327
+ continue;
328
+ }
329
+ if (allKnownOperators && this._allowedOperators) continue;
330
+ }
331
+ const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
332
+ if (!match) continue;
333
+ const [, fieldName, operator] = match;
334
+ if (!fieldName) continue;
335
+ if (operator && this.operators[operator] && (!this._allowedOperators || this._allowedOperators.has(operator))) {
336
+ const mongoOp = this.operators[operator];
337
+ const parsedValue = this.parseFilterValue(value, operator);
338
+ if (!filters[fieldName]) filters[fieldName] = {};
339
+ const fieldFilter = filters[fieldName];
340
+ fieldFilter[mongoOp] = parsedValue;
341
+ if (operator === "contains" || operator === "like") fieldFilter.$options = "i";
342
+ } else if (!operator) filters[fieldName] = this.parseFilterValue(value);
343
+ }
344
+ return filters;
345
+ }
346
+ parseFilterValue(value, operator) {
347
+ if (operator === "in" || operator === "nin") {
348
+ if (Array.isArray(value)) return value.map((v) => this.coerceValue(v));
349
+ if (typeof value === "string" && value.includes(",")) return value.split(",").map((v) => this.coerceValue(v.trim()));
350
+ return [this.coerceValue(value)];
351
+ }
352
+ if (operator === "like" || operator === "contains" || operator === "regex") return this.sanitizeRegex(String(value));
353
+ if (operator === "exists") {
354
+ const str = String(value).toLowerCase();
355
+ return str === "true" || str === "1";
356
+ }
357
+ return this.coerceValue(value);
358
+ }
359
+ coerceValue(value) {
360
+ if (value === "true") return true;
361
+ if (value === "false") return false;
362
+ if (value === "null") return null;
363
+ if (typeof value === "string") {
364
+ const num = Number(value);
365
+ if (!Number.isNaN(num) && value.trim() !== "") return num;
366
+ }
367
+ return value;
368
+ }
369
+ /**
370
+ * Generate OpenAPI-compatible JSON Schema for query parameters.
371
+ * Arc's defineResource() auto-detects this method and uses it
372
+ * to document list endpoint query parameters in OpenAPI/Swagger.
373
+ */
374
+ getQuerySchema() {
375
+ const operatorLines = Object.entries(this.operators).map(([op, mongoOp]) => {
376
+ return ` ${op} → ${mongoOp}: ${{
377
+ eq: "Equal (default when no operator specified)",
378
+ ne: "Not equal",
379
+ gt: "Greater than",
380
+ gte: "Greater than or equal",
381
+ lt: "Less than",
382
+ lte: "Less than or equal",
383
+ in: "In list (comma-separated values)",
384
+ nin: "Not in list",
385
+ like: "Pattern match (case-insensitive)",
386
+ contains: "Contains substring (case-insensitive)",
387
+ regex: "Regex pattern",
388
+ exists: "Field exists (true/false)"
389
+ }[op] || op}`;
390
+ });
391
+ return {
392
+ type: "object",
393
+ properties: {
394
+ page: {
395
+ type: "integer",
396
+ description: "Page number for offset pagination",
397
+ default: 1,
398
+ minimum: 1
399
+ },
400
+ limit: {
401
+ type: "integer",
402
+ description: "Number of items per page",
403
+ default: this.defaultLimit,
404
+ minimum: 1,
405
+ maximum: this.maxLimit
406
+ },
407
+ sort: {
408
+ type: "string",
409
+ description: "Sort fields (comma-separated). Prefix with - for descending. Example: -createdAt,name"
410
+ },
411
+ search: {
412
+ type: "string",
413
+ description: "Full-text search query",
414
+ maxLength: this.maxSearchLength
415
+ },
416
+ select: {
417
+ type: "string",
418
+ description: "Fields to include/exclude (comma-separated). Prefix with - to exclude. Example: name,email,-password"
419
+ },
420
+ populate: {
421
+ type: "string",
422
+ description: "Fields to populate/join (comma-separated). Example: author,category"
423
+ },
424
+ after: {
425
+ type: "string",
426
+ description: "Cursor value for keyset pagination"
427
+ },
428
+ _filterOperators: {
429
+ type: "string",
430
+ description: ["Available filter operators (use as field[operator]=value):", ...operatorLines].join("\n")
431
+ }
432
+ }
433
+ };
434
+ }
435
+ sanitizeRegex(pattern) {
436
+ const truncated = pattern.length > this.maxRegexLength;
437
+ let sanitized = pattern.slice(0, this.maxRegexLength);
438
+ if (DANGEROUS_REGEX_PATTERNS.test(sanitized)) {
439
+ sanitized = sanitized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
440
+ log.warn(`regex pattern matched a ReDoS-dangerous shape and was escaped to a literal string. original="${pattern}" sanitized="${sanitized}"`);
441
+ } else if (truncated) log.warn(`regex pattern exceeded maxRegexLength (${this.maxRegexLength}) and was truncated. original-length=${pattern.length}`);
442
+ return sanitized;
443
+ }
444
+ };
445
+ /**
446
+ * Create a new ArcQueryParser instance
447
+ */
448
+ function createQueryParser(options) {
449
+ return new ArcQueryParser(options);
450
+ }
451
+ //#endregion
452
+ //#region src/utils/responseSchemas.ts
453
+ /**
454
+ * Base success response schema
455
+ */
456
+ const successResponseSchema = {
457
+ type: "object",
458
+ properties: { success: {
459
+ type: "boolean",
460
+ example: true
461
+ } },
462
+ required: ["success"]
463
+ };
464
+ /**
465
+ * Error response schema
466
+ */
467
+ const errorResponseSchema = {
468
+ type: "object",
469
+ properties: {
470
+ success: {
471
+ type: "boolean",
472
+ example: false
473
+ },
474
+ error: {
475
+ type: "string",
476
+ description: "Error message"
477
+ },
478
+ code: {
479
+ type: "string",
480
+ description: "Error code"
481
+ },
482
+ message: {
483
+ type: "string",
484
+ description: "Detailed message"
485
+ }
486
+ },
487
+ required: ["success", "error"]
488
+ };
489
+ /**
490
+ * Pagination schema - matches MongoKit/Arc runtime format
491
+ *
492
+ * Runtime format (flat fields):
493
+ * { page, limit, total, pages, hasNext, hasPrev }
494
+ */
495
+ const paginationSchema = {
496
+ type: "object",
497
+ properties: {
498
+ page: {
499
+ type: "integer",
500
+ example: 1
501
+ },
502
+ limit: {
503
+ type: "integer",
504
+ example: 20
505
+ },
506
+ total: {
507
+ type: "integer",
508
+ example: 100
509
+ },
510
+ pages: {
511
+ type: "integer",
512
+ example: 5
513
+ },
514
+ hasNext: {
515
+ type: "boolean",
516
+ example: true
517
+ },
518
+ hasPrev: {
519
+ type: "boolean",
520
+ example: false
521
+ }
522
+ },
523
+ required: [
524
+ "page",
525
+ "limit",
526
+ "total",
527
+ "pages",
528
+ "hasNext",
529
+ "hasPrev"
530
+ ]
531
+ };
532
+ /**
533
+ * Wrap a data schema in a success response
534
+ */
535
+ function wrapResponse(dataSchema) {
536
+ return {
537
+ type: "object",
538
+ properties: {
539
+ success: {
540
+ type: "boolean",
541
+ example: true
542
+ },
543
+ data: dataSchema
544
+ },
545
+ required: ["success", "data"],
546
+ additionalProperties: true
547
+ };
548
+ }
549
+ /**
550
+ * Create a list response schema with pagination - matches MongoKit/Arc runtime format
551
+ *
552
+ * Runtime format:
553
+ * { success, docs: [...], page, limit, total, pages, hasNext, hasPrev }
554
+ *
555
+ * Note: Uses 'docs' array (not 'data') with flat pagination fields
556
+ */
557
+ function listResponse(itemSchema) {
558
+ return {
559
+ type: "object",
560
+ properties: {
561
+ success: {
562
+ type: "boolean",
563
+ example: true
564
+ },
565
+ docs: {
566
+ type: "array",
567
+ items: itemSchema
568
+ },
569
+ page: {
570
+ type: "integer",
571
+ example: 1
572
+ },
573
+ limit: {
574
+ type: "integer",
575
+ example: 20
576
+ },
577
+ total: {
578
+ type: "integer",
579
+ example: 100
580
+ },
581
+ pages: {
582
+ type: "integer",
583
+ example: 5
584
+ },
585
+ hasNext: {
586
+ type: "boolean",
587
+ example: false
588
+ },
589
+ hasPrev: {
590
+ type: "boolean",
591
+ example: false
592
+ }
593
+ },
594
+ required: ["success", "docs"],
595
+ additionalProperties: true
596
+ };
597
+ }
598
+ /**
599
+ * Create a single item response schema
600
+ *
601
+ * Runtime format: { success, data: {...} }
602
+ */
603
+ function itemResponse(itemSchema) {
604
+ return wrapResponse(itemSchema);
605
+ }
606
+ /**
607
+ * Create a create/update response schema
608
+ */
609
+ function mutationResponse(itemSchema) {
610
+ return {
611
+ type: "object",
612
+ properties: {
613
+ success: {
614
+ type: "boolean",
615
+ example: true
616
+ },
617
+ data: itemSchema,
618
+ message: {
619
+ type: "string",
620
+ example: "Created successfully"
621
+ }
622
+ },
623
+ required: ["success", "data"],
624
+ additionalProperties: true
625
+ };
626
+ }
627
+ /**
628
+ * Create a delete response schema
629
+ *
630
+ * Runtime format: { success, data: { message, id?, soft? } }
631
+ */
632
+ function deleteResponse() {
633
+ return {
634
+ type: "object",
635
+ properties: {
636
+ success: {
637
+ type: "boolean",
638
+ example: true
639
+ },
640
+ data: {
641
+ type: "object",
642
+ properties: {
643
+ message: {
644
+ type: "string",
645
+ example: "Deleted successfully"
646
+ },
647
+ id: {
648
+ type: "string",
649
+ example: "507f1f77bcf86cd799439011"
650
+ },
651
+ soft: {
652
+ type: "boolean",
653
+ example: false
654
+ }
655
+ },
656
+ required: ["message"]
657
+ }
658
+ },
659
+ required: ["success"],
660
+ additionalProperties: true
661
+ };
662
+ }
663
+ const responses = {
664
+ 200: (schema) => ({
665
+ description: "Successful response",
666
+ content: { "application/json": { schema } }
667
+ }),
668
+ 201: (schema) => ({
669
+ description: "Created successfully",
670
+ content: { "application/json": { schema: mutationResponse(schema) } }
671
+ }),
672
+ 400: {
673
+ description: "Bad Request",
674
+ content: { "application/json": { schema: {
675
+ ...errorResponseSchema,
676
+ properties: {
677
+ ...errorResponseSchema.properties,
678
+ code: {
679
+ type: "string",
680
+ example: "VALIDATION_ERROR"
681
+ },
682
+ details: {
683
+ type: "object",
684
+ properties: { errors: {
685
+ type: "array",
686
+ items: {
687
+ type: "object",
688
+ properties: {
689
+ field: { type: "string" },
690
+ message: { type: "string" }
691
+ }
692
+ }
693
+ } }
694
+ }
695
+ }
696
+ } } }
697
+ },
698
+ 401: {
699
+ description: "Unauthorized",
700
+ content: { "application/json": { schema: {
701
+ ...errorResponseSchema,
702
+ properties: {
703
+ ...errorResponseSchema.properties,
704
+ code: {
705
+ type: "string",
706
+ example: "UNAUTHORIZED"
707
+ }
708
+ }
709
+ } } }
710
+ },
711
+ 403: {
712
+ description: "Forbidden",
713
+ content: { "application/json": { schema: {
714
+ ...errorResponseSchema,
715
+ properties: {
716
+ ...errorResponseSchema.properties,
717
+ code: {
718
+ type: "string",
719
+ example: "FORBIDDEN"
720
+ }
721
+ }
722
+ } } }
723
+ },
724
+ 404: {
725
+ description: "Not Found",
726
+ content: { "application/json": { schema: {
727
+ ...errorResponseSchema,
728
+ properties: {
729
+ ...errorResponseSchema.properties,
730
+ code: {
731
+ type: "string",
732
+ example: "NOT_FOUND"
733
+ }
734
+ }
735
+ } } }
736
+ },
737
+ 409: {
738
+ description: "Conflict",
739
+ content: { "application/json": { schema: {
740
+ ...errorResponseSchema,
741
+ properties: {
742
+ ...errorResponseSchema.properties,
743
+ code: {
744
+ type: "string",
745
+ example: "CONFLICT"
746
+ }
747
+ }
748
+ } } }
749
+ },
750
+ 500: {
751
+ description: "Internal Server Error",
752
+ content: { "application/json": { schema: {
753
+ ...errorResponseSchema,
754
+ properties: {
755
+ ...errorResponseSchema.properties,
756
+ code: {
757
+ type: "string",
758
+ example: "INTERNAL_ERROR"
759
+ }
760
+ }
761
+ } } }
762
+ }
763
+ };
764
+ const queryParams = {
765
+ pagination: {
766
+ page: {
767
+ type: "integer",
768
+ minimum: 1,
769
+ default: 1,
770
+ description: "Page number"
771
+ },
772
+ limit: {
773
+ type: "integer",
774
+ minimum: 1,
775
+ maximum: 100,
776
+ default: 20,
777
+ description: "Items per page"
778
+ }
779
+ },
780
+ sorting: { sort: {
781
+ type: "string",
782
+ description: "Sort field (prefix with - for descending)",
783
+ example: "-createdAt"
784
+ } },
785
+ filtering: {
786
+ select: {
787
+ description: "Fields to include (space-separated or object)",
788
+ example: "name email createdAt"
789
+ },
790
+ populate: {
791
+ description: "Relations to populate (comma-separated string or bracket-notation object)",
792
+ example: "author,category"
793
+ }
794
+ }
795
+ };
796
+ /**
797
+ * Get standard list query parameters schema
798
+ */
799
+ function getListQueryParams() {
800
+ return {
801
+ type: "object",
802
+ properties: {
803
+ ...queryParams.pagination,
804
+ ...queryParams.sorting,
805
+ ...queryParams.filtering
806
+ },
807
+ additionalProperties: true
808
+ };
809
+ }
810
+ /**
811
+ * Generic item schema that allows any properties.
812
+ * Used as default when no user schema is provided.
813
+ * Enables fast-json-stringify while still passing through all fields.
814
+ */
815
+ const genericItemSchema = {
816
+ type: "object",
817
+ additionalProperties: true
818
+ };
819
+ /**
820
+ * Recursively strip `example` keys from a schema object.
821
+ * The `example` keyword is OpenAPI metadata — not standard JSON Schema —
822
+ * and triggers Ajv strict mode errors when used on routes without the
823
+ * `keywords: ['example']` AJV config (e.g., raw Fastify without createApp).
824
+ */
825
+ function stripExamples(schema) {
826
+ if (schema === null || typeof schema !== "object") return schema;
827
+ if (Array.isArray(schema)) return schema.map(stripExamples);
828
+ const result = {};
829
+ for (const [key, value] of Object.entries(schema)) {
830
+ if (key === "example") continue;
831
+ result[key] = stripExamples(value);
832
+ }
833
+ return result;
834
+ }
835
+ /**
836
+ * Get default response schemas for all CRUD operations.
837
+ *
838
+ * When routes have response schemas, Fastify compiles them with
839
+ * fast-json-stringify for 2-3x faster serialization and prevents
840
+ * accidental field disclosure.
841
+ *
842
+ * These defaults use `additionalProperties: true` so all fields pass through.
843
+ * Override with specific schemas for full serialization performance + safety.
844
+ *
845
+ * Note: `example` properties are stripped from defaults so they work with
846
+ * any Fastify instance (not just createApp which adds `keywords: ['example']`).
847
+ */
848
+ function getDefaultCrudSchemas() {
849
+ return stripExamples({
850
+ list: {
851
+ querystring: getListQueryParams(),
852
+ response: { 200: listResponse(genericItemSchema) }
853
+ },
854
+ get: { response: { 200: itemResponse(genericItemSchema) } },
855
+ create: { response: { 201: mutationResponse(genericItemSchema) } },
856
+ update: { response: { 200: itemResponse(genericItemSchema) } },
857
+ delete: { response: { 200: deleteResponse() } }
858
+ });
859
+ }
860
+ //#endregion
861
+ //#region src/core/validateResourceConfig.ts
862
+ /**
863
+ * Resource Configuration Validator
864
+ *
865
+ * Fail-fast validation at definition time.
866
+ * Invalid configs throw immediately with clear, actionable errors.
867
+ *
868
+ * @example
869
+ * const result = validateResourceConfig(config);
870
+ * if (!result.valid) {
871
+ * console.error(formatValidationErrors(result.errors));
872
+ * }
873
+ */
874
+ /**
875
+ * Validate a resource configuration
876
+ */
877
+ function validateResourceConfig(config, options = {}) {
878
+ const errors = [];
879
+ const warnings = [];
880
+ if (!config.name) errors.push({
881
+ field: "name",
882
+ message: "Resource name is required",
883
+ suggestion: "Add a unique resource name (e.g., \"product\", \"user\")"
884
+ });
885
+ else if (!/^[a-z][a-z0-9-]*$/i.test(config.name)) errors.push({
886
+ field: "name",
887
+ message: `Invalid resource name "${config.name}"`,
888
+ suggestion: "Use alphanumeric characters and hyphens, starting with a letter"
889
+ });
890
+ const crudRoutes = CRUD_OPERATIONS;
891
+ const disabledRoutes = new Set(config.disabledRoutes ?? []);
892
+ const enabledCrudRoutes = crudRoutes.filter((route) => !disabledRoutes.has(route));
893
+ if (!config.disableDefaultRoutes && enabledCrudRoutes.length > 0) {
894
+ if (!config.adapter) errors.push({
895
+ field: "adapter",
896
+ message: "Data adapter is required when CRUD routes are enabled",
897
+ suggestion: "Provide an adapter: createMongooseAdapter({ model, repository })"
898
+ });
899
+ else if (!config.adapter.repository) errors.push({
900
+ field: "adapter.repository",
901
+ message: "Adapter must provide a repository",
902
+ suggestion: "Ensure your adapter returns a valid StandardRepo (see @classytic/repo-core)"
903
+ });
904
+ } else if (!config.adapter && !config.routes?.length) warnings.push({
905
+ field: "config",
906
+ message: "Resource has no adapter and no routes",
907
+ suggestion: "Provide either adapter for CRUD or routes for custom logic"
908
+ });
909
+ if (config.controller && !options.skipControllerCheck && !config.disableDefaultRoutes) {
910
+ const ctrl = config.controller;
911
+ const requiredMethods = CRUD_OPERATIONS;
912
+ for (const method of requiredMethods) if (typeof ctrl[method] !== "function") errors.push({
913
+ field: `controller.${method}`,
914
+ message: `Missing required CRUD method "${method}"`,
915
+ suggestion: "Extend BaseController which implements IController interface"
916
+ });
917
+ }
918
+ if (config.controller && config.routes) validateRouteHandlers(config.controller, config.routes, errors);
919
+ if (config.permissions) validatePermissionKeys(config, options, errors, warnings);
920
+ if (config.presets && !options.allowUnknownPresets) validatePresets(config.presets, errors, warnings);
921
+ if (config.prefix) {
922
+ if (!config.prefix.startsWith("/")) errors.push({
923
+ field: "prefix",
924
+ message: `Prefix must start with "/" (got "${config.prefix}")`,
925
+ suggestion: `Change to "/${config.prefix}"`
926
+ });
927
+ if (config.prefix.endsWith("/") && config.prefix !== "/") warnings.push({
928
+ field: "prefix",
929
+ message: `Prefix should not end with "/" (got "${config.prefix}")`,
930
+ suggestion: `Change to "${config.prefix.slice(0, -1)}"`
931
+ });
932
+ }
933
+ if (config.routes) validateRoutes(config.routes, errors);
934
+ return {
935
+ valid: errors.length === 0,
936
+ errors,
937
+ warnings
938
+ };
939
+ }
940
+ function validateRouteHandlers(controller, routes, errors) {
941
+ const ctrl = controller;
942
+ for (const route of routes) if (typeof route.handler === "string") {
943
+ if (typeof ctrl[route.handler] !== "function") errors.push({
944
+ field: `routes[${route.method} ${route.path}]`,
945
+ message: `Handler "${route.handler}" not found on controller`,
946
+ suggestion: `Add method "${route.handler}" to controller or use a function handler`
947
+ });
948
+ }
949
+ }
950
+ function validatePermissionKeys(config, options, _errors, warnings) {
951
+ const validKeys = new Set([...CRUD_OPERATIONS, ...options.additionalPermissionKeys ?? []]);
952
+ for (const route of config.routes ?? []) if (typeof route.handler === "string") validKeys.add(route.handler);
953
+ for (const preset of config.presets ?? []) {
954
+ const presetName = typeof preset === "string" ? preset : preset.name;
955
+ if (presetName === "softDelete") {
956
+ validKeys.add("deleted");
957
+ validKeys.add("restore");
958
+ }
959
+ if (presetName === "slugLookup") validKeys.add("getBySlug");
960
+ if (presetName === "tree") {
961
+ validKeys.add("tree");
962
+ validKeys.add("children");
963
+ validKeys.add("getTree");
964
+ validKeys.add("getChildren");
965
+ }
966
+ }
967
+ for (const key of Object.keys(config.permissions ?? {})) if (!validKeys.has(key)) warnings.push({
968
+ field: `permissions.${key}`,
969
+ message: `Unknown permission key "${key}"`,
970
+ suggestion: `Valid keys: ${Array.from(validKeys).join(", ")}`
971
+ });
972
+ }
973
+ function validatePresets(presets, errors, warnings) {
974
+ const availablePresets = getAvailablePresets();
975
+ for (const preset of presets) {
976
+ if (typeof preset === "object" && ("middlewares" in preset || "routes" in preset)) continue;
977
+ const presetName = typeof preset === "string" ? preset : preset.name;
978
+ if (!availablePresets.includes(presetName)) errors.push({
979
+ field: "presets",
980
+ message: `Unknown preset "${presetName}"`,
981
+ suggestion: `Available presets: ${availablePresets.join(", ")}`
982
+ });
983
+ if (typeof preset === "object") validatePresetOptions(preset, warnings);
984
+ }
985
+ }
986
+ function validatePresetOptions(preset, warnings) {
987
+ const validOptions = {
988
+ slugLookup: ["slugField"],
989
+ tree: ["parentField"],
990
+ softDelete: ["deletedField"],
991
+ ownedByUser: ["ownerField"],
992
+ multiTenant: ["tenantField", "allowPublic"]
993
+ }[preset.name] ?? [];
994
+ const providedOptions = Object.keys(preset).filter((k) => k !== "name");
995
+ for (const opt of providedOptions) if (!validOptions.includes(opt)) warnings.push({
996
+ field: `presets[${preset.name}].${opt}`,
997
+ message: `Unknown option "${opt}" for preset "${preset.name}"`,
998
+ suggestion: validOptions.length > 0 ? `Valid options: ${validOptions.join(", ")}` : `Preset "${preset.name}" has no configurable options`
999
+ });
1000
+ }
1001
+ function validateRoutes(routes, errors) {
1002
+ const validMethods = [
1003
+ "GET",
1004
+ "POST",
1005
+ "PUT",
1006
+ "PATCH",
1007
+ "DELETE",
1008
+ "OPTIONS",
1009
+ "HEAD"
1010
+ ];
1011
+ const seenRoutes = /* @__PURE__ */ new Set();
1012
+ for (const [i, route] of routes.entries()) {
1013
+ if (!validMethods.includes(route.method)) errors.push({
1014
+ field: `routes[${i}].method`,
1015
+ message: `Invalid HTTP method "${route.method}"`,
1016
+ suggestion: `Valid methods: ${validMethods.join(", ")}`
1017
+ });
1018
+ if (!route.path) errors.push({
1019
+ field: `routes[${i}].path`,
1020
+ message: "Route path is required"
1021
+ });
1022
+ else if (!route.path.startsWith("/")) errors.push({
1023
+ field: `routes[${i}].path`,
1024
+ message: `Route path must start with "/" (got "${route.path}")`,
1025
+ suggestion: `Change to "/${route.path}"`
1026
+ });
1027
+ if (!route.handler) errors.push({
1028
+ field: `routes[${i}].handler`,
1029
+ message: "Route handler is required"
1030
+ });
1031
+ const routeKey = `${route.method} ${route.path}`;
1032
+ if (seenRoutes.has(routeKey)) errors.push({
1033
+ field: `routes[${i}]`,
1034
+ message: `Duplicate route "${routeKey}"`
1035
+ });
1036
+ seenRoutes.add(routeKey);
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Format validation errors for display
1041
+ */
1042
+ function formatValidationErrors(resourceName, result) {
1043
+ const lines = [];
1044
+ if (result.errors.length > 0) {
1045
+ lines.push(`Resource "${resourceName}" validation failed:`);
1046
+ lines.push("");
1047
+ lines.push("ERRORS:");
1048
+ for (const err of result.errors) {
1049
+ lines.push(` ✗ ${err.field}: ${err.message}`);
1050
+ if (err.suggestion) lines.push(` → ${err.suggestion}`);
1051
+ }
1052
+ }
1053
+ if (result.warnings.length > 0) {
1054
+ if (lines.length > 0) lines.push("");
1055
+ lines.push("WARNINGS:");
1056
+ for (const warn of result.warnings) {
1057
+ lines.push(` ⚠ ${warn.field}: ${warn.message}`);
1058
+ if (warn.suggestion) lines.push(` → ${warn.suggestion}`);
1059
+ }
1060
+ }
1061
+ return lines.join("\n");
1062
+ }
1063
+ /**
1064
+ * Validate and throw if invalid
1065
+ */
1066
+ function assertValidConfig(config, options) {
1067
+ const result = validateResourceConfig(config, options);
1068
+ if (!result.valid) {
1069
+ const errorMsg = formatValidationErrors(config.name ?? "unknown", result);
1070
+ throw new Error(errorMsg);
1071
+ }
1072
+ }
1073
+ //#endregion
1074
+ //#region src/utils/circuitBreaker.ts
1075
+ /**
1076
+ * Circuit Breaker Pattern
1077
+ *
1078
+ * Wraps external service calls with failure protection.
1079
+ * Prevents cascading failures by "opening" the circuit when
1080
+ * a service is failing, allowing it time to recover.
1081
+ *
1082
+ * States:
1083
+ * - CLOSED: Normal operation, requests pass through
1084
+ * - OPEN: Too many failures, all requests fail fast
1085
+ * - HALF_OPEN: Testing if service recovered, limited requests
1086
+ *
1087
+ * @example
1088
+ * import { CircuitBreaker } from '@classytic/arc/utils';
1089
+ *
1090
+ * const paymentBreaker = new CircuitBreaker(async (amount) => {
1091
+ * return await stripe.charges.create({ amount });
1092
+ * }, {
1093
+ * failureThreshold: 5,
1094
+ * resetTimeout: 30000,
1095
+ * timeout: 5000,
1096
+ * });
1097
+ *
1098
+ * try {
1099
+ * const result = await paymentBreaker.call(100);
1100
+ * } catch (error) {
1101
+ * // Handle failure or circuit open
1102
+ * }
1103
+ */
1104
+ const CircuitState = {
1105
+ CLOSED: "CLOSED",
1106
+ OPEN: "OPEN",
1107
+ HALF_OPEN: "HALF_OPEN"
1108
+ };
1109
+ var CircuitBreakerError = class extends Error {
1110
+ state;
1111
+ constructor(message, state) {
1112
+ super(message);
1113
+ this.name = "CircuitBreakerError";
1114
+ this.state = state;
1115
+ }
1116
+ };
1117
+ var CircuitBreaker = class {
1118
+ state = CircuitState.CLOSED;
1119
+ failures = 0;
1120
+ successes = 0;
1121
+ totalCalls = 0;
1122
+ nextAttempt = 0;
1123
+ lastCallAt = null;
1124
+ openedAt = null;
1125
+ failureThreshold;
1126
+ resetTimeout;
1127
+ timeout;
1128
+ successThreshold;
1129
+ fallback;
1130
+ onStateChange;
1131
+ onError;
1132
+ name;
1133
+ fn;
1134
+ constructor(fn, options = {}) {
1135
+ this.fn = fn;
1136
+ this.failureThreshold = options.failureThreshold ?? 5;
1137
+ this.resetTimeout = options.resetTimeout ?? 6e4;
1138
+ this.timeout = options.timeout ?? 1e4;
1139
+ this.successThreshold = options.successThreshold ?? 1;
1140
+ this.fallback = options.fallback;
1141
+ this.onStateChange = options.onStateChange;
1142
+ this.onError = options.onError;
1143
+ this.name = options.name ?? "CircuitBreaker";
1144
+ }
1145
+ /**
1146
+ * Call the wrapped function with circuit breaker protection
1147
+ */
1148
+ async call(...args) {
1149
+ this.totalCalls++;
1150
+ this.lastCallAt = Date.now();
1151
+ if (this.state === CircuitState.OPEN) {
1152
+ if (Date.now() < this.nextAttempt) {
1153
+ const error = new CircuitBreakerError(`Circuit breaker is OPEN for ${this.name}`, CircuitState.OPEN);
1154
+ if (this.fallback) return this.fallback(...args);
1155
+ throw error;
1156
+ }
1157
+ this.setState(CircuitState.HALF_OPEN);
1158
+ }
1159
+ try {
1160
+ const result = await this.executeWithTimeout(args);
1161
+ this.onSuccess();
1162
+ return result;
1163
+ } catch (err) {
1164
+ this.onFailure(err instanceof Error ? err : new Error(String(err)));
1165
+ throw err;
1166
+ }
1167
+ }
1168
+ /**
1169
+ * Execute function with timeout
1170
+ */
1171
+ async executeWithTimeout(args) {
1172
+ return new Promise((resolve, reject) => {
1173
+ const timeoutId = setTimeout(() => {
1174
+ reject(/* @__PURE__ */ new Error(`Request timeout after ${this.timeout}ms`));
1175
+ }, this.timeout);
1176
+ this.fn(...args).then((result) => {
1177
+ clearTimeout(timeoutId);
1178
+ resolve(result);
1179
+ }).catch((error) => {
1180
+ clearTimeout(timeoutId);
1181
+ reject(error);
1182
+ });
1183
+ });
1184
+ }
1185
+ /**
1186
+ * Handle successful call
1187
+ */
1188
+ onSuccess() {
1189
+ this.failures = 0;
1190
+ this.successes++;
1191
+ if (this.state === CircuitState.HALF_OPEN) {
1192
+ if (this.successes >= this.successThreshold) {
1193
+ this.setState(CircuitState.CLOSED);
1194
+ this.successes = 0;
1195
+ }
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Handle failed call
1200
+ */
1201
+ onFailure(error) {
1202
+ this.failures++;
1203
+ this.successes = 0;
1204
+ if (this.onError) this.onError(error);
1205
+ if (this.state === CircuitState.HALF_OPEN || this.failures >= this.failureThreshold) {
1206
+ this.setState(CircuitState.OPEN);
1207
+ this.nextAttempt = Date.now() + this.resetTimeout;
1208
+ this.openedAt = Date.now();
1209
+ }
1210
+ }
1211
+ /**
1212
+ * Change circuit state
1213
+ */
1214
+ setState(newState) {
1215
+ const oldState = this.state;
1216
+ if (oldState !== newState) {
1217
+ this.state = newState;
1218
+ if (this.onStateChange) this.onStateChange(oldState, newState);
1219
+ }
1220
+ }
1221
+ /**
1222
+ * Manually open the circuit
1223
+ */
1224
+ open() {
1225
+ this.setState(CircuitState.OPEN);
1226
+ this.nextAttempt = Date.now() + this.resetTimeout;
1227
+ this.openedAt = Date.now();
1228
+ }
1229
+ /**
1230
+ * Manually close the circuit
1231
+ */
1232
+ close() {
1233
+ this.failures = 0;
1234
+ this.successes = 0;
1235
+ this.setState(CircuitState.CLOSED);
1236
+ this.openedAt = null;
1237
+ }
1238
+ /**
1239
+ * Get current statistics
1240
+ */
1241
+ getStats() {
1242
+ return {
1243
+ name: this.name,
1244
+ state: this.state,
1245
+ failures: this.failures,
1246
+ successes: this.successes,
1247
+ totalCalls: this.totalCalls,
1248
+ openedAt: this.openedAt,
1249
+ lastCallAt: this.lastCallAt
1250
+ };
1251
+ }
1252
+ /**
1253
+ * Get current state
1254
+ */
1255
+ getState() {
1256
+ return this.state;
1257
+ }
1258
+ /**
1259
+ * Check if circuit is open
1260
+ */
1261
+ isOpen() {
1262
+ return this.state === CircuitState.OPEN;
1263
+ }
1264
+ /**
1265
+ * Check if circuit is closed
1266
+ */
1267
+ isClosed() {
1268
+ return this.state === CircuitState.CLOSED;
1269
+ }
1270
+ /**
1271
+ * Reset statistics
1272
+ */
1273
+ reset() {
1274
+ this.failures = 0;
1275
+ this.successes = 0;
1276
+ this.totalCalls = 0;
1277
+ this.lastCallAt = null;
1278
+ this.openedAt = null;
1279
+ this.setState(CircuitState.CLOSED);
1280
+ }
1281
+ };
1282
+ /**
1283
+ * Create a circuit breaker with sensible defaults
1284
+ *
1285
+ * @example
1286
+ * const emailBreaker = createCircuitBreaker(
1287
+ * async (to, subject, body) => sendEmail(to, subject, body),
1288
+ * { name: 'email-service' }
1289
+ * );
1290
+ */
1291
+ function createCircuitBreaker(fn, options) {
1292
+ return new CircuitBreaker(fn, options);
1293
+ }
1294
+ /**
1295
+ * Circuit breaker registry for managing multiple breakers
1296
+ */
1297
+ var CircuitBreakerRegistry = class {
1298
+ breakers = /* @__PURE__ */ new Map();
1299
+ /**
1300
+ * Register a circuit breaker
1301
+ */
1302
+ register(name, fn, options) {
1303
+ const breaker = new CircuitBreaker(fn, {
1304
+ ...options,
1305
+ name
1306
+ });
1307
+ this.breakers.set(name, breaker);
1308
+ return breaker;
1309
+ }
1310
+ /**
1311
+ * Get a circuit breaker by name
1312
+ */
1313
+ get(name) {
1314
+ return this.breakers.get(name);
1315
+ }
1316
+ /**
1317
+ * Get all breakers
1318
+ */
1319
+ getAll() {
1320
+ return this.breakers;
1321
+ }
1322
+ /**
1323
+ * Get statistics for all breakers
1324
+ */
1325
+ getAllStats() {
1326
+ const stats = {};
1327
+ for (const [name, breaker] of this.breakers.entries()) stats[name] = breaker.getStats();
1328
+ return stats;
1329
+ }
1330
+ /**
1331
+ * Reset all breakers
1332
+ */
1333
+ resetAll() {
1334
+ for (const breaker of this.breakers.values()) breaker.reset();
1335
+ }
1336
+ /**
1337
+ * Open all breakers
1338
+ */
1339
+ openAll() {
1340
+ for (const breaker of this.breakers.values()) breaker.open();
1341
+ }
1342
+ /**
1343
+ * Close all breakers
1344
+ */
1345
+ closeAll() {
1346
+ for (const breaker of this.breakers.values()) breaker.close();
1347
+ }
1348
+ };
1349
+ /**
1350
+ * Create a new CircuitBreakerRegistry instance.
1351
+ * Use this instead of a global singleton — attach to fastify.arc or pass explicitly.
1352
+ */
1353
+ function createCircuitBreakerRegistry() {
1354
+ return new CircuitBreakerRegistry();
1355
+ }
1356
+ //#endregion
1357
+ //#region src/utils/compensation.ts
1358
+ /**
1359
+ * Run steps in order with automatic compensation on failure.
1360
+ *
1361
+ * @typeParam TCtx - Context type shared across steps (defaults to Record<string, unknown>)
1362
+ */
1363
+ async function withCompensation(_name, steps, initialContext, hooks) {
1364
+ const ctx = { ...initialContext };
1365
+ const completedSteps = [];
1366
+ const results = {};
1367
+ const completed = [];
1368
+ for (const step of steps) {
1369
+ if (step.fireAndForget) {
1370
+ completedSteps.push(step.name);
1371
+ step.execute(ctx).then((result) => hooks?.onStepComplete?.(step.name, result), () => {});
1372
+ continue;
1373
+ }
1374
+ try {
1375
+ const result = await step.execute(ctx);
1376
+ completedSteps.push(step.name);
1377
+ results[step.name] = result;
1378
+ completed.push({
1379
+ step,
1380
+ result
1381
+ });
1382
+ hooks?.onStepComplete?.(step.name, result);
1383
+ } catch (err) {
1384
+ const error = err instanceof Error ? err : new Error(String(err));
1385
+ hooks?.onStepFailed?.(step.name, error);
1386
+ const compensationErrors = await rollback(ctx, completed, hooks);
1387
+ return {
1388
+ success: false,
1389
+ completedSteps,
1390
+ results,
1391
+ failedStep: step.name,
1392
+ error: error.message,
1393
+ ...compensationErrors.length > 0 ? { compensationErrors } : {}
1394
+ };
1395
+ }
1396
+ }
1397
+ return {
1398
+ success: true,
1399
+ completedSteps,
1400
+ results
1401
+ };
1402
+ }
1403
+ async function rollback(ctx, completed, hooks) {
1404
+ const errors = [];
1405
+ for (let i = completed.length - 1; i >= 0; i--) {
1406
+ const entry = completed[i];
1407
+ if (!entry?.step.compensate) continue;
1408
+ const compensateFn = entry.step.compensate;
1409
+ try {
1410
+ await compensateFn(ctx, entry.result);
1411
+ hooks?.onCompensate?.(entry.step.name);
1412
+ } catch (err) {
1413
+ errors.push({
1414
+ step: entry.step.name,
1415
+ error: err instanceof Error ? err.message : String(err)
1416
+ });
1417
+ }
1418
+ }
1419
+ return errors;
1420
+ }
1421
+ function defineCompensation(name, steps) {
1422
+ return {
1423
+ name,
1424
+ execute: (initialContext, hooks) => withCompensation(name, steps, initialContext, hooks)
1425
+ };
1426
+ }
1427
+ //#endregion
1428
+ //#region src/utils/defineErrorMapper.ts
1429
+ /**
1430
+ * Register an `ErrorMapper` with its domain-specific generic argument and
1431
+ * have it assign cleanly into `ErrorMapper[]` (no `as unknown as ErrorMapper`).
1432
+ *
1433
+ * The returned mapper is identical at runtime — `type` and `toResponse` are
1434
+ * passed through untouched. Only the declared type widens from
1435
+ * `ErrorMapper<T>` to `ErrorMapper` so the array inference works.
1436
+ *
1437
+ * Safety: the `errorHandlerPlugin` dispatches via `error instanceof mapper.type`
1438
+ * before invoking `toResponse`, so the widened callback signature is never
1439
+ * called with a non-`T` error at runtime. This helper codifies that invariant
1440
+ * in one place.
1441
+ */
1442
+ function defineErrorMapper(mapper) {
1443
+ return mapper;
1444
+ }
1445
+ //#endregion
1446
+ //#region src/utils/defineGuard.ts
1447
+ /** Hidden property key for guard context storage on the request object. */
1448
+ const GUARD_STORE_KEY = "__arcGuardContext";
1449
+ /**
1450
+ * Create a typed guard. See module JSDoc for usage.
1451
+ */
1452
+ function defineGuard(config) {
1453
+ const { name, resolve } = config;
1454
+ const preHandler = async (req, reply) => {
1455
+ const ctx = await resolve(req, reply);
1456
+ if (!reply.sent) {
1457
+ const store = req[GUARD_STORE_KEY] ?? {};
1458
+ store[name] = ctx;
1459
+ req[GUARD_STORE_KEY] = store;
1460
+ }
1461
+ };
1462
+ return {
1463
+ preHandler,
1464
+ name,
1465
+ from(req) {
1466
+ const store = req[GUARD_STORE_KEY];
1467
+ 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.`);
1468
+ return store[name];
1469
+ }
1470
+ };
1471
+ }
1472
+ //#endregion
1473
+ //#region src/utils/envelope.ts
1474
+ /**
1475
+ * Standard response envelope helper.
1476
+ *
1477
+ * Wraps a handler return value in arc's `{ success: true, data, ...meta }`
1478
+ * shape. Pure utility — no types module coupling, no runtime dependencies.
1479
+ *
1480
+ * @example
1481
+ * ```ts
1482
+ * import { envelope } from '@classytic/arc';
1483
+ *
1484
+ * handler: async (req, reply) => {
1485
+ * const results = await search(req.query.q);
1486
+ * return envelope(results, { took: performance.now() - t0 });
1487
+ * }
1488
+ * ```
1489
+ */
1490
+ /**
1491
+ * Wrap data in arc's standard `{ success: true, data }` envelope, with
1492
+ * optional top-level meta keys merged in.
1493
+ */
1494
+ function envelope(data, meta) {
1495
+ return {
1496
+ success: true,
1497
+ data,
1498
+ ...meta
1499
+ };
1500
+ }
1501
+ //#endregion
1502
+ //#region src/utils/handleRaw.ts
1503
+ /**
1504
+ * Wrap a raw Fastify handler with Arc's response envelope and error handling.
1505
+ *
1506
+ * @param handler - Async function that receives `(request, reply)` and returns data.
1507
+ * The return value is sent as `{ success: true, data }`. If it returns
1508
+ * `undefined` or `null`, `{ success: true }` is sent (no `data` field).
1509
+ * @param statusCode - HTTP status code for successful responses (default: 200)
1510
+ */
1511
+ function handleRaw(handler, statusCode = 200) {
1512
+ return async (request, reply) => {
1513
+ try {
1514
+ const result = await handler(request, reply);
1515
+ if (reply.sent) return;
1516
+ if (result === void 0 || result === null) reply.code(statusCode).send({ success: true });
1517
+ else reply.code(statusCode).send({
1518
+ success: true,
1519
+ data: result
1520
+ });
1521
+ } catch (err) {
1522
+ if (reply.sent) return;
1523
+ if (err instanceof ArcError) {
1524
+ reply.code(err.statusCode).send(err.toJSON());
1525
+ return;
1526
+ }
1527
+ const error = err;
1528
+ const code = error.statusCode ?? error.status ?? 500;
1529
+ reply.code(code).send({
1530
+ success: false,
1531
+ error: error.message ?? "Internal server error",
1532
+ ...error.code && { code: error.code }
1533
+ });
1534
+ }
1535
+ };
1536
+ }
1537
+ //#endregion
1538
+ //#region src/utils/stateMachine.ts
1539
+ /**
1540
+ * Create a state machine for validating transitions
1541
+ *
1542
+ * @param name - Name of the state machine (used in error messages)
1543
+ * @param transitions - Map of actions to allowed source statuses
1544
+ * @param options - Additional options (history, guards, actions)
1545
+ * @returns State machine with can() and assert() methods
1546
+ *
1547
+ * @example
1548
+ * // Basic usage
1549
+ * const transferState = createStateMachine('Transfer', {
1550
+ * approve: ['draft'],
1551
+ * dispatch: ['approved'],
1552
+ * receive: ['dispatched', 'in_transit'],
1553
+ * cancel: ['draft', 'approved'],
1554
+ * });
1555
+ *
1556
+ * @example
1557
+ * // With guards and actions
1558
+ * const orderState = createStateMachine('Order', {
1559
+ * approve: {
1560
+ * from: ['pending'],
1561
+ * to: 'approved',
1562
+ * guard: ({ data }) => data.paymentConfirmed,
1563
+ * before: ({ from, to }) => console.log(`Approving order from ${from} to ${to}`),
1564
+ * after: ({ data }) => sendApprovalEmail(data.customerId),
1565
+ * },
1566
+ * }, { trackHistory: true });
1567
+ */
1568
+ function createStateMachine(name, transitions = {}, options = {}) {
1569
+ const normalized = /* @__PURE__ */ new Map();
1570
+ const history = options.trackHistory ? [] : void 0;
1571
+ Object.entries(transitions).forEach(([action, allowed]) => {
1572
+ if (Array.isArray(allowed)) normalized.set(action, { from: new Set(allowed) });
1573
+ else if (typeof allowed === "object" && "from" in allowed) normalized.set(action, {
1574
+ from: new Set(Array.isArray(allowed.from) ? allowed.from : [allowed.from]),
1575
+ to: allowed.to,
1576
+ guard: allowed.guard,
1577
+ before: allowed.before,
1578
+ after: allowed.after
1579
+ });
1580
+ });
1581
+ const can = (action, status) => {
1582
+ const transition = normalized.get(action);
1583
+ if (!transition || !status) return false;
1584
+ return transition.from.has(status);
1585
+ };
1586
+ const canAsync = async (action, status, context) => {
1587
+ const transition = normalized.get(action);
1588
+ if (!transition || !status) return false;
1589
+ if (!transition.from.has(status)) return false;
1590
+ if (transition.guard) try {
1591
+ return await transition.guard({
1592
+ from: status,
1593
+ to: transition.to || "",
1594
+ action,
1595
+ data: context
1596
+ });
1597
+ } catch {
1598
+ return false;
1599
+ }
1600
+ return true;
1601
+ };
1602
+ const assert = (action, status, errorFactory, message) => {
1603
+ if (can(action, status)) return;
1604
+ const errorMessage = message || `${name} cannot '${action}' when status is '${status || "unknown"}'`;
1605
+ if (typeof errorFactory === "function") throw errorFactory(errorMessage);
1606
+ throw new Error(errorMessage);
1607
+ };
1608
+ const recordTransition = (from, to, action, metadata) => {
1609
+ if (history) history.push({
1610
+ from,
1611
+ to,
1612
+ action,
1613
+ timestamp: /* @__PURE__ */ new Date(),
1614
+ metadata
1615
+ });
1616
+ };
1617
+ const getHistory = () => {
1618
+ return history ? [...history] : [];
1619
+ };
1620
+ const clearHistory = () => {
1621
+ if (history) history.length = 0;
1622
+ };
1623
+ const getAvailableActions = (status) => {
1624
+ const actions = [];
1625
+ for (const [action, transition] of normalized.entries()) if (transition.from.has(status)) actions.push(action);
1626
+ return actions;
1627
+ };
1628
+ return {
1629
+ can,
1630
+ canAsync,
1631
+ assert,
1632
+ recordTransition,
1633
+ getHistory,
1634
+ clearHistory,
1635
+ getAvailableActions
1636
+ };
1637
+ }
1638
+ //#endregion
1639
+ export { createQueryParser as A, mutationResponse as C, successResponseSchema as D, responses as E, simpleEqualityMatcher as M, wrapResponse as O, listResponse as S, queryParams as T, deleteResponse as _, defineErrorMapper as a, getListQueryParams as b, CircuitBreaker as c, CircuitState as d, createCircuitBreaker as f, validateResourceConfig as g, formatValidationErrors as h, defineGuard as i, getUserId as j, ArcQueryParser as k, CircuitBreakerError as l, assertValidConfig as m, handleRaw as n, defineCompensation as o, createCircuitBreakerRegistry as p, envelope as r, withCompensation as s, createStateMachine as t, CircuitBreakerRegistry as u, errorResponseSchema as v, paginationSchema as w, itemResponse as x, getDefaultCrudSchemas as y };