@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.
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-P1d6rjPy.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 +2 -2
  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-C_bgx9o4.d.mts} +712 -500
  39. package/dist/{index-BziRPS4H.d.mts → index-CvM1e09j.d.mts} +29 -10
  40. package/dist/{index-EqQN6p0W.d.mts → index-pUczGjO0.d.mts} +11 -8
  41. package/dist/index-smCAoA5W.d.mts +1179 -0
  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-CPpkyKfM.mjs} +32 -8
  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-BdA4uMBV.d.mts} +191 -28
  97. package/dist/{types-CVKBssX5.d.mts → types-Bh_gEJBi.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 +124 -39
  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
@@ -1,419 +0,0 @@
1
- import { m as RESERVED_QUERY_PARAMS } from "./constants-BhY1OHoH.mjs";
2
- //#region src/utils/simpleEqualityMatcher.ts
3
- /**
4
- * `simpleEqualityMatcher` — a minimal, dialect-agnostic flat-key equality
5
- * matcher for `DataAdapter.matchesFilter` / `BaseController({ matchesFilter })`.
6
- *
7
- * **What it does:** for each `[key, expected]` in the filter, compares
8
- * `item[key]` to `expected` via string coercion (so Mongo `ObjectId` values
9
- * match their string representation) and returns `true` only if every
10
- * filter entry matches. Array item values are matched implicitly (contains).
11
- *
12
- * **What it does NOT do:**
13
- * - No `$eq` / `$ne` / `$in` / `$nin` / `$gt` / `$lt` / `$regex` / `$exists`
14
- * - No `$and` / `$or`
15
- * - No dot-path traversal (`"owner.id"`)
16
- * - No schema-specific coercion
17
- *
18
- * **Why it exists:** 95%+ of arc's `_policyFilters` are produced by built-in
19
- * permission helpers and are shaped like `{ ownerId: "u1" }` or
20
- * `{ organizationId: "org_x" }` — flat equality. For that common shape,
21
- * this helper is a safe, tested, 15-line defense-in-depth matcher that
22
- * hosts using minimal repos (no `getOne(compoundFilter)` DB path) can opt
23
- * into without arc shipping a full Mongo-syntax engine.
24
- *
25
- * **When to use:**
26
- * - Your adapter/repo doesn't natively filter on `getOne(compoundFilter)`
27
- * - Your `_policyFilters` are flat equality (from arc's built-in permission helpers)
28
- * - You want defense-in-depth on `validateItemAccess` / `fetchDetailed`'s `getById` fallback
29
- *
30
- * **When NOT to use:**
31
- * - Your `_policyFilters` use operators (`$in`, `$ne`, etc.) — supply a
32
- * native matcher (mongokit's repo does the filter at the DB layer; for
33
- * custom repos, wrap the kit's own predicate engine).
34
- * - You're a mongokit / sqlitekit / Prisma user — the DB-level filter
35
- * applied by `getOne(compoundFilter)` already covers this.
36
- *
37
- * @example
38
- * ```ts
39
- * import { simpleEqualityMatcher } from '@classytic/arc/utils';
40
- *
41
- * // On a custom adapter
42
- * const adapter: DataAdapter = {
43
- * repository,
44
- * type: 'custom',
45
- * name: 'in-memory',
46
- * matchesFilter: simpleEqualityMatcher,
47
- * };
48
- *
49
- * // Or directly on BaseController for ad-hoc controllers
50
- * new BaseController(repo, { matchesFilter: simpleEqualityMatcher });
51
- * ```
52
- */
53
- function simpleEqualityMatcher(item, filters) {
54
- if (!item || typeof item !== "object") return false;
55
- const obj = item;
56
- for (const [key, expected] of Object.entries(filters)) {
57
- if (expected && typeof expected === "object" && !Array.isArray(expected) && Object.getPrototypeOf(expected) === Object.prototype && Object.keys(expected).some((k) => k.startsWith("$"))) return false;
58
- const actual = obj[key];
59
- if (Array.isArray(actual)) {
60
- const expectedStr = String(expected);
61
- if (!actual.some((v) => String(v) === expectedStr)) return false;
62
- continue;
63
- }
64
- if (String(actual) !== String(expected)) return false;
65
- }
66
- return true;
67
- }
68
- //#endregion
69
- //#region src/utils/queryParser.ts
70
- /**
71
- * Arc Query Parser - Default URL-to-Query Parser
72
- *
73
- * Framework-agnostic query parser that converts URL parameters to query options.
74
- * This is Arc's built-in parser; users can swap in MongoKit's QueryParser,
75
- * pgkit's parser, or any custom parser implementing QueryParserInterface.
76
- *
77
- * @example
78
- * // Use Arc default parser (auto-applied if no queryParser option)
79
- * defineResource({ name: 'product', adapter: ... });
80
- *
81
- * // Use MongoKit's QueryParser (recommended for MongoDB - has $lookup, aggregations, etc.)
82
- * import { QueryParser } from '@classytic/mongokit';
83
- * defineResource({
84
- * name: 'product',
85
- * adapter: ...,
86
- * queryParser: new QueryParser(),
87
- * });
88
- *
89
- * // Use custom parser for SQL databases
90
- * defineResource({
91
- * name: 'user',
92
- * adapter: ...,
93
- * queryParser: new PgQueryParser(),
94
- * });
95
- */
96
- /**
97
- * Regex patterns that can cause catastrophic backtracking (ReDoS attacks)
98
- * Detects:
99
- * - Quantifiers: {n,m}
100
- * - Possessive quantifiers: *+, ++, ?+
101
- * - Nested quantifiers: (a+)+, (a*)*
102
- * - Backreferences: \1, \2, etc.
103
- */
104
- const DANGEROUS_REGEX_PATTERNS = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\(.+\))\+|\(\?:|\\[0-9]|(\[.+\]).+(\[.+\]))/;
105
- /**
106
- * Arc's default query parser
107
- *
108
- * Converts URL query parameters to a structured query format:
109
- * - Pagination: ?page=1&limit=20
110
- * - Sorting: ?sort=-createdAt,name (- prefix = descending)
111
- * - Filtering: ?status=active&price[gte]=100&price[lte]=500
112
- * - Search: ?search=keyword
113
- * - Populate: ?populate=author,category
114
- * - Field selection: ?select=name,price,status
115
- * - Keyset pagination: ?after=cursor_value
116
- *
117
- * For advanced MongoDB features ($lookup, aggregations), use MongoKit's QueryParser.
118
- */
119
- var ArcQueryParser = class {
120
- maxLimit;
121
- defaultLimit;
122
- maxRegexLength;
123
- maxSearchLength;
124
- maxFilterDepth;
125
- _allowedFilterFields;
126
- _allowedSortFields;
127
- _allowedOperators;
128
- /** Allowed filter fields (used by MCP for auto-derive) */
129
- allowedFilterFields;
130
- /** Allowed sort fields (used by MCP for sort descriptions) */
131
- allowedSortFields;
132
- /** Allowed operators (used by MCP for operator descriptions) */
133
- allowedOperators;
134
- /** Supported filter operators */
135
- operators = {
136
- eq: "$eq",
137
- ne: "$ne",
138
- gt: "$gt",
139
- gte: "$gte",
140
- lt: "$lt",
141
- lte: "$lte",
142
- in: "$in",
143
- nin: "$nin",
144
- like: "$regex",
145
- contains: "$regex",
146
- regex: "$regex",
147
- exists: "$exists"
148
- };
149
- constructor(options = {}) {
150
- this.maxLimit = options.maxLimit ?? 1e3;
151
- this.defaultLimit = options.defaultLimit ?? 20;
152
- this.maxRegexLength = options.maxRegexLength ?? 200;
153
- this.maxSearchLength = options.maxSearchLength ?? 200;
154
- this.maxFilterDepth = options.maxFilterDepth ?? 10;
155
- if (options.allowedFilterFields) {
156
- this._allowedFilterFields = new Set(options.allowedFilterFields);
157
- this.allowedFilterFields = options.allowedFilterFields;
158
- }
159
- if (options.allowedSortFields) {
160
- this._allowedSortFields = new Set(options.allowedSortFields);
161
- this.allowedSortFields = options.allowedSortFields;
162
- }
163
- if (options.allowedOperators) {
164
- this._allowedOperators = new Set(options.allowedOperators);
165
- this.allowedOperators = options.allowedOperators;
166
- }
167
- }
168
- /**
169
- * Parse URL query parameters into structured query options
170
- */
171
- parse(query) {
172
- const q = query ?? {};
173
- const page = this.parseNumber(q.page, 1);
174
- const limit = Math.min(this.parseNumber(q.limit, this.defaultLimit), this.maxLimit);
175
- const after = this.parseString(q.after ?? q.cursor);
176
- const sort = this.parseSort(q.sort);
177
- const { populate, populateOptions } = this.parsePopulate(q.populate);
178
- const search = this.parseSearch(q.search);
179
- const select = this.parseSelect(q.select);
180
- return {
181
- filters: this.parseFilters(q),
182
- limit,
183
- sort,
184
- populate,
185
- populateOptions,
186
- search,
187
- page: after ? void 0 : page,
188
- after,
189
- select
190
- };
191
- }
192
- parseNumber(value, defaultValue) {
193
- if (value === void 0 || value === null) return defaultValue;
194
- const num = parseInt(String(value), 10);
195
- return Number.isNaN(num) ? defaultValue : Math.max(1, num);
196
- }
197
- parseString(value) {
198
- if (value === void 0 || value === null) return void 0;
199
- const str = String(value).trim();
200
- return str.length > 0 ? str : void 0;
201
- }
202
- /**
203
- * Parse populate parameter — handles both simple string and bracket notation.
204
- *
205
- * Simple: ?populate=author,category → { populate: 'author,category' }
206
- * Bracket: ?populate[author][select]=name,email → { populateOptions: [{ path: 'author', select: 'name email' }] }
207
- */
208
- parsePopulate(value) {
209
- if (value === void 0 || value === null) return {};
210
- if (typeof value === "string") {
211
- const trimmed = value.trim();
212
- return trimmed.length > 0 ? { populate: trimmed } : {};
213
- }
214
- if (typeof value === "object" && !Array.isArray(value)) {
215
- const obj = value;
216
- const keys = Object.keys(obj);
217
- if (keys.length === 0) return {};
218
- const options = [];
219
- for (const path of keys) {
220
- if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(path)) continue;
221
- const config = obj[path];
222
- if (typeof config === "object" && config !== null && !Array.isArray(config)) {
223
- const cfg = config;
224
- const option = { path };
225
- if (typeof cfg.select === "string") option.select = cfg.select.split(",").map((s) => s.trim()).filter(Boolean).join(" ");
226
- if (typeof cfg.match === "object" && cfg.match !== null) option.match = cfg.match;
227
- options.push(option);
228
- } else options.push({ path });
229
- }
230
- return options.length > 0 ? { populateOptions: options } : {};
231
- }
232
- return {};
233
- }
234
- parseSort(value) {
235
- if (!value) return void 0;
236
- const sortStr = String(value);
237
- const result = {};
238
- for (const field of sortStr.split(",")) {
239
- const trimmed = field.trim();
240
- if (!trimmed) continue;
241
- if (!/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
242
- const fieldName = trimmed.startsWith("-") ? trimmed.slice(1) : trimmed;
243
- if (this._allowedSortFields && !this._allowedSortFields.has(fieldName)) continue;
244
- if (trimmed.startsWith("-")) result[fieldName] = -1;
245
- else result[fieldName] = 1;
246
- }
247
- return Object.keys(result).length > 0 ? result : void 0;
248
- }
249
- parseSearch(value) {
250
- if (!value) return void 0;
251
- const search = String(value).trim();
252
- if (search.length === 0) return void 0;
253
- if (search.length > this.maxSearchLength) return search.slice(0, this.maxSearchLength);
254
- return search;
255
- }
256
- parseSelect(value) {
257
- if (!value) return void 0;
258
- const selectStr = String(value);
259
- const result = {};
260
- for (const field of selectStr.split(",")) {
261
- const trimmed = field.trim();
262
- if (!trimmed) continue;
263
- if (!/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
264
- if (trimmed.startsWith("-")) result[trimmed.slice(1)] = 0;
265
- else result[trimmed] = 1;
266
- }
267
- return Object.keys(result).length > 0 ? result : void 0;
268
- }
269
- /**
270
- * Check if a value exceeds the maximum nesting depth.
271
- * Prevents filter bombs where deeply nested objects consume excessive memory/CPU.
272
- */
273
- exceedsDepth(obj, currentDepth = 0) {
274
- if (currentDepth > this.maxFilterDepth) return true;
275
- if (obj === null || obj === void 0) return false;
276
- if (Array.isArray(obj)) return obj.some((v) => this.exceedsDepth(v, currentDepth));
277
- if (typeof obj !== "object") return false;
278
- return Object.values(obj).some((v) => this.exceedsDepth(v, currentDepth + 1));
279
- }
280
- parseFilters(query) {
281
- const filters = {};
282
- for (const [key, value] of Object.entries(query)) {
283
- if (RESERVED_QUERY_PARAMS.has(key)) continue;
284
- if (value === void 0 || value === null) continue;
285
- if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key)) continue;
286
- if (this._allowedFilterFields && !this._allowedFilterFields.has(key)) continue;
287
- if (this.exceedsDepth(value)) continue;
288
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
289
- const operatorObj = value;
290
- const operatorKeys = Object.keys(operatorObj);
291
- const allOperators = operatorKeys.every((op) => this.operators[op] && (!this._allowedOperators || this._allowedOperators.has(op)));
292
- const allKnownOperators = operatorKeys.every((op) => this.operators[op]);
293
- if (allOperators && operatorKeys.length > 0) {
294
- const mongoFilters = {};
295
- for (const [op, opValue] of Object.entries(operatorObj)) {
296
- const mongoOp = this.operators[op];
297
- if (mongoOp) mongoFilters[mongoOp] = this.parseFilterValue(opValue, op);
298
- }
299
- filters[key] = mongoFilters;
300
- continue;
301
- }
302
- if (allKnownOperators && this._allowedOperators) continue;
303
- }
304
- const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
305
- if (!match) continue;
306
- const [, fieldName, operator] = match;
307
- if (!fieldName) continue;
308
- if (operator && this.operators[operator] && (!this._allowedOperators || this._allowedOperators.has(operator))) {
309
- const mongoOp = this.operators[operator];
310
- const parsedValue = this.parseFilterValue(value, operator);
311
- if (!filters[fieldName]) filters[fieldName] = {};
312
- filters[fieldName][mongoOp] = parsedValue;
313
- } else if (!operator) filters[fieldName] = this.parseFilterValue(value);
314
- }
315
- return filters;
316
- }
317
- parseFilterValue(value, operator) {
318
- if (operator === "in" || operator === "nin") {
319
- if (Array.isArray(value)) return value.map((v) => this.coerceValue(v));
320
- if (typeof value === "string" && value.includes(",")) return value.split(",").map((v) => this.coerceValue(v.trim()));
321
- return [this.coerceValue(value)];
322
- }
323
- if (operator === "like" || operator === "contains" || operator === "regex") return this.sanitizeRegex(String(value));
324
- if (operator === "exists") {
325
- const str = String(value).toLowerCase();
326
- return str === "true" || str === "1";
327
- }
328
- return this.coerceValue(value);
329
- }
330
- coerceValue(value) {
331
- if (value === "true") return true;
332
- if (value === "false") return false;
333
- if (value === "null") return null;
334
- if (typeof value === "string") {
335
- const num = Number(value);
336
- if (!Number.isNaN(num) && value.trim() !== "") return num;
337
- }
338
- return value;
339
- }
340
- /**
341
- * Generate OpenAPI-compatible JSON Schema for query parameters.
342
- * Arc's defineResource() auto-detects this method and uses it
343
- * to document list endpoint query parameters in OpenAPI/Swagger.
344
- */
345
- getQuerySchema() {
346
- const operatorLines = Object.entries(this.operators).map(([op, mongoOp]) => {
347
- return ` ${op} → ${mongoOp}: ${{
348
- eq: "Equal (default when no operator specified)",
349
- ne: "Not equal",
350
- gt: "Greater than",
351
- gte: "Greater than or equal",
352
- lt: "Less than",
353
- lte: "Less than or equal",
354
- in: "In list (comma-separated values)",
355
- nin: "Not in list",
356
- like: "Pattern match (case-insensitive)",
357
- contains: "Contains substring (case-insensitive)",
358
- regex: "Regex pattern",
359
- exists: "Field exists (true/false)"
360
- }[op] || op}`;
361
- });
362
- return {
363
- type: "object",
364
- properties: {
365
- page: {
366
- type: "integer",
367
- description: "Page number for offset pagination",
368
- default: 1,
369
- minimum: 1
370
- },
371
- limit: {
372
- type: "integer",
373
- description: "Number of items per page",
374
- default: this.defaultLimit,
375
- minimum: 1,
376
- maximum: this.maxLimit
377
- },
378
- sort: {
379
- type: "string",
380
- description: "Sort fields (comma-separated). Prefix with - for descending. Example: -createdAt,name"
381
- },
382
- search: {
383
- type: "string",
384
- description: "Full-text search query",
385
- maxLength: this.maxSearchLength
386
- },
387
- select: {
388
- type: "string",
389
- description: "Fields to include/exclude (comma-separated). Prefix with - to exclude. Example: name,email,-password"
390
- },
391
- populate: {
392
- type: "string",
393
- description: "Fields to populate/join (comma-separated). Example: author,category"
394
- },
395
- after: {
396
- type: "string",
397
- description: "Cursor value for keyset pagination"
398
- },
399
- _filterOperators: {
400
- type: "string",
401
- description: ["Available filter operators (use as field[operator]=value):", ...operatorLines].join("\n")
402
- }
403
- }
404
- };
405
- }
406
- sanitizeRegex(pattern) {
407
- let sanitized = pattern.slice(0, this.maxRegexLength);
408
- if (DANGEROUS_REGEX_PATTERNS.test(sanitized)) sanitized = sanitized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
409
- return sanitized;
410
- }
411
- };
412
- /**
413
- * Create a new ArcQueryParser instance
414
- */
415
- function createQueryParser(options) {
416
- return new ArcQueryParser(options);
417
- }
418
- //#endregion
419
- export { createQueryParser as n, simpleEqualityMatcher as r, ArcQueryParser as t };
@@ -1,27 +0,0 @@
1
- //#region src/types/base.ts
2
- /** Extract user ID from a user object (supports both id and _id). */
3
- function getUserId(user) {
4
- if (!user) return void 0;
5
- const id = user.id ?? user._id;
6
- return id ? String(id) : void 0;
7
- }
8
- /**
9
- * Wrap data in Arc's standard `{ success: true, data }` envelope.
10
- *
11
- * @example
12
- * ```typescript
13
- * handler: async (req, reply) => {
14
- * const data = await getResults();
15
- * return envelope(data); // → { success: true, data }
16
- * }
17
- * ```
18
- */
19
- function envelope(data, meta) {
20
- return {
21
- success: true,
22
- data,
23
- ...meta
24
- };
25
- }
26
- //#endregion
27
- export { getUserId as n, envelope as t };