@classytic/arc 1.0.8 → 1.1.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.
@@ -1,4 +1,4 @@
1
- import { A as AnyRecord, I as IController, R as RouteSchemaOptions, Q as QueryParserInterface, a as IRequestContext, S as ServiceContext, C as ControllerQueryOptions, b as RequestContext, H as HookSystem, c as IControllerResponse, P as PaginatedResult } from './index-D5QTob1X.js';
1
+ import { A as AnyRecord, I as IController, R as RouteSchemaOptions, Q as QueryParserInterface, a as IRequestContext, S as ServiceContext, C as ControllerQueryOptions, b as RequestContext, H as HookSystem, c as IControllerResponse, P as PaginatedResult } from './index-B4t03KQ0.js';
2
2
 
3
3
  /**
4
4
  * Base Controller - Framework-Agnostic CRUD Operations
@@ -1,5 +1,5 @@
1
- import { D as DataAdapter, y as CrudRepository, o as RepositoryLike, m as SchemaMetadata, R as RouteSchemaOptions, af as OpenApiSchemas, Q as QueryParserInterface, ae as ParsedQuery, V as ValidationResult } from '../index-D5QTob1X.js';
2
- export { ag as AdapterFactory, F as FieldMetadata, n as RelationMetadata } from '../index-D5QTob1X.js';
1
+ import { D as DataAdapter, y as CrudRepository, o as RepositoryLike, m as SchemaMetadata, R as RouteSchemaOptions, af as OpenApiSchemas, Q as QueryParserInterface, ae as ParsedQuery, V as ValidationResult } from '../index-B4t03KQ0.js';
2
+ export { ag as AdapterFactory, F as FieldMetadata, n as RelationMetadata } from '../index-B4t03KQ0.js';
3
3
  import { Model } from 'mongoose';
4
4
  import 'fastify';
5
5
  import '../types-B99TBmFV.js';
@@ -1,5 +1,5 @@
1
1
  import { FastifyPluginAsync, FastifyInstance, FastifyRequest } from 'fastify';
2
- import { H as HookSystem, j as ResourceRegistry } from './index-D5QTob1X.js';
2
+ import { H as HookSystem, j as ResourceRegistry } from './index-B4t03KQ0.js';
3
3
 
4
4
  /**
5
5
  * Request ID Plugin
@@ -1,5 +1,5 @@
1
1
  import { FastifyPluginAsync } from 'fastify';
2
- import { g as AuthHelpers, h as AuthPluginOptions } from '../index-D5QTob1X.js';
2
+ import { g as AuthHelpers, h as AuthPluginOptions } from '../index-B4t03KQ0.js';
3
3
  import 'mongoose';
4
4
  import '../types-B99TBmFV.js';
5
5
 
@@ -1,7 +1,7 @@
1
- export { B as BaseController, a as BaseControllerOptions } from '../BaseController-nNRS3vpA.js';
1
+ export { B as BaseController, a as BaseControllerOptions } from '../BaseController-DVAiHxEQ.js';
2
2
  import { RouteHandlerMethod, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
3
- import { w as FastifyWithDecorators, B as CrudController, _ as CrudRouterOptions, d as RequestWithExtras, a as IRequestContext, c as IControllerResponse, I as IController } from '../index-D5QTob1X.js';
4
- export { q as ResourceDefinition, p as defineResource } from '../index-D5QTob1X.js';
3
+ import { w as FastifyWithDecorators, B as CrudController, _ as CrudRouterOptions, d as RequestWithExtras, a as IRequestContext, c as IControllerResponse, I as IController } from '../index-B4t03KQ0.js';
4
+ export { q as ResourceDefinition, p as defineResource } from '../index-B4t03KQ0.js';
5
5
  import { P as PermissionCheck } from '../types-B99TBmFV.js';
6
6
  import 'mongoose';
7
7
 
@@ -312,6 +312,23 @@ var ArcQueryParser = class {
312
312
  for (const [key, value] of Object.entries(query)) {
313
313
  if (reservedKeys.has(key)) continue;
314
314
  if (value === void 0 || value === null) continue;
315
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key)) continue;
316
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
317
+ const operatorObj = value;
318
+ const operatorKeys = Object.keys(operatorObj);
319
+ const allOperators = operatorKeys.every((op) => this.operators[op]);
320
+ if (allOperators && operatorKeys.length > 0) {
321
+ const mongoFilters = {};
322
+ for (const [op, opValue] of Object.entries(operatorObj)) {
323
+ const mongoOp = this.operators[op];
324
+ if (mongoOp) {
325
+ mongoFilters[mongoOp] = this.parseFilterValue(opValue, op);
326
+ }
327
+ }
328
+ filters[key] = mongoFilters;
329
+ continue;
330
+ }
331
+ }
315
332
  const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
316
333
  if (!match) continue;
317
334
  const [, fieldName, operator] = match;
@@ -453,6 +470,8 @@ var BaseController = class _BaseController {
453
470
  sort: sortString,
454
471
  select: this._sanitizeSelect(selectString, this.schemaOptions),
455
472
  populate: this._sanitizePopulate(parsed.populate, this.schemaOptions),
473
+ // Advanced populate options from MongoKit QueryParser (takes precedence over simple populate)
474
+ populateOptions: parsed.populateOptions,
456
475
  filters: parsed.filters,
457
476
  // MongoKit features
458
477
  search: parsed.search,
@@ -568,7 +587,7 @@ var BaseController = class _BaseController {
568
587
  return true;
569
588
  }
570
589
  }
571
- return itemValue === filterValue;
590
+ return String(itemValue) === String(filterValue);
572
591
  }
573
592
  /**
574
593
  * Check if item matches policy filters (for get/update/delete operations)
@@ -724,7 +743,10 @@ var BaseController = class _BaseController {
724
743
  const arcContext = req.metadata;
725
744
  try {
726
745
  const item = await this.repository.getById(id, options);
727
- if (!item || !this._checkOrgScope(item, arcContext) || !this._checkPolicyFilters(item, req)) {
746
+ const hasItem = !!item;
747
+ const orgScopeOk = this._checkOrgScope(item, arcContext);
748
+ const policyFiltersOk = this._checkPolicyFilters(item, req);
749
+ if (!hasItem || !orgScopeOk || !policyFiltersOk) {
728
750
  return {
729
751
  success: false,
730
752
  error: "Resource not found",
@@ -1,5 +1,5 @@
1
1
  import { FastifyInstance } from 'fastify';
2
- import { C as CreateAppOptions } from './types-zpN48n6B.js';
2
+ import { C as CreateAppOptions } from './types-BvckRbs2.js';
3
3
 
4
4
  /**
5
5
  * ArcFactory - Production-ready Fastify application factory
@@ -1,11 +1,11 @@
1
- export { A as ArcFactory, c as createApp } from '../createApp-CjN9zZSL.js';
2
- import { C as CreateAppOptions } from '../types-zpN48n6B.js';
3
- export { M as MultipartOptions, R as RawBodyOptions, U as UnderPressureOptions } from '../types-zpN48n6B.js';
1
+ export { A as ArcFactory, c as createApp } from '../createApp-Ce9wl8W9.js';
2
+ import { C as CreateAppOptions } from '../types-BvckRbs2.js';
3
+ export { M as MultipartOptions, R as RawBodyOptions, U as UnderPressureOptions } from '../types-BvckRbs2.js';
4
4
  import 'fastify';
5
5
  import '@fastify/cors';
6
6
  import '@fastify/helmet';
7
7
  import '@fastify/rate-limit';
8
- import '../index-D5QTob1X.js';
8
+ import '../index-B4t03KQ0.js';
9
9
  import 'mongoose';
10
10
  import '../types-B99TBmFV.js';
11
11
 
@@ -2,6 +2,7 @@ import fp from 'fastify-plugin';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { createRequire } from 'module';
4
4
  import Fastify from 'fastify';
5
+ import qs from 'qs';
5
6
 
6
7
  var __defProp = Object.defineProperty;
7
8
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1507,6 +1508,10 @@ async function createApp(options) {
1507
1508
  const fastify = Fastify({
1508
1509
  logger: config.logger ?? true,
1509
1510
  trustProxy: config.trustProxy ?? false,
1511
+ // Use qs parser to support nested bracket notation in query strings
1512
+ // e.g., ?populate[author][select]=name,email → { populate: { author: { select: 'name,email' } } }
1513
+ // This is required for MongoKit's advanced populate options to work
1514
+ querystringParser: (str) => qs.parse(str),
1510
1515
  ajv: {
1511
1516
  customOptions: {
1512
1517
  coerceTypes: true,
@@ -1,4 +1,4 @@
1
- export { ac as HookContext, ad as HookHandler, aH as HookOperation, aG as HookPhase, aI as HookRegistration, H as HookSystem, aJ as HookSystemOptions, aB as afterCreate, aF as afterDelete, aD as afterUpdate, aA as beforeCreate, aE as beforeDelete, aC as beforeUpdate, az as createHookSystem, ab as hookSystem } from '../index-D5QTob1X.js';
1
+ export { ac as HookContext, ad as HookHandler, aI as HookOperation, aH as HookPhase, aJ as HookRegistration, H as HookSystem, aK as HookSystemOptions, aC as afterCreate, aG as afterDelete, aE as afterUpdate, aB as beforeCreate, aF as beforeDelete, aD as beforeUpdate, aA as createHookSystem, ab as hookSystem } from '../index-B4t03KQ0.js';
2
2
  import 'mongoose';
3
3
  import 'fastify';
4
4
  import '../types-B99TBmFV.js';
@@ -553,7 +553,13 @@ interface ControllerQueryOptions {
553
553
  page?: number;
554
554
  limit?: number;
555
555
  sort?: string | Record<string, 1 | -1>;
556
+ /** Simple populate (comma-separated string or array) */
556
557
  populate?: string | string[] | Record<string, unknown>;
558
+ /**
559
+ * Advanced populate options (Mongoose-compatible)
560
+ * When set, takes precedence over simple `populate`
561
+ */
562
+ populateOptions?: PopulateOption[];
557
563
  select?: string | string[] | Record<string, 0 | 1>;
558
564
  filters?: Record<string, unknown>;
559
565
  search?: string;
@@ -565,19 +571,57 @@ interface ControllerQueryOptions {
565
571
  /** Allow additional options */
566
572
  [key: string]: unknown;
567
573
  }
574
+ /**
575
+ * Mongoose-compatible populate option for advanced field selection
576
+ * Used when you need to select specific fields from populated documents
577
+ *
578
+ * @example
579
+ * ```typescript
580
+ * // URL: ?populate[author][select]=name,email
581
+ * // Generates: { path: 'author', select: 'name email' }
582
+ * ```
583
+ */
584
+ interface PopulateOption {
585
+ /** Field path to populate */
586
+ path: string;
587
+ /** Fields to select (space-separated) */
588
+ select?: string;
589
+ /** Filter conditions for populated documents */
590
+ match?: Record<string, unknown>;
591
+ /** Query options (limit, sort, skip) */
592
+ options?: {
593
+ limit?: number;
594
+ sort?: Record<string, 1 | -1>;
595
+ skip?: number;
596
+ };
597
+ /** Nested populate configuration */
598
+ populate?: PopulateOption;
599
+ }
568
600
  /**
569
601
  * Parsed query result from QueryParser
570
602
  * Includes pagination, sorting, filtering, etc.
603
+ *
604
+ * The index signature allows custom query parsers (like MongoKit's QueryParser)
605
+ * to add additional fields without breaking Arc's type system.
571
606
  */
572
607
  interface ParsedQuery {
573
608
  filters?: Record<string, unknown>;
574
609
  limit?: number;
575
610
  sort?: string | Record<string, 1 | -1>;
611
+ /** Simple populate (comma-separated string or array) */
576
612
  populate?: string | string[] | Record<string, unknown>;
613
+ /**
614
+ * Advanced populate options (Mongoose-compatible)
615
+ * When set, takes precedence over simple `populate`
616
+ * @example [{ path: 'author', select: 'name email' }]
617
+ */
618
+ populateOptions?: PopulateOption[];
577
619
  search?: string;
578
620
  page?: number;
579
621
  after?: string;
580
622
  select?: string | string[] | Record<string, 0 | 1>;
623
+ /** Allow additional fields from custom query parsers */
624
+ [key: string]: unknown;
581
625
  }
582
626
  /**
583
627
  * Query Parser Interface
@@ -1319,4 +1363,4 @@ interface ValidationResult {
1319
1363
  }
1320
1364
  type AdapterFactory<TDoc> = (config: unknown) => DataAdapter<TDoc>;
1321
1365
 
1322
- export { type InferDocType as $, type AnyRecord as A, type CrudController as B, type ControllerQueryOptions as C, type DataAdapter as D, type FieldRule as E, type FieldMetadata as F, type CrudSchemas as G, HookSystem as H, type IController as I, type JWTPayload as J, type AdditionalRoute as K, type PresetFunction as L, type MiddlewareConfig as M, type EventDefinition as N, type OwnershipCheck as O, type PaginatedResult as P, type QueryParserInterface as Q, type RouteSchemaOptions as R, type ServiceContext as S, type ResourceMetadata as T, type UserOrganization as U, type ValidationResult as V, type RegistryEntry as W, type RegistryStats as X, type IntrospectionData as Y, type OrgScopeOptions as Z, type CrudRouterOptions as _, type IRequestContext as a, type InferResourceDoc as a0, type TypedResourceConfig as a1, type TypedController as a2, type TypedRepository as a3, type ConfigError as a4, type ValidationResult$1 as a5, type ValidateOptions as a6, type HealthCheck as a7, type HealthOptions as a8, type GracefulShutdownOptions as a9, beforeCreate as aA, afterCreate as aB, beforeUpdate as aC, afterUpdate as aD, beforeDelete as aE, afterDelete as aF, type HookPhase as aG, type HookOperation as aH, type HookRegistration as aI, type HookSystemOptions as aJ, type RequestIdOptions as aa, hookSystem as ab, type HookContext as ac, type HookHandler as ad, type ParsedQuery as ae, type OpenApiSchemas as af, type AdapterFactory as ag, type ObjectId as ah, type UserLike as ai, getUserId as aj, type ArcDecorator as ak, type EventsDecorator as al, type ResourcePermissions as am, type ResourceHooks as an, type MiddlewareHandler as ao, type JwtContext as ap, type AuthenticatorContext as aq, type Authenticator as ar, type TokenPair as as, type PresetHook as at, type BaseControllerOptions as au, type PaginationParams as av, type InferDoc as aw, type ControllerHandler as ax, type FastifyHandler as ay, createHookSystem as az, type RequestContext as b, type IControllerResponse as c, type RequestWithExtras as d, type CrudRouteKey as e, type PresetResult as f, type AuthHelpers as g, type AuthPluginOptions as h, type IntrospectionPluginOptions as i, ResourceRegistry as j, type RegisterOptions as k, type ResourceConfig as l, type SchemaMetadata as m, type RelationMetadata as n, type RepositoryLike as o, defineResource as p, ResourceDefinition as q, resourceRegistry as r, type ApiResponse as s, type ControllerLike as t, type FastifyRequestExtras as u, type FastifyWithAuth as v, type FastifyWithDecorators as w, type QueryOptions as x, type CrudRepository as y, type RouteHandler as z };
1366
+ export { type InferDocType as $, type AnyRecord as A, type CrudController as B, type ControllerQueryOptions as C, type DataAdapter as D, type FieldRule as E, type FieldMetadata as F, type CrudSchemas as G, HookSystem as H, type IController as I, type JWTPayload as J, type AdditionalRoute as K, type PresetFunction as L, type MiddlewareConfig as M, type EventDefinition as N, type OwnershipCheck as O, type PaginatedResult as P, type QueryParserInterface as Q, type RouteSchemaOptions as R, type ServiceContext as S, type ResourceMetadata as T, type UserOrganization as U, type ValidationResult as V, type RegistryEntry as W, type RegistryStats as X, type IntrospectionData as Y, type OrgScopeOptions as Z, type CrudRouterOptions as _, type IRequestContext as a, type InferResourceDoc as a0, type TypedResourceConfig as a1, type TypedController as a2, type TypedRepository as a3, type ConfigError as a4, type ValidationResult$1 as a5, type ValidateOptions as a6, type HealthCheck as a7, type HealthOptions as a8, type GracefulShutdownOptions as a9, createHookSystem as aA, beforeCreate as aB, afterCreate as aC, beforeUpdate as aD, afterUpdate as aE, beforeDelete as aF, afterDelete as aG, type HookPhase as aH, type HookOperation as aI, type HookRegistration as aJ, type HookSystemOptions as aK, type RequestIdOptions as aa, hookSystem as ab, type HookContext as ac, type HookHandler as ad, type ParsedQuery as ae, type OpenApiSchemas as af, type AdapterFactory as ag, type ObjectId as ah, type UserLike as ai, getUserId as aj, type PopulateOption as ak, type ArcDecorator as al, type EventsDecorator as am, type ResourcePermissions as an, type ResourceHooks as ao, type MiddlewareHandler as ap, type JwtContext as aq, type AuthenticatorContext as ar, type Authenticator as as, type TokenPair as at, type PresetHook as au, type BaseControllerOptions as av, type PaginationParams as aw, type InferDoc as ax, type ControllerHandler as ay, type FastifyHandler as az, type RequestContext as b, type IControllerResponse as c, type RequestWithExtras as d, type CrudRouteKey as e, type PresetResult as f, type AuthHelpers as g, type AuthPluginOptions as h, type IntrospectionPluginOptions as i, ResourceRegistry as j, type RegisterOptions as k, type ResourceConfig as l, type SchemaMetadata as m, type RelationMetadata as n, type RepositoryLike as o, defineResource as p, ResourceDefinition as q, resourceRegistry as r, type ApiResponse as s, type ControllerLike as t, type FastifyRequestExtras as u, type FastifyWithAuth as v, type FastifyWithDecorators as w, type QueryOptions as x, type CrudRepository as y, type RouteHandler as z };
package/dist/index.d.ts CHANGED
@@ -1,15 +1,15 @@
1
- import { l as ResourceConfig } from './index-D5QTob1X.js';
2
- export { V as AdapterValidationResult, K as AdditionalRoute, A as AnyRecord, s as ApiResponse, h as AuthPluginOptions, a4 as ConfigError, t as ControllerLike, B as CrudController, y as CrudRepository, e as CrudRouteKey, _ as CrudRouterOptions, G as CrudSchemas, D as DataAdapter, N as EventDefinition, u as FastifyRequestExtras, v as FastifyWithAuth, w as FastifyWithDecorators, F as FieldMetadata, E as FieldRule, a9 as GracefulShutdownOptions, a7 as HealthCheck, a8 as HealthOptions, ac as HookContext, ad as HookHandler, H as HookSystem, I as IController, c as IControllerResponse, a as IRequestContext, $ as InferDocType, a0 as InferResourceDoc, Y as IntrospectionData, i as IntrospectionPluginOptions, J as JWTPayload, M as MiddlewareConfig, Z as OrgScopeOptions, O as OwnershipCheck, P as PaginatedResult, L as PresetFunction, f as PresetResult, x as QueryOptions, W as RegistryEntry, X as RegistryStats, n as RelationMetadata, o as RepositoryLike, b as RequestContext, aa as RequestIdOptions, d as RequestWithExtras, q as ResourceDefinition, T as ResourceMetadata, z as RouteHandler, R as RouteSchemaOptions, m as SchemaMetadata, S as ServiceContext, a2 as TypedController, a3 as TypedRepository, a1 as TypedResourceConfig, U as UserOrganization, a6 as ValidateOptions, a5 as ValidationResult, p as defineResource, ab as hookSystem, r as resourceRegistry } from './index-D5QTob1X.js';
1
+ import { l as ResourceConfig } from './index-B4t03KQ0.js';
2
+ export { V as AdapterValidationResult, K as AdditionalRoute, A as AnyRecord, s as ApiResponse, h as AuthPluginOptions, a4 as ConfigError, t as ControllerLike, B as CrudController, y as CrudRepository, e as CrudRouteKey, _ as CrudRouterOptions, G as CrudSchemas, D as DataAdapter, N as EventDefinition, u as FastifyRequestExtras, v as FastifyWithAuth, w as FastifyWithDecorators, F as FieldMetadata, E as FieldRule, a9 as GracefulShutdownOptions, a7 as HealthCheck, a8 as HealthOptions, ac as HookContext, ad as HookHandler, H as HookSystem, I as IController, c as IControllerResponse, a as IRequestContext, $ as InferDocType, a0 as InferResourceDoc, Y as IntrospectionData, i as IntrospectionPluginOptions, J as JWTPayload, M as MiddlewareConfig, Z as OrgScopeOptions, O as OwnershipCheck, P as PaginatedResult, L as PresetFunction, f as PresetResult, x as QueryOptions, W as RegistryEntry, X as RegistryStats, n as RelationMetadata, o as RepositoryLike, b as RequestContext, aa as RequestIdOptions, d as RequestWithExtras, q as ResourceDefinition, T as ResourceMetadata, z as RouteHandler, R as RouteSchemaOptions, m as SchemaMetadata, S as ServiceContext, a2 as TypedController, a3 as TypedRepository, a1 as TypedResourceConfig, U as UserOrganization, a6 as ValidateOptions, a5 as ValidationResult, p as defineResource, ab as hookSystem, r as resourceRegistry } from './index-B4t03KQ0.js';
3
3
  export { MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, createMongooseAdapter, createPrismaAdapter } from './adapters/index.js';
4
- export { B as BaseController, a as BaseControllerOptions } from './BaseController-nNRS3vpA.js';
4
+ export { B as BaseController, a as BaseControllerOptions } from './BaseController-DVAiHxEQ.js';
5
5
  export { RouteHandlerMethod } from 'fastify';
6
6
  export { P as PermissionCheck, a as PermissionContext, b as PermissionResult, U as UserBase } from './types-B99TBmFV.js';
7
7
  export { A as ArcError, F as ForbiddenError, N as NotFoundError, U as UnauthorizedError, V as ValidationError } from './errors-8WIxGS_6.js';
8
- export { e as gracefulShutdownPlugin, a as healthPlugin, _ as requestIdPlugin } from './arcCorePlugin-CAjBQtZB.js';
8
+ export { e as gracefulShutdownPlugin, a as healthPlugin, _ as requestIdPlugin } from './arcCorePlugin-CsShQdyP.js';
9
9
  export { DomainEvent, EventHandler, eventPlugin } from './events/index.js';
10
10
  export { allOf, allowPublic, anyOf, denyAll, requireAuth, requireOwnership, requireRoles, when } from './permissions/index.js';
11
- export { A as ArcFactory, c as createApp } from './createApp-CjN9zZSL.js';
12
- export { C as CreateAppOptions } from './types-zpN48n6B.js';
11
+ export { A as ArcFactory, c as createApp } from './createApp-Ce9wl8W9.js';
12
+ export { C as CreateAppOptions } from './types-BvckRbs2.js';
13
13
  import 'mongoose';
14
14
  import '@fastify/cors';
15
15
  import '@fastify/helmet';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import fp from 'fastify-plugin';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { createRequire } from 'module';
4
4
  import Fastify from 'fastify';
5
+ import qs from 'qs';
5
6
 
6
7
  var __defProp = Object.defineProperty;
7
8
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -2175,6 +2176,23 @@ var ArcQueryParser = class {
2175
2176
  for (const [key, value] of Object.entries(query)) {
2176
2177
  if (reservedKeys.has(key)) continue;
2177
2178
  if (value === void 0 || value === null) continue;
2179
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key)) continue;
2180
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2181
+ const operatorObj = value;
2182
+ const operatorKeys = Object.keys(operatorObj);
2183
+ const allOperators = operatorKeys.every((op) => this.operators[op]);
2184
+ if (allOperators && operatorKeys.length > 0) {
2185
+ const mongoFilters = {};
2186
+ for (const [op, opValue] of Object.entries(operatorObj)) {
2187
+ const mongoOp = this.operators[op];
2188
+ if (mongoOp) {
2189
+ mongoFilters[mongoOp] = this.parseFilterValue(opValue, op);
2190
+ }
2191
+ }
2192
+ filters[key] = mongoFilters;
2193
+ continue;
2194
+ }
2195
+ }
2178
2196
  const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
2179
2197
  if (!match) continue;
2180
2198
  const [, fieldName, operator] = match;
@@ -2316,6 +2334,8 @@ var BaseController = class _BaseController {
2316
2334
  sort: sortString,
2317
2335
  select: this._sanitizeSelect(selectString, this.schemaOptions),
2318
2336
  populate: this._sanitizePopulate(parsed.populate, this.schemaOptions),
2337
+ // Advanced populate options from MongoKit QueryParser (takes precedence over simple populate)
2338
+ populateOptions: parsed.populateOptions,
2319
2339
  filters: parsed.filters,
2320
2340
  // MongoKit features
2321
2341
  search: parsed.search,
@@ -2431,7 +2451,7 @@ var BaseController = class _BaseController {
2431
2451
  return true;
2432
2452
  }
2433
2453
  }
2434
- return itemValue === filterValue;
2454
+ return String(itemValue) === String(filterValue);
2435
2455
  }
2436
2456
  /**
2437
2457
  * Check if item matches policy filters (for get/update/delete operations)
@@ -2587,7 +2607,10 @@ var BaseController = class _BaseController {
2587
2607
  const arcContext = req.metadata;
2588
2608
  try {
2589
2609
  const item = await this.repository.getById(id, options);
2590
- if (!item || !this._checkOrgScope(item, arcContext) || !this._checkPolicyFilters(item, req)) {
2610
+ const hasItem = !!item;
2611
+ const orgScopeOk = this._checkOrgScope(item, arcContext);
2612
+ const policyFiltersOk = this._checkPolicyFilters(item, req);
2613
+ if (!hasItem || !orgScopeOk || !policyFiltersOk) {
2591
2614
  return {
2592
2615
  success: false,
2593
2616
  error: "Resource not found",
@@ -4586,6 +4609,10 @@ async function createApp(options) {
4586
4609
  const fastify = Fastify({
4587
4610
  logger: config.logger ?? true,
4588
4611
  trustProxy: config.trustProxy ?? false,
4612
+ // Use qs parser to support nested bracket notation in query strings
4613
+ // e.g., ?populate[author][select]=name,email → { populate: { author: { select: 'name,email' } } }
4614
+ // This is required for MongoKit's advanced populate options to work
4615
+ querystringParser: (str) => qs.parse(str),
4589
4616
  ajv: {
4590
4617
  customOptions: {
4591
4618
  coerceTypes: true,
@@ -1,5 +1,5 @@
1
1
  import { RouteHandlerMethod, FastifyPluginAsync } from 'fastify';
2
- import { b as RequestContext, Z as OrgScopeOptions, z as RouteHandler } from '../index-D5QTob1X.js';
2
+ import { b as RequestContext, Z as OrgScopeOptions, z as RouteHandler } from '../index-B4t03KQ0.js';
3
3
  import { U as UserBase } from '../types-B99TBmFV.js';
4
4
  import 'mongoose';
5
5
 
@@ -1,6 +1,6 @@
1
- export { k as ArcCore, A as ArcCorePluginOptions, G as GracefulShutdownOptions, b as HealthCheck, H as HealthOptions, R as RequestIdOptions, T as TracingOptions, f as arcCorePlugin, j as arcCorePluginFn, d as createSpan, e as gracefulShutdownPlugin, g as gracefulShutdownPluginFn, a as healthPlugin, h as healthPluginFn, i as isTracingAvailable, _ as requestIdPlugin, r as requestIdPluginFn, t as traced, c as tracingPlugin } from '../arcCorePlugin-CAjBQtZB.js';
1
+ export { k as ArcCore, A as ArcCorePluginOptions, G as GracefulShutdownOptions, b as HealthCheck, H as HealthOptions, R as RequestIdOptions, T as TracingOptions, f as arcCorePlugin, j as arcCorePluginFn, d as createSpan, e as gracefulShutdownPlugin, g as gracefulShutdownPluginFn, a as healthPlugin, h as healthPluginFn, i as isTracingAvailable, _ as requestIdPlugin, r as requestIdPluginFn, t as traced, c as tracingPlugin } from '../arcCorePlugin-CsShQdyP.js';
2
2
  import { FastifyInstance, FastifyRequest } from 'fastify';
3
- import '../index-D5QTob1X.js';
3
+ import '../index-B4t03KQ0.js';
4
4
  import 'mongoose';
5
5
  import '../types-B99TBmFV.js';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { MultiTenantOptions } from './multiTenant.js';
2
2
  export { default as multiTenantPreset } from './multiTenant.js';
3
- import { f as PresetResult, a as IRequestContext, c as IControllerResponse, P as PaginatedResult, A as AnyRecord, l as ResourceConfig } from '../index-D5QTob1X.js';
3
+ import { f as PresetResult, a as IRequestContext, c as IControllerResponse, P as PaginatedResult, A as AnyRecord, l as ResourceConfig } from '../index-B4t03KQ0.js';
4
4
  import 'mongoose';
5
5
  import 'fastify';
6
6
  import '../types-B99TBmFV.js';
@@ -1,4 +1,4 @@
1
- import { d as RequestWithExtras, e as CrudRouteKey, f as PresetResult } from '../index-D5QTob1X.js';
1
+ import { d as RequestWithExtras, e as CrudRouteKey, f as PresetResult } from '../index-B4t03KQ0.js';
2
2
  import 'mongoose';
3
3
  import 'fastify';
4
4
  import '../types-B99TBmFV.js';
@@ -1,5 +1,5 @@
1
- import { i as IntrospectionPluginOptions } from '../index-D5QTob1X.js';
2
- export { k as RegisterOptions, j as ResourceRegistry, r as resourceRegistry } from '../index-D5QTob1X.js';
1
+ import { i as IntrospectionPluginOptions } from '../index-B4t03KQ0.js';
2
+ export { k as RegisterOptions, j as ResourceRegistry, r as resourceRegistry } from '../index-B4t03KQ0.js';
3
3
  import { FastifyPluginAsync } from 'fastify';
4
4
  import 'mongoose';
5
5
  import '../types-B99TBmFV.js';
@@ -1,6 +1,6 @@
1
- import { q as ResourceDefinition, A as AnyRecord, y as CrudRepository } from '../index-D5QTob1X.js';
1
+ import { q as ResourceDefinition, A as AnyRecord, y as CrudRepository } from '../index-B4t03KQ0.js';
2
2
  import Fastify, { FastifyInstance } from 'fastify';
3
- import { C as CreateAppOptions } from '../types-zpN48n6B.js';
3
+ import { C as CreateAppOptions } from '../types-BvckRbs2.js';
4
4
  import { Mock } from 'vitest';
5
5
  import { Connection } from 'mongoose';
6
6
  import '../types-B99TBmFV.js';
@@ -2,6 +2,7 @@ import fp from 'fastify-plugin';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { createRequire } from 'module';
4
4
  import Fastify from 'fastify';
5
+ import qs from 'qs';
5
6
  import { describe, beforeAll, afterAll, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
7
  import mongoose from 'mongoose';
7
8
 
@@ -46964,6 +46965,10 @@ async function createApp(options2) {
46964
46965
  const fastify = Fastify({
46965
46966
  logger: config.logger ?? true,
46966
46967
  trustProxy: config.trustProxy ?? false,
46968
+ // Use qs parser to support nested bracket notation in query strings
46969
+ // e.g., ?populate[author][select]=name,email → { populate: { author: { select: 'name,email' } } }
46970
+ // This is required for MongoKit's advanced populate options to work
46971
+ querystringParser: (str) => qs.parse(str),
46967
46972
  ajv: {
46968
46973
  customOptions: {
46969
46974
  coerceTypes: true,
@@ -1,4 +1,4 @@
1
- export { K as AdditionalRoute, A as AnyRecord, s as ApiResponse, ak as ArcDecorator, g as AuthHelpers, h as AuthPluginOptions, ar as Authenticator, aq as AuthenticatorContext, au as BaseControllerOptions, a4 as ConfigError, ax as ControllerHandler, t as ControllerLike, C as ControllerQueryOptions, B as CrudController, y as CrudRepository, e as CrudRouteKey, _ as CrudRouterOptions, G as CrudSchemas, N as EventDefinition, al as EventsDecorator, ay as FastifyHandler, u as FastifyRequestExtras, v as FastifyWithAuth, w as FastifyWithDecorators, E as FieldRule, a9 as GracefulShutdownOptions, a7 as HealthCheck, a8 as HealthOptions, I as IController, c as IControllerResponse, a as IRequestContext, aw as InferDoc, $ as InferDocType, a0 as InferResourceDoc, Y as IntrospectionData, i as IntrospectionPluginOptions, J as JWTPayload, ap as JwtContext, M as MiddlewareConfig, ao as MiddlewareHandler, ah as ObjectId, af as OpenApiSchemas, Z as OrgScopeOptions, O as OwnershipCheck, P as PaginatedResult, av as PaginationParams, ae as ParsedQuery, L as PresetFunction, at as PresetHook, f as PresetResult, x as QueryOptions, Q as QueryParserInterface, W as RegistryEntry, X as RegistryStats, b as RequestContext, aa as RequestIdOptions, d as RequestWithExtras, l as ResourceConfig, an as ResourceHooks, T as ResourceMetadata, am as ResourcePermissions, z as RouteHandler, R as RouteSchemaOptions, S as ServiceContext, as as TokenPair, a2 as TypedController, a3 as TypedRepository, a1 as TypedResourceConfig, ai as UserLike, U as UserOrganization, a6 as ValidateOptions, a5 as ValidationResult, aj as getUserId } from '../index-D5QTob1X.js';
1
+ export { K as AdditionalRoute, A as AnyRecord, s as ApiResponse, al as ArcDecorator, g as AuthHelpers, h as AuthPluginOptions, as as Authenticator, ar as AuthenticatorContext, av as BaseControllerOptions, a4 as ConfigError, ay as ControllerHandler, t as ControllerLike, C as ControllerQueryOptions, B as CrudController, y as CrudRepository, e as CrudRouteKey, _ as CrudRouterOptions, G as CrudSchemas, N as EventDefinition, am as EventsDecorator, az as FastifyHandler, u as FastifyRequestExtras, v as FastifyWithAuth, w as FastifyWithDecorators, E as FieldRule, a9 as GracefulShutdownOptions, a7 as HealthCheck, a8 as HealthOptions, I as IController, c as IControllerResponse, a as IRequestContext, ax as InferDoc, $ as InferDocType, a0 as InferResourceDoc, Y as IntrospectionData, i as IntrospectionPluginOptions, J as JWTPayload, aq as JwtContext, M as MiddlewareConfig, ap as MiddlewareHandler, ah as ObjectId, af as OpenApiSchemas, Z as OrgScopeOptions, O as OwnershipCheck, P as PaginatedResult, aw as PaginationParams, ae as ParsedQuery, ak as PopulateOption, L as PresetFunction, au as PresetHook, f as PresetResult, x as QueryOptions, Q as QueryParserInterface, W as RegistryEntry, X as RegistryStats, b as RequestContext, aa as RequestIdOptions, d as RequestWithExtras, l as ResourceConfig, ao as ResourceHooks, T as ResourceMetadata, an as ResourcePermissions, z as RouteHandler, R as RouteSchemaOptions, S as ServiceContext, at as TokenPair, a2 as TypedController, a3 as TypedRepository, a1 as TypedResourceConfig, ai as UserLike, U as UserOrganization, a6 as ValidateOptions, a5 as ValidationResult, aj as getUserId } from '../index-B4t03KQ0.js';
2
2
  export { RouteHandlerMethod } from 'fastify';
3
3
  export { Document, Model } from 'mongoose';
4
4
  export { P as PermissionCheck, a as PermissionContext, b as PermissionResult, U as UserBase } from '../types-B99TBmFV.js';
@@ -2,7 +2,7 @@ import { FastifyServerOptions } from 'fastify';
2
2
  import { FastifyCorsOptions } from '@fastify/cors';
3
3
  import { FastifyHelmetOptions } from '@fastify/helmet';
4
4
  import { RateLimitOptions } from '@fastify/rate-limit';
5
- import { h as AuthPluginOptions } from './index-D5QTob1X.js';
5
+ import { h as AuthPluginOptions } from './index-B4t03KQ0.js';
6
6
 
7
7
  /**
8
8
  * Types for createApp factory
@@ -1,5 +1,5 @@
1
1
  export { A as ArcError, C as ConflictError, E as ErrorDetails, F as ForbiddenError, N as NotFoundError, a as OrgAccessDeniedError, O as OrgRequiredError, R as RateLimitError, S as ServiceUnavailableError, U as UnauthorizedError, V as ValidationError, c as createError, i as isArcError } from '../errors-8WIxGS_6.js';
2
- import { A as AnyRecord, Q as QueryParserInterface, ae as ParsedQuery } from '../index-D5QTob1X.js';
2
+ import { A as AnyRecord, Q as QueryParserInterface, ae as ParsedQuery } from '../index-B4t03KQ0.js';
3
3
  import 'mongoose';
4
4
  import 'fastify';
5
5
  import '../types-B99TBmFV.js';
@@ -851,6 +851,23 @@ var ArcQueryParser = class {
851
851
  for (const [key, value] of Object.entries(query)) {
852
852
  if (reservedKeys.has(key)) continue;
853
853
  if (value === void 0 || value === null) continue;
854
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key)) continue;
855
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
856
+ const operatorObj = value;
857
+ const operatorKeys = Object.keys(operatorObj);
858
+ const allOperators = operatorKeys.every((op) => this.operators[op]);
859
+ if (allOperators && operatorKeys.length > 0) {
860
+ const mongoFilters = {};
861
+ for (const [op, opValue] of Object.entries(operatorObj)) {
862
+ const mongoOp = this.operators[op];
863
+ if (mongoOp) {
864
+ mongoFilters[mongoOp] = this.parseFilterValue(opValue, op);
865
+ }
866
+ }
867
+ filters[key] = mongoFilters;
868
+ continue;
869
+ }
870
+ }
854
871
  const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
855
872
  if (!match) continue;
856
873
  const [, fieldName, operator] = match;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "Resource-oriented backend framework for Fastify + MongoDB",
5
5
  "type": "module",
6
6
  "exports": {
@@ -119,16 +119,16 @@
119
119
  "node": ">=20"
120
120
  },
121
121
  "peerDependencies": {
122
- "@classytic/mongokit": "^3.0.0",
123
- "fastify": "^5.0.0",
124
- "mongoose": "^8.0.0 || ^9.0.0",
122
+ "@classytic/mongokit": "^3.1.6",
125
123
  "@fastify/cors": "^11.0.0",
126
124
  "@fastify/helmet": "^13.0.0",
125
+ "@fastify/multipart": "^9.0.0",
127
126
  "@fastify/rate-limit": "^10.0.0",
128
127
  "@fastify/sensible": "^6.0.0",
129
128
  "@fastify/under-pressure": "^9.0.0",
130
- "@fastify/multipart": "^9.0.0",
129
+ "fastify": "^5.0.0",
131
130
  "fastify-raw-body": "^5.0.0",
131
+ "mongoose": "^8.0.0 || ^9.0.0",
132
132
  "pino-pretty": "^13.0.0"
133
133
  },
134
134
  "peerDependenciesMeta": {
@@ -185,14 +185,16 @@
185
185
  }
186
186
  },
187
187
  "dependencies": {
188
- "fastify-plugin": "^5.0.1"
188
+ "fastify-plugin": "^5.0.1",
189
+ "qs": "^6.14.1"
189
190
  },
190
191
  "devDependencies": {
191
- "@classytic/mongokit": "^3.1.0",
192
+ "@classytic/mongokit": "^3.1.6",
192
193
  "@fastify/jwt": "^9.0.0",
193
194
  "@fastify/multipart": "^9.0.0",
194
195
  "@types/jsonwebtoken": "^9.0.0",
195
196
  "@types/node": "^22.10.0",
197
+ "@types/qs": "^6.14.0",
196
198
  "fastify-raw-body": "^5.0.0",
197
199
  "jsonwebtoken": "^9.0.0",
198
200
  "mongodb-memory-server": "^11.0.1",
@@ -218,4 +220,4 @@
218
220
  "type": "git",
219
221
  "url": "https://github.com/classytic/arc.git"
220
222
  }
221
- }
223
+ }