@common-stack/store-mongo 7.1.1-alpha.8 → 7.1.1-alpha.9
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.
- package/lib/module.d.ts +2 -1
- package/lib/templates/constants/SERVER_TYPES.ts.template +5 -0
- package/lib/templates/repositories/DatabaseMigration.ts.template +13 -0
- package/lib/templates/repositories/IBaseMongoRepository.ts.template +284 -0
- package/lib/templates/repositories/IBaseService.ts.template +231 -0
- package/lib/templates/repositories/IBaseServiceMixin.ts.template +460 -0
- package/lib/templates/repositories/IDataloader.ts.template +211 -0
- package/lib/templates/repositories/dbCommonTypes.ts.template +139 -0
- package/lib/templates/repositories/mongoCommonTypes.ts.template +21 -0
- package/package.json +2 -2
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/**
|
|
3
|
+
* @file IBaseServiceMixin.ts
|
|
4
|
+
* @description Defines the interface and types for the BaseServiceMixin functionality.
|
|
5
|
+
* This file documents the contract and behavior of the BaseServiceMixin without implementation details.
|
|
6
|
+
*
|
|
7
|
+
* The BaseServiceMixin provides a way to add standardized service methods to Moleculer service schemas.
|
|
8
|
+
* It implements the IBaseService interface and connects it to Moleculer's service framework,
|
|
9
|
+
* creating action handlers for all standard service operations.
|
|
10
|
+
*
|
|
11
|
+
* Architecture Context:
|
|
12
|
+
* - Part of the service layer in a microservices architecture
|
|
13
|
+
* - Bridges domain services with the Moleculer microservice framework
|
|
14
|
+
* - Follows a consistent pattern for exposing domain operations as microservice actions
|
|
15
|
+
* - Implements an event-driven architecture by emitting events on data changes
|
|
16
|
+
* - Provides automatic parameter validation through Moleculer's built-in validator
|
|
17
|
+
*
|
|
18
|
+
* Key features:
|
|
19
|
+
* - Automatic action creation for all IBaseService methods
|
|
20
|
+
* - Event emission for data changes (create, update, delete)
|
|
21
|
+
* - Parameter validation for all actions
|
|
22
|
+
* - Consistent error handling
|
|
23
|
+
* - Type safety through generics
|
|
24
|
+
*
|
|
25
|
+
* Usage Patterns:
|
|
26
|
+
* - Include this mixin in any Moleculer service that needs standard CRUD operations
|
|
27
|
+
* - Customize event emission by providing a custom events array
|
|
28
|
+
* - Extend with additional service-specific actions
|
|
29
|
+
* - Use with any implementation of IBaseService (typically a concrete service class)
|
|
30
|
+
*
|
|
31
|
+
* @see IBaseService - The interface this mixin implements
|
|
32
|
+
* @see IBaseMongoRepository - The repository interface used for data access
|
|
33
|
+
* @see BaseServiceCommands - Enum of standard service commands used as action names
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { ServiceBroker, ServiceSchema } from 'moleculer';
|
|
37
|
+
import { BaseServiceCommands } from 'common';
|
|
38
|
+
import { IBaseService } from './IBaseService';
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Interface for the BaseServiceMixin function
|
|
43
|
+
* This function creates a Moleculer service mixin that implements the IBaseService interface
|
|
44
|
+
*
|
|
45
|
+
* @param service - Implementation of IBaseService that will handle the actual business logic
|
|
46
|
+
* @param broker - Moleculer service broker for event emission
|
|
47
|
+
* @param name - Service name used for event naming
|
|
48
|
+
* @param events - List of operations that should emit events
|
|
49
|
+
* @returns A partial ServiceSchema that can be merged with other service definitions
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Create a user service with the base service mixin
|
|
53
|
+
* const UserService: ServiceSchema = {
|
|
54
|
+
* name: "users",
|
|
55
|
+
* mixins: [
|
|
56
|
+
* BaseServiceMixin(userService, broker, "users")
|
|
57
|
+
* ],
|
|
58
|
+
* // Additional service-specific actions and methods
|
|
59
|
+
* actions: {
|
|
60
|
+
* changePassword: { ... }
|
|
61
|
+
* }
|
|
62
|
+
* };
|
|
63
|
+
*
|
|
64
|
+
* // Example of customizing event emission
|
|
65
|
+
* const ProductService: ServiceSchema = {
|
|
66
|
+
* name: "products",
|
|
67
|
+
* mixins: [
|
|
68
|
+
* // Only emit events for create and update operations
|
|
69
|
+
* BaseServiceMixin(
|
|
70
|
+
* productService,
|
|
71
|
+
* broker,
|
|
72
|
+
* "products",
|
|
73
|
+
* [BaseServiceCommands.Create, BaseServiceCommands.Update]
|
|
74
|
+
* )
|
|
75
|
+
* ]
|
|
76
|
+
* };
|
|
77
|
+
*/
|
|
78
|
+
export interface IBaseServiceMixinFunction {
|
|
79
|
+
<T>(
|
|
80
|
+
service: IBaseService<T>,
|
|
81
|
+
broker?: ServiceBroker,
|
|
82
|
+
name?: string,
|
|
83
|
+
events?: BaseServiceCommands[],
|
|
84
|
+
): Partial<ServiceSchema>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Interface describing the actions created by the BaseServiceMixin
|
|
89
|
+
* Each action corresponds to a method in the IBaseService interface
|
|
90
|
+
*/
|
|
91
|
+
export interface IBaseServiceMixinActions {
|
|
92
|
+
/**
|
|
93
|
+
* Get a single document by ID
|
|
94
|
+
*
|
|
95
|
+
* This action retrieves a single entity by its unique identifier.
|
|
96
|
+
* It maps to the IBaseService.get method and handles parameter validation.
|
|
97
|
+
*
|
|
98
|
+
* @param id - The unique identifier of the document to retrieve
|
|
99
|
+
* @returns The domain entity or null if not found
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* // Call from another service
|
|
103
|
+
* const user = await broker.call('users.get', { id: '123' });
|
|
104
|
+
*
|
|
105
|
+
* // Call from API
|
|
106
|
+
* app.get('/api/users/:id', async (req, res) => {
|
|
107
|
+
* const user = await broker.call('users.get', { id: req.params.id });
|
|
108
|
+
* res.json(user);
|
|
109
|
+
* });
|
|
110
|
+
*/
|
|
111
|
+
[BaseServiceCommands.Get]: {
|
|
112
|
+
params: {
|
|
113
|
+
id: string;
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Count documents matching criteria
|
|
119
|
+
*
|
|
120
|
+
* This action counts entities matching the provided filter criteria.
|
|
121
|
+
* It maps to the IBaseService.count method and handles parameter validation.
|
|
122
|
+
*
|
|
123
|
+
* @param criteria - Optional filter criteria to count specific documents
|
|
124
|
+
* @returns The count of matching documents
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* // Count active users
|
|
128
|
+
* const count = await broker.call('users.count', {
|
|
129
|
+
* criteria: { active: true }
|
|
130
|
+
* });
|
|
131
|
+
*/
|
|
132
|
+
[BaseServiceCommands.Count]: {
|
|
133
|
+
params: {
|
|
134
|
+
criteria?: Record<string, any>;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create multiple documents
|
|
140
|
+
*
|
|
141
|
+
* This action creates multiple entities in a single operation.
|
|
142
|
+
* It maps to the IBaseService.bulkCreate method and handles parameter validation.
|
|
143
|
+
* It also emits events for the created entities if configured.
|
|
144
|
+
*
|
|
145
|
+
* @param data - Array of data objects for the new documents
|
|
146
|
+
* @returns Array of created domain entities
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // Create multiple users
|
|
150
|
+
* const users = await broker.call('users.bulkCreate', {
|
|
151
|
+
* data: [
|
|
152
|
+
* { name: 'John', email: 'john@example.com' },
|
|
153
|
+
* { name: 'Jane', email: 'jane@example.com' }
|
|
154
|
+
* ]
|
|
155
|
+
* });
|
|
156
|
+
*/
|
|
157
|
+
[BaseServiceCommands.BulkCreate]: {
|
|
158
|
+
params: {
|
|
159
|
+
data: any[];
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a single document
|
|
165
|
+
*
|
|
166
|
+
* This action creates a single entity.
|
|
167
|
+
* It maps to the IBaseService.create method and handles parameter validation.
|
|
168
|
+
* It also emits an event for the created entity if configured.
|
|
169
|
+
*
|
|
170
|
+
* @param data - Data object for the new document
|
|
171
|
+
* @returns The created domain entity
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* // Create a new user
|
|
175
|
+
* const user = await broker.call('users.create', {
|
|
176
|
+
* data: { name: 'John', email: 'john@example.com' }
|
|
177
|
+
* });
|
|
178
|
+
*/
|
|
179
|
+
[BaseServiceCommands.Create]: {
|
|
180
|
+
params: {
|
|
181
|
+
data: any;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Delete a document by ID
|
|
187
|
+
*
|
|
188
|
+
* This action deletes a single entity by its unique identifier.
|
|
189
|
+
* It maps to the IBaseService.delete method and handles parameter validation.
|
|
190
|
+
* It also emits an event for the deleted entity if configured.
|
|
191
|
+
*
|
|
192
|
+
* @param id - The unique identifier of the document to delete
|
|
193
|
+
* @returns Boolean indicating success
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* // Delete a user
|
|
197
|
+
* const success = await broker.call('users.delete', { id: '123' });
|
|
198
|
+
*/
|
|
199
|
+
[BaseServiceCommands.Delete]: {
|
|
200
|
+
params: {
|
|
201
|
+
id: string;
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get documents with pagination
|
|
207
|
+
*
|
|
208
|
+
* This action retrieves multiple entities with filtering, sorting, and pagination.
|
|
209
|
+
* It maps to the IBaseService.getAll method and handles parameter validation.
|
|
210
|
+
*
|
|
211
|
+
* @param criteria - Optional filter criteria
|
|
212
|
+
* @param sort - Optional sorting configuration
|
|
213
|
+
* @param skip - Optional number of documents to skip (for pagination)
|
|
214
|
+
* @param limit - Optional maximum number of documents to return
|
|
215
|
+
* @param selectedFields - Optional space-separated list of fields to include
|
|
216
|
+
* @returns Array of domain entities
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* // Get active users sorted by creation date
|
|
220
|
+
* const users = await broker.call('users.getAll', {
|
|
221
|
+
* criteria: { active: true },
|
|
222
|
+
* sort: { createdAt: -1 },
|
|
223
|
+
* skip: 0,
|
|
224
|
+
* limit: 10
|
|
225
|
+
* });
|
|
226
|
+
*/
|
|
227
|
+
[BaseServiceCommands.GetAll]: {
|
|
228
|
+
params: {
|
|
229
|
+
criteria?: Record<string, any>;
|
|
230
|
+
sort?: Record<string, 1 | -1>;
|
|
231
|
+
skip?: number;
|
|
232
|
+
limit?: number;
|
|
233
|
+
selectedFields?: string;
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get documents with pagination and total count
|
|
239
|
+
*
|
|
240
|
+
* This action retrieves multiple entities with filtering, sorting, pagination,
|
|
241
|
+
* and includes the total count of matching documents (useful for UI pagination).
|
|
242
|
+
* It maps to the IBaseService.getAllWithCount method and handles parameter validation.
|
|
243
|
+
*
|
|
244
|
+
* @param criteria - Optional filter criteria
|
|
245
|
+
* @param sort - Optional sorting configuration
|
|
246
|
+
* @param skip - Optional number of documents to skip (for pagination)
|
|
247
|
+
* @param limit - Optional maximum number of documents to return
|
|
248
|
+
* @param selectedFields - Optional space-separated list of fields to include
|
|
249
|
+
* @returns Object with data array and total count
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* // Get paginated users with total count for UI pagination
|
|
253
|
+
* const { data, totalCount } = await broker.call('users.getAllWithCount', {
|
|
254
|
+
* criteria: { role: 'user' },
|
|
255
|
+
* skip: 20,
|
|
256
|
+
* limit: 10,
|
|
257
|
+
* sort: { createdAt: -1 }
|
|
258
|
+
* });
|
|
259
|
+
*/
|
|
260
|
+
[BaseServiceCommands.GetAllWithCount]: {
|
|
261
|
+
params: {
|
|
262
|
+
criteria?: Record<string, any>;
|
|
263
|
+
sort?: Record<string, 1 | -1>;
|
|
264
|
+
skip?: number;
|
|
265
|
+
limit?: number;
|
|
266
|
+
selectedFields?: string;
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Create or update a document
|
|
272
|
+
*
|
|
273
|
+
* This action creates a new entity or updates an existing one if an ID is provided.
|
|
274
|
+
* It maps to the IBaseService.insert method and handles parameter validation.
|
|
275
|
+
* It also emits an event for the created or updated entity if configured.
|
|
276
|
+
*
|
|
277
|
+
* @param data - Data object for the new or updated document
|
|
278
|
+
* @param overwrite - Optional flag to completely replace the document instead of merging
|
|
279
|
+
* @returns The created or updated domain entity
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* // Create a new product
|
|
283
|
+
* const newProduct = await broker.call('products.insert', {
|
|
284
|
+
* data: { name: 'New Product', price: 99.99 }
|
|
285
|
+
* });
|
|
286
|
+
*
|
|
287
|
+
* // Update an existing product
|
|
288
|
+
* const updatedProduct = await broker.call('products.insert', {
|
|
289
|
+
* data: { id: 'product123', name: 'Updated Product', price: 129.99 }
|
|
290
|
+
* });
|
|
291
|
+
*/
|
|
292
|
+
[BaseServiceCommands.Insert]: {
|
|
293
|
+
params: {
|
|
294
|
+
data: any & { id?: string };
|
|
295
|
+
overwrite?: boolean;
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Update a document by ID
|
|
301
|
+
*
|
|
302
|
+
* This action updates an existing entity by its unique identifier.
|
|
303
|
+
* It maps to the IBaseService.update method and handles parameter validation.
|
|
304
|
+
* It also emits an event for the updated entity if configured.
|
|
305
|
+
*
|
|
306
|
+
* @param id - The unique identifier of the document to update
|
|
307
|
+
* @param data - Data object with fields to update
|
|
308
|
+
* @param overwrite - Optional flag to completely replace the document instead of merging
|
|
309
|
+
* @returns The updated domain entity
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* // Update a user's role
|
|
313
|
+
* const updatedUser = await broker.call('users.update', {
|
|
314
|
+
* id: 'user123',
|
|
315
|
+
* data: { role: 'admin', active: true }
|
|
316
|
+
* });
|
|
317
|
+
*
|
|
318
|
+
* // Replace a user document entirely
|
|
319
|
+
* const replacedUser = await broker.call('users.update', {
|
|
320
|
+
* id: 'user123',
|
|
321
|
+
* data: { name: 'New Name', email: 'new@example.com', role: 'user' },
|
|
322
|
+
* overwrite: true
|
|
323
|
+
* });
|
|
324
|
+
*/
|
|
325
|
+
[BaseServiceCommands.Update]: {
|
|
326
|
+
params: {
|
|
327
|
+
id: string;
|
|
328
|
+
data: any;
|
|
329
|
+
overwrite?: boolean;
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Delete multiple documents matching criteria
|
|
335
|
+
*
|
|
336
|
+
* This action deletes multiple entities matching the provided filter criteria.
|
|
337
|
+
* It maps to the IBaseService.delete method and handles parameter validation.
|
|
338
|
+
* It also emits an event with the deletion criteria if configured.
|
|
339
|
+
*
|
|
340
|
+
* @param criteria - Filter criteria to find documents to delete
|
|
341
|
+
* @returns Number of deleted documents
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* // Delete all inactive users
|
|
345
|
+
* const deletedCount = await broker.call('users.deleteMany', {
|
|
346
|
+
* criteria: { active: false }
|
|
347
|
+
* });
|
|
348
|
+
*
|
|
349
|
+
* // Delete users with a specific role
|
|
350
|
+
* const deletedCount = await broker.call('users.deleteMany', {
|
|
351
|
+
* criteria: { role: 'guest' }
|
|
352
|
+
* });
|
|
353
|
+
*/
|
|
354
|
+
[BaseServiceCommands.DeleteMany]: {
|
|
355
|
+
params: {
|
|
356
|
+
criteria: Record<string, any>;
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Interface describing the events emitted by the BaseServiceMixin
|
|
363
|
+
* Events follow a consistent naming pattern: `${serviceName}.on${CommandName}`
|
|
364
|
+
*/
|
|
365
|
+
export interface IBaseServiceMixinEvents {
|
|
366
|
+
/**
|
|
367
|
+
* Event emitted when a document is created
|
|
368
|
+
* Event name format: `${serviceName}.onCreate`
|
|
369
|
+
* Payload: The created domain entity
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* // Listen for user creation events
|
|
373
|
+
* broker.createService({
|
|
374
|
+
* name: 'notification',
|
|
375
|
+
* events: {
|
|
376
|
+
* 'users.onCreate': function(payload) {
|
|
377
|
+
* // Send welcome email to new user
|
|
378
|
+
* this.sendWelcomeEmail(payload.email);
|
|
379
|
+
* }
|
|
380
|
+
* }
|
|
381
|
+
* });
|
|
382
|
+
*/
|
|
383
|
+
onCreate: any;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Event emitted when multiple documents are created
|
|
387
|
+
* Event name format: `${serviceName}.onBulkCreate`
|
|
388
|
+
* Payload: Array of created domain entities
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* // Listen for bulk user creation events
|
|
392
|
+
* broker.createService({
|
|
393
|
+
* name: 'notification',
|
|
394
|
+
* events: {
|
|
395
|
+
* 'users.onBulkCreate': function(payload) {
|
|
396
|
+
* // Send welcome emails to all new users
|
|
397
|
+
* payload.forEach(user => this.sendWelcomeEmail(user.email));
|
|
398
|
+
* }
|
|
399
|
+
* }
|
|
400
|
+
* });
|
|
401
|
+
*/
|
|
402
|
+
onBulkCreate: any[];
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Event emitted when a document is updated
|
|
406
|
+
* Event name format: `${serviceName}.onUpdate`
|
|
407
|
+
* Payload: The updated domain entity
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* // Listen for user update events
|
|
411
|
+
* broker.createService({
|
|
412
|
+
* name: 'audit',
|
|
413
|
+
* events: {
|
|
414
|
+
* 'users.onUpdate': function(payload) {
|
|
415
|
+
* // Log user update for audit trail
|
|
416
|
+
* this.logAuditEvent('user_updated', payload.id);
|
|
417
|
+
* }
|
|
418
|
+
* }
|
|
419
|
+
* });
|
|
420
|
+
*/
|
|
421
|
+
onUpdate: any;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Event emitted when a document is deleted
|
|
425
|
+
* Event name format: `${serviceName}.onDelete`
|
|
426
|
+
* Payload: Object containing the ID of the deleted entity
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* // Listen for user deletion events
|
|
430
|
+
* broker.createService({
|
|
431
|
+
* name: 'cleanup',
|
|
432
|
+
* events: {
|
|
433
|
+
* 'users.onDelete': function(payload) {
|
|
434
|
+
* // Clean up related user data
|
|
435
|
+
* this.cleanupUserData(payload.id);
|
|
436
|
+
* }
|
|
437
|
+
* }
|
|
438
|
+
* });
|
|
439
|
+
*/
|
|
440
|
+
onDelete: { id: string };
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Event emitted when multiple documents are deleted
|
|
444
|
+
* Event name format: `${serviceName}.onDeleteMany`
|
|
445
|
+
* Payload: The criteria used for deletion
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* // Listen for bulk user deletion events
|
|
449
|
+
* broker.createService({
|
|
450
|
+
* name: 'cleanup',
|
|
451
|
+
* events: {
|
|
452
|
+
* 'users.onDeleteMany': function(payload) {
|
|
453
|
+
* // Clean up related data for deleted users
|
|
454
|
+
* this.cleanupBulkUserData(payload.criteria);
|
|
455
|
+
* }
|
|
456
|
+
* }
|
|
457
|
+
* });
|
|
458
|
+
*/
|
|
459
|
+
onDeleteMany: { criteria: Record<string, any> };
|
|
460
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/**
|
|
3
|
+
* @file IDataLoader.ts
|
|
4
|
+
* @description Defines interfaces for implementing the DataLoader pattern to efficiently batch and cache database queries.
|
|
5
|
+
*
|
|
6
|
+
* The DataLoader pattern helps prevent the N+1 query problem by consolidating multiple individual requests
|
|
7
|
+
* into a single batch request. This significantly improves performance for nested queries and relationship
|
|
8
|
+
* traversal in GraphQL or REST APIs.
|
|
9
|
+
*
|
|
10
|
+
* Architecture Context:
|
|
11
|
+
* - Part of the data access optimization layer
|
|
12
|
+
* - Sits between repositories and services/resolvers
|
|
13
|
+
* - Implements request batching and caching for efficient data access
|
|
14
|
+
* - Particularly valuable for GraphQL implementations to solve the N+1 query problem
|
|
15
|
+
* - Works with MongoDB collections through Mongoose
|
|
16
|
+
*
|
|
17
|
+
* Key features:
|
|
18
|
+
* - Request batching: Combines multiple individual requests into a single database query
|
|
19
|
+
* - In-memory caching: Prevents duplicate requests for the same entity
|
|
20
|
+
* - Custom loading options: Supports flexible query criteria beyond simple ID lookups
|
|
21
|
+
* - Clear cache methods: Allows for cache invalidation when data changes
|
|
22
|
+
*
|
|
23
|
+
* Usage Patterns:
|
|
24
|
+
* - Create a DataLoader instance for each entity type in your application
|
|
25
|
+
* - Use the load method for single entity retrieval by ID
|
|
26
|
+
* - Use loadMany for retrieving multiple entities by their IDs
|
|
27
|
+
* - Use withOptions for more complex queries with custom criteria
|
|
28
|
+
* - Clear cache entries when entities are modified
|
|
29
|
+
*
|
|
30
|
+
* Performance Impact:
|
|
31
|
+
* - Reduces database round trips by batching requests
|
|
32
|
+
* - Minimizes redundant queries through caching
|
|
33
|
+
* - Particularly effective for nested relationship queries in GraphQL
|
|
34
|
+
* - Can dramatically improve response times for complex data graphs
|
|
35
|
+
*
|
|
36
|
+
* @see IBaseMongoRepository - Repository layer that provides the underlying data access
|
|
37
|
+
* @see AsDomainType - Type transformation for domain objects
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import DataLoader from 'dataloader';
|
|
41
|
+
import type { FilterQuery } from 'mongoose';
|
|
42
|
+
import { AsDomainType } from './dbCommonTypes';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Options for configuring how entities are loaded through the DataLoader
|
|
46
|
+
*
|
|
47
|
+
* @property id - Unique identifier for the request (used for caching)
|
|
48
|
+
* @property searchKey - The field to search by (typically 'id' or another indexed field)
|
|
49
|
+
* @property comparator - Optional custom function to determine if an item matches the search criteria
|
|
50
|
+
* @property criteria - Optional additional filter criteria to apply to the query
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Basic options for loading by ID
|
|
54
|
+
* const options: DataLoaderOptions<UserSchema> = {
|
|
55
|
+
* id: '123',
|
|
56
|
+
* searchKey: 'id'
|
|
57
|
+
* };
|
|
58
|
+
*
|
|
59
|
+
* // Advanced options with custom criteria
|
|
60
|
+
* const options: DataLoaderOptions<UserSchema> = {
|
|
61
|
+
* id: 'active-admins',
|
|
62
|
+
* searchKey: 'role',
|
|
63
|
+
* criteria: { active: true, role: 'admin' },
|
|
64
|
+
* comparator: (opt, item) => item.role === 'admin' && item.active === true
|
|
65
|
+
* };
|
|
66
|
+
*/
|
|
67
|
+
export interface DataLoaderOptions<SchemaType> {
|
|
68
|
+
/** Unique identifier for the request (used for caching) */
|
|
69
|
+
id: string;
|
|
70
|
+
|
|
71
|
+
/** The field to search by (typically 'id' or another indexed field) */
|
|
72
|
+
searchKey: keyof AsDomainType<SchemaType> | string;
|
|
73
|
+
|
|
74
|
+
/** Optional custom function to determine if an item matches the search criteria */
|
|
75
|
+
comparator?: (option: DataLoaderOptions<SchemaType>, item: AsDomainType<SchemaType>) => boolean;
|
|
76
|
+
|
|
77
|
+
/** Optional additional filter criteria to apply to the query */
|
|
78
|
+
criteria?: FilterQuery<SchemaType>;
|
|
79
|
+
|
|
80
|
+
/** Additional custom properties can be added as needed */
|
|
81
|
+
[key: string]: any;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Interface for implementing the DataLoader pattern to efficiently batch and cache database queries
|
|
86
|
+
*
|
|
87
|
+
* The IDataLoader interface provides methods for loading single or multiple entities by ID,
|
|
88
|
+
* clearing cache entries, and loading entities with custom options. This implementation
|
|
89
|
+
* supports MongoDB collections through Mongoose.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // Create a user data loader
|
|
93
|
+
* const userLoader = new MongoDataLoader(UserModel);
|
|
94
|
+
*
|
|
95
|
+
* // Load a user by ID (batched and cached)
|
|
96
|
+
* const user1 = await userLoader.load('user1');
|
|
97
|
+
* const user2 = await userLoader.load('user2');
|
|
98
|
+
*
|
|
99
|
+
* // Load multiple users by ID in a single batch
|
|
100
|
+
* const users = await userLoader.loadMany(['user3', 'user4', 'user5']);
|
|
101
|
+
*
|
|
102
|
+
* // Load users with custom criteria
|
|
103
|
+
* const activeAdmins = await userLoader.withOptions.load({
|
|
104
|
+
* id: 'active-admins',
|
|
105
|
+
* searchKey: 'role',
|
|
106
|
+
* criteria: { active: true, role: 'admin' }
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* // Clear cache when a user is updated
|
|
110
|
+
* userLoader.clear('user1');
|
|
111
|
+
*/
|
|
112
|
+
export interface IDataLoader<SchemaType> {
|
|
113
|
+
/**
|
|
114
|
+
* Load a single entity by ID
|
|
115
|
+
*
|
|
116
|
+
* This method retrieves a single entity by its unique identifier.
|
|
117
|
+
* Requests are batched with other load calls in the same tick of the event loop
|
|
118
|
+
* and results are cached to prevent duplicate database queries.
|
|
119
|
+
*
|
|
120
|
+
* @param id - The unique identifier of the entity to load
|
|
121
|
+
* @returns Promise resolving to the domain entity or null if not found
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* // Load a user by ID
|
|
125
|
+
* const user = await userLoader.load('user123');
|
|
126
|
+
*
|
|
127
|
+
* // Handle case where entity might not exist
|
|
128
|
+
* const product = await productLoader.load('product456');
|
|
129
|
+
* if (product) {
|
|
130
|
+
* // Product exists, use it
|
|
131
|
+
* } else {
|
|
132
|
+
* // Product not found
|
|
133
|
+
* }
|
|
134
|
+
*/
|
|
135
|
+
load: (id: string) => Promise<AsDomainType<SchemaType> | null>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Load multiple entities by their IDs
|
|
139
|
+
*
|
|
140
|
+
* This method retrieves multiple entities by their unique identifiers in a single batch.
|
|
141
|
+
* Results are cached to prevent duplicate database queries.
|
|
142
|
+
*
|
|
143
|
+
* @param ids - Array of unique identifiers of the entities to load
|
|
144
|
+
* @returns Promise resolving to an array of domain entities, nulls, or errors
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* // Load multiple users by their IDs
|
|
148
|
+
* const users = await userLoader.loadMany(['user1', 'user2', 'user3']);
|
|
149
|
+
*
|
|
150
|
+
* // Handle results which may include nulls for not found entities
|
|
151
|
+
* const validUsers = users.filter(user => user !== null && !(user instanceof Error));
|
|
152
|
+
*/
|
|
153
|
+
loadMany: (ids: string[]) => Promise<(AsDomainType<SchemaType> | null | Error)[]>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Clear a single entity from the cache
|
|
157
|
+
*
|
|
158
|
+
* This method removes a specific entity from the cache, forcing the next load
|
|
159
|
+
* request for this ID to query the database. Use this when an entity is updated
|
|
160
|
+
* or deleted to ensure fresh data is loaded.
|
|
161
|
+
*
|
|
162
|
+
* @param id - The unique identifier of the entity to remove from cache
|
|
163
|
+
* @returns The DataLoader instance for method chaining
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* // Update a user and clear the cache
|
|
167
|
+
* await userService.update('user123', { name: 'New Name' });
|
|
168
|
+
* userLoader.clear('user123');
|
|
169
|
+
*/
|
|
170
|
+
clear: (id: string) => DataLoader<string, AsDomainType<SchemaType> | null>;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Clear all entities from the cache
|
|
174
|
+
*
|
|
175
|
+
* This method removes all entities from the cache, forcing the next load
|
|
176
|
+
* requests to query the database. Use this when multiple entities are updated
|
|
177
|
+
* or when you need to ensure all data is fresh.
|
|
178
|
+
*
|
|
179
|
+
* @returns The DataLoader instance for method chaining
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* // After a bulk update operation, clear the entire cache
|
|
183
|
+
* await userService.bulkUpdate({ role: 'user' }, { active: true });
|
|
184
|
+
* userLoader.clearAll();
|
|
185
|
+
*/
|
|
186
|
+
clearAll: () => DataLoader<string, AsDomainType<SchemaType> | null>;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* DataLoader for loading entities with custom options
|
|
190
|
+
*
|
|
191
|
+
* This property provides a DataLoader instance that supports more complex
|
|
192
|
+
* loading scenarios beyond simple ID lookups. Use this when you need to
|
|
193
|
+
* load entities based on other criteria or with additional filtering.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* // Load active users with the admin role
|
|
197
|
+
* const adminUsers = await userLoader.withOptions.load({
|
|
198
|
+
* id: 'active-admins', // Unique cache key
|
|
199
|
+
* searchKey: 'role',
|
|
200
|
+
* criteria: { active: true, role: 'admin' }
|
|
201
|
+
* });
|
|
202
|
+
*
|
|
203
|
+
* // Load products in a specific price range
|
|
204
|
+
* const affordableProducts = await productLoader.withOptions.load({
|
|
205
|
+
* id: 'affordable-products',
|
|
206
|
+
* searchKey: 'price',
|
|
207
|
+
* criteria: { price: { $lt: 100 } }
|
|
208
|
+
* });
|
|
209
|
+
*/
|
|
210
|
+
withOptions: DataLoader<DataLoaderOptions<SchemaType>, AsDomainType<SchemaType>[]>;
|
|
211
|
+
}
|