@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.
- package/.nvmrc +1 -1
- package/dist/application.js +5 -4
- package/dist/class/collection_definition.d.ts +28 -3
- package/dist/class/collection_definition.js +72 -2
- package/dist/class/combinator.js +7 -3
- package/dist/class/directory.d.ts +2 -4
- package/dist/class/directory.js +42 -64
- package/dist/helper/data_insertion.js +93 -26
- package/dist/index.d.ts +12 -1
- package/dist/index.js +13 -1
- package/dist/services/data_provider/model_registry.d.ts +72 -0
- package/dist/services/data_provider/model_registry.js +214 -0
- package/dist/services/data_provider/service.js +73 -14
- package/dist/services/file/service.d.ts +47 -78
- package/dist/services/file/service.js +114 -155
- package/dist/services/functions/service.js +4 -4
- package/dist/services/jwt/router.js +2 -1
- package/dist/services/user_manager/router.js +1 -1
- package/dist/services/user_manager/service.js +48 -17
- package/jest.config.ts +18 -0
- package/package.json +11 -2
- package/src/application.ts +5 -4
- package/src/class/collection_definition.ts +94 -4
- package/src/class/combinator.ts +10 -3
- package/src/class/directory.ts +40 -58
- package/src/helper/data_insertion.ts +101 -27
- package/src/index.ts +13 -1
- package/src/services/data_provider/model_registry.ts +243 -0
- package/src/services/data_provider/service.ts +74 -14
- package/src/services/file/service.ts +136 -178
- package/src/services/functions/service.ts +4 -4
- package/src/services/jwt/router.ts +2 -1
- package/src/services/user_manager/router.ts +1 -1
- package/src/services/user_manager/service.ts +49 -20
- package/tests/helpers/test-app.ts +182 -0
- package/tests/router/data-provider.router.int.test.ts +192 -0
- package/tests/router/file.router.int.test.ts +104 -0
- package/tests/router/functions.router.int.test.ts +91 -0
- package/tests/router/jwt.router.int.test.ts +69 -0
- package/tests/router/user-manager.router.int.test.ts +85 -0
- 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
|
-
//
|
|
62
|
-
|
|
63
|
-
const connectionString = mongoOption.mongoBaseAddress;
|
|
62
|
+
// Check if connection already exists in registry
|
|
63
|
+
let connection = modelRegistry.getConnection(dbName);
|
|
64
64
|
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
}
|