@classytic/mongokit 1.0.2 → 2.0.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.
Files changed (87) hide show
  1. package/README.md +562 -155
  2. package/package.json +17 -10
  3. package/src/Repository.js +296 -225
  4. package/src/actions/aggregate.js +266 -191
  5. package/src/actions/create.js +47 -47
  6. package/src/actions/delete.js +88 -88
  7. package/src/actions/index.js +11 -11
  8. package/src/actions/read.js +176 -144
  9. package/src/actions/update.js +144 -144
  10. package/src/hooks/lifecycle.js +146 -146
  11. package/src/index.js +71 -60
  12. package/src/pagination/PaginationEngine.js +348 -0
  13. package/src/pagination/utils/cursor.js +119 -0
  14. package/src/pagination/utils/filter.js +42 -0
  15. package/src/pagination/utils/limits.js +82 -0
  16. package/src/pagination/utils/sort.js +101 -0
  17. package/src/plugins/aggregate-helpers.plugin.js +71 -71
  18. package/src/plugins/audit-log.plugin.js +60 -60
  19. package/src/plugins/batch-operations.plugin.js +66 -66
  20. package/src/plugins/field-filter.plugin.js +27 -27
  21. package/src/plugins/index.js +19 -19
  22. package/src/plugins/method-registry.plugin.js +140 -140
  23. package/src/plugins/mongo-operations.plugin.js +317 -313
  24. package/src/plugins/soft-delete.plugin.js +46 -46
  25. package/src/plugins/subdocument.plugin.js +66 -66
  26. package/src/plugins/timestamp.plugin.js +19 -19
  27. package/src/plugins/validation-chain.plugin.js +145 -145
  28. package/src/types.d.ts +87 -0
  29. package/src/utils/error.js +12 -0
  30. package/src/utils/field-selection.js +156 -156
  31. package/src/utils/index.js +12 -12
  32. package/types/Repository.d.ts +95 -0
  33. package/types/Repository.d.ts.map +1 -0
  34. package/types/actions/aggregate.d.ts +112 -0
  35. package/types/actions/aggregate.d.ts.map +1 -0
  36. package/types/actions/create.d.ts +21 -0
  37. package/types/actions/create.d.ts.map +1 -0
  38. package/types/actions/delete.d.ts +37 -0
  39. package/types/actions/delete.d.ts.map +1 -0
  40. package/types/actions/index.d.ts +6 -121
  41. package/types/actions/index.d.ts.map +1 -0
  42. package/types/actions/read.d.ts +135 -0
  43. package/types/actions/read.d.ts.map +1 -0
  44. package/types/actions/update.d.ts +58 -0
  45. package/types/actions/update.d.ts.map +1 -0
  46. package/types/hooks/lifecycle.d.ts +44 -0
  47. package/types/hooks/lifecycle.d.ts.map +1 -0
  48. package/types/index.d.ts +25 -104
  49. package/types/index.d.ts.map +1 -0
  50. package/types/pagination/PaginationEngine.d.ts +386 -0
  51. package/types/pagination/PaginationEngine.d.ts.map +1 -0
  52. package/types/pagination/utils/cursor.d.ts +40 -0
  53. package/types/pagination/utils/cursor.d.ts.map +1 -0
  54. package/types/pagination/utils/filter.d.ts +28 -0
  55. package/types/pagination/utils/filter.d.ts.map +1 -0
  56. package/types/pagination/utils/limits.d.ts +64 -0
  57. package/types/pagination/utils/limits.d.ts.map +1 -0
  58. package/types/pagination/utils/sort.d.ts +41 -0
  59. package/types/pagination/utils/sort.d.ts.map +1 -0
  60. package/types/plugins/aggregate-helpers.plugin.d.ts +6 -0
  61. package/types/plugins/aggregate-helpers.plugin.d.ts.map +1 -0
  62. package/types/plugins/audit-log.plugin.d.ts +6 -0
  63. package/types/plugins/audit-log.plugin.d.ts.map +1 -0
  64. package/types/plugins/batch-operations.plugin.d.ts +6 -0
  65. package/types/plugins/batch-operations.plugin.d.ts.map +1 -0
  66. package/types/plugins/field-filter.plugin.d.ts +6 -0
  67. package/types/plugins/field-filter.plugin.d.ts.map +1 -0
  68. package/types/plugins/index.d.ts +11 -88
  69. package/types/plugins/index.d.ts.map +1 -0
  70. package/types/plugins/method-registry.plugin.d.ts +3 -0
  71. package/types/plugins/method-registry.plugin.d.ts.map +1 -0
  72. package/types/plugins/mongo-operations.plugin.d.ts +4 -0
  73. package/types/plugins/mongo-operations.plugin.d.ts.map +1 -0
  74. package/types/plugins/soft-delete.plugin.d.ts +6 -0
  75. package/types/plugins/soft-delete.plugin.d.ts.map +1 -0
  76. package/types/plugins/subdocument.plugin.d.ts +6 -0
  77. package/types/plugins/subdocument.plugin.d.ts.map +1 -0
  78. package/types/plugins/timestamp.plugin.d.ts +6 -0
  79. package/types/plugins/timestamp.plugin.d.ts.map +1 -0
  80. package/types/plugins/validation-chain.plugin.d.ts +31 -0
  81. package/types/plugins/validation-chain.plugin.d.ts.map +1 -0
  82. package/types/utils/error.d.ts +11 -0
  83. package/types/utils/error.d.ts.map +1 -0
  84. package/types/utils/field-selection.d.ts +9 -0
  85. package/types/utils/field-selection.d.ts.map +1 -0
  86. package/types/utils/index.d.ts +2 -24
  87. package/types/utils/index.d.ts.map +1 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "1.0.2",
4
- "description": "Event-driven MongoDB repositories for any Node.js framework",
3
+ "version": "2.0.0",
4
+ "description": "Production-grade MongoDB repositories with zero dependencies - smart pagination, events, and plugins",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./types/index.d.ts",
@@ -10,6 +10,10 @@
10
10
  "types": "./types/index.d.ts",
11
11
  "import": "./src/index.js"
12
12
  },
13
+ "./pagination": {
14
+ "types": "./types/pagination/PaginationEngine.d.ts",
15
+ "import": "./src/pagination/PaginationEngine.js"
16
+ },
13
17
  "./plugins": {
14
18
  "types": "./types/plugins/index.d.ts",
15
19
  "import": "./src/plugins/index.js"
@@ -31,10 +35,16 @@
31
35
  "repository",
32
36
  "repository-pattern",
33
37
  "data-access",
38
+ "pagination",
39
+ "cursor-pagination",
40
+ "infinite-scroll",
41
+ "offset-pagination",
42
+ "keyset-pagination",
34
43
  "multi-tenant",
35
44
  "multi-tenancy",
36
45
  "event-driven",
37
46
  "plugin-based",
47
+ "zero-dependencies",
38
48
  "express",
39
49
  "fastify",
40
50
  "nestjs",
@@ -52,24 +62,21 @@
52
62
  },
53
63
  "homepage": "https://github.com/classytic/mongokit#readme",
54
64
  "peerDependencies": {
55
- "mongoose": "^8.0.0 || ^9.0.0",
56
- "mongoose-paginate-v2": "^1.9.0",
57
- "mongoose-aggregate-paginate-v2": "^1.1.0"
58
- },
59
- "dependencies": {
60
- "http-errors": "^2.0.0"
65
+ "mongoose": "^8.0.0 || ^9.0.0"
61
66
  },
62
67
  "engines": {
63
68
  "node": ">=18"
64
69
  },
65
70
  "scripts": {
71
+ "build": "tsc",
66
72
  "test": "node --test test/*.test.js",
67
73
  "test:watch": "node --test --watch test/*.test.js",
74
+ "prepublishOnly": "npm run build",
68
75
  "publish:npm": "npm publish --access public"
69
76
  },
70
77
  "devDependencies": {
78
+ "@types/node": "^24.10.1",
71
79
  "mongoose": "^9.0.0-rc1",
72
- "mongoose-paginate-v2": "^1.9.0",
73
- "mongoose-aggregate-paginate-v2": "^1.1.0"
80
+ "typescript": "^5.9.3"
74
81
  }
75
82
  }
package/src/Repository.js CHANGED
@@ -1,225 +1,296 @@
1
- import mongoose from 'mongoose';
2
- import createError from 'http-errors';
3
- import * as createActions from './actions/create.js';
4
- import * as readActions from './actions/read.js';
5
- import * as updateActions from './actions/update.js';
6
- import * as deleteActions from './actions/delete.js';
7
- import * as aggregateActions from './actions/aggregate.js';
8
-
9
- export class Repository {
10
- constructor(Model, plugins = []) {
11
- this.Model = Model;
12
- this.model = Model.modelName;
13
- this._hooks = new Map();
14
- plugins.forEach(plugin => this.use(plugin));
15
- }
16
-
17
- use(plugin) {
18
- if (typeof plugin === 'function') {
19
- plugin(this);
20
- } else if (plugin && typeof plugin.apply === 'function') {
21
- plugin.apply(this);
22
- }
23
- return this;
24
- }
25
-
26
- on(event, listener) {
27
- if (!this._hooks.has(event)) {
28
- this._hooks.set(event, []);
29
- }
30
- this._hooks.get(event).push(listener);
31
- return this;
32
- }
33
-
34
- emit(event, data) {
35
- const listeners = this._hooks.get(event) || [];
36
- listeners.forEach(listener => listener(data));
37
- }
38
-
39
- async create(data, options = {}) {
40
- const context = await this._buildContext('create', { data, ...options });
41
-
42
- try {
43
- const result = await createActions.create(this.Model, context.data, options);
44
- this.emit('after:create', { context, result });
45
- return result;
46
- } catch (error) {
47
- this.emit('error:create', { context, error });
48
- throw this._handleError(error);
49
- }
50
- }
51
-
52
- async createMany(dataArray, options = {}) {
53
- const context = await this._buildContext('createMany', { dataArray, ...options });
54
-
55
- try {
56
- const result = await createActions.createMany(this.Model, context.dataArray || dataArray, options);
57
- this.emit('after:createMany', { context, result });
58
- return result;
59
- } catch (error) {
60
- this.emit('error:createMany', { context, error });
61
- throw this._handleError(error);
62
- }
63
- }
64
-
65
- async getById(id, options = {}) {
66
- const context = await this._buildContext('getById', { id, ...options });
67
- return readActions.getById(this.Model, id, context);
68
- }
69
-
70
- async getByQuery(query, options = {}) {
71
- const context = await this._buildContext('getByQuery', { query, ...options });
72
- return readActions.getByQuery(this.Model, query, context);
73
- }
74
-
75
- async getAll(queryParams = {}, options = {}) {
76
- const context = await this._buildContext('getAll', { queryParams, ...options });
77
-
78
- const {
79
- pagination = { page: 1, limit: 10 },
80
- search,
81
- sort = '-createdAt',
82
- filters = {},
83
- } = context.queryParams || queryParams;
84
-
85
- let query = { ...filters };
86
- if (search) query.$text = { $search: search };
87
-
88
- const paginateOptions = {
89
- page: parseInt(pagination.page, 10),
90
- limit: parseInt(pagination.limit, 10),
91
- sort: this._parseSort(sort),
92
- populate: this._parsePopulate(context.populate || options.populate),
93
- select: context.select || options.select,
94
- lean: context.lean ?? options.lean ?? true,
95
- session: options.session,
96
- };
97
-
98
- if (!this.Model.paginate) {
99
- throw createError(500, `Model ${this.model} missing paginate plugin`);
100
- }
101
-
102
- return this.Model.paginate(query, paginateOptions);
103
- }
104
-
105
- async getOrCreate(query, createData, options = {}) {
106
- return readActions.getOrCreate(this.Model, query, createData, options);
107
- }
108
-
109
- async count(query = {}, options = {}) {
110
- return readActions.count(this.Model, query, options);
111
- }
112
-
113
- async exists(query, options = {}) {
114
- return readActions.exists(this.Model, query, options);
115
- }
116
-
117
- async update(id, data, options = {}) {
118
- const context = await this._buildContext('update', { id, data, ...options });
119
-
120
- try {
121
- const result = await updateActions.update(this.Model, id, context.data, context);
122
- this.emit('after:update', { context, result });
123
- return result;
124
- } catch (error) {
125
- this.emit('error:update', { context, error });
126
- throw this._handleError(error);
127
- }
128
- }
129
-
130
- async delete(id, options = {}) {
131
- const context = await this._buildContext('delete', { id, ...options });
132
-
133
- try {
134
- const result = await deleteActions.deleteById(this.Model, id, options);
135
- this.emit('after:delete', { context, result });
136
- return result;
137
- } catch (error) {
138
- this.emit('error:delete', { context, error });
139
- throw this._handleError(error);
140
- }
141
- }
142
-
143
- async aggregate(pipeline, options = {}) {
144
- return aggregateActions.aggregate(this.Model, pipeline, options);
145
- }
146
-
147
- async aggregatePaginate(pipeline, options = {}) {
148
- return aggregateActions.aggregatePaginate(this.Model, pipeline, options);
149
- }
150
-
151
- async distinct(field, query = {}, options = {}) {
152
- return aggregateActions.distinct(this.Model, field, query, options);
153
- }
154
-
155
- async withTransaction(callback) {
156
- const session = await mongoose.startSession();
157
- session.startTransaction();
158
- try {
159
- const result = await callback(session);
160
- await session.commitTransaction();
161
- return result;
162
- } catch (error) {
163
- await session.abortTransaction();
164
- throw error;
165
- } finally {
166
- session.endSession();
167
- }
168
- }
169
-
170
- async _executeQuery(buildQuery) {
171
- const operation = buildQuery.name || 'custom';
172
- const context = await this._buildContext(operation, {});
173
-
174
- try {
175
- const result = await buildQuery(this.Model);
176
- this.emit(`after:${operation}`, { context, result });
177
- return result;
178
- } catch (error) {
179
- this.emit(`error:${operation}`, { context, error });
180
- throw this._handleError(error);
181
- }
182
- }
183
-
184
- async _buildContext(operation, options) {
185
- const context = { operation, model: this.model, ...options };
186
- const event = `before:${operation}`;
187
- const hooks = this._hooks.get(event) || [];
188
-
189
- for (const hook of hooks) {
190
- await hook(context);
191
- }
192
-
193
- return context;
194
- }
195
-
196
- _parseSort(sort) {
197
- if (!sort) return { createdAt: -1 };
198
- if (typeof sort === 'object') return sort;
199
-
200
- const sortOrder = sort.startsWith('-') ? -1 : 1;
201
- const sortField = sort.startsWith('-') ? sort.substring(1) : sort;
202
- return { [sortField]: sortOrder };
203
- }
204
-
205
- _parsePopulate(populate) {
206
- if (!populate) return [];
207
- if (typeof populate === 'string') return populate.split(',').map(p => p.trim());
208
- if (Array.isArray(populate)) return populate.map(p => (typeof p === 'string' ? p.trim() : p));
209
- return [populate];
210
- }
211
-
212
- _handleError(error) {
213
- if (error instanceof mongoose.Error.ValidationError) {
214
- const messages = Object.values(error.errors).map(err => err.message);
215
- return createError(400, `Validation Error: ${messages.join(', ')}`);
216
- }
217
- if (error instanceof mongoose.Error.CastError) {
218
- return createError(400, `Invalid ${error.path}: ${error.value}`);
219
- }
220
- if (error.status && error.message) return error;
221
- return createError(500, error.message || 'Internal Server Error');
222
- }
223
- }
224
-
225
- export default Repository;
1
+ import mongoose from 'mongoose';
2
+ import { createError } from './utils/error.js';
3
+ import * as createActions from './actions/create.js';
4
+ import * as readActions from './actions/read.js';
5
+ import * as updateActions from './actions/update.js';
6
+ import * as deleteActions from './actions/delete.js';
7
+ import * as aggregateActions from './actions/aggregate.js';
8
+ import { PaginationEngine } from './pagination/PaginationEngine.js';
9
+
10
+ /**
11
+ * @typedef {import('./types.js').OffsetPaginationResult} OffsetPaginationResult
12
+ * @typedef {import('./types.js').KeysetPaginationResult} KeysetPaginationResult
13
+ * @typedef {import('./types.js').AggregatePaginationResult} AggregatePaginationResult
14
+ * @typedef {import('./types.js').ObjectId} ObjectId
15
+ */
16
+
17
+ export class Repository {
18
+ constructor(Model, plugins = [], paginationConfig = {}) {
19
+ this.Model = Model;
20
+ this.model = Model.modelName;
21
+ this._hooks = new Map();
22
+ this._pagination = new PaginationEngine(Model, paginationConfig);
23
+ plugins.forEach(plugin => this.use(plugin));
24
+ }
25
+
26
+ use(plugin) {
27
+ if (typeof plugin === 'function') {
28
+ plugin(this);
29
+ } else if (plugin && typeof plugin.apply === 'function') {
30
+ plugin.apply(this);
31
+ }
32
+ return this;
33
+ }
34
+
35
+ on(event, listener) {
36
+ if (!this._hooks.has(event)) {
37
+ this._hooks.set(event, []);
38
+ }
39
+ this._hooks.get(event).push(listener);
40
+ return this;
41
+ }
42
+
43
+ emit(event, data) {
44
+ const listeners = this._hooks.get(event) || [];
45
+ listeners.forEach(listener => listener(data));
46
+ }
47
+
48
+ async create(data, options = {}) {
49
+ const context = await this._buildContext('create', { data, ...options });
50
+
51
+ try {
52
+ const result = await createActions.create(this.Model, context.data, options);
53
+ this.emit('after:create', { context, result });
54
+ return result;
55
+ } catch (error) {
56
+ this.emit('error:create', { context, error });
57
+ throw this._handleError(error);
58
+ }
59
+ }
60
+
61
+ async createMany(dataArray, options = {}) {
62
+ const context = await this._buildContext('createMany', { dataArray, ...options });
63
+
64
+ try {
65
+ const result = await createActions.createMany(this.Model, context.dataArray || dataArray, options);
66
+ this.emit('after:createMany', { context, result });
67
+ return result;
68
+ } catch (error) {
69
+ this.emit('error:createMany', { context, error });
70
+ throw this._handleError(error);
71
+ }
72
+ }
73
+
74
+ async getById(id, options = {}) {
75
+ const context = await this._buildContext('getById', { id, ...options });
76
+ return readActions.getById(this.Model, id, context);
77
+ }
78
+
79
+ async getByQuery(query, options = {}) {
80
+ const context = await this._buildContext('getByQuery', { query, ...options });
81
+ return readActions.getByQuery(this.Model, query, context);
82
+ }
83
+
84
+ /**
85
+ * Unified pagination - auto-detects offset vs keyset based on params
86
+ *
87
+ * Auto-detection logic:
88
+ * - If params has 'cursor' or 'after' → uses keyset pagination (stream)
89
+ * - If params has 'pagination' or 'page' → uses offset pagination (paginate)
90
+ * - Else → defaults to offset pagination with page=1
91
+ *
92
+ * @param {Object} params - Query and pagination parameters
93
+ * @param {Object} [params.filters] - MongoDB query filters
94
+ * @param {string|Object} [params.sort] - Sort specification
95
+ * @param {string} [params.cursor] - Cursor token for keyset pagination
96
+ * @param {string} [params.after] - Alias for cursor
97
+ * @param {number} [params.page] - Page number for offset pagination
98
+ * @param {Object} [params.pagination] - Pagination config { page, limit }
99
+ * @param {number} [params.limit] - Documents per page
100
+ * @param {string} [params.search] - Full-text search query
101
+ * @param {Object} [options] - Additional options (select, populate, lean, session)
102
+ * @returns {Promise<OffsetPaginationResult|KeysetPaginationResult>} Discriminated union based on method
103
+ *
104
+ * @example
105
+ * // Offset pagination (page-based)
106
+ * await repo.getAll({ page: 1, limit: 50, filters: { status: 'active' } });
107
+ * await repo.getAll({ pagination: { page: 2, limit: 20 } });
108
+ *
109
+ * // Keyset pagination (cursor-based)
110
+ * await repo.getAll({ cursor: 'eyJ2Ij...', limit: 50 });
111
+ * await repo.getAll({ after: 'eyJ2Ij...', sort: { createdAt: -1 } });
112
+ *
113
+ * // Simple query (defaults to page 1)
114
+ * await repo.getAll({ filters: { status: 'active' } });
115
+ */
116
+ async getAll(params = {}, options = {}) {
117
+ const context = await this._buildContext('getAll', { ...params, ...options });
118
+
119
+ // Auto-detect pagination mode
120
+ // Priority:
121
+ // 1. If 'page' param offset pagination
122
+ // 2. If 'after' or 'cursor' param → keyset pagination
123
+ // 3. If explicit 'sort' provided without 'page' → keyset pagination (first page)
124
+ // 4. Otherwise → offset pagination (default, page=1)
125
+ const hasPageParam = params.page !== undefined || params.pagination;
126
+ const hasCursorParam = 'cursor' in params || 'after' in params;
127
+ const hasExplicitSort = params.sort !== undefined;
128
+
129
+ const useKeyset = !hasPageParam && (hasCursorParam || hasExplicitSort);
130
+
131
+ // Extract common params
132
+ const filters = params.filters || {};
133
+ const search = params.search;
134
+ const sort = params.sort || '-createdAt';
135
+ const limit = params.limit || params.pagination?.limit || this._pagination.config.defaultLimit;
136
+
137
+ // Build query with search support
138
+ let query = { ...filters };
139
+ if (search) query.$text = { $search: search };
140
+
141
+ // Common options
142
+ const paginationOptions = {
143
+ filters: query,
144
+ sort: this._parseSort(sort),
145
+ limit,
146
+ populate: this._parsePopulate(context.populate || options.populate),
147
+ select: context.select || options.select,
148
+ lean: context.lean ?? options.lean ?? true,
149
+ session: options.session,
150
+ };
151
+
152
+ if (useKeyset) {
153
+ // Keyset pagination (cursor-based)
154
+ return this._pagination.stream({
155
+ ...paginationOptions,
156
+ after: params.cursor || params.after,
157
+ });
158
+ } else {
159
+ // Offset pagination (page-based) - default
160
+ const page = params.pagination?.page || params.page || 1;
161
+ return this._pagination.paginate({
162
+ ...paginationOptions,
163
+ page,
164
+ });
165
+ }
166
+ }
167
+
168
+ async getOrCreate(query, createData, options = {}) {
169
+ return readActions.getOrCreate(this.Model, query, createData, options);
170
+ }
171
+
172
+ async count(query = {}, options = {}) {
173
+ return readActions.count(this.Model, query, options);
174
+ }
175
+
176
+ async exists(query, options = {}) {
177
+ return readActions.exists(this.Model, query, options);
178
+ }
179
+
180
+ async update(id, data, options = {}) {
181
+ const context = await this._buildContext('update', { id, data, ...options });
182
+
183
+ try {
184
+ const result = await updateActions.update(this.Model, id, context.data, context);
185
+ this.emit('after:update', { context, result });
186
+ return result;
187
+ } catch (error) {
188
+ this.emit('error:update', { context, error });
189
+ throw this._handleError(error);
190
+ }
191
+ }
192
+
193
+ async delete(id, options = {}) {
194
+ const context = await this._buildContext('delete', { id, ...options });
195
+
196
+ try {
197
+ const result = await deleteActions.deleteById(this.Model, id, options);
198
+ this.emit('after:delete', { context, result });
199
+ return result;
200
+ } catch (error) {
201
+ this.emit('error:delete', { context, error });
202
+ throw this._handleError(error);
203
+ }
204
+ }
205
+
206
+ async aggregate(pipeline, options = {}) {
207
+ return aggregateActions.aggregate(this.Model, pipeline, options);
208
+ }
209
+
210
+ /**
211
+ * Aggregate pipeline with pagination
212
+ * Best for: Complex queries, grouping, joins
213
+ *
214
+ * @param {Object} options - Aggregate pagination options
215
+ * @returns {Promise<AggregatePaginationResult>}
216
+ */
217
+ async aggregatePaginate(options = {}) {
218
+ const context = await this._buildContext('aggregatePaginate', options);
219
+ return this._pagination.aggregatePaginate(context);
220
+ }
221
+
222
+ async distinct(field, query = {}, options = {}) {
223
+ return aggregateActions.distinct(this.Model, field, query, options);
224
+ }
225
+
226
+ async withTransaction(callback) {
227
+ const session = await mongoose.startSession();
228
+ session.startTransaction();
229
+ try {
230
+ const result = await callback(session);
231
+ await session.commitTransaction();
232
+ return result;
233
+ } catch (error) {
234
+ await session.abortTransaction();
235
+ throw error;
236
+ } finally {
237
+ session.endSession();
238
+ }
239
+ }
240
+
241
+ async _executeQuery(buildQuery) {
242
+ const operation = buildQuery.name || 'custom';
243
+ const context = await this._buildContext(operation, {});
244
+
245
+ try {
246
+ const result = await buildQuery(this.Model);
247
+ this.emit(`after:${operation}`, { context, result });
248
+ return result;
249
+ } catch (error) {
250
+ this.emit(`error:${operation}`, { context, error });
251
+ throw this._handleError(error);
252
+ }
253
+ }
254
+
255
+ async _buildContext(operation, options) {
256
+ const context = { operation, model: this.model, ...options };
257
+ const event = `before:${operation}`;
258
+ const hooks = this._hooks.get(event) || [];
259
+
260
+ for (const hook of hooks) {
261
+ await hook(context);
262
+ }
263
+
264
+ return context;
265
+ }
266
+
267
+ _parseSort(sort) {
268
+ if (!sort) return { createdAt: -1 };
269
+ if (typeof sort === 'object') return sort;
270
+
271
+ const sortOrder = sort.startsWith('-') ? -1 : 1;
272
+ const sortField = sort.startsWith('-') ? sort.substring(1) : sort;
273
+ return { [sortField]: sortOrder };
274
+ }
275
+
276
+ _parsePopulate(populate) {
277
+ if (!populate) return [];
278
+ if (typeof populate === 'string') return populate.split(',').map(p => p.trim());
279
+ if (Array.isArray(populate)) return populate.map(p => (typeof p === 'string' ? p.trim() : p));
280
+ return [populate];
281
+ }
282
+
283
+ _handleError(error) {
284
+ if (error instanceof mongoose.Error.ValidationError) {
285
+ const messages = Object.values(error.errors).map(err => /** @type {any} */(err).message);
286
+ return createError(400, `Validation Error: ${messages.join(', ')}`);
287
+ }
288
+ if (error instanceof mongoose.Error.CastError) {
289
+ return createError(400, `Invalid ${error.path}: ${error.value}`);
290
+ }
291
+ if (error.status && error.message) return error;
292
+ return createError(500, error.message || 'Internal Server Error');
293
+ }
294
+ }
295
+
296
+ export default Repository;