@modular-rest/server 1.18.1 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.nvmrc +1 -1
  2. package/dist/application.js +5 -4
  3. package/dist/class/collection_definition.d.ts +28 -3
  4. package/dist/class/collection_definition.js +72 -2
  5. package/dist/class/combinator.js +7 -3
  6. package/dist/class/directory.d.ts +2 -4
  7. package/dist/class/directory.js +42 -64
  8. package/dist/helper/data_insertion.js +93 -26
  9. package/dist/index.d.ts +12 -1
  10. package/dist/index.js +13 -1
  11. package/dist/services/data_provider/model_registry.d.ts +72 -0
  12. package/dist/services/data_provider/model_registry.js +214 -0
  13. package/dist/services/data_provider/service.js +73 -14
  14. package/dist/services/file/service.d.ts +47 -78
  15. package/dist/services/file/service.js +114 -155
  16. package/dist/services/functions/service.js +4 -4
  17. package/dist/services/jwt/router.js +2 -1
  18. package/dist/services/user_manager/router.js +1 -1
  19. package/dist/services/user_manager/service.js +48 -17
  20. package/jest.config.ts +18 -0
  21. package/package.json +11 -2
  22. package/src/application.ts +5 -4
  23. package/src/class/collection_definition.ts +94 -4
  24. package/src/class/combinator.ts +10 -3
  25. package/src/class/directory.ts +40 -58
  26. package/src/helper/data_insertion.ts +101 -27
  27. package/src/index.ts +13 -1
  28. package/src/services/data_provider/model_registry.ts +243 -0
  29. package/src/services/data_provider/service.ts +74 -14
  30. package/src/services/file/service.ts +136 -178
  31. package/src/services/functions/service.ts +4 -4
  32. package/src/services/jwt/router.ts +2 -1
  33. package/src/services/user_manager/router.ts +1 -1
  34. package/src/services/user_manager/service.ts +49 -20
  35. package/tests/helpers/test-app.ts +182 -0
  36. package/tests/router/data-provider.router.int.test.ts +192 -0
  37. package/tests/router/file.router.int.test.ts +104 -0
  38. package/tests/router/functions.router.int.test.ts +91 -0
  39. package/tests/router/jwt.router.int.test.ts +69 -0
  40. package/tests/router/user-manager.router.int.test.ts +85 -0
  41. package/tests/setup/jest.setup.ts +5 -0
@@ -0,0 +1,243 @@
1
+ import mongoose, { Connection, Model } from 'mongoose';
2
+ import { CollectionDefinition } from '../../class/collection_definition';
3
+ import { MongoOption } from './service';
4
+
5
+ /**
6
+ * ModelRegistry - Singleton class for managing mongoose models and connections
7
+ * Pre-creates all models before database connections are established
8
+ */
9
+ class ModelRegistry {
10
+ private static instance: ModelRegistry;
11
+ private connections: Record<string, Connection> = {};
12
+ private models: Record<string, Record<string, Model<any>>> = {};
13
+ private collectionDefinitions: CollectionDefinition[] = [];
14
+ private mongoOption: MongoOption | null = null;
15
+
16
+ private constructor() {
17
+ // Private constructor for singleton pattern
18
+ }
19
+
20
+ /**
21
+ * Get the singleton instance of ModelRegistry
22
+ * @returns {ModelRegistry} The singleton instance
23
+ */
24
+ static getInstance(): ModelRegistry {
25
+ if (!ModelRegistry.instance) {
26
+ ModelRegistry.instance = new ModelRegistry();
27
+ }
28
+ return ModelRegistry.instance;
29
+ }
30
+
31
+ /**
32
+ * Register a collection definition and create its model
33
+ * @param {CollectionDefinition} definition - The collection definition
34
+ * @param {MongoOption} mongoOption - MongoDB connection options
35
+ * @returns {Model<any>} The created mongoose model
36
+ */
37
+ registerCollection(definition: CollectionDefinition, mongoOption: MongoOption): Model<any> {
38
+ // Store mongoOption if not already stored
39
+ if (!this.mongoOption) {
40
+ this.mongoOption = mongoOption;
41
+ }
42
+
43
+ const { database, collection, schema } = definition;
44
+
45
+ // Check if this definition is already registered
46
+ const isAlreadyRegistered = this.collectionDefinitions.some(
47
+ def => def.database === database && def.collection === collection
48
+ );
49
+
50
+ // Store collection definition if not already registered
51
+ if (!isAlreadyRegistered) {
52
+ this.collectionDefinitions.push(definition);
53
+ }
54
+
55
+ // Initialize models object for this database if needed
56
+ if (!this.models[database]) {
57
+ this.models[database] = {};
58
+ }
59
+
60
+ // Check if model already exists
61
+ if (this.models[database][collection]) {
62
+ return this.models[database][collection];
63
+ }
64
+
65
+ // Get or create connection for this database
66
+ let connection = this.connections[database];
67
+ if (!connection) {
68
+ const fullDbName = (mongoOption.dbPrefix || '') + database;
69
+ const connectionString = mongoOption.mongoBaseAddress;
70
+
71
+ // Create connection (but don't connect yet)
72
+ connection = mongoose.createConnection(connectionString, {
73
+ useUnifiedTopology: true,
74
+ useNewUrlParser: true,
75
+ dbName: fullDbName,
76
+ serverSelectionTimeoutMS: 10000, // 10 second timeout for server selection
77
+ socketTimeoutMS: 45000, // 45 second timeout for socket operations
78
+ });
79
+
80
+ this.connections[database] = connection;
81
+ }
82
+
83
+ // Create model on the connection
84
+ // Mongoose allows creating models before connection is established
85
+ const model = connection.model(collection, schema);
86
+ this.models[database][collection] = model;
87
+
88
+ return model;
89
+ }
90
+
91
+ /**
92
+ * Get a model by database and collection name
93
+ * @param {string} database - Database name
94
+ * @param {string} collection - Collection name
95
+ * @returns {Model<any> | null} The mongoose model or null if not found
96
+ */
97
+ getModel(database: string, collection: string): Model<any> | null {
98
+ if (!this.models[database] || !this.models[database][collection]) {
99
+ return null;
100
+ }
101
+ return this.models[database][collection];
102
+ }
103
+
104
+ /**
105
+ * Get a connection for a database
106
+ * @param {string} database - Database name
107
+ * @returns {Connection | null} The mongoose connection or null if not found
108
+ */
109
+ getConnection(database: string): Connection | null {
110
+ return this.connections[database] || null;
111
+ }
112
+
113
+ /**
114
+ * Initialize all connections and connect to databases
115
+ * This should be called during server startup
116
+ * @param {MongoOption} mongoOption - MongoDB connection options
117
+ * @returns {Promise<void>} A promise that resolves when all connections are established
118
+ */
119
+ async initializeConnections(mongoOption: MongoOption): Promise<void> {
120
+ this.mongoOption = mongoOption;
121
+
122
+ // Group collection definitions by database
123
+ const dbGroups: Record<string, CollectionDefinition[]> = {};
124
+ this.collectionDefinitions.forEach(definition => {
125
+ if (!dbGroups[definition.database]) {
126
+ dbGroups[definition.database] = [];
127
+ }
128
+ dbGroups[definition.database].push(definition);
129
+ });
130
+
131
+ // Connect to each database
132
+ const connectionPromises = Object.entries(dbGroups).map(([dbName]) => {
133
+ return new Promise<void>((done, reject) => {
134
+ const connection = this.connections[dbName];
135
+ if (!connection) {
136
+ // If connection doesn't exist, create it
137
+ const fullDbName = (mongoOption.dbPrefix || '') + dbName;
138
+ const connectionString = mongoOption.mongoBaseAddress;
139
+
140
+ const newConnection = mongoose.createConnection(connectionString, {
141
+ useUnifiedTopology: true,
142
+ useNewUrlParser: true,
143
+ dbName: fullDbName,
144
+ serverSelectionTimeoutMS: 10000, // 10 second timeout for server selection
145
+ socketTimeoutMS: 45000, // 45 second timeout for socket operations
146
+ });
147
+
148
+ this.connections[dbName] = newConnection;
149
+
150
+ // Create models for this database if they don't exist
151
+ const definitions = dbGroups[dbName];
152
+ definitions.forEach(def => {
153
+ if (!this.models[dbName]) {
154
+ this.models[dbName] = {};
155
+ }
156
+ if (!this.models[dbName][def.collection]) {
157
+ this.models[dbName][def.collection] = newConnection.model(def.collection, def.schema);
158
+ }
159
+ });
160
+
161
+ newConnection.on('connected', () => {
162
+ console.info(`- ${fullDbName} database has been connected`);
163
+ done();
164
+ });
165
+
166
+ newConnection.on('error', err => {
167
+ reject(err);
168
+ });
169
+ } else {
170
+ // Connection already exists, just wait for it to be ready
171
+ if (connection.readyState === 1) {
172
+ // Already connected
173
+ done();
174
+ } else {
175
+ connection.once('connected', () => {
176
+ done();
177
+ });
178
+ connection.once('error', err => {
179
+ reject(err);
180
+ });
181
+ }
182
+ }
183
+ });
184
+ });
185
+
186
+ await Promise.all(connectionPromises);
187
+ }
188
+
189
+ /**
190
+ * Get all registered collection definitions
191
+ * @returns {CollectionDefinition[]} Array of collection definitions
192
+ */
193
+ getCollectionDefinitions(): CollectionDefinition[] {
194
+ return [...this.collectionDefinitions];
195
+ }
196
+
197
+ /**
198
+ * Get all models for a specific database
199
+ * @param {string} database - Database name
200
+ * @returns {Record<string, Model<any>> | null} Object mapping collection names to models, or null if database not found
201
+ */
202
+ getModelsForDatabase(database: string): Record<string, Model<any>> | null {
203
+ return this.models[database] || null;
204
+ }
205
+
206
+ /**
207
+ * Check if a model exists
208
+ * @param {string} database - Database name
209
+ * @param {string} collection - Collection name
210
+ * @returns {boolean} True if model exists, false otherwise
211
+ */
212
+ hasModel(database: string, collection: string): boolean {
213
+ return !!(this.models[database] && this.models[database][collection]);
214
+ }
215
+
216
+ /**
217
+ * Clear all internal state, including connections and models.
218
+ * Useful for testing when different database prefixes are used.
219
+ */
220
+ async clear(): Promise<void> {
221
+ // Close all connections first
222
+ await Promise.all(
223
+ Object.values(this.connections).map(async connection => {
224
+ if (connection.readyState !== 0) {
225
+ try {
226
+ await connection.close();
227
+ } catch (err) {
228
+ // Ignore close errors
229
+ }
230
+ }
231
+ })
232
+ );
233
+
234
+ this.connections = {};
235
+ this.models = {};
236
+ this.collectionDefinitions = [];
237
+ this.mongoOption = null;
238
+ }
239
+ }
240
+
241
+ // Export singleton instance
242
+ export const modelRegistry = ModelRegistry.getInstance();
243
+ export default modelRegistry;
@@ -5,6 +5,7 @@ import TypeCasters from './typeCasters';
5
5
  import { config } from '../../config';
6
6
  import { CollectionDefinition } from '../../class/collection_definition';
7
7
  import { User } from '../../class/user';
8
+ import modelRegistry from './model_registry';
8
9
 
9
10
  /**
10
11
  * Service name constant
@@ -58,17 +59,27 @@ function connectToDatabaseByCollectionDefinitionList(
58
59
  mongoOption: MongoOption
59
60
  ): Promise<void> {
60
61
  return new Promise((done, reject) => {
61
- // Create db connection
62
- const fullDbName = (mongoOption.dbPrefix || '') + dbName;
63
- const connectionString = mongoOption.mongoBaseAddress;
62
+ // Check if connection already exists in registry
63
+ let connection = modelRegistry.getConnection(dbName);
64
64
 
65
- console.info(`- Connecting to database: ${fullDbName}`);
65
+ // Create db connection if it doesn't exist
66
+ if (!connection) {
67
+ const fullDbName = (mongoOption.dbPrefix || '') + dbName;
68
+ const connectionString = mongoOption.mongoBaseAddress;
66
69
 
67
- const connection = mongoose.createConnection(connectionString, {
68
- useUnifiedTopology: true,
69
- useNewUrlParser: true,
70
- dbName: fullDbName,
71
- });
70
+ console.info(`- Connecting to database: ${fullDbName}`);
71
+
72
+ connection = mongoose.createConnection(connectionString, {
73
+ useUnifiedTopology: true,
74
+ useNewUrlParser: true,
75
+ dbName: fullDbName,
76
+ serverSelectionTimeoutMS: 10000, // 10 second timeout for server selection
77
+ socketTimeoutMS: 45000, // 45 second timeout for socket operations
78
+ });
79
+ } else {
80
+ const fullDbName = (mongoOption.dbPrefix || '') + dbName;
81
+ console.info(`- Using existing connection for database: ${fullDbName}`);
82
+ }
72
83
 
73
84
  // Store connection
74
85
  connections[dbName] = connection;
@@ -82,11 +93,33 @@ function connectToDatabaseByCollectionDefinitionList(
82
93
 
83
94
  if (permissionDefinitions[dbName] == undefined) permissionDefinitions[dbName] = {};
84
95
 
85
- // create model from schema
86
- // and store in on global collection object
87
- const model = connection.model(collection, schema);
96
+ // Check if model already exists in registry (pre-created)
97
+ let model = modelRegistry.getModel(dbName, collection);
98
+
99
+ if (!model) {
100
+ // Model doesn't exist in registry, create it on the connection
101
+ // This can happen if defineCollection was called without mongoOption
102
+ model = connection.model(collection, schema);
103
+ } else {
104
+ // Model exists in registry, verify it's using the same connection
105
+ // Mongoose models are bound to their connection, so we should use the registry model
106
+ // But we need to ensure the connection matches
107
+ const registryConnection = modelRegistry.getConnection(dbName);
108
+ if (registryConnection && registryConnection !== connection) {
109
+ // Connections don't match, but this shouldn't happen in normal flow
110
+ // Use the model from registry as it's already created
111
+ model = modelRegistry.getModel(dbName, collection)!;
112
+ }
113
+ }
114
+
115
+ // Store model in global collections object
88
116
  collections[dbName][collection] = model;
89
117
 
118
+ // Also update the CollectionDefinition with the model if it has a setModel method
119
+ if (collectionDefinition.setModel) {
120
+ collectionDefinition.setModel(model);
121
+ }
122
+
90
123
  // define Access Definition from component permissions
91
124
  // and store it on global access definition object
92
125
  permissionDefinitions[dbName][collection] = new AccessDefinition({
@@ -111,10 +144,22 @@ function connectToDatabaseByCollectionDefinitionList(
111
144
  }
112
145
  });
113
146
 
114
- connection.on('connected', () => {
147
+ // If connection is already connected, resolve immediately
148
+ if (connection.readyState === 1) {
149
+ const fullDbName = (mongoOption.dbPrefix || '') + dbName;
115
150
  console.info(`- ${fullDbName} database has been connected`);
116
151
  done();
117
- });
152
+ } else {
153
+ connection.on('connected', () => {
154
+ const fullDbName = (mongoOption.dbPrefix || '') + dbName;
155
+ console.info(`- ${fullDbName} database has been connected`);
156
+ done();
157
+ });
158
+
159
+ connection.on('error', err => {
160
+ reject(err);
161
+ });
162
+ }
118
163
  });
119
164
  }
120
165
 
@@ -145,6 +190,16 @@ export async function addCollectionDefinitionByList({
145
190
  list,
146
191
  mongoOption,
147
192
  }: CollectionDefinitionListOption): Promise<void> {
193
+ // First, ensure all collections are registered in the ModelRegistry
194
+ // This pre-creates models before connections are established
195
+ list.forEach(collectionDefinition => {
196
+ // Check if model already exists in registry
197
+ if (!modelRegistry.hasModel(collectionDefinition.database, collectionDefinition.collection)) {
198
+ // Register the collection and create its model
199
+ modelRegistry.registerCollection(collectionDefinition, mongoOption);
200
+ }
201
+ });
202
+
148
203
  // Group collection definitions by database
149
204
  const dbGroups: Record<string, CollectionDefinition[]> = {};
150
205
  list.forEach(collectionDefinition => {
@@ -155,6 +210,7 @@ export async function addCollectionDefinitionByList({
155
210
  });
156
211
 
157
212
  // Connect to each database
213
+ // Models are already pre-created, so connections will use existing models
158
214
  const connectionPromises = Object.entries(dbGroups).map(([dbName, collectionDefinitionList]) =>
159
215
  connectToDatabaseByCollectionDefinitionList(dbName, collectionDefinitionList, mongoOption)
160
216
  );
@@ -227,6 +283,10 @@ export function checkAccess(
227
283
  if (permission.accessType === 'god_access') return true;
228
284
  if (permission.accessType === 'anonymous_access' && user.type === 'anonymous') return true;
229
285
  if (permission.accessType === 'user_access' && user.type === 'user') return true;
286
+ if (typeof (user as any).hasPermission === 'function' && user.hasPermission(permission.accessType)) {
287
+ if (operationType === AccessTypes.read) return permission.read;
288
+ if (operationType === AccessTypes.write) return permission.write;
289
+ }
230
290
  return false;
231
291
  });
232
292
  }