@classytic/mongokit 3.0.3 → 3.0.5

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,113 +1,6 @@
1
- export { b as createError, c as createFieldPreset, d as createMemoryCache, f as filterResponseData, g as getFieldsForUser, a as getMongooseProjection } from '../memory-cache-Bn_-Kk-0.js';
2
- import { v as ParsedQuery, x as SchemaBuilderOptions, y as CrudSchemas, V as ValidationResult, S as SelectSpec, g as PopulateSpec, h as SortSpec } from '../types-B3dPUKjs.js';
3
- import mongoose__default, { Schema } from 'mongoose';
4
-
5
- /**
6
- * Query Parser
7
- *
8
- * Parses HTTP query parameters into MongoDB-compatible query objects.
9
- * Supports operators, pagination, sorting, and filtering.
10
- */
11
-
12
- declare class QueryParser {
13
- private operators;
14
- /**
15
- * Dangerous MongoDB operators that should never be accepted from user input
16
- * Security: Prevent NoSQL injection attacks
17
- */
18
- private dangerousOperators;
19
- /**
20
- * Parse query parameters into MongoDB query format
21
- */
22
- parseQuery(query: Record<string, unknown> | null | undefined): ParsedQuery;
23
- /**
24
- * Parse sort parameter
25
- * Converts string like '-createdAt' to { createdAt: -1 }
26
- * Handles multiple sorts: '-createdAt,name' → { createdAt: -1, name: 1 }
27
- */
28
- private _parseSort;
29
- /**
30
- * Parse standard filter parameter (filter[field]=value)
31
- */
32
- private _parseFilters;
33
- /**
34
- * Handle operator syntax: field[operator]=value
35
- */
36
- private _handleOperatorSyntax;
37
- /**
38
- * Handle bracket syntax with object value
39
- */
40
- private _handleBracketSyntax;
41
- /**
42
- * Convert operator to MongoDB format
43
- */
44
- private _toMongoOperator;
45
- /**
46
- * Convert values based on operator type
47
- */
48
- private _convertValue;
49
- /**
50
- * Parse $or conditions
51
- */
52
- private _parseOr;
53
- /**
54
- * Enhance filters with between operator
55
- */
56
- private _enhanceWithBetween;
57
- }
58
- declare const _default: QueryParser;
59
-
60
- /**
61
- * Mongoose to JSON Schema Converter with Field Rules
62
- *
63
- * Generates Fastify JSON schemas from Mongoose models with declarative field rules.
64
- *
65
- * Field Rules (options.fieldRules):
66
- * - immutable: Field cannot be updated (omitted from update schema)
67
- * - immutableAfterCreate: Alias for immutable
68
- * - systemManaged: System-only field (omitted from create/update)
69
- * - optional: Remove from required array
70
- *
71
- * Additional Options:
72
- * - strictAdditionalProperties: Set to true to add "additionalProperties: false" to schemas
73
- * This makes Fastify reject unknown fields at validation level (default: false for backward compatibility)
74
- *
75
- * @example
76
- * buildCrudSchemasFromModel(Model, {
77
- * strictAdditionalProperties: true, // Reject unknown fields
78
- * fieldRules: {
79
- * organizationId: { immutable: true },
80
- * status: { systemManaged: true },
81
- * },
82
- * create: { omitFields: ['verifiedAt'] },
83
- * update: { omitFields: ['customerId'] }
84
- * })
85
- */
86
-
87
- /**
88
- * Build CRUD schemas from Mongoose schema
89
- */
90
- declare function buildCrudSchemasFromMongooseSchema(mongooseSchema: Schema, options?: SchemaBuilderOptions): CrudSchemas;
91
- /**
92
- * Build CRUD schemas from Mongoose model
93
- */
94
- declare function buildCrudSchemasFromModel(mongooseModel: mongoose__default.Model<unknown>, options?: SchemaBuilderOptions): CrudSchemas;
95
- /**
96
- * Get fields that are immutable (cannot be updated)
97
- */
98
- declare function getImmutableFields(options?: SchemaBuilderOptions): string[];
99
- /**
100
- * Get fields that are system-managed (cannot be set by users)
101
- */
102
- declare function getSystemManagedFields(options?: SchemaBuilderOptions): string[];
103
- /**
104
- * Check if field is allowed in update
105
- */
106
- declare function isFieldUpdateAllowed(fieldName: string, options?: SchemaBuilderOptions): boolean;
107
- /**
108
- * Validate update body against field rules
109
- */
110
- declare function validateUpdateBody(body?: Record<string, unknown>, options?: SchemaBuilderOptions): ValidationResult;
1
+ export { F as FilterValue, O as OperatorMap, Q as QueryParser, b as QueryParserOptions, h as buildCrudSchemasFromModel, e as buildCrudSchemasFromMongooseSchema, l as createError, c as createFieldPreset, m as createMemoryCache, f as filterResponseData, g as getFieldsForUser, i as getImmutableFields, a as getMongooseProjection, j as getSystemManagedFields, k as isFieldUpdateAllowed, d as queryParser, v as validateUpdateBody } from '../queryParser-CxzCjzXd.js';
2
+ import { S as SelectSpec, e as PopulateSpec, f as SortSpec } from '../types-CHIDluaP.js';
3
+ import 'mongoose';
111
4
 
112
5
  /**
113
6
  * Cache Key Utilities
@@ -186,4 +79,4 @@ declare function modelPattern(prefix: string, model: string): string;
186
79
  */
187
80
  declare function listPattern(prefix: string, model: string): string;
188
81
 
189
- export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, _default as queryParser, validateUpdateBody, versionKey };
82
+ export { byIdKey, byQueryKey, listPattern, listQueryKey, modelPattern, versionKey };
@@ -46,6 +46,7 @@ function createFieldPreset(config) {
46
46
  };
47
47
  }
48
48
  var QueryParser = class {
49
+ options;
49
50
  operators = {
50
51
  eq: "$eq",
51
52
  ne: "$ne",
@@ -66,7 +67,26 @@ var QueryParser = class {
66
67
  * Dangerous MongoDB operators that should never be accepted from user input
67
68
  * Security: Prevent NoSQL injection attacks
68
69
  */
69
- dangerousOperators = ["$where", "$function", "$accumulator", "$expr"];
70
+ dangerousOperators;
71
+ /**
72
+ * Regex pattern characters that can cause catastrophic backtracking (ReDoS)
73
+ */
74
+ dangerousRegexPatterns = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\([^)]*\))\1|\(\?[^)]*\)|[\[\]].*[\[\]])/;
75
+ constructor(options = {}) {
76
+ this.options = {
77
+ maxRegexLength: options.maxRegexLength ?? 500,
78
+ maxSearchLength: options.maxSearchLength ?? 200,
79
+ maxFilterDepth: options.maxFilterDepth ?? 10,
80
+ additionalDangerousOperators: options.additionalDangerousOperators ?? []
81
+ };
82
+ this.dangerousOperators = [
83
+ "$where",
84
+ "$function",
85
+ "$accumulator",
86
+ "$expr",
87
+ ...this.options.additionalDangerousOperators
88
+ ];
89
+ }
70
90
  /**
71
91
  * Parse query parameters into MongoDB query format
72
92
  */
@@ -86,7 +106,7 @@ var QueryParser = class {
86
106
  limit: parseInt(String(limit), 10),
87
107
  sort: this._parseSort(sort),
88
108
  populate,
89
- search
109
+ search: this._sanitizeSearch(search)
90
110
  };
91
111
  if (after || cursor) {
92
112
  parsed.after = after || cursor;
@@ -126,6 +146,7 @@ var QueryParser = class {
126
146
  */
127
147
  _parseFilters(filters) {
128
148
  const parsedFilters = {};
149
+ const regexFields = {};
129
150
  for (const [key, value] of Object.entries(filters)) {
130
151
  if (this.dangerousOperators.includes(key) || key.startsWith("$") && !["$or", "$and"].includes(key)) {
131
152
  console.warn(`[mongokit] Blocked dangerous operator: ${key}`);
@@ -141,7 +162,7 @@ var QueryParser = class {
141
162
  console.warn(`[mongokit] Blocked dangerous operator: ${operator}`);
142
163
  continue;
143
164
  }
144
- this._handleOperatorSyntax(parsedFilters, {}, operatorMatch, value);
165
+ this._handleOperatorSyntax(parsedFilters, regexFields, operatorMatch, value);
145
166
  continue;
146
167
  }
147
168
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
@@ -165,8 +186,11 @@ var QueryParser = class {
165
186
  return;
166
187
  }
167
188
  if (operator.toLowerCase() === "contains" || operator.toLowerCase() === "like") {
168
- filters[field] = { $regex: new RegExp(String(value), "i") };
169
- regexFields[field] = true;
189
+ const safeRegex = this._createSafeRegex(value);
190
+ if (safeRegex) {
191
+ filters[field] = { $regex: safeRegex };
192
+ regexFields[field] = true;
193
+ }
170
194
  return;
171
195
  }
172
196
  const mongoOperator = this._toMongoOperator(operator);
@@ -217,7 +241,9 @@ var QueryParser = class {
217
241
  } else if (operator === "in" || operator === "nin") {
218
242
  processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
219
243
  } else if (operator === "like" || operator === "contains") {
220
- processedValue = value !== void 0 && value !== null ? new RegExp(String(value), "i") : /.*/;
244
+ const safeRegex = this._createSafeRegex(value);
245
+ if (!safeRegex) continue;
246
+ processedValue = safeRegex;
221
247
  } else {
222
248
  processedValue = this._convertValue(value);
223
249
  }
@@ -232,6 +258,56 @@ var QueryParser = class {
232
258
  const op = operator.toLowerCase();
233
259
  return op.startsWith("$") ? op : "$" + op;
234
260
  }
261
+ /**
262
+ * Create a safe regex pattern with protection against ReDoS attacks
263
+ * @param pattern - The pattern string from user input
264
+ * @param flags - Regex flags (default: 'i' for case-insensitive)
265
+ * @returns A safe RegExp or null if pattern is invalid/dangerous
266
+ */
267
+ _createSafeRegex(pattern, flags = "i") {
268
+ if (pattern === null || pattern === void 0) {
269
+ return null;
270
+ }
271
+ const patternStr = String(pattern);
272
+ if (patternStr.length > this.options.maxRegexLength) {
273
+ console.warn(`[mongokit] Regex pattern too long (${patternStr.length} > ${this.options.maxRegexLength}), truncating`);
274
+ return new RegExp(this._escapeRegex(patternStr.substring(0, this.options.maxRegexLength)), flags);
275
+ }
276
+ if (this.dangerousRegexPatterns.test(patternStr)) {
277
+ console.warn("[mongokit] Potentially dangerous regex pattern detected, escaping");
278
+ return new RegExp(this._escapeRegex(patternStr), flags);
279
+ }
280
+ try {
281
+ return new RegExp(patternStr, flags);
282
+ } catch {
283
+ return new RegExp(this._escapeRegex(patternStr), flags);
284
+ }
285
+ }
286
+ /**
287
+ * Escape special regex characters for literal matching
288
+ */
289
+ _escapeRegex(str) {
290
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
291
+ }
292
+ /**
293
+ * Sanitize text search query for MongoDB $text search
294
+ * @param search - Raw search input from user
295
+ * @returns Sanitized search string or undefined
296
+ */
297
+ _sanitizeSearch(search) {
298
+ if (search === null || search === void 0 || search === "") {
299
+ return void 0;
300
+ }
301
+ let searchStr = String(search).trim();
302
+ if (!searchStr) {
303
+ return void 0;
304
+ }
305
+ if (searchStr.length > this.options.maxSearchLength) {
306
+ console.warn(`[mongokit] Search query too long (${searchStr.length} > ${this.options.maxSearchLength}), truncating`);
307
+ searchStr = searchStr.substring(0, this.options.maxSearchLength);
308
+ }
309
+ return searchStr;
310
+ }
235
311
  /**
236
312
  * Convert values based on operator type
237
313
  */
@@ -282,7 +358,8 @@ var QueryParser = class {
282
358
  return output;
283
359
  }
284
360
  };
285
- var queryParser_default = new QueryParser();
361
+ var defaultQueryParser = new QueryParser();
362
+ var queryParser_default = defaultQueryParser;
286
363
  function isMongooseSchema(value) {
287
364
  return value instanceof mongoose2.Schema;
288
365
  }
@@ -302,14 +379,7 @@ function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
302
379
  required: ["id"]
303
380
  };
304
381
  const jsonQuery = buildJsonSchemaForQuery(tree, options);
305
- const crudSchemas = {
306
- create: { body: jsonCreate },
307
- update: { body: jsonUpdate, params: jsonParams },
308
- get: { params: jsonParams },
309
- list: { query: jsonQuery },
310
- remove: { params: jsonParams }
311
- };
312
- return { createBody: jsonCreate, updateBody: jsonUpdate, params: jsonParams, listQuery: jsonQuery, crudSchemas };
382
+ return { createBody: jsonCreate, updateBody: jsonUpdate, params: jsonParams, listQuery: jsonQuery };
313
383
  }
314
384
  function buildCrudSchemasFromModel(mongooseModel, options = {}) {
315
385
  if (!mongooseModel || !mongooseModel.schema) {
@@ -638,4 +708,4 @@ function listPattern(prefix, model) {
638
708
  return `${prefix}:list:${model}:*`;
639
709
  }
640
710
 
641
- export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, queryParser_default as queryParser, validateUpdateBody, versionKey };
711
+ export { QueryParser, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, queryParser_default as queryParser, validateUpdateBody, versionKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "Production-grade MongoDB repositories with zero dependencies - smart pagination, events, and plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,142 +0,0 @@
1
- import { q as UserContext, F as FieldPreset, H as HttpError, T as CacheAdapter } from './types-B3dPUKjs.js';
2
-
3
- /**
4
- * Field Selection Utilities
5
- *
6
- * Provides explicit, performant field filtering using Mongoose projections.
7
- *
8
- * Philosophy:
9
- * - Explicit is better than implicit
10
- * - Filter at DB level (10x faster than in-memory)
11
- * - Progressive disclosure (show more fields as trust increases)
12
- *
13
- * @example
14
- * ```typescript
15
- * // For Mongoose queries (PREFERRED - 90% of cases)
16
- * const projection = getMongooseProjection(request.user, fieldPresets.gymPlans);
17
- * const plans = await GymPlan.find().select(projection).lean();
18
- *
19
- * // For complex data (10% of cases - aggregations, multiple sources)
20
- * const filtered = filterResponseData(complexData, fieldPresets.gymPlans, request.user);
21
- * ```
22
- */
23
-
24
- /**
25
- * Get allowed fields for a user based on their context
26
- *
27
- * @param user - User object from request.user (or null for public)
28
- * @param preset - Field preset configuration
29
- * @returns Array of allowed field names
30
- *
31
- * @example
32
- * const fields = getFieldsForUser(request.user, {
33
- * public: ['id', 'name', 'price'],
34
- * authenticated: ['description', 'features'],
35
- * admin: ['createdAt', 'internalNotes']
36
- * });
37
- */
38
- declare function getFieldsForUser(user: UserContext | null | undefined, preset: FieldPreset): string[];
39
- /**
40
- * Get Mongoose projection string for query .select()
41
- *
42
- * @param user - User object from request.user
43
- * @param preset - Field preset configuration
44
- * @returns Space-separated field names for Mongoose .select()
45
- *
46
- * @example
47
- * const projection = getMongooseProjection(request.user, fieldPresets.gymPlans);
48
- * const plans = await GymPlan.find({ organizationId }).select(projection).lean();
49
- */
50
- declare function getMongooseProjection(user: UserContext | null | undefined, preset: FieldPreset): string;
51
- /**
52
- * Filter response data to include only allowed fields
53
- *
54
- * Use this for complex responses where Mongoose projections aren't applicable:
55
- * - Aggregation pipeline results
56
- * - Data from multiple sources
57
- * - Custom computed fields
58
- *
59
- * For simple DB queries, prefer getMongooseProjection() (10x faster)
60
- *
61
- * @param data - Data to filter
62
- * @param preset - Field preset configuration
63
- * @param user - User object from request.user
64
- * @returns Filtered data
65
- *
66
- * @example
67
- * const stats = await calculateComplexStats();
68
- * const filtered = filterResponseData(stats, fieldPresets.dashboard, request.user);
69
- * return reply.send(filtered);
70
- */
71
- declare function filterResponseData<T extends Record<string, unknown>>(data: T | T[], preset: FieldPreset, user?: UserContext | null): Partial<T> | Partial<T>[];
72
- /**
73
- * Helper to create field presets (module-level)
74
- *
75
- * Each module should define its own field preset in its own directory.
76
- * This keeps modules independent and self-contained.
77
- *
78
- * @param config - Field configuration
79
- * @returns Field preset
80
- *
81
- * @example
82
- * // In modules/gym-plan/gym-plan.fields.ts
83
- * export const gymPlanFieldPreset = createFieldPreset({
84
- * public: ['id', 'name', 'price'],
85
- * authenticated: ['features', 'description'],
86
- * admin: ['createdAt', 'updatedAt', 'internalNotes']
87
- * });
88
- */
89
- declare function createFieldPreset(config: Partial<FieldPreset>): FieldPreset;
90
-
91
- /**
92
- * Error Utilities
93
- *
94
- * HTTP-compatible error creation for repository operations
95
- */
96
-
97
- /**
98
- * Creates an error with HTTP status code
99
- *
100
- * @param status - HTTP status code
101
- * @param message - Error message
102
- * @returns Error with status property
103
- *
104
- * @example
105
- * throw createError(404, 'Document not found');
106
- * throw createError(400, 'Invalid input');
107
- * throw createError(403, 'Access denied');
108
- */
109
- declare function createError(status: number, message: string): HttpError;
110
-
111
- /**
112
- * In-Memory Cache Adapter
113
- *
114
- * Simple cache adapter for development and testing.
115
- * NOT recommended for production - use Redis or similar.
116
- *
117
- * @example
118
- * ```typescript
119
- * import { cachePlugin, createMemoryCache } from '@classytic/mongokit';
120
- *
121
- * const repo = new Repository(UserModel, [
122
- * cachePlugin({
123
- * adapter: createMemoryCache(),
124
- * ttl: 60,
125
- * })
126
- * ]);
127
- * ```
128
- */
129
-
130
- /**
131
- * Creates an in-memory cache adapter
132
- *
133
- * Features:
134
- * - Automatic TTL expiration
135
- * - Pattern-based clearing (simple glob with *)
136
- * - Max entries limit to prevent memory leaks
137
- *
138
- * @param maxEntries - Maximum cache entries before oldest are evicted (default: 1000)
139
- */
140
- declare function createMemoryCache(maxEntries?: number): CacheAdapter;
141
-
142
- export { getMongooseProjection as a, createError as b, createFieldPreset as c, createMemoryCache as d, filterResponseData as f, getFieldsForUser as g };