@classytic/arc 2.15.4 → 2.16.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/README.md +1 -0
- package/bin/arc.js +12 -0
- package/dist/{BaseController-dx3m2J8V.mjs → BaseController-Vk8I7C0x.mjs} +61 -19
- package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
- package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-8YmAQ-zW.d.mts} +1 -1
- package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-CzSzVK1m.mjs} +55 -9
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +4 -2
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +4 -4
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
- package/dist/buildHandler-C_SkBNF3.mjs +302 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/{caching-SM8gghN6.mjs → caching-8MfrrUYU.mjs} +1 -1
- package/dist/cli/commands/describe.d.mts +35 -1
- package/dist/cli/commands/describe.mjs +52 -12
- package/dist/cli/commands/docs.d.mts +1 -4
- package/dist/cli/commands/docs.mjs +4 -16
- package/dist/cli/commands/generate.d.mts +2 -20
- package/dist/cli/commands/generate.mjs +1 -546
- package/dist/cli/commands/init.d.mts +2 -40
- package/dist/cli/commands/init.mjs +1 -3045
- package/dist/cli/commands/introspect.mjs +53 -64
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/{core-CvmOqEms.mjs → core-wKeGXn38.mjs} +222 -44
- package/dist/createActionRouter-eiOFtnC3.mjs +288 -0
- package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-C4unhBnV.mjs} +3 -2
- package/dist/{createApp-PFegs47-.mjs → createApp-DdPBtmzp.mjs} +16 -14
- package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-NPKUJRt8.mjs} +1 -1
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DNPzqaTT.mjs} +17 -14
- package/dist/errors-4ZloYFvm.d.mts +91 -0
- package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-CnJCUyM0.mjs} +1 -1
- package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-DjEx_J3j.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +5 -5
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +4 -3
- package/dist/fastify-augmentation-CJm0vh-l.d.mts +42 -0
- package/dist/{fields-COhcH3fk.d.mts → fields-DqZLp5d6.d.mts} +1 -1
- package/dist/generate-CcM24hoq.mjs +654 -0
- package/dist/generate-cRnsafMS.d.mts +38 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +2 -2
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BTqLEvhu.d.mts → index-3L-adFJO.d.mts} +12 -12
- package/dist/{index-BstGxcc3.d.mts → index-CC_pgZjY.d.mts} +88 -17
- package/dist/{index-BswOSJCE.d.mts → index-DBDgy6Hw.d.mts} +430 -19
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -8
- package/dist/init-D-vrTZ1G.d.mts +71 -0
- package/dist/init-DYzfniuX.mjs +3098 -0
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/jobs.mjs +3 -3
- package/dist/integrations/mcp/index.d.mts +239 -7
- package/dist/integrations/mcp/index.mjs +2 -528
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +6 -10
- package/dist/integrations/streamline.mjs +26 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +1 -0
- package/dist/loadResourcesFromEntry-DCjYMgfC.mjs +51 -0
- package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-Dbgz9_Rj.mjs} +1021 -318
- package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{openapi-BHXhoX8O.mjs → openapi-BRnd3pT7.mjs} +82 -44
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
- package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +5 -59
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +5 -5
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
- package/dist/preloadResources-BOXzPhKU.mjs +45 -0
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +2 -2
- 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 +4 -3
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
- package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
- package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Do4P_WBC.d.mts} +2 -2
- package/dist/{redis-DiMkdHEl.d.mts → redis-cpqVb85w.d.mts} +1 -1
- package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-BuiAS3aE.d.mts} +1 -1
- package/dist/registry/index.d.mts +341 -2
- package/dist/registry/index.mjs +3 -3
- package/dist/registry-BrlHkrRS.mjs +582 -0
- package/dist/{routerShared-DrOa-26E.mjs → routerShared-ClV4cCaa.mjs} +40 -16
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +3 -3
- package/dist/{sse-Bz-5ZeTt.mjs → sse-jh5OqFjR.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -25
- package/dist/testing/index.mjs +17 -51
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +5 -5
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BQsjgQzS.d.mts → types-BD0BCdf6.d.mts} +106 -12
- package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
- package/dist/{types-CTYvcwHe.d.mts → types-CfapW2t8.d.mts} +42 -1
- package/dist/{types-DrBaUwyV.d.mts → types-Dw--SyTq.d.mts} +57 -11
- package/dist/utils/index.d.mts +1287 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-_h9B3c57.mjs → utils-DPXZnZUx.mjs} +158 -41
- package/dist/{buildHandler-CcFOpJLh.mjs → validate-DFIP2VrM.mjs} +73 -302
- package/dist/{versioning-hmkPcDlX.d.mts → versioning-5H4930tj.d.mts} +1 -1
- package/package.json +22 -29
- package/skills/arc/SKILL.md +300 -706
- package/skills/arc/references/auth.md +19 -7
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
- package/dist/createActionRouter-S3MLVYot.mjs +0 -220
- package/dist/index-bRjYu21O.d.mts +0 -1320
- package/dist/org/index.d.mts +0 -66
- package/dist/org/index.mjs +0 -486
- package/dist/org/types.d.mts +0 -82
- package/dist/org/types.mjs +0 -1
- package/dist/registry-I-ogLgL9.mjs +0 -46
- /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-B1GFiYUy.d.mts} +0 -0
- /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-ClEDfiMS.mjs} +0 -0
- /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
- /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
- /package/dist/{elevation-BXOWoGCF.d.mts → elevation-DvwtjDwu.d.mts} +0 -0
- /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-DuqTqeqn.d.mts} +0 -0
- /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-ClykOQqa.d.mts} +0 -0
- /package/dist/{interface-beEtJyWM.d.mts → interface-BVkZsvEe.d.mts} +0 -0
- /package/dist/{interface-DfLGcus7.d.mts → interface-DoE6UoC2.d.mts} +0 -0
- /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
- /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-fol6JuvW.mjs} +0 -0
- /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-B-PU4-Yu.mjs} +0 -0
- /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-boS5Rx26.mjs} +0 -0
- /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-DAOvBWgT.mjs} +0 -0
- /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-DlmaHUAl.d.mts} +0 -0
- /package/dist/{storage-Dfzt4VTl.d.mts → storage-HcAgP5Ni.d.mts} +0 -0
- /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-5JgaxieH.mjs} +0 -0
- /package/dist/{tracing-QJVprktp.d.mts → tracing-bRqqUxFO.d.mts} +0 -0
- /package/dist/{versioning-BUrT5aP4.mjs → versioning-CuIwUQZ2.mjs} +0 -0
- /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BJ0w-3Dt.d.mts} +0 -0
package/dist/utils/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createDomainError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-j4aJm1Wg.mjs";
|
|
2
|
-
import { A as assertValidConfig, C as withCompensation, D as CircuitState, E as CircuitBreakerRegistry, M as validateResourceConfig, N as simpleEqualityMatcher, O as createCircuitBreaker, S as defineCompensation, T as CircuitBreakerError, _ as ArcQueryParser, a as bareListResponse, b as defineGuard, c as errorDetailSchema, d as keysetListResponse, f as listResponse, g as responses, h as queryParams, i as aggregateListResponse, j as formatValidationErrors, k as createCircuitBreakerRegistry, l as getDefaultCrudSchemas, m as paginationSchema, n as createStateMachine, o as deleteResponse, p as offsetListResponse, r as scheduleBackground, s as errorContractSchema, t as getUserId, u as getListQueryParams, v as createQueryParser, w as CircuitBreaker, x as defineErrorMapper, y as handleRaw } from "../utils-
|
|
2
|
+
import { A as assertValidConfig, C as withCompensation, D as CircuitState, E as CircuitBreakerRegistry, M as validateResourceConfig, N as simpleEqualityMatcher, O as createCircuitBreaker, S as defineCompensation, T as CircuitBreakerError, _ as ArcQueryParser, a as bareListResponse, b as defineGuard, c as errorDetailSchema, d as keysetListResponse, f as listResponse, g as responses, h as queryParams, i as aggregateListResponse, j as formatValidationErrors, k as createCircuitBreakerRegistry, l as getDefaultCrudSchemas, m as paginationSchema, n as createStateMachine, o as deleteResponse, p as offsetListResponse, r as scheduleBackground, s as errorContractSchema, t as getUserId, u as getListQueryParams, v as createQueryParser, w as CircuitBreaker, x as defineErrorMapper, y as handleRaw } from "../utils-DPXZnZUx.mjs";
|
|
3
3
|
import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-De34B1ZG.mjs";
|
|
4
4
|
import { t as hasEvents } from "../typeGuards-BzkXkvVv.mjs";
|
|
5
5
|
export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UnauthorizedError, ValidationError, aggregateListResponse, assertValidConfig, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, formatValidationErrors, getDefaultCrudSchemas, getListQueryParams, getUserId, handleRaw, hasEvents, isArcError, isJsonSchema, isZodSchema, keysetListResponse, listResponse, offsetListResponse, paginationSchema, queryParams, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, validateResourceConfig, withCompensation };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { m as RESERVED_QUERY_PARAMS, t as CRUD_OPERATIONS } from "./constants-
|
|
1
|
+
import { m as RESERVED_QUERY_PARAMS, t as CRUD_OPERATIONS } from "./constants-TrJVIJl0.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { t as ArcError } from "./errors-j4aJm1Wg.mjs";
|
|
4
|
-
import { r as getAvailablePresets } from "./presets-
|
|
4
|
+
import { r as getAvailablePresets } from "./presets-C9BE6WaZ.mjs";
|
|
5
5
|
import { errorContractSchema as errorContractSchema$1, errorDetailSchema as errorDetailSchema$1 } from "@classytic/repo-core/errors";
|
|
6
6
|
//#region src/utils/simpleEqualityMatcher.ts
|
|
7
7
|
/**
|
|
@@ -142,7 +142,10 @@ function validateResourceConfig(config, options = {}) {
|
|
|
142
142
|
suggestion: `Change to "${config.prefix.slice(0, -1)}"`
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
-
if (config.routes)
|
|
145
|
+
if (config.routes) {
|
|
146
|
+
validateRoutes(config.routes, errors);
|
|
147
|
+
validateRouteCrudCollisions(config, errors);
|
|
148
|
+
}
|
|
146
149
|
return {
|
|
147
150
|
valid: errors.length === 0,
|
|
148
151
|
errors,
|
|
@@ -210,6 +213,71 @@ function validatePresetOptions(preset, warnings) {
|
|
|
210
213
|
suggestion: validOptions.length > 0 ? `Valid options: ${validOptions.join(", ")}` : `Preset "${preset.name}" has no configurable options`
|
|
211
214
|
});
|
|
212
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Detect collisions between user-declared `routes:` entries and the
|
|
218
|
+
* auto-CRUD route table that `createCrudRouter` will register.
|
|
219
|
+
*
|
|
220
|
+
* Without this check, a custom `POST /` on the same prefix as auto-CRUD's
|
|
221
|
+
* `create` boots fine through `defineResource()` and only blows up at
|
|
222
|
+
* `app.register()` time with Fastify's opaque `FST_ERR_DUPLICATED_ROUTE`.
|
|
223
|
+
* That error doesn't mention `disabledRoutes` and doesn't distinguish
|
|
224
|
+
* "you collided with auto-CRUD" from "you collided with yourself" — so
|
|
225
|
+
* the consumer has to grep arc's source to discover the fix.
|
|
226
|
+
*
|
|
227
|
+
* We surface the fix in the error message, naming the conflicting op
|
|
228
|
+
* and the literal line to add.
|
|
229
|
+
*
|
|
230
|
+
* Honored config:
|
|
231
|
+
* - `disableDefaultRoutes: true` → no auto-CRUD, no collisions possible
|
|
232
|
+
* - `disabledRoutes: ['create']` → that op's path is free for custom use
|
|
233
|
+
* - `updateMethod: 'PUT' | 'both'` → update slot covers PUT/PATCH accordingly
|
|
234
|
+
*/
|
|
235
|
+
function validateRouteCrudCollisions(config, errors) {
|
|
236
|
+
if (config.disableDefaultRoutes) return;
|
|
237
|
+
if (!config.routes || config.routes.length === 0) return;
|
|
238
|
+
const updateMethod = config.updateMethod ?? "PATCH";
|
|
239
|
+
const updateMethods = updateMethod === "both" ? ["PUT", "PATCH"] : [updateMethod];
|
|
240
|
+
const disabled = new Set(config.disabledRoutes ?? []);
|
|
241
|
+
const crudTable = [
|
|
242
|
+
{
|
|
243
|
+
op: "list",
|
|
244
|
+
method: "GET",
|
|
245
|
+
path: "/"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
op: "get",
|
|
249
|
+
method: "GET",
|
|
250
|
+
path: "/:id"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
op: "create",
|
|
254
|
+
method: "POST",
|
|
255
|
+
path: "/"
|
|
256
|
+
},
|
|
257
|
+
...updateMethods.map((m) => ({
|
|
258
|
+
op: "update",
|
|
259
|
+
method: m,
|
|
260
|
+
path: "/:id"
|
|
261
|
+
})),
|
|
262
|
+
{
|
|
263
|
+
op: "delete",
|
|
264
|
+
method: "DELETE",
|
|
265
|
+
path: "/:id"
|
|
266
|
+
}
|
|
267
|
+
];
|
|
268
|
+
for (const [i, route] of config.routes.entries()) {
|
|
269
|
+
if (!route.method || !route.path) continue;
|
|
270
|
+
for (const crud of crudTable) {
|
|
271
|
+
if (disabled.has(crud.op)) continue;
|
|
272
|
+
if (route.method !== crud.method || route.path !== crud.path) continue;
|
|
273
|
+
errors.push({
|
|
274
|
+
field: `routes[${i}]`,
|
|
275
|
+
message: `Route ${route.method} ${route.path} collides with auto-CRUD "${crud.op}"`,
|
|
276
|
+
suggestion: `Add \`disabledRoutes: ['${crud.op}']\` to suppress the auto-CRUD route, or move your custom handler to a different path. Set \`disableDefaultRoutes: true\` to disable all auto-CRUD on this resource.`
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
213
281
|
function validateRoutes(routes, errors) {
|
|
214
282
|
const validMethods = [
|
|
215
283
|
"GET",
|
|
@@ -236,9 +304,11 @@ function validateRoutes(routes, errors) {
|
|
|
236
304
|
message: `Route path must start with "/" (got "${route.path}")`,
|
|
237
305
|
suggestion: `Change to "/${route.path}"`
|
|
238
306
|
});
|
|
239
|
-
|
|
307
|
+
const routeWithRefs = route;
|
|
308
|
+
if (!route.handler && typeof routeWithRefs.controllerMethod !== "function") errors.push({
|
|
240
309
|
field: `routes[${i}].handler`,
|
|
241
|
-
message: "Route handler
|
|
310
|
+
message: "Route must declare either `handler` (string / function) or `controllerMethod`",
|
|
311
|
+
suggestion: "Prefer `controllerMethod: (c: MyController) => c.method` for typed handler refs."
|
|
242
312
|
});
|
|
243
313
|
const routeKey = `${route.method} ${route.path}`;
|
|
244
314
|
if (seenRoutes.has(routeKey)) errors.push({
|
|
@@ -761,12 +831,26 @@ const DANGEROUS_REGEX_PATTERNS = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\(.+\))\+|\(\?:|\
|
|
|
761
831
|
* Converts URL query parameters to a structured query format:
|
|
762
832
|
* - Pagination: ?page=1&limit=20
|
|
763
833
|
* - Sorting: ?sort=-createdAt,name (- prefix = descending)
|
|
764
|
-
* - Filtering:
|
|
834
|
+
* - Filtering — TWO supported forms (both produce identical output):
|
|
835
|
+
* - Bare (default, terse): `?status=active&price[gte]=100&price[lte]=500`
|
|
836
|
+
* - Bracket envelope: `?filter[status]=active&filter[price][gte]=100`
|
|
837
|
+
* The envelope form matches the convention used by JSON:API / Stripe /
|
|
838
|
+
* most modern REST style guides, and is the safe choice on busy URLs
|
|
839
|
+
* that might otherwise collide with reserved meta keys (`page`, `sort`,
|
|
840
|
+
* `select`, `populate`, etc.). Operator notation (`[gte]`, `[lte]`,
|
|
841
|
+
* `[ne]`, `[in]`, ...) works inside either form.
|
|
765
842
|
* - Search: ?search=keyword
|
|
766
843
|
* - Populate: ?populate=author,category
|
|
767
844
|
* - Field selection: ?select=name,price,status
|
|
768
845
|
* - Keyset pagination: ?after=cursor_value
|
|
769
846
|
*
|
|
847
|
+
* **NOTE on parser portability.** `ArcQueryParser` accepts both filter
|
|
848
|
+
* forms. Other parser implementations (`@classytic/mongokit`'s
|
|
849
|
+
* `QueryParser`, custom hosts-side parsers) may accept only one form —
|
|
850
|
+
* MongoKit historically only accepts the bare form. If your host swaps
|
|
851
|
+
* `queryParser` to a different implementation, double-check which forms
|
|
852
|
+
* it understands before publishing URLs that mix the two.
|
|
853
|
+
*
|
|
770
854
|
* For advanced MongoDB features ($lookup, aggregations), use MongoKit's QueryParser.
|
|
771
855
|
*/
|
|
772
856
|
var ArcQueryParser = class {
|
|
@@ -930,49 +1014,82 @@ var ArcQueryParser = class {
|
|
|
930
1014
|
if (typeof obj !== "object") return false;
|
|
931
1015
|
return Object.values(obj).some((v) => this.exceedsDepth(v, currentDepth + 1));
|
|
932
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Parse all filter entries from the query, supporting BOTH the bare
|
|
1019
|
+
* top-level form and the bracket-wrapped `filter[...]` envelope.
|
|
1020
|
+
*
|
|
1021
|
+
* Two equivalent ways to express the same query:
|
|
1022
|
+
* - Bare (legacy, arc default): `?status=active&price[gte]=40`
|
|
1023
|
+
* - Bracket envelope (REST convention): `?filter[status]=active&filter[price][gte]=40`
|
|
1024
|
+
*
|
|
1025
|
+
* Both forms parse to the same `filters: { status: 'active', price: { $gte: 40 } }`.
|
|
1026
|
+
* The envelope form mirrors the convention used by most REST API style
|
|
1027
|
+
* guides (JSON:API, Stripe API, etc.) and disambiguates filter fields
|
|
1028
|
+
* from reserved meta params on busy URLs — a query string with a field
|
|
1029
|
+
* literally named `page` or `sort` can't collide with the framework's
|
|
1030
|
+
* meta keys when wrapped under `filter[...]`. The bare form stays as
|
|
1031
|
+
* the default since it's the shortest path for simple queries; expect
|
|
1032
|
+
* it to remain supported indefinitely.
|
|
1033
|
+
*
|
|
1034
|
+
* Precedence when the same key appears in both forms:
|
|
1035
|
+
* `?status=closed&filter[status]=active` → bare wins (`status: closed`).
|
|
1036
|
+
* Mixing should be rare; the deterministic rule avoids silent surprises.
|
|
1037
|
+
*/
|
|
933
1038
|
parseFilters(query) {
|
|
934
1039
|
const filters = {};
|
|
1040
|
+
const envelope = query.filter;
|
|
1041
|
+
if (typeof envelope === "object" && envelope !== null && !Array.isArray(envelope)) for (const [key, value] of Object.entries(envelope)) this.applyFilterEntry(filters, key, value);
|
|
935
1042
|
for (const [key, value] of Object.entries(query)) {
|
|
936
1043
|
if (RESERVED_QUERY_PARAMS.has(key)) continue;
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1044
|
+
this.applyFilterEntry(filters, key, value);
|
|
1045
|
+
}
|
|
1046
|
+
return filters;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Apply one filter entry — used by both the bracket-envelope branch and
|
|
1050
|
+
* the bare top-level branch of `parseFilters`. Centralising the field-
|
|
1051
|
+
* validation + operator-conversion logic keeps the two forms in
|
|
1052
|
+
* lockstep; a future regex/security tweak only changes one place.
|
|
1053
|
+
*/
|
|
1054
|
+
applyFilterEntry(filters, key, value) {
|
|
1055
|
+
if (value === void 0 || value === null) return;
|
|
1056
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_.]*(?:\[[a-z]+\])?$/.test(key)) return;
|
|
1057
|
+
const bareKey = key.replace(/\[[a-z]+\]$/, "");
|
|
1058
|
+
if (this._allowedFilterFields && !this._allowedFilterFields.has(bareKey)) return;
|
|
1059
|
+
if (this.exceedsDepth(value)) return;
|
|
1060
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1061
|
+
const operatorObj = value;
|
|
1062
|
+
const operatorKeys = Object.keys(operatorObj);
|
|
1063
|
+
const allOperators = operatorKeys.every((op) => this.operators[op] && (!this._allowedOperators || this._allowedOperators.has(op)));
|
|
1064
|
+
const allKnownOperators = operatorKeys.every((op) => this.operators[op]);
|
|
1065
|
+
if (allOperators && operatorKeys.length > 0) {
|
|
1066
|
+
const mongoFilters = {};
|
|
1067
|
+
let needsCaseInsensitive = false;
|
|
1068
|
+
for (const [op, opValue] of Object.entries(operatorObj)) {
|
|
1069
|
+
const mongoOp = this.operators[op];
|
|
1070
|
+
if (mongoOp) {
|
|
1071
|
+
mongoFilters[mongoOp] = this.parseFilterValue(opValue, op);
|
|
1072
|
+
if (op === "contains" || op === "like") needsCaseInsensitive = true;
|
|
955
1073
|
}
|
|
956
|
-
if (needsCaseInsensitive) mongoFilters.$options = "i";
|
|
957
|
-
filters[key] = mongoFilters;
|
|
958
|
-
continue;
|
|
959
1074
|
}
|
|
960
|
-
if (
|
|
1075
|
+
if (needsCaseInsensitive) mongoFilters.$options = "i";
|
|
1076
|
+
filters[key] = mongoFilters;
|
|
1077
|
+
return;
|
|
961
1078
|
}
|
|
962
|
-
|
|
963
|
-
if (!match) continue;
|
|
964
|
-
const [, fieldName, operator] = match;
|
|
965
|
-
if (!fieldName) continue;
|
|
966
|
-
if (operator && this.operators[operator] && (!this._allowedOperators || this._allowedOperators.has(operator))) {
|
|
967
|
-
const mongoOp = this.operators[operator];
|
|
968
|
-
const parsedValue = this.parseFilterValue(value, operator);
|
|
969
|
-
if (!filters[fieldName]) filters[fieldName] = {};
|
|
970
|
-
const fieldFilter = filters[fieldName];
|
|
971
|
-
fieldFilter[mongoOp] = parsedValue;
|
|
972
|
-
if (operator === "contains" || operator === "like") fieldFilter.$options = "i";
|
|
973
|
-
} else if (!operator) filters[fieldName] = this.parseFilterValue(value);
|
|
1079
|
+
if (allKnownOperators && this._allowedOperators) return;
|
|
974
1080
|
}
|
|
975
|
-
|
|
1081
|
+
const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
|
|
1082
|
+
if (!match) return;
|
|
1083
|
+
const [, fieldName, operator] = match;
|
|
1084
|
+
if (!fieldName) return;
|
|
1085
|
+
if (operator && this.operators[operator] && (!this._allowedOperators || this._allowedOperators.has(operator))) {
|
|
1086
|
+
const mongoOp = this.operators[operator];
|
|
1087
|
+
const parsedValue = this.parseFilterValue(value, operator);
|
|
1088
|
+
if (!filters[fieldName]) filters[fieldName] = {};
|
|
1089
|
+
const fieldFilter = filters[fieldName];
|
|
1090
|
+
fieldFilter[mongoOp] = parsedValue;
|
|
1091
|
+
if (operator === "contains" || operator === "like") fieldFilter.$options = "i";
|
|
1092
|
+
} else if (!operator) filters[fieldName] = this.parseFilterValue(value);
|
|
976
1093
|
}
|
|
977
1094
|
parseFilterValue(value, operator) {
|
|
978
1095
|
if (operator === "in" || operator === "nin") {
|