@classytic/mongokit 1.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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Batch Operations Plugin
3
+ * Adds bulk update/delete operations with proper event emission
4
+ */
5
+
6
+ export const batchOperationsPlugin = () => ({
7
+ name: 'batch-operations',
8
+
9
+ apply(repo) {
10
+ if (!repo.registerMethod) {
11
+ throw new Error('batchOperationsPlugin requires methodRegistryPlugin');
12
+ }
13
+
14
+ /**
15
+ * Update multiple documents
16
+ * @param {Object} query - MongoDB query to match documents
17
+ * @param {Object} data - Update data
18
+ * @param {Object} options - Additional options (session, context)
19
+ * @returns {Promise<Object>} MongoDB update result
20
+ */
21
+ repo.registerMethod('updateMany', async function (query, data, options = {}) {
22
+ const context = await this._buildContext('updateMany', { query, data, options });
23
+
24
+ try {
25
+ this.emit('before:updateMany', context);
26
+
27
+ const result = await this.Model.updateMany(query, data, {
28
+ runValidators: true,
29
+ session: options.session,
30
+ }).exec();
31
+
32
+ this.emit('after:updateMany', { context, result });
33
+ return result;
34
+ } catch (error) {
35
+ this.emit('error:updateMany', { context, error });
36
+ throw this._handleError(error);
37
+ }
38
+ });
39
+
40
+ /**
41
+ * Delete multiple documents
42
+ * @param {Object} query - MongoDB query to match documents
43
+ * @param {Object} options - Additional options (session, context)
44
+ * @returns {Promise<Object>} MongoDB delete result
45
+ */
46
+ repo.registerMethod('deleteMany', async function (query, options = {}) {
47
+ const context = await this._buildContext('deleteMany', { query, options });
48
+
49
+ try {
50
+ this.emit('before:deleteMany', context);
51
+
52
+ const result = await this.Model.deleteMany(query, {
53
+ session: options.session,
54
+ }).exec();
55
+
56
+ this.emit('after:deleteMany', { context, result });
57
+ return result;
58
+ } catch (error) {
59
+ this.emit('error:deleteMany', { context, error });
60
+ throw this._handleError(error);
61
+ }
62
+ });
63
+ }
64
+ });
65
+
66
+ export default batchOperationsPlugin;
@@ -0,0 +1,27 @@
1
+ import { getFieldsForUser } from '../utils/field-selection.js';
2
+
3
+ export const fieldFilterPlugin = (fieldPreset) => ({
4
+ name: 'fieldFilter',
5
+
6
+ apply(repo) {
7
+ const applyFieldFiltering = (context) => {
8
+ if (!fieldPreset) return;
9
+
10
+ const user = context.context?.user || context.user;
11
+ const fields = getFieldsForUser(user, fieldPreset);
12
+ const presetSelect = fields.join(' ');
13
+
14
+ if (context.select) {
15
+ context.select = `${presetSelect} ${context.select}`;
16
+ } else {
17
+ context.select = presetSelect;
18
+ }
19
+ };
20
+
21
+ repo.on('before:getAll', applyFieldFiltering);
22
+ repo.on('before:getById', applyFieldFiltering);
23
+ repo.on('before:getByQuery', applyFieldFiltering);
24
+ },
25
+ });
26
+
27
+ export default fieldFilterPlugin;
@@ -0,0 +1,19 @@
1
+ // Core plugins
2
+ export { fieldFilterPlugin } from './field-filter.plugin.js';
3
+ export { timestampPlugin } from './timestamp.plugin.js';
4
+ export { auditLogPlugin } from './audit-log.plugin.js';
5
+ export { softDeletePlugin } from './soft-delete.plugin.js';
6
+ export { methodRegistryPlugin } from './method-registry.plugin.js';
7
+ export {
8
+ validationChainPlugin,
9
+ blockIf,
10
+ requireField,
11
+ autoInject,
12
+ immutableField,
13
+ uniqueField,
14
+ } from './validation-chain.plugin.js';
15
+ export { mongoOperationsPlugin } from './mongo-operations.plugin.js';
16
+ export { batchOperationsPlugin } from './batch-operations.plugin.js';
17
+ export { aggregateHelpersPlugin } from './aggregate-helpers.plugin.js';
18
+ export { subdocumentPlugin } from './subdocument.plugin.js';
19
+
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Method Registry Plugin
3
+ *
4
+ * Enables plugins to dynamically add methods to repository instances.
5
+ * Foundation for extensibility - allows other plugins to extend repositories
6
+ * with custom methods while maintaining type safety and proper binding.
7
+ *
8
+ * **Pattern:** Inspired by Stripe's extension system
9
+ * **Philosophy:** Repositories start minimal, plugins add capabilities
10
+ *
11
+ * @module common/repositories/plugins/method-registry
12
+ *
13
+ * @example Basic Usage
14
+ * ```js
15
+ * import { Repository } from '../Repository.js';
16
+ * import { methodRegistryPlugin } from './method-registry.plugin.js';
17
+ *
18
+ * class UserRepository extends Repository {
19
+ * constructor() {
20
+ * super(User, [methodRegistryPlugin()]);
21
+ *
22
+ * // Now you can register custom methods
23
+ * this.registerMethod('findActive', async function() {
24
+ * return this.getAll({ filters: { status: 'active' } });
25
+ * });
26
+ * }
27
+ * }
28
+ * ```
29
+ *
30
+ * @example Plugin Using Method Registry
31
+ * ```js
32
+ * // Other plugins can use registerMethod to add functionality
33
+ * export const mongoOperationsPlugin = () => ({
34
+ * name: 'mongo-operations',
35
+ * apply(repo) {
36
+ * repo.registerMethod('increment', async function(id, field, value = 1, options = {}) {
37
+ * return this.update(id, { $inc: { [field]: value } }, options);
38
+ * });
39
+ * }
40
+ * });
41
+ * ```
42
+ */
43
+
44
+ /**
45
+ * Method Registry Plugin
46
+ *
47
+ * Adds `registerMethod()` to repository instance, allowing dynamic method addition.
48
+ *
49
+ * @returns {Object} Plugin configuration
50
+ */
51
+ export const methodRegistryPlugin = () => ({
52
+ name: 'method-registry',
53
+
54
+ apply(repo) {
55
+ /**
56
+ * Register a new method on the repository instance
57
+ *
58
+ * **Rules:**
59
+ * - Method name must not conflict with existing methods
60
+ * - Method is automatically bound to repository instance
61
+ * - Method has access to all repository methods via `this`
62
+ * - Async methods are recommended for consistency
63
+ *
64
+ * @param {string} name - Method name
65
+ * @param {Function} fn - Method implementation (will be bound to repo)
66
+ * @throws {Error} If method name already exists
67
+ *
68
+ * @example
69
+ * repo.registerMethod('findByEmail', async function(email) {
70
+ * return this.getByQuery({ email }, { lean: true });
71
+ * });
72
+ *
73
+ * @example With options
74
+ * repo.registerMethod('incrementViews', async function(id, amount = 1) {
75
+ * return this.update(id, { $inc: { views: amount } });
76
+ * });
77
+ */
78
+ repo.registerMethod = function (name, fn) {
79
+ // Check for naming conflicts
80
+ if (repo[name]) {
81
+ throw new Error(
82
+ `Cannot register method '${name}': Method already exists on repository. ` +
83
+ `Choose a different name or use a plugin that doesn't conflict.`
84
+ );
85
+ }
86
+
87
+ // Validate method name
88
+ if (!name || typeof name !== 'string') {
89
+ throw new Error('Method name must be a non-empty string');
90
+ }
91
+
92
+ // Validate function
93
+ if (typeof fn !== 'function') {
94
+ throw new Error(`Method '${name}' must be a function`);
95
+ }
96
+
97
+ // Bind function to repository instance
98
+ repo[name] = fn.bind(repo);
99
+
100
+ // Emit event for plugin system awareness
101
+ repo.emit('method:registered', { name, fn });
102
+ };
103
+
104
+ /**
105
+ * Check if a method is registered
106
+ *
107
+ * @param {string} name - Method name to check
108
+ * @returns {boolean} True if method exists
109
+ *
110
+ * @example
111
+ * if (repo.hasMethod('increment')) {
112
+ * await repo.increment(id, 'count', 1);
113
+ * }
114
+ */
115
+ repo.hasMethod = function (name) {
116
+ return typeof repo[name] === 'function';
117
+ };
118
+
119
+ /**
120
+ * Get list of all dynamically registered methods
121
+ *
122
+ * @returns {Array<string>} Array of method names
123
+ *
124
+ * @example
125
+ * const methods = repo.getRegisteredMethods();
126
+ * console.log('Available methods:', methods);
127
+ */
128
+ repo.getRegisteredMethods = function () {
129
+ const registeredMethods = [];
130
+
131
+ repo.on('method:registered', ({ name }) => {
132
+ registeredMethods.push(name);
133
+ });
134
+
135
+ return registeredMethods;
136
+ };
137
+ }
138
+ });
139
+
140
+ export default methodRegistryPlugin;
@@ -0,0 +1,313 @@
1
+ /**
2
+ * MongoDB Operations Plugin
3
+ *
4
+ * Adds MongoDB-specific operations to repositories.
5
+ * Requires method-registry.plugin.js to be loaded first.
6
+ *
7
+ * **Operations Added:**
8
+ * - upsert(query, data, options) - Update or insert
9
+ * - increment(id, field, value, options) - Atomic increment
10
+ * - decrement(id, field, value, options) - Atomic decrement
11
+ * - pushToArray(id, field, value, options) - Add to array
12
+ * - pullFromArray(id, field, value, options) - Remove from array
13
+ * - addToSet(id, field, value, options) - Add unique to array
14
+ *
15
+ * **Pattern:** Opt-in MongoDB features
16
+ * **Philosophy:** Keep core pure, add database-specific features via plugins
17
+ *
18
+ * @module common/repositories/plugins/mongo-operations
19
+ * @requires method-registry.plugin
20
+ *
21
+ * @example Basic Usage
22
+ * ```js
23
+ * import { Repository } from '../Repository.js';
24
+ * import { method
25
+
26
+ RegistryPlugin } from './method-registry.plugin.js';
27
+ * import { mongoOperationsPlugin } from './mongo-operations.plugin.js';
28
+ *
29
+ * class ProductRepository extends Repository {
30
+ * constructor() {
31
+ * super(Product, [
32
+ * methodRegistryPlugin(),
33
+ * mongoOperationsPlugin(),
34
+ * ]);
35
+ * }
36
+ * }
37
+ *
38
+ * // Now you can use MongoDB operations
39
+ * await productRepo.increment(productId, 'views', 1);
40
+ * await productRepo.pushToArray(productId, 'tags', 'featured');
41
+ * ```
42
+ */
43
+
44
+ import createError from 'http-errors';
45
+ import * as createActions from '../actions/create.js';
46
+
47
+ /**
48
+ * MongoDB Operations Plugin
49
+ *
50
+ * Adds common MongoDB atomic operations to repository.
51
+ * All operations use repository's update method internally (events/plugins run).
52
+ *
53
+ * @returns {Object} Plugin configuration
54
+ */
55
+ export const mongoOperationsPlugin = () => ({
56
+ name: 'mongo-operations',
57
+
58
+ apply(repo) {
59
+ // Check if method-registry is available
60
+ if (!repo.registerMethod) {
61
+ throw new Error(
62
+ 'mongoOperationsPlugin requires methodRegistryPlugin. ' +
63
+ 'Add methodRegistryPlugin() before mongoOperationsPlugin() in plugins array.'
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Update existing document or insert new one
69
+ *
70
+ * @param {Object} query - Query to find existing document
71
+ * @param {Object} data - Data to insert/update
72
+ * @param {Object} [options={}] - Options
73
+ * @returns {Promise<Object>} Upserted document
74
+ *
75
+ * @example
76
+ * // Create or update user session
77
+ * await repo.upsert(
78
+ * { userId, deviceId },
79
+ * { lastActive: new Date(), ipAddress },
80
+ * { lean: true }
81
+ * );
82
+ */
83
+ repo.registerMethod('upsert', async function (query, data, options = {}) {
84
+ return createActions.upsert(this.Model, query, data, options);
85
+ });
86
+
87
+ // Helper: Validate and perform numeric operation
88
+ const validateAndUpdateNumeric = function (id, field, value, operator, operationName, options) {
89
+ if (typeof value !== 'number') {
90
+ throw createError(400, `${operationName} value must be a number`);
91
+ }
92
+ return this.update(id, { [operator]: { [field]: value } }, options);
93
+ };
94
+
95
+ /**
96
+ * Atomically increment numeric field
97
+ *
98
+ * @param {string|ObjectId} id - Document ID
99
+ * @param {string} field - Field name to increment
100
+ * @param {number} [value=1] - Amount to increment by
101
+ * @param {Object} [options={}] - Options
102
+ * @returns {Promise<Object>} Updated document
103
+ *
104
+ * @example
105
+ * // Increment product views
106
+ * await productRepo.increment(productId, 'views', 1);
107
+ *
108
+ * @example
109
+ * // Increment multiple times
110
+ * await productRepo.increment(userId, 'points', 100);
111
+ */
112
+ repo.registerMethod('increment', async function (id, field, value = 1, options = {}) {
113
+ return validateAndUpdateNumeric.call(this, id, field, value, '$inc', 'Increment', options);
114
+ });
115
+
116
+ /**
117
+ * Atomically decrement numeric field
118
+ *
119
+ * @param {string|ObjectId} id - Document ID
120
+ * @param {string} field - Field name to decrement
121
+ * @param {number} [value=1] - Amount to decrement by
122
+ * @param {Object} [options={}] - Options
123
+ * @returns {Promise<Object>} Updated document
124
+ *
125
+ * @example
126
+ * // Decrement product stock
127
+ * await productRepo.decrement(productId, 'stock', 1);
128
+ */
129
+ repo.registerMethod('decrement', async function (id, field, value = 1, options = {}) {
130
+ return validateAndUpdateNumeric.call(this, id, field, -value, '$inc', 'Decrement', options);
131
+ });
132
+
133
+ // Helper: Generic MongoDB operator update
134
+ const applyOperator = function (id, field, value, operator, options) {
135
+ return this.update(id, { [operator]: { [field]: value } }, options);
136
+ };
137
+
138
+ /**
139
+ * Push value to array field
140
+ *
141
+ * @param {string|ObjectId} id - Document ID
142
+ * @param {string} field - Array field name
143
+ * @param {*} value - Value to push
144
+ * @param {Object} [options={}] - Options
145
+ * @returns {Promise<Object>} Updated document
146
+ *
147
+ * @example
148
+ * // Add tag to product
149
+ * await productRepo.pushToArray(productId, 'tags', 'new-arrival');
150
+ *
151
+ * @example
152
+ * // Add multiple items
153
+ * await productRepo.pushToArray(userId, 'notifications', {
154
+ * message: 'Welcome!',
155
+ * createdAt: new Date()
156
+ * });
157
+ */
158
+ repo.registerMethod('pushToArray', async function (id, field, value, options = {}) {
159
+ return applyOperator.call(this, id, field, value, '$push', options);
160
+ });
161
+
162
+ /**
163
+ * Remove value from array field
164
+ *
165
+ * @param {string|ObjectId} id - Document ID
166
+ * @param {string} field - Array field name
167
+ * @param {*} value - Value to remove (can be query object)
168
+ * @param {Object} [options={}] - Options
169
+ * @returns {Promise<Object>} Updated document
170
+ *
171
+ * @example
172
+ * // Remove tag from product
173
+ * await productRepo.pullFromArray(productId, 'tags', 'old-tag');
174
+ *
175
+ * @example
176
+ * // Remove by query
177
+ * await productRepo.pullFromArray(userId, 'notifications', { read: true });
178
+ */
179
+ repo.registerMethod('pullFromArray', async function (id, field, value, options = {}) {
180
+ return applyOperator.call(this, id, field, value, '$pull', options);
181
+ });
182
+
183
+ /**
184
+ * Add value to array only if not already present (unique)
185
+ *
186
+ * @param {string|ObjectId} id - Document ID
187
+ * @param {string} field - Array field name
188
+ * @param {*} value - Value to add
189
+ * @param {Object} [options={}] - Options
190
+ * @returns {Promise<Object>} Updated document
191
+ *
192
+ * @example
193
+ * // Add unique follower
194
+ * await userRepo.addToSet(userId, 'followers', followerId);
195
+ */
196
+ repo.registerMethod('addToSet', async function (id, field, value, options = {}) {
197
+ return applyOperator.call(this, id, field, value, '$addToSet', options);
198
+ });
199
+
200
+ /**
201
+ * Set field value (alias for update with $set)
202
+ *
203
+ * @param {string|ObjectId} id - Document ID
204
+ * @param {string} field - Field name
205
+ * @param {*} value - New value
206
+ * @param {Object} [options={}] - Options
207
+ * @returns {Promise<Object>} Updated document
208
+ *
209
+ * @example
210
+ * // Set last login
211
+ * await userRepo.setField(userId, 'lastLogin', new Date());
212
+ */
213
+ repo.registerMethod('setField', async function (id, field, value, options = {}) {
214
+ return applyOperator.call(this, id, field, value, '$set', options);
215
+ });
216
+
217
+ /**
218
+ * Unset (remove) field from document
219
+ *
220
+ * @param {string|ObjectId} id - Document ID
221
+ * @param {string|string[]} fields - Field name(s) to remove
222
+ * @param {Object} [options={}] - Options
223
+ * @returns {Promise<Object>} Updated document
224
+ *
225
+ * @example
226
+ * // Remove temporary field
227
+ * await userRepo.unsetField(userId, 'tempToken');
228
+ *
229
+ * @example
230
+ * // Remove multiple fields
231
+ * await userRepo.unsetField(userId, ['tempToken', 'tempData']);
232
+ */
233
+ repo.registerMethod('unsetField', async function (id, fields, options = {}) {
234
+ const fieldArray = Array.isArray(fields) ? fields : [fields];
235
+ const unsetObj = fieldArray.reduce((acc, field) => {
236
+ acc[field] = '';
237
+ return acc;
238
+ }, {});
239
+
240
+ return this.update(id, { $unset: unsetObj }, options);
241
+ });
242
+
243
+ /**
244
+ * Rename field in document
245
+ *
246
+ * @param {string|ObjectId} id - Document ID
247
+ * @param {string} oldName - Current field name
248
+ * @param {string} newName - New field name
249
+ * @param {Object} [options={}] - Options
250
+ * @returns {Promise<Object>} Updated document
251
+ *
252
+ * @example
253
+ * // Rename field
254
+ * await userRepo.renameField(userId, 'username', 'displayName');
255
+ */
256
+ repo.registerMethod('renameField', async function (id, oldName, newName, options = {}) {
257
+ return this.update(id, { $rename: { [oldName]: newName } }, options);
258
+ });
259
+
260
+ /**
261
+ * Multiply numeric field by value
262
+ *
263
+ * @param {string|ObjectId} id - Document ID
264
+ * @param {string} field - Field name
265
+ * @param {number} multiplier - Multiplier value
266
+ * @param {Object} [options={}] - Options
267
+ * @returns {Promise<Object>} Updated document
268
+ *
269
+ * @example
270
+ * // Double points
271
+ * await userRepo.multiplyField(userId, 'points', 2);
272
+ */
273
+ repo.registerMethod('multiplyField', async function (id, field, multiplier, options = {}) {
274
+ return validateAndUpdateNumeric.call(this, id, field, multiplier, '$mul', 'Multiplier', options);
275
+ });
276
+
277
+ /**
278
+ * Set field to minimum value (only if current value is greater)
279
+ *
280
+ * @param {string|ObjectId} id - Document ID
281
+ * @param {string} field - Field name
282
+ * @param {number} value - Minimum value
283
+ * @param {Object} [options={}] - Options
284
+ * @returns {Promise<Object>} Updated document
285
+ *
286
+ * @example
287
+ * // Set minimum price
288
+ * await productRepo.setMin(productId, 'price', 999);
289
+ */
290
+ repo.registerMethod('setMin', async function (id, field, value, options = {}) {
291
+ return applyOperator.call(this, id, field, value, '$min', options);
292
+ });
293
+
294
+ /**
295
+ * Set field to maximum value (only if current value is less)
296
+ *
297
+ * @param {string|ObjectId} id - Document ID
298
+ * @param {string} field - Field name
299
+ * @param {number} value - Maximum value
300
+ * @param {Object} [options={}] - Options
301
+ * @returns {Promise<Object>} Updated document
302
+ *
303
+ * @example
304
+ * // Set maximum score
305
+ * await gameRepo.setMax(gameId, 'highScore', newScore);
306
+ */
307
+ repo.registerMethod('setMax', async function (id, field, value, options = {}) {
308
+ return applyOperator.call(this, id, field, value, '$max', options);
309
+ });
310
+ }
311
+ });
312
+
313
+ export default mongoOperationsPlugin;
@@ -0,0 +1,46 @@
1
+ export const softDeletePlugin = (options = {}) => ({
2
+ name: 'softDelete',
3
+
4
+ apply(repo) {
5
+ const deletedField = options.deletedField || 'deletedAt';
6
+ const deletedByField = options.deletedByField || 'deletedBy';
7
+
8
+ repo.on('before:delete', async (context) => {
9
+ if (options.soft !== false) {
10
+ const updateData = {
11
+ [deletedField]: new Date(),
12
+ };
13
+
14
+ if (context.user) {
15
+ updateData[deletedByField] = context.user._id || context.user.id;
16
+ }
17
+
18
+ await repo.Model.findByIdAndUpdate(context.id, updateData, { session: context.session });
19
+
20
+ context.softDeleted = true;
21
+ }
22
+ });
23
+
24
+ repo.on('before:getAll', (context) => {
25
+ if (!context.includeDeleted && options.soft !== false) {
26
+ const queryParams = context.queryParams || {};
27
+ queryParams.filters = {
28
+ ...(queryParams.filters || {}),
29
+ [deletedField]: { $exists: false },
30
+ };
31
+ context.queryParams = queryParams;
32
+ }
33
+ });
34
+
35
+ repo.on('before:getById', (context) => {
36
+ if (!context.includeDeleted && options.soft !== false) {
37
+ context.query = {
38
+ ...(context.query || {}),
39
+ [deletedField]: { $exists: false },
40
+ };
41
+ }
42
+ });
43
+ },
44
+ });
45
+
46
+ export default softDeletePlugin;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Subdocument Plugin
3
+ * Adds subdocument array operations
4
+ */
5
+
6
+ import createError from 'http-errors';
7
+
8
+ export const subdocumentPlugin = () => ({
9
+ name: 'subdocument',
10
+
11
+ apply(repo) {
12
+ if (!repo.registerMethod) {
13
+ throw new Error('subdocumentPlugin requires methodRegistryPlugin');
14
+ }
15
+
16
+ /**
17
+ * Add subdocument to array
18
+ */
19
+ repo.registerMethod('addSubdocument', async function (parentId, arrayPath, subData, options = {}) {
20
+ return this.update(parentId, { $push: { [arrayPath]: subData } }, options);
21
+ });
22
+
23
+ /**
24
+ * Get subdocument from array
25
+ */
26
+ repo.registerMethod('getSubdocument', async function (parentId, arrayPath, subId, options = {}) {
27
+ return this._executeQuery(async (Model) => {
28
+ const parent = await Model.findById(parentId).session(options.session).exec();
29
+ if (!parent) throw createError(404, 'Parent not found');
30
+
31
+ const sub = parent[arrayPath].id(subId);
32
+ if (!sub) throw createError(404, 'Subdocument not found');
33
+
34
+ return options.lean ? sub.toObject() : sub;
35
+ });
36
+ });
37
+
38
+ /**
39
+ * Update subdocument in array
40
+ */
41
+ repo.registerMethod('updateSubdocument', async function (parentId, arrayPath, subId, updateData, options = {}) {
42
+ return this._executeQuery(async (Model) => {
43
+ const query = { _id: parentId, [`${arrayPath}._id`]: subId };
44
+ const update = { $set: { [`${arrayPath}.$`]: { ...updateData, _id: subId } } };
45
+
46
+ const result = await Model.findOneAndUpdate(query, update, {
47
+ new: true,
48
+ runValidators: true,
49
+ session: options.session,
50
+ }).exec();
51
+
52
+ if (!result) throw createError(404, 'Parent or subdocument not found');
53
+ return result;
54
+ });
55
+ });
56
+
57
+ /**
58
+ * Delete subdocument from array
59
+ */
60
+ repo.registerMethod('deleteSubdocument', async function (parentId, arrayPath, subId, options = {}) {
61
+ return this.update(parentId, { $pull: { [arrayPath]: { _id: subId } } }, options);
62
+ });
63
+ }
64
+ });
65
+
66
+ export default subdocumentPlugin;
@@ -0,0 +1,19 @@
1
+ export const timestampPlugin = () => ({
2
+ name: 'timestamp',
3
+
4
+ apply(repo) {
5
+ repo.on('before:create', (context) => {
6
+ if (!context.data) return;
7
+ const now = new Date();
8
+ if (!context.data.createdAt) context.data.createdAt = now;
9
+ if (!context.data.updatedAt) context.data.updatedAt = now;
10
+ });
11
+
12
+ repo.on('before:update', (context) => {
13
+ if (!context.data) return;
14
+ context.data.updatedAt = new Date();
15
+ });
16
+ },
17
+ });
18
+
19
+ export default timestampPlugin;