@modular-rest/server 1.20.1 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { DatabaseTriggerContext } from './database_trigger';
1
+ import DatabaseTrigger, { DatabaseTriggerContext } from './database_trigger';
2
2
  /**
3
3
  * Type for CMS operations that can trigger a callback
4
4
  * @typedef {('update-one' | 'insert-one' | 'remove-one')} CmsOperation
@@ -27,9 +27,7 @@ export type CmsOperation = 'update-one' | 'insert-one' | 'remove-one';
27
27
  * });
28
28
  * ```
29
29
  */
30
- export declare class CmsTrigger {
31
- operation: CmsOperation;
32
- callback: (context: DatabaseTriggerContext) => void;
30
+ export declare class CmsTrigger extends DatabaseTrigger {
33
31
  /**
34
32
  * Creates a new CmsTrigger instance
35
33
  * @param {CmsOperation} operation - The CMS operation to trigger on
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.CmsTrigger = void 0;
7
+ const database_trigger_1 = __importDefault(require("./database_trigger"));
4
8
  /**
5
9
  * Defines a callback to be executed on specific CMS operations
6
10
  * @class CmsTrigger
@@ -20,7 +24,7 @@ exports.CmsTrigger = void 0;
20
24
  * });
21
25
  * ```
22
26
  */
23
- class CmsTrigger {
27
+ class CmsTrigger extends database_trigger_1.default {
24
28
  /**
25
29
  * Creates a new CmsTrigger instance
26
30
  * @param {CmsOperation} operation - The CMS operation to trigger on
@@ -39,8 +43,7 @@ class CmsTrigger {
39
43
  * ```
40
44
  */
41
45
  constructor(operation, callback = () => { }) {
42
- this.operation = operation;
43
- this.callback = callback;
46
+ super(operation, callback);
44
47
  }
45
48
  }
46
49
  exports.CmsTrigger = CmsTrigger;
@@ -138,6 +138,12 @@ class CollectionDefinition {
138
138
  this.schema = schema;
139
139
  this.permissions = permissions;
140
140
  this.triggers = triggers;
141
+ // Apply triggers to schema
142
+ if (this.triggers) {
143
+ this.triggers.forEach(trigger => {
144
+ trigger.applyToSchema(this.schema);
145
+ });
146
+ }
141
147
  }
142
148
  /**
143
149
  * Get the mongoose model for this collection
@@ -1,22 +1,24 @@
1
1
  /**
2
2
  * Type for database operations that can trigger a callback
3
3
  */
4
- export type DatabaseOperation = 'find' | 'find-one' | 'count' | 'update-one' | 'insert-one' | 'remove-one' | 'aggregate';
4
+ export type DatabaseOperation = 'find' | 'find-one' | 'count' | 'update-one' | 'insert-one' | 'remove-one' | 'aggregate' | 'insert-many' | 'delete-many' | 'find-one-and-update' | 'find-one-and-delete' | 'distinct' | 'validate';
5
5
  /**
6
6
  * Context interface for database trigger callbacks
7
7
  * @interface DatabaseTriggerContext
8
- * @property {Record<string, any>} doc - The document data for insert/update operations
9
- * @property {Record<string, any>} query - The query data for find/find-one operations
8
+ * @property {Record<string, any>} doc - The document data for insert/update/validate operations.
9
+ * @property {Record<string, any>[]} docs - The documents data for insert-many operations.
10
+ * @property {Record<string, any>} query - The query data for find/remove/count/distinct operations
10
11
  * @property {Record<string, any>} update - The update data for update operations
11
12
  * @property {Record<string, any>[]} pipelines - The aggregation pipelines for aggregate operations
12
- * @property {Record<string, any> } queryResult - The result of the database operation
13
+ * @property {any} queryResult - The result of the database operation (can be document, array of documents, number, or null)
13
14
  */
14
15
  export interface DatabaseTriggerContext {
15
16
  doc?: Record<string, any>;
17
+ docs?: Record<string, any>[];
16
18
  query?: Record<string, any>;
17
19
  update?: Record<string, any>;
18
20
  pipelines?: Record<string, any>[];
19
- queryResult: Record<string, any>;
21
+ queryResult: any;
20
22
  }
21
23
  /**
22
24
  * The callback function to be executed on specific database operations
@@ -30,36 +32,39 @@ export interface DatabaseTriggerContext {
30
32
  */
31
33
  type DatabaseTriggerCallback = (context: DatabaseTriggerContext) => void;
32
34
  /**
33
- * in a complex application, you may need to perform additional actions after a database operation.
34
- * this is where DatabaseTrigger comes in. so you can define a callback to be executed on specific database operations for a collection.
35
+ * In a complex application, you may need to perform additional actions after a database operation.
36
+ * This is where DatabaseTrigger comes in, allowing you to define callbacks for specific database operations on a collection.
35
37
  *
36
- * Supported triggers are:
38
+ * ### Supported Triggers and Contexts
37
39
  *
38
- * | Trigger | Description |
39
- * | ------------ | ------------------------------------------------------------ |
40
- * | `find` | Triggered when a find query is executed on collection. |
41
- * | `find-one` | Triggered when a find one query is executed on collection. |
42
- * | `count` | Triggered when a count query is executed on collection. |
43
- * | `update-one` | Triggered when a update one query is executed on collection. |
44
- * | `insert-one` | Triggered when a insert one query is executed on collection. |
45
- * | `remove-one` | Triggered when a remove one query is executed on collection. |
46
- * | `aggregate` | Triggered when a aggregate query is executed on collection. |
40
+ * The `callback` function associated with a trigger receives a {@link DatabaseTriggerContext} object. The properties available in this context vary depending on the trigger operation.
41
+ *
42
+ * | Trigger Operation | Description | Context Data |
43
+ * | :--- | :--- | :--- |
44
+ * | `insert-one` | Triggered after a single document insertion. | `doc`: The inserted document.<br>`queryResult`: The inserted document. |
45
+ * | `insert-many` | Triggered after multiple document insertions. | `docs`: The array of inserted documents.<br>`queryResult`: The array of inserted documents. |
46
+ * | `update-one` | Triggered after updating a single document (using `updateOne`). | `query`: The query filter used.<br>`update`: The update operations applied.<br>`queryResult`: The operation result. |
47
+ * | `find-one-and-update` | Triggered after `findOneAndUpdate`. | `query`: The query filter used.<br>`update`: The update operations applied.<br>`queryResult`: The updated document. |
48
+ * | `delete-many` | Triggered after removing multiple documents (using `deleteMany`). | `query`: The query filter used.<br>`queryResult`: The operation result. |
49
+ * | `remove-one` | Triggered after removing a single document (using `deleteOne`). | `query`: The query filter used.<br>`queryResult`: The operation result. |
50
+ * | `find-one-and-delete` | Triggered after `findOneAndDelete` or `findOneAndRemove`. | `query`: The query filter used.<br>`queryResult`: The removed document. |
51
+ * | `find` | Triggered after a `find` query. | `query`: The query filter used.<br>`queryResult`: Array of found documents. |
52
+ * | `find-one` | Triggered after a `findOne` query. | `query`: The query filter used.<br>`queryResult`: The found document. |
53
+ * | `count` | Triggered after a `countDocuments` query. | `query`: The query filter used.<br>`queryResult`: The count (number). |
54
+ * | `aggregate` | Triggered after an aggregation pipeline. | `pipelines`: The aggregation pipeline used.<br>`queryResult`: The aggregation result. |
55
+ * | `distinct` | Triggered after a `distinct` query. | `query`: The query filter used.<br>`queryResult`: The distinct values. |
56
+ * | `validate` | Triggered after document validation. | `doc`: The validated document.<br>`queryResult`: The validated document. |
47
57
  *
48
58
  * @property {DatabaseOperation} operation - The database operation that triggers the callback
49
59
  * @property {DatabaseTriggerCallback} callback - The callback function to be executed
50
60
  *
51
61
  * @example
52
62
  * ```typescript
53
- * import { DatabaseTrigger } from '@server-ts/database';
63
+ * import { DatabaseTrigger } from '@modular-rest/server';
54
64
  *
55
- * const trigger = new DatabaseTrigger('insert-one', ({ query, queryResult }) => {
65
+ * const trigger = new DatabaseTrigger('insert-one', ({ doc, queryResult }) => {
56
66
  * console.log('New document inserted:', queryResult);
57
- *
58
- * try {
59
- * // Perform additional actions after document insertion
60
- * } catch (error) {
61
- * console.error('Error performing additional actions:', error);
62
- * }
67
+ * // Perform additional actions
63
68
  * });
64
69
  *
65
70
  * // Use the trigger in a collection definition
@@ -86,5 +91,10 @@ export declare class DatabaseTrigger {
86
91
  *
87
92
  */
88
93
  constructor(operation: DatabaseOperation, callback?: DatabaseTriggerCallback);
94
+ /**
95
+ * Applies the trigger to a Mongoose schema
96
+ * @param {Schema} schema - The mongoose schema to apply the trigger to
97
+ */
98
+ applyToSchema(schema: any): void;
89
99
  }
90
100
  export default DatabaseTrigger;
@@ -2,36 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DatabaseTrigger = void 0;
4
4
  /**
5
- * in a complex application, you may need to perform additional actions after a database operation.
6
- * this is where DatabaseTrigger comes in. so you can define a callback to be executed on specific database operations for a collection.
5
+ * In a complex application, you may need to perform additional actions after a database operation.
6
+ * This is where DatabaseTrigger comes in, allowing you to define callbacks for specific database operations on a collection.
7
7
  *
8
- * Supported triggers are:
8
+ * ### Supported Triggers and Contexts
9
9
  *
10
- * | Trigger | Description |
11
- * | ------------ | ------------------------------------------------------------ |
12
- * | `find` | Triggered when a find query is executed on collection. |
13
- * | `find-one` | Triggered when a find one query is executed on collection. |
14
- * | `count` | Triggered when a count query is executed on collection. |
15
- * | `update-one` | Triggered when a update one query is executed on collection. |
16
- * | `insert-one` | Triggered when a insert one query is executed on collection. |
17
- * | `remove-one` | Triggered when a remove one query is executed on collection. |
18
- * | `aggregate` | Triggered when a aggregate query is executed on collection. |
10
+ * The `callback` function associated with a trigger receives a {@link DatabaseTriggerContext} object. The properties available in this context vary depending on the trigger operation.
11
+ *
12
+ * | Trigger Operation | Description | Context Data |
13
+ * | :--- | :--- | :--- |
14
+ * | `insert-one` | Triggered after a single document insertion. | `doc`: The inserted document.<br>`queryResult`: The inserted document. |
15
+ * | `insert-many` | Triggered after multiple document insertions. | `docs`: The array of inserted documents.<br>`queryResult`: The array of inserted documents. |
16
+ * | `update-one` | Triggered after updating a single document (using `updateOne`). | `query`: The query filter used.<br>`update`: The update operations applied.<br>`queryResult`: The operation result. |
17
+ * | `find-one-and-update` | Triggered after `findOneAndUpdate`. | `query`: The query filter used.<br>`update`: The update operations applied.<br>`queryResult`: The updated document. |
18
+ * | `delete-many` | Triggered after removing multiple documents (using `deleteMany`). | `query`: The query filter used.<br>`queryResult`: The operation result. |
19
+ * | `remove-one` | Triggered after removing a single document (using `deleteOne`). | `query`: The query filter used.<br>`queryResult`: The operation result. |
20
+ * | `find-one-and-delete` | Triggered after `findOneAndDelete` or `findOneAndRemove`. | `query`: The query filter used.<br>`queryResult`: The removed document. |
21
+ * | `find` | Triggered after a `find` query. | `query`: The query filter used.<br>`queryResult`: Array of found documents. |
22
+ * | `find-one` | Triggered after a `findOne` query. | `query`: The query filter used.<br>`queryResult`: The found document. |
23
+ * | `count` | Triggered after a `countDocuments` query. | `query`: The query filter used.<br>`queryResult`: The count (number). |
24
+ * | `aggregate` | Triggered after an aggregation pipeline. | `pipelines`: The aggregation pipeline used.<br>`queryResult`: The aggregation result. |
25
+ * | `distinct` | Triggered after a `distinct` query. | `query`: The query filter used.<br>`queryResult`: The distinct values. |
26
+ * | `validate` | Triggered after document validation. | `doc`: The validated document.<br>`queryResult`: The validated document. |
19
27
  *
20
28
  * @property {DatabaseOperation} operation - The database operation that triggers the callback
21
29
  * @property {DatabaseTriggerCallback} callback - The callback function to be executed
22
30
  *
23
31
  * @example
24
32
  * ```typescript
25
- * import { DatabaseTrigger } from '@server-ts/database';
33
+ * import { DatabaseTrigger } from '@modular-rest/server';
26
34
  *
27
- * const trigger = new DatabaseTrigger('insert-one', ({ query, queryResult }) => {
35
+ * const trigger = new DatabaseTrigger('insert-one', ({ doc, queryResult }) => {
28
36
  * console.log('New document inserted:', queryResult);
29
- *
30
- * try {
31
- * // Perform additional actions after document insertion
32
- * } catch (error) {
33
- * console.error('Error performing additional actions:', error);
34
- * }
37
+ * // Perform additional actions
35
38
  * });
36
39
  *
37
40
  * // Use the trigger in a collection definition
@@ -59,6 +62,130 @@ class DatabaseTrigger {
59
62
  this.operation = operation;
60
63
  this.callback = callback;
61
64
  }
65
+ /**
66
+ * Applies the trigger to a Mongoose schema
67
+ * @param {Schema} schema - The mongoose schema to apply the trigger to
68
+ */
69
+ applyToSchema(schema) {
70
+ const callback = this.callback;
71
+ switch (this.operation) {
72
+ case 'insert-one':
73
+ schema.post('save', function (doc) {
74
+ callback({
75
+ doc: doc.toObject ? doc.toObject() : doc,
76
+ queryResult: doc,
77
+ });
78
+ });
79
+ break;
80
+ case 'update-one':
81
+ schema.post('updateOne', function (result) {
82
+ callback({
83
+ query: this.getQuery(),
84
+ update: this.getUpdate(),
85
+ queryResult: result,
86
+ });
87
+ });
88
+ break;
89
+ case 'remove-one':
90
+ schema.post('deleteOne', function (result) {
91
+ callback({
92
+ query: this.getQuery(),
93
+ queryResult: result,
94
+ });
95
+ });
96
+ break;
97
+ case 'find':
98
+ schema.post('find', function (docs) {
99
+ callback({
100
+ query: this.getQuery(),
101
+ queryResult: docs, // Pass docs directly, Mongoose returns documents
102
+ });
103
+ });
104
+ break;
105
+ case 'find-one':
106
+ schema.post('findOne', function (doc) {
107
+ callback({
108
+ query: this.getQuery(),
109
+ queryResult: doc,
110
+ });
111
+ });
112
+ break;
113
+ case 'count':
114
+ schema.post('countDocuments', function (count) {
115
+ callback({
116
+ query: this.getQuery(),
117
+ queryResult: count,
118
+ });
119
+ });
120
+ break;
121
+ case 'aggregate':
122
+ schema.post('aggregate', function (docs) {
123
+ callback({
124
+ pipelines: this.pipeline(),
125
+ queryResult: docs,
126
+ });
127
+ });
128
+ break;
129
+ case 'insert-many':
130
+ schema.post('insertMany', function (docs) {
131
+ callback({
132
+ docs: docs,
133
+ queryResult: docs,
134
+ });
135
+ });
136
+ break;
137
+ case 'delete-many':
138
+ schema.post('deleteMany', function (result) {
139
+ callback({
140
+ query: this.getQuery(),
141
+ queryResult: result,
142
+ });
143
+ });
144
+ break;
145
+ case 'find-one-and-update':
146
+ schema.post('findOneAndUpdate', function (doc) {
147
+ callback({
148
+ query: this.getQuery(),
149
+ update: this.getUpdate(),
150
+ queryResult: doc,
151
+ });
152
+ });
153
+ break;
154
+ case 'find-one-and-delete':
155
+ schema.post('findOneAndDelete', function (doc) {
156
+ callback({
157
+ query: this.getQuery(),
158
+ queryResult: doc,
159
+ });
160
+ });
161
+ // Also support findOneAndRemove which is deprecated but might be used
162
+ schema.post('findOneAndRemove', function (doc) {
163
+ callback({
164
+ query: this.getQuery(),
165
+ queryResult: doc,
166
+ });
167
+ });
168
+ break;
169
+ case 'distinct':
170
+ schema.post('distinct', function (result) {
171
+ callback({
172
+ query: this.getQuery(),
173
+ queryResult: result,
174
+ });
175
+ });
176
+ break;
177
+ case 'validate':
178
+ schema.post('validate', function (doc) {
179
+ callback({
180
+ doc: doc.toObject ? doc.toObject() : doc,
181
+ queryResult: doc,
182
+ });
183
+ });
184
+ break;
185
+ default:
186
+ console.warn(`Unsupported trigger operation: ${this.operation}`);
187
+ }
188
+ }
62
189
  }
63
190
  exports.DatabaseTrigger = DatabaseTrigger;
64
191
  exports.default = DatabaseTrigger;
@@ -111,11 +111,6 @@ dataProvider.post('/find', async (ctx) => {
111
111
  await queryRequest
112
112
  .exec()
113
113
  .then(async (docs) => {
114
- // Call trigger
115
- service.triggers.call('find', body.database, body.collection, {
116
- query: body.query,
117
- queryResult: docs,
118
- });
119
114
  ctx.body = { data: docs };
120
115
  })
121
116
  .catch(err => {
@@ -157,11 +152,6 @@ dataProvider.post('/find-one', async (ctx) => {
157
152
  await queryRequest
158
153
  .exec()
159
154
  .then(async (doc) => {
160
- // Call trigger
161
- service.triggers.call('find-one', body.database, body.collection, {
162
- query: body.query,
163
- queryResult: doc,
164
- });
165
155
  ctx.body = { data: doc };
166
156
  })
167
157
  .catch(err => {
@@ -189,11 +179,6 @@ dataProvider.post('/count', async (ctx) => {
189
179
  .countDocuments(body.query)
190
180
  .exec()
191
181
  .then(count => {
192
- // Call trigger
193
- service.triggers.call('count', body.database, body.collection, {
194
- query: body.query,
195
- queryResult: count,
196
- });
197
182
  ctx.body = { data: count };
198
183
  })
199
184
  .catch(err => {
@@ -224,12 +209,6 @@ dataProvider.post('/update-one', async (ctx) => {
224
209
  .updateOne(body.query, body.update, body.options)
225
210
  .exec()
226
211
  .then(writeOpResult => {
227
- // Call trigger
228
- service.triggers.call('update-one', body.database, body.collection, {
229
- query: body.query,
230
- update: body.update,
231
- queryResult: writeOpResult,
232
- });
233
212
  ctx.body = { data: writeOpResult };
234
213
  })
235
214
  .catch(err => {
@@ -260,11 +239,6 @@ dataProvider.post('/insert-one', async (ctx) => {
260
239
  await new collection(body.doc)
261
240
  .save()
262
241
  .then(async (newDoc) => {
263
- // Call trigger
264
- service.triggers.call('insert-one', body.database, body.collection, {
265
- doc: body.doc,
266
- queryResult: newDoc,
267
- });
268
242
  ctx.body = { data: newDoc };
269
243
  })
270
244
  .catch(err => {
@@ -295,11 +269,6 @@ dataProvider.post('/remove-one', async (ctx) => {
295
269
  .deleteOne(body.query)
296
270
  .exec()
297
271
  .then(async (result) => {
298
- // Call trigger
299
- service.triggers.call('remove-one', body.database, body.collection, {
300
- query: body.query,
301
- queryResult: result,
302
- });
303
272
  ctx.body = { data: result };
304
273
  })
305
274
  .catch((err) => {
@@ -328,11 +297,6 @@ dataProvider.post('/aggregate', async (ctx) => {
328
297
  .aggregate(body.pipelines)
329
298
  .exec()
330
299
  .then(async (result) => {
331
- // Call trigger
332
- service.triggers.call('aggregate', body.database, body.collection, {
333
- pipelines: body.pipelines,
334
- queryResult: result,
335
- });
336
300
  ctx.body = { data: result };
337
301
  })
338
302
  .catch((err) => {
@@ -1,5 +1,4 @@
1
1
  import mongoose, { Model, PopulateOptions, Query } from 'mongoose';
2
- import triggerOperator from '../../class/trigger_operator';
3
2
  import TypeCasters from './typeCasters';
4
3
  import { CollectionDefinition } from '../../class/collection_definition';
5
4
  import { User } from '../../class/user';
@@ -129,4 +128,4 @@ export declare function performPopulateToQueryObject<T = any>(queryObj: Query<T,
129
128
  * ```
130
129
  */
131
130
  export declare function performAdditionalOptionsToQueryObject<T = any>(queryObj: Query<T, any>, options: Record<string, any>): Query<T, any>;
132
- export { triggerOperator as triggers, TypeCasters };
131
+ export { TypeCasters };
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.TypeCasters = exports.triggers = exports.name = void 0;
6
+ exports.TypeCasters = exports.name = void 0;
7
7
  exports.addCollectionDefinitionByList = addCollectionDefinitionByList;
8
8
  exports.getCollection = getCollection;
9
9
  exports.checkAccess = checkAccess;
@@ -12,8 +12,6 @@ exports.performPopulateToQueryObject = performPopulateToQueryObject;
12
12
  exports.performAdditionalOptionsToQueryObject = performAdditionalOptionsToQueryObject;
13
13
  const mongoose_1 = __importDefault(require("mongoose"));
14
14
  const security_1 = require("../../class/security");
15
- const trigger_operator_1 = __importDefault(require("../../class/trigger_operator"));
16
- exports.triggers = trigger_operator_1.default;
17
15
  const typeCasters_1 = __importDefault(require("./typeCasters"));
18
16
  exports.TypeCasters = typeCasters_1.default;
19
17
  const model_registry_1 = __importDefault(require("./model_registry"));
@@ -100,19 +98,6 @@ function connectToDatabaseByCollectionDefinitionList(dbName, collectionDefinitio
100
98
  collection: collection,
101
99
  permissionList: collectionDefinition.permissions,
102
100
  });
103
- // add trigger
104
- if (collectionDefinition.triggers != undefined) {
105
- if (!Array.isArray(collectionDefinition.triggers)) {
106
- throw new Error('Triggers must be an array');
107
- }
108
- collectionDefinition.triggers.forEach(trigger => {
109
- trigger_operator_1.default.addTrigger({
110
- ...trigger,
111
- database: collectionDefinition.database,
112
- collection: collectionDefinition.collection,
113
- });
114
- });
115
- }
116
101
  });
117
102
  // If connection is already connected, resolve immediately
118
103
  if (connection.readyState === 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modular-rest/server",
3
- "version": "1.20.1",
3
+ "version": "1.21.0",
4
4
  "description": "TypeScript version of a nodejs module based on KOAJS for developing Rest-APIs in a modular solution.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +1,4 @@
1
- import { DatabaseTriggerContext } from './database_trigger';
1
+ import DatabaseTrigger, { DatabaseTriggerContext } from './database_trigger';
2
2
 
3
3
  /**
4
4
  * Type for CMS operations that can trigger a callback
@@ -29,9 +29,8 @@ export type CmsOperation = 'update-one' | 'insert-one' | 'remove-one';
29
29
  * });
30
30
  * ```
31
31
  */
32
- export class CmsTrigger {
33
- operation: CmsOperation;
34
- callback: (context: DatabaseTriggerContext) => void;
32
+ export class CmsTrigger extends DatabaseTrigger {
33
+
35
34
 
36
35
  /**
37
36
  * Creates a new CmsTrigger instance
@@ -52,10 +51,9 @@ export class CmsTrigger {
52
51
  */
53
52
  constructor(
54
53
  operation: CmsOperation,
55
- callback: (context: DatabaseTriggerContext) => void = () => {}
54
+ callback: (context: DatabaseTriggerContext) => void = () => { }
56
55
  ) {
57
- this.operation = operation;
58
- this.callback = callback;
56
+ super(operation, callback);
59
57
  }
60
58
  }
61
59
 
@@ -117,8 +117,8 @@ export function defineCollection(
117
117
 
118
118
  throw new Error(
119
119
  `Model for ${definition.database}.${definition.collection} is not available. ` +
120
- `Ensure mongoOption is provided in defineCollection options, config.mongo is set, ` +
121
- `or the collection is registered via addCollectionDefinitionByList before accessing the model.`
120
+ `Ensure mongoOption is provided in defineCollection options, config.mongo is set, ` +
121
+ `or the collection is registered via addCollectionDefinitionByList before accessing the model.`
122
122
  );
123
123
  },
124
124
  enumerable: true,
@@ -204,6 +204,13 @@ export class CollectionDefinition {
204
204
  this.schema = schema;
205
205
  this.permissions = permissions;
206
206
  this.triggers = triggers;
207
+
208
+ // Apply triggers to schema
209
+ if (this.triggers) {
210
+ this.triggers.forEach(trigger => {
211
+ trigger.applyToSchema(this.schema);
212
+ });
213
+ }
207
214
  }
208
215
 
209
216
  /**
@@ -8,25 +8,34 @@ export type DatabaseOperation =
8
8
  | 'update-one'
9
9
  | 'insert-one'
10
10
  | 'remove-one'
11
- | 'aggregate';
11
+ | 'aggregate'
12
+ | 'insert-many'
13
+ | 'delete-many'
14
+ | 'find-one-and-update'
15
+ | 'find-one-and-delete'
16
+ | 'distinct'
17
+ | 'validate';
12
18
 
13
19
  /**
14
20
  * Context interface for database trigger callbacks
15
21
  * @interface DatabaseTriggerContext
16
- * @property {Record<string, any>} doc - The document data for insert/update operations
17
- * @property {Record<string, any>} query - The query data for find/find-one operations
22
+ * @property {Record<string, any>} doc - The document data for insert/update/validate operations.
23
+ * @property {Record<string, any>[]} docs - The documents data for insert-many operations.
24
+ * @property {Record<string, any>} query - The query data for find/remove/count/distinct operations
18
25
  * @property {Record<string, any>} update - The update data for update operations
19
26
  * @property {Record<string, any>[]} pipelines - The aggregation pipelines for aggregate operations
20
- * @property {Record<string, any> } queryResult - The result of the database operation
27
+ * @property {any} queryResult - The result of the database operation (can be document, array of documents, number, or null)
21
28
  */
22
29
  export interface DatabaseTriggerContext {
23
30
  doc?: Record<string, any>;
31
+ docs?: Record<string, any>[];
24
32
  query?: Record<string, any>;
25
33
  update?: Record<string, any>;
26
34
  pipelines?: Record<string, any>[];
27
- queryResult: Record<string, any>;
35
+ queryResult: any;
28
36
  }
29
37
 
38
+
30
39
  /**
31
40
  * The callback function to be executed on specific database operations
32
41
  * @param {DatabaseTriggerContext} context - The context of the database operation
@@ -40,36 +49,39 @@ export interface DatabaseTriggerContext {
40
49
  type DatabaseTriggerCallback = (context: DatabaseTriggerContext) => void;
41
50
 
42
51
  /**
43
- * in a complex application, you may need to perform additional actions after a database operation.
44
- * this is where DatabaseTrigger comes in. so you can define a callback to be executed on specific database operations for a collection.
52
+ * In a complex application, you may need to perform additional actions after a database operation.
53
+ * This is where DatabaseTrigger comes in, allowing you to define callbacks for specific database operations on a collection.
54
+ *
55
+ * ### Supported Triggers and Contexts
45
56
  *
46
- * Supported triggers are:
57
+ * The `callback` function associated with a trigger receives a {@link DatabaseTriggerContext} object. The properties available in this context vary depending on the trigger operation.
47
58
  *
48
- * | Trigger | Description |
49
- * | ------------ | ------------------------------------------------------------ |
50
- * | `find` | Triggered when a find query is executed on collection. |
51
- * | `find-one` | Triggered when a find one query is executed on collection. |
52
- * | `count` | Triggered when a count query is executed on collection. |
53
- * | `update-one` | Triggered when a update one query is executed on collection. |
54
- * | `insert-one` | Triggered when a insert one query is executed on collection. |
55
- * | `remove-one` | Triggered when a remove one query is executed on collection. |
56
- * | `aggregate` | Triggered when a aggregate query is executed on collection. |
59
+ * | Trigger Operation | Description | Context Data |
60
+ * | :--- | :--- | :--- |
61
+ * | `insert-one` | Triggered after a single document insertion. | `doc`: The inserted document.<br>`queryResult`: The inserted document. |
62
+ * | `insert-many` | Triggered after multiple document insertions. | `docs`: The array of inserted documents.<br>`queryResult`: The array of inserted documents. |
63
+ * | `update-one` | Triggered after updating a single document (using `updateOne`). | `query`: The query filter used.<br>`update`: The update operations applied.<br>`queryResult`: The operation result. |
64
+ * | `find-one-and-update` | Triggered after `findOneAndUpdate`. | `query`: The query filter used.<br>`update`: The update operations applied.<br>`queryResult`: The updated document. |
65
+ * | `delete-many` | Triggered after removing multiple documents (using `deleteMany`). | `query`: The query filter used.<br>`queryResult`: The operation result. |
66
+ * | `remove-one` | Triggered after removing a single document (using `deleteOne`). | `query`: The query filter used.<br>`queryResult`: The operation result. |
67
+ * | `find-one-and-delete` | Triggered after `findOneAndDelete` or `findOneAndRemove`. | `query`: The query filter used.<br>`queryResult`: The removed document. |
68
+ * | `find` | Triggered after a `find` query. | `query`: The query filter used.<br>`queryResult`: Array of found documents. |
69
+ * | `find-one` | Triggered after a `findOne` query. | `query`: The query filter used.<br>`queryResult`: The found document. |
70
+ * | `count` | Triggered after a `countDocuments` query. | `query`: The query filter used.<br>`queryResult`: The count (number). |
71
+ * | `aggregate` | Triggered after an aggregation pipeline. | `pipelines`: The aggregation pipeline used.<br>`queryResult`: The aggregation result. |
72
+ * | `distinct` | Triggered after a `distinct` query. | `query`: The query filter used.<br>`queryResult`: The distinct values. |
73
+ * | `validate` | Triggered after document validation. | `doc`: The validated document.<br>`queryResult`: The validated document. |
57
74
  *
58
75
  * @property {DatabaseOperation} operation - The database operation that triggers the callback
59
76
  * @property {DatabaseTriggerCallback} callback - The callback function to be executed
60
77
  *
61
78
  * @example
62
79
  * ```typescript
63
- * import { DatabaseTrigger } from '@server-ts/database';
80
+ * import { DatabaseTrigger } from '@modular-rest/server';
64
81
  *
65
- * const trigger = new DatabaseTrigger('insert-one', ({ query, queryResult }) => {
82
+ * const trigger = new DatabaseTrigger('insert-one', ({ doc, queryResult }) => {
66
83
  * console.log('New document inserted:', queryResult);
67
- *
68
- * try {
69
- * // Perform additional actions after document insertion
70
- * } catch (error) {
71
- * console.error('Error performing additional actions:', error);
72
- * }
84
+ * // Perform additional actions
73
85
  * });
74
86
  *
75
87
  * // Use the trigger in a collection definition
@@ -96,10 +108,149 @@ export class DatabaseTrigger {
96
108
  * });
97
109
  *
98
110
  */
99
- constructor(operation: DatabaseOperation, callback: DatabaseTriggerCallback = () => {}) {
111
+ constructor(operation: DatabaseOperation, callback: DatabaseTriggerCallback = () => { }) {
100
112
  this.operation = operation;
101
113
  this.callback = callback;
102
114
  }
115
+ /**
116
+ * Applies the trigger to a Mongoose schema
117
+ * @param {Schema} schema - The mongoose schema to apply the trigger to
118
+ */
119
+ applyToSchema(schema: any): void {
120
+ const callback = this.callback;
121
+
122
+ switch (this.operation) {
123
+ case 'insert-one':
124
+ schema.post('save', function (doc: any) {
125
+ callback({
126
+ doc: doc.toObject ? doc.toObject() : doc,
127
+ queryResult: doc,
128
+ });
129
+ });
130
+ break;
131
+
132
+ case 'update-one':
133
+ schema.post('updateOne', function (this: any, result: any) {
134
+ callback({
135
+ query: this.getQuery(),
136
+ update: this.getUpdate(),
137
+ queryResult: result,
138
+ });
139
+ });
140
+ break;
141
+
142
+ case 'remove-one':
143
+ schema.post('deleteOne', function (this: any, result: any) {
144
+ callback({
145
+ query: this.getQuery(),
146
+ queryResult: result,
147
+ });
148
+ });
149
+ break;
150
+
151
+ case 'find':
152
+ schema.post('find', function (this: any, docs: any[]) {
153
+ callback({
154
+ query: this.getQuery(),
155
+ queryResult: docs, // Pass docs directly, Mongoose returns documents
156
+ });
157
+ });
158
+ break;
159
+
160
+ case 'find-one':
161
+ schema.post('findOne', function (this: any, doc: any) {
162
+ callback({
163
+ query: this.getQuery(),
164
+ queryResult: doc,
165
+ });
166
+ });
167
+ break;
168
+
169
+ case 'count':
170
+ schema.post('countDocuments', function (this: any, count: any) {
171
+ callback({
172
+ query: this.getQuery(),
173
+ queryResult: count,
174
+ });
175
+ });
176
+ break;
177
+
178
+ case 'aggregate':
179
+ schema.post('aggregate', function (this: any, docs: any[]) {
180
+ callback({
181
+ pipelines: this.pipeline(),
182
+ queryResult: docs,
183
+ });
184
+ });
185
+ break;
186
+
187
+ case 'insert-many':
188
+ schema.post('insertMany', function (docs: any[]) {
189
+ callback({
190
+ docs: docs,
191
+ queryResult: docs,
192
+ });
193
+ });
194
+ break;
195
+
196
+ case 'delete-many':
197
+ schema.post('deleteMany', function (this: any, result: any) {
198
+ callback({
199
+ query: this.getQuery(),
200
+ queryResult: result,
201
+ });
202
+ });
203
+ break;
204
+
205
+ case 'find-one-and-update':
206
+ schema.post('findOneAndUpdate', function (this: any, doc: any) {
207
+ callback({
208
+ query: this.getQuery(),
209
+ update: this.getUpdate(),
210
+ queryResult: doc,
211
+ });
212
+ });
213
+ break;
214
+
215
+ case 'find-one-and-delete':
216
+ schema.post('findOneAndDelete', function (this: any, doc: any) {
217
+ callback({
218
+ query: this.getQuery(),
219
+ queryResult: doc,
220
+ });
221
+ });
222
+ // Also support findOneAndRemove which is deprecated but might be used
223
+ schema.post('findOneAndRemove', function (this: any, doc: any) {
224
+ callback({
225
+ query: this.getQuery(),
226
+ queryResult: doc,
227
+ });
228
+ });
229
+ break;
230
+
231
+ case 'distinct':
232
+ schema.post('distinct', function (this: any, result: any) {
233
+ callback({
234
+ query: this.getQuery(),
235
+ queryResult: result,
236
+ });
237
+ });
238
+ break;
239
+
240
+ case 'validate':
241
+ schema.post('validate', function (doc: any) {
242
+ callback({
243
+ doc: doc.toObject ? doc.toObject() : doc,
244
+ queryResult: doc,
245
+ });
246
+ });
247
+ break;
248
+
249
+
250
+ default:
251
+ console.warn(`Unsupported trigger operation: ${this.operation}`);
252
+ }
253
+ }
103
254
  }
104
255
 
105
256
  export default DatabaseTrigger;
@@ -92,11 +92,7 @@ dataProvider.post('/find', async (ctx: Context) => {
92
92
  await queryRequest
93
93
  .exec()
94
94
  .then(async docs => {
95
- // Call trigger
96
- service.triggers.call('find', body.database, body.collection, {
97
- query: body.query,
98
- queryResult: docs,
99
- });
95
+
100
96
 
101
97
  ctx.body = { data: docs };
102
98
  })
@@ -151,11 +147,7 @@ dataProvider.post('/find-one', async (ctx: Context) => {
151
147
  await queryRequest
152
148
  .exec()
153
149
  .then(async doc => {
154
- // Call trigger
155
- service.triggers.call('find-one', body.database, body.collection, {
156
- query: body.query,
157
- queryResult: doc,
158
- });
150
+
159
151
 
160
152
  ctx.body = { data: doc };
161
153
  })
@@ -194,11 +186,7 @@ dataProvider.post('/count', async (ctx: Context) => {
194
186
  .countDocuments(body.query)
195
187
  .exec()
196
188
  .then(count => {
197
- // Call trigger
198
- service.triggers.call('count', body.database, body.collection, {
199
- query: body.query,
200
- queryResult: count,
201
- });
189
+
202
190
 
203
191
  ctx.body = { data: count };
204
192
  })
@@ -241,12 +229,7 @@ dataProvider.post('/update-one', async (ctx: Context) => {
241
229
  .updateOne(body.query, body.update, body.options)
242
230
  .exec()
243
231
  .then(writeOpResult => {
244
- // Call trigger
245
- service.triggers.call('update-one', body.database, body.collection, {
246
- query: body.query,
247
- update: body.update,
248
- queryResult: writeOpResult,
249
- });
232
+
250
233
 
251
234
  ctx.body = { data: writeOpResult };
252
235
  })
@@ -289,11 +272,7 @@ dataProvider.post('/insert-one', async (ctx: Context) => {
289
272
  await new collection(body.doc)
290
273
  .save()
291
274
  .then(async newDoc => {
292
- // Call trigger
293
- service.triggers.call('insert-one', body.database, body.collection, {
294
- doc: body.doc,
295
- queryResult: newDoc,
296
- });
275
+
297
276
 
298
277
  ctx.body = { data: newDoc };
299
278
  })
@@ -336,11 +315,7 @@ dataProvider.post('/remove-one', async (ctx: Context) => {
336
315
  .deleteOne(body.query)
337
316
  .exec()
338
317
  .then(async (result: any) => {
339
- // Call trigger
340
- service.triggers.call('remove-one', body.database, body.collection, {
341
- query: body.query,
342
- queryResult: result,
343
- });
318
+
344
319
 
345
320
  ctx.body = { data: result };
346
321
  })
@@ -380,11 +355,7 @@ dataProvider.post('/aggregate', async (ctx: Context) => {
380
355
  .aggregate(body.pipelines)
381
356
  .exec()
382
357
  .then(async (result: any) => {
383
- // Call trigger
384
- service.triggers.call('aggregate', body.database, body.collection, {
385
- pipelines: body.pipelines,
386
- queryResult: result,
387
- });
358
+
388
359
 
389
360
  ctx.body = { data: result };
390
361
  })
@@ -1,6 +1,6 @@
1
1
  import mongoose, { Connection, Model, PopulateOptions, Query } from 'mongoose';
2
2
  import { AccessTypes, AccessDefinition, Permission } from '../../class/security';
3
- import triggerOperator from '../../class/trigger_operator';
3
+
4
4
  import TypeCasters from './typeCasters';
5
5
  import { config } from '../../config';
6
6
  import { CollectionDefinition } from '../../class/collection_definition';
@@ -128,20 +128,7 @@ function connectToDatabaseByCollectionDefinitionList(
128
128
  permissionList: collectionDefinition.permissions,
129
129
  });
130
130
 
131
- // add trigger
132
- if (collectionDefinition.triggers != undefined) {
133
- if (!Array.isArray(collectionDefinition.triggers)) {
134
- throw new Error('Triggers must be an array');
135
- }
136
131
 
137
- collectionDefinition.triggers.forEach(trigger => {
138
- triggerOperator.addTrigger({
139
- ...trigger,
140
- database: collectionDefinition.database,
141
- collection: collectionDefinition.collection,
142
- });
143
- });
144
- }
145
132
  });
146
133
 
147
134
  // If connection is already connected, resolve immediately
@@ -362,5 +349,5 @@ export function performAdditionalOptionsToQueryObject<T = any>(
362
349
  return queryObj;
363
350
  }
364
351
 
365
- // Instead, export triggerOperator as triggers
366
- export { triggerOperator as triggers, TypeCasters };
352
+ // Instead, export TypeCasters
353
+ export { TypeCasters };
@@ -28,6 +28,7 @@ export interface TestAppContext {
28
28
  uploadDir: string;
29
29
  adminToken: string;
30
30
  dbPrefix: string;
31
+ mongoOption?: RestOptions['mongo'];
31
32
  cleanup: () => Promise<void>;
32
33
  }
33
34
 
@@ -138,6 +139,7 @@ export async function createIntegrationTestApp(
138
139
  uploadDir,
139
140
  adminToken,
140
141
  dbPrefix,
142
+ mongoOption: options.mongo,
141
143
  cleanup,
142
144
  };
143
145
  }
@@ -189,4 +189,162 @@ describe('data-provider router integration', () => {
189
189
  });
190
190
  expect(verifyRes.body.data).toBeNull();
191
191
  });
192
+
193
+ it('triggers are executed on direct model usage and api usage', async () => {
194
+ // 1. Define a trigger
195
+ const triggerMock = jest.fn();
196
+
197
+ // We need to access the collection definition to add a trigger dynamically for testing
198
+ // or we assume the existing setup allows us to mock/spy on something.
199
+ // However, since we are in an integration test with a real app context,
200
+ // modifying the collection definition on the fly might be tricky if models are already compiled.
201
+ // Ideally we should have a test collection with triggers defined.
202
+
203
+ // Let's create a dynamic collection definition with a trigger for this test if possible,
204
+ // or allow the test helper to set one up.
205
+ // Since we cannot easily modify the setup here without changing helpers,
206
+ // and the user asked to "add new test case" to this file,
207
+ // we need to assume there is a way or we need to add a collection definition here.
208
+
209
+ // Looking at the imports, we can import defineCollection etc.
210
+ // But the app is already created in beforeAll via createIntegrationTestApp().
211
+
212
+ // IF we cannot add a new collection easily, we'll try to spy on console or a side effect
213
+ // if there are existing triggers. But the current test app seems to use standard collections.
214
+
215
+ // Let's assume we can register a new collection via a service method if available,
216
+ // or we might need to modify the test helper.
217
+ // For now, I will write the test assuming we can verify the trigger via a mocked callback
218
+ // attached to a new collection we define locally and register.
219
+
220
+ const { defineCollection } = require('../../src/class/collection_definition');
221
+ const { Permission } = require('../../src/class/security');
222
+ const { DatabaseTrigger } = require('../../src/class/database_trigger');
223
+ const { Schema } = require('mongoose');
224
+ const modelRegistry = require('../../src/services/data_provider/model_registry').default;
225
+ const service = require('../../src/services/data_provider/service');
226
+
227
+ const triggerCallback = jest.fn();
228
+ const trigger = new DatabaseTrigger('insert-one', triggerCallback);
229
+
230
+ const testCollectionDef = defineCollection({
231
+ database: 'test_db',
232
+ collection: 'trigger_test',
233
+ schema: new Schema({ name: String }),
234
+ permissions: [
235
+ new Permission({ accessType: 'anonymous_access', read: true, write: true }),
236
+ new Permission({ accessType: 'god_access', read: true, write: true })
237
+ ],
238
+ triggers: [trigger]
239
+ });
240
+
241
+ // Register this new collection
242
+ await service.addCollectionDefinitionByList({
243
+ list: [testCollectionDef],
244
+ mongoOption: ctx.mongoOption // We need to access mongoOption from ctx
245
+ });
246
+
247
+ // 2. Perform operation via API
248
+ await ctx.request
249
+ .post('/data-provider/insert-one')
250
+ .set('authorization', ctx.adminToken)
251
+ .send({
252
+ database: 'test_db',
253
+ collection: 'trigger_test',
254
+ doc: { name: 'api_insert' }
255
+ })
256
+ .expect(200);
257
+
258
+ // Verify trigger called
259
+ expect(triggerCallback).toHaveBeenCalledTimes(1);
260
+ expect(triggerCallback.mock.calls[0][0].doc).toMatchObject({ name: 'api_insert' });
261
+
262
+ // 3. Perform operation via Mongoose Model directly
263
+ const Model = testCollectionDef.model;
264
+ await new Model({ name: 'direct_insert' }).save();
265
+
266
+ // Verify trigger called again
267
+ expect(triggerCallback).toHaveBeenCalledTimes(2);
268
+ expect(triggerCallback.mock.calls[1][0].doc).toMatchObject({ name: 'direct_insert' });
269
+ });
270
+
271
+ it('triggers find-one-and-update', async () => {
272
+ // 1. Setup Trigger
273
+ const { defineCollection } = require('../../src/class/collection_definition');
274
+ const { Permission } = require('../../src/class/security');
275
+ const { DatabaseTrigger } = require('../../src/class/database_trigger');
276
+ const { Schema } = require('mongoose');
277
+ const service = require('../../src/services/data_provider/service');
278
+
279
+ const triggerCallback = jest.fn();
280
+ const trigger = new DatabaseTrigger('find-one-and-update', triggerCallback);
281
+
282
+ const testCollectionDef = defineCollection({
283
+ database: 'test_db',
284
+ collection: 'trigger_update_test',
285
+ schema: new Schema({ name: String, version: Number }),
286
+ permissions: [
287
+ new Permission({ accessType: 'anonymous_access', read: true, write: true }),
288
+ new Permission({ accessType: 'god_access', read: true, write: true })
289
+ ],
290
+ triggers: [trigger]
291
+ });
292
+
293
+ await service.addCollectionDefinitionByList({
294
+ list: [testCollectionDef],
295
+ mongoOption: ctx.mongoOption
296
+ });
297
+
298
+ // 2. Insert initial doc
299
+ const Model = testCollectionDef.model;
300
+ const doc = await new Model({ name: 'initial', version: 1 }).save();
301
+
302
+ // 3. Update via Mongoose
303
+ await Model.findOneAndUpdate({ _id: doc._id }, { $set: { version: 2 } });
304
+
305
+ // 4. Verify Trigger
306
+ expect(triggerCallback).toHaveBeenCalledTimes(1);
307
+ expect(triggerCallback.mock.calls[0][0].query).toMatchObject({ _id: doc._id });
308
+ expect(triggerCallback.mock.calls[0][0].update).toMatchObject({ $set: { version: 2 } });
309
+ });
310
+
311
+ it('triggers delete-many', async () => {
312
+ // 1. Setup Trigger
313
+ const { defineCollection } = require('../../src/class/collection_definition');
314
+ const { Permission } = require('../../src/class/security');
315
+ const { DatabaseTrigger } = require('../../src/class/database_trigger');
316
+ const { Schema } = require('mongoose');
317
+ const service = require('../../src/services/data_provider/service');
318
+
319
+ const triggerCallback = jest.fn();
320
+ const trigger = new DatabaseTrigger('delete-many', triggerCallback);
321
+
322
+ const testCollectionDef = defineCollection({
323
+ database: 'test_db',
324
+ collection: 'trigger_delete_many_test',
325
+ schema: new Schema({ name: String }),
326
+ permissions: [
327
+ new Permission({ accessType: 'anonymous_access', read: true, write: true }),
328
+ new Permission({ accessType: 'god_access', read: true, write: true })
329
+ ],
330
+ triggers: [trigger]
331
+ });
332
+
333
+ await service.addCollectionDefinitionByList({
334
+ list: [testCollectionDef],
335
+ mongoOption: ctx.mongoOption
336
+ });
337
+
338
+ // 2. Insert docs
339
+ const Model = testCollectionDef.model;
340
+ await Model.insertMany([{ name: 'doc1' }, { name: 'doc2' }]);
341
+
342
+ // 3. Delete via Mongoose
343
+ await Model.deleteMany({ name: { $regex: 'doc' } });
344
+
345
+ // 4. Verify Trigger
346
+ expect(triggerCallback).toHaveBeenCalledTimes(1);
347
+ expect(triggerCallback.mock.calls[0][0].query).toMatchObject({ name: { $regex: 'doc' } });
348
+ expect(triggerCallback.mock.calls[0][0].queryResult.deletedCount).toBe(2);
349
+ });
192
350
  });