@classytic/arc 2.14.1 → 2.14.2

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.
@@ -560,13 +560,73 @@ function parseDateRange(query, field) {
560
560
  };
561
561
  }
562
562
  /**
563
+ * Bracket-syntax operator shorthand → canonical Mongo operator. Mirrors
564
+ * the `operators` map in `ArcQueryParser` so the aggregation route emits
565
+ * the same shape the CRUD list route produces. Aggregations don't run
566
+ * through the resource-level QueryParser (they have their own URL→IR
567
+ * compile path), so this translation has to happen in arc itself —
568
+ * downstream kits' filter compilers expect canonical `$gte/$lte/$in/...`
569
+ * keys, not bare `gte/lte/in/...` shorthand.
570
+ */
571
+ const OPERATOR_SHORTHAND = {
572
+ eq: "$eq",
573
+ ne: "$ne",
574
+ gt: "$gt",
575
+ gte: "$gte",
576
+ lt: "$lt",
577
+ lte: "$lte",
578
+ in: "$in",
579
+ nin: "$nin",
580
+ like: "$regex",
581
+ contains: "$regex",
582
+ regex: "$regex",
583
+ exists: "$exists",
584
+ size: "$size",
585
+ type: "$type"
586
+ };
587
+ const SHORTHAND_RANGE_OPS = new Set([
588
+ "gt",
589
+ "gte",
590
+ "lt",
591
+ "lte"
592
+ ]);
593
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
594
+ function tryCoerceDate(v) {
595
+ if (typeof v !== "string" || !ISO_DATE_RE.test(v)) return v;
596
+ const d = new Date(v);
597
+ return Number.isNaN(d.getTime()) ? v : d;
598
+ }
599
+ /**
600
+ * Translate a qs-parsed nested-operator object (`{ field: { gte, lte } }`)
601
+ * into Mongo-shape (`{ field: { $gte: Date, $lte: Date } }`). Only fires
602
+ * when EVERY key is a known shorthand operator — leaves user-data
603
+ * objects untouched so callers can still equality-match on a stored
604
+ * sub-document.
605
+ */
606
+ function expandShorthandOperators(value) {
607
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
608
+ const nested = value;
609
+ const keys = Object.keys(nested);
610
+ if (keys.length === 0) return value;
611
+ if (!keys.every((k) => !k.startsWith("$") && OPERATOR_SHORTHAND[k] !== void 0)) return value;
612
+ const expanded = {};
613
+ for (const [op, opVal] of Object.entries(nested)) {
614
+ const mongoOp = OPERATOR_SHORTHAND[op];
615
+ if (!mongoOp) continue;
616
+ expanded[mongoOp] = SHORTHAND_RANGE_OPS.has(op) ? tryCoerceDate(opVal) : opVal;
617
+ }
618
+ return expanded;
619
+ }
620
+ /**
563
621
  * Strip control params (page/limit/sort/select/...) and the resource-
564
622
  * dispatch verbs from the query, leaving only filter predicates the
565
- * caller used to narrow the aggregation.
623
+ * caller used to narrow the aggregation. Bracket-syntax operator
624
+ * shorthand (`createdAt[gte]=...`) gets translated to canonical Mongo-
625
+ * shape here so kits don't have to reimplement the URL grammar — same
626
+ * contract `ArcQueryParser` enforces for the CRUD list route.
566
627
  *
567
628
  * The resulting record is shallow-merged into the AggRequest filter
568
- * via `compileAggRequest`. Bracket-syntax keys (`createdAt[gte]`) are
569
- * preserved — the kit's filter compiler handles them.
629
+ * via `compileAggRequest`.
570
630
  */
571
631
  function extractCallerFilter(query) {
572
632
  const out = {};
@@ -585,7 +645,7 @@ function extractCallerFilter(query) {
585
645
  for (const [key, value] of Object.entries(query)) {
586
646
  if (reserved.has(key)) continue;
587
647
  if (value === void 0 || value === "") continue;
588
- out[key] = value;
648
+ out[key] = expandShorthandOperators(value);
589
649
  }
590
650
  return out;
591
651
  }
@@ -1,5 +1,5 @@
1
1
  import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-Cxde4rpC.mjs";
2
2
  import { a as BulkMixin, c as collectReadBlockedFields, d as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-Dv60tU83.mjs";
3
3
  import { _ as getControllerContext, g as createRequestContext, h as createFastifyHandler, m as createCrudHandlers, v as getControllerScope, y as sendControllerResponse } from "../routerShared-DrOa-26E.mjs";
4
- import { a as defineResource, c as createPermissionMiddleware, i as defineResourceVariants, l as defineAggregation, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as createCrudRouter, t as getEntityId } from "../core-DEdN6zKD.mjs";
4
+ import { a as defineResource, c as createPermissionMiddleware, i as defineResourceVariants, l as defineAggregation, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as createCrudRouter, t as getEntityId } from "../core-D29kkRL5.mjs";
5
5
  export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
@@ -945,7 +945,7 @@ function buildResourcePlugin(resource) {
945
945
  });
946
946
  }
947
947
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
948
- const { createAggregationRouter } = await import("./createAggregationRouter-Bk-58SbZ.mjs");
948
+ const { createAggregationRouter } = await import("./createAggregationRouter-DhR-Ofiz.mjs");
949
949
  const repoForAgg = resource.controller?.repository;
950
950
  const buildOptions = (req) => {
951
951
  return resource.controller?.tenantRepoOptions?.(req) ?? {};
@@ -1,6 +1,6 @@
1
1
  import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-j4aJm1Wg.mjs";
2
2
  import { c as buildPreHandlerChain, f as resolveRouterPluginMw, i as buildAuthMiddleware, l as buildRateLimitConfig, p as selectPluginMw, r as buildArcDecorator } from "./routerShared-DrOa-26E.mjs";
3
- import { r as validateAggregations, t as buildAggregationHandler } from "./buildHandler-BamHHpH8.mjs";
3
+ import { r as validateAggregations, t as buildAggregationHandler } from "./buildHandler-jSZ6Fdvi.mjs";
4
4
  //#region src/core/aggregation/createAggregationRouter.ts
5
5
  /**
6
6
  * Register one Fastify route per aggregation. No-op when the map is
package/dist/index.mjs CHANGED
@@ -4,8 +4,8 @@ import { t as getUserId } from "./utils-_h9B3c57.mjs";
4
4
  import { a as BulkMixin, i as SlugMixin, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, t as BaseController } from "./BaseController-Dv60tU83.mjs";
5
5
  import { C as allowPublic, D as requireAuth, O as requireOwnership, S as allOf, T as denyAll, _ as requireOrgMembership, a as presets_exports, b as requireServiceScope, c as readOnly, d as applyFieldWritePermissions, f as fields, g as requireOrgInScope, h as createOrgPermissions, i as ownerWithAdminBypass, j as when, k as requireRoles, m as createDynamicPermissionMatrix, n as authenticated, o as publicRead, r as fullPublic, s as publicReadAdminWrite, t as adminOnly, u as applyFieldReadPermissions, v as requireOrgRole, w as anyOf, x as requireTeamMembership, y as requireScopeContext } from "./permissions-ohQyv50e.mjs";
6
6
  import { v as getControllerScope } from "./routerShared-DrOa-26E.mjs";
7
- import { a as defineResource, i as defineResourceVariants, l as defineAggregation, o as ResourceDefinition } from "./core-DEdN6zKD.mjs";
7
+ import { a as defineResource, i as defineResourceVariants, l as defineAggregation, o as ResourceDefinition } from "./core-D29kkRL5.mjs";
8
8
  //#region src/index.ts
9
- const version = "2.14.1";
9
+ const version = "2.14.2";
10
10
  //#endregion
11
11
  export { ArcError, BaseController, BaseCrudController, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, ForbiddenError, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, NotFoundError, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, UnauthorizedError, ValidationError, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, authenticated, createDomainError, createDynamicPermissionMatrix, createOrgPermissions, defineAggregation, defineResource, defineResourceVariants, denyAll, fields, fullPublic, getControllerScope, getUserId, ownerWithAdminBypass, presets_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAuth, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, version, when };
@@ -1,4 +1,4 @@
1
- import { n as fieldRulesToZod, r as createMcpServer, t as resourceToTools } from "../../resourceToTools-CZ-ZhS7v.mjs";
1
+ import { n as fieldRulesToZod, r as createMcpServer, t as resourceToTools } from "../../resourceToTools-BM686jB4.mjs";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/integrations/mcp/defineTool.ts
@@ -1,4 +1,4 @@
1
- import { r as createMcpServer, t as resourceToTools } from "../../resourceToTools-CZ-ZhS7v.mjs";
1
+ import { r as createMcpServer, t as resourceToTools } from "../../resourceToTools-BM686jB4.mjs";
2
2
  //#region src/integrations/mcp/testing.ts
3
3
  /**
4
4
  * @classytic/arc/mcp/testing — MCP Test Utilities
@@ -58,7 +58,7 @@ try {
58
58
  function createTracerProvider(options) {
59
59
  if (!isAvailable || !NodeTracerProvider || !BatchSpanProcessor || !OTLPTraceExporter) return null;
60
60
  const { serviceName = "@classytic/arc", serviceVersion, exporterUrl = "http://localhost:4318/v1/traces" } = options;
61
- const resolvedVersion = serviceVersion ?? "2.14.1";
61
+ const resolvedVersion = serviceVersion ?? "2.14.2";
62
62
  const exporter = new OTLPTraceExporter({ url: exporterUrl });
63
63
  const provider = new NodeTracerProvider({ resource: { attributes: {
64
64
  "service.name": serviceName,
@@ -3,7 +3,7 @@ import { t as BaseController } from "./BaseController-Dv60tU83.mjs";
3
3
  import { L as normalizePermissionResult } from "./permissions-ohQyv50e.mjs";
4
4
  import { t as executePipeline } from "./pipe-Zr0KXjQe.mjs";
5
5
  import { u as resolvePipelineSteps } from "./routerShared-DrOa-26E.mjs";
6
- import { n as executeAggregation, r as validateAggregations } from "./buildHandler-BamHHpH8.mjs";
6
+ import { n as executeAggregation, r as validateAggregations } from "./buildHandler-jSZ6Fdvi.mjs";
7
7
  import { t as resolveActionPermission } from "./actionPermissions-CyUkQu6O.mjs";
8
8
  import { i as shouldRejectAdditionalProperties, r as schemaIRToZodShape, t as normalizeSchemaIR } from "./schemaIR-lYhC2gE5.mjs";
9
9
  import { t as pluralize } from "./pluralize-DQgqgifU.mjs";
@@ -564,9 +564,8 @@ var HttpTestHarness = class {
564
564
  });
565
565
  expect(res.statusCode).toBeLessThan(300);
566
566
  const body = JSON.parse(res.body);
567
- expect(body.success).toBe(true);
568
- expect(body.data?._id).toBeDefined();
569
- createdId = body.data._id;
567
+ expect(body._id).toBeDefined();
568
+ createdId = body._id;
570
569
  });
571
570
  if (enabledRoutes.has("list")) it("GET should list resources", async () => {
572
571
  const { app } = this.getOptions();
@@ -576,9 +575,7 @@ var HttpTestHarness = class {
576
575
  headers: this.adminHeaders()
577
576
  });
578
577
  expect(res.statusCode).toBe(200);
579
- const body = JSON.parse(res.body);
580
- expect(body.success).toBe(true);
581
- const list = body.data ?? body.data;
578
+ const list = JSON.parse(res.body).data;
582
579
  expect(Array.isArray(list)).toBe(true);
583
580
  });
584
581
  if (enabledRoutes.has("get")) {
@@ -591,7 +588,7 @@ var HttpTestHarness = class {
591
588
  headers: this.adminHeaders()
592
589
  });
593
590
  expect(res.statusCode).toBe(200);
594
- expect(JSON.parse(res.body).data?._id).toBe(createdId);
591
+ expect(JSON.parse(res.body)._id).toBe(createdId);
595
592
  });
596
593
  it("GET /:id with non-existent ID should return 404", async () => {
597
594
  const { app } = this.getOptions();
@@ -601,7 +598,7 @@ var HttpTestHarness = class {
601
598
  headers: this.adminHeaders()
602
599
  });
603
600
  expect(res.statusCode).toBe(404);
604
- expect(JSON.parse(res.body).success).toBe(false);
601
+ expect(JSON.parse(res.body).status).toBe(404);
605
602
  });
606
603
  }
607
604
  if (enabledRoutes.has("update")) for (const verb of updateMethods) {
@@ -616,7 +613,7 @@ var HttpTestHarness = class {
616
613
  payload
617
614
  });
618
615
  expect(res.statusCode).toBe(200);
619
- expect(JSON.parse(res.body).success).toBe(true);
616
+ expect(JSON.parse(res.body)._id).toBe(createdId);
620
617
  });
621
618
  it(`${verb} /:id with non-existent ID should return 404`, async () => {
622
619
  const { app, fixtures } = this.getOptions();
@@ -640,7 +637,7 @@ var HttpTestHarness = class {
640
637
  headers: this.adminHeaders(),
641
638
  payload: fixtures.valid
642
639
  });
643
- deleteId = JSON.parse(createRes.body).data?._id;
640
+ deleteId = JSON.parse(createRes.body)._id;
644
641
  }
645
642
  if (!deleteId) return;
646
643
  expect((await app.inject({
@@ -731,9 +728,9 @@ var HttpTestHarness = class {
731
728
  });
732
729
  expect(res.statusCode).toBeLessThan(400);
733
730
  const body = JSON.parse(res.body);
734
- if (body.data?._id && enabledRoutes.has("delete")) await app.inject({
731
+ if (body._id && enabledRoutes.has("delete")) await app.inject({
735
732
  method: "DELETE",
736
- url: `${this.getBaseUrl()}/${body.data._id}`,
733
+ url: `${this.getBaseUrl()}/${body._id}`,
737
734
  headers: this.adminHeaders()
738
735
  });
739
736
  });
@@ -753,7 +750,7 @@ var HttpTestHarness = class {
753
750
  payload: fixtures.invalid
754
751
  });
755
752
  expect(res.statusCode).toBeGreaterThanOrEqual(400);
756
- expect(JSON.parse(res.body).success).toBe(false);
753
+ expect(JSON.parse(res.body).status).toBeGreaterThanOrEqual(400);
757
754
  });
758
755
  });
759
756
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.14.1",
3
+ "version": "2.14.2",
4
4
  "description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {