@modular-rest/server 1.19.0 → 1.20.1

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 (35) hide show
  1. package/.nvmrc +1 -1
  2. package/dist/application.js +5 -4
  3. package/dist/class/combinator.js +7 -3
  4. package/dist/class/directory.d.ts +2 -4
  5. package/dist/class/directory.js +42 -64
  6. package/dist/helper/data_insertion.js +93 -26
  7. package/dist/services/data_provider/model_registry.d.ts +5 -0
  8. package/dist/services/data_provider/model_registry.js +25 -0
  9. package/dist/services/data_provider/service.js +8 -0
  10. package/dist/services/file/service.d.ts +47 -78
  11. package/dist/services/file/service.js +124 -155
  12. package/dist/services/functions/service.js +4 -4
  13. package/dist/services/jwt/router.js +2 -1
  14. package/dist/services/user_manager/router.js +1 -1
  15. package/dist/services/user_manager/service.js +48 -17
  16. package/jest.config.ts +18 -0
  17. package/package.json +11 -2
  18. package/src/application.ts +5 -4
  19. package/src/class/combinator.ts +10 -3
  20. package/src/class/directory.ts +40 -58
  21. package/src/helper/data_insertion.ts +101 -27
  22. package/src/services/data_provider/model_registry.ts +28 -0
  23. package/src/services/data_provider/service.ts +6 -0
  24. package/src/services/file/service.ts +146 -178
  25. package/src/services/functions/service.ts +4 -4
  26. package/src/services/jwt/router.ts +2 -1
  27. package/src/services/user_manager/router.ts +1 -1
  28. package/src/services/user_manager/service.ts +49 -20
  29. package/tests/helpers/test-app.ts +182 -0
  30. package/tests/router/data-provider.router.int.test.ts +192 -0
  31. package/tests/router/file.router.int.test.ts +104 -0
  32. package/tests/router/functions.router.int.test.ts +91 -0
  33. package/tests/router/jwt.router.int.test.ts +69 -0
  34. package/tests/router/user-manager.router.int.test.ts +85 -0
  35. package/tests/setup/jest.setup.ts +5 -0
@@ -25,7 +25,7 @@ class Combinator {
25
25
  // find route paths
26
26
  const option = {
27
27
  name: 'router',
28
- filter: ['.js'],
28
+ filter: ['.js', '.ts'],
29
29
  };
30
30
 
31
31
  let routerPaths: string[] = [];
@@ -35,6 +35,9 @@ class Combinator {
35
35
  console.log(e);
36
36
  }
37
37
 
38
+ // ignore type declarations
39
+ routerPaths = routerPaths.filter(routePath => !routePath.endsWith('.d.ts'));
40
+
38
41
  // create and combine routes into the app
39
42
  for (let i = 0; i < routerPaths.length; i++) {
40
43
  const service = require(routerPaths[i]);
@@ -62,7 +65,7 @@ class Combinator {
62
65
 
63
66
  const option = {
64
67
  name: filename.name,
65
- filter: [filename.extension],
68
+ filter: [filename.extension, '.ts'],
66
69
  };
67
70
 
68
71
  let modulesPath: string[] = [];
@@ -72,6 +75,8 @@ class Combinator {
72
75
  console.log(e);
73
76
  }
74
77
 
78
+ modulesPath = modulesPath.filter(modulePath => !modulePath.endsWith('.d.ts'));
79
+
75
80
  // create and combine routes into the app
76
81
  for (let i = 0; i < modulesPath.length; i++) {
77
82
  const moduleObject = require(modulesPath[i]);
@@ -113,7 +118,7 @@ class Combinator {
113
118
  // find route paths
114
119
  const option = {
115
120
  name: filename.name,
116
- filter: [filename.extension],
121
+ filter: [filename.extension, '.ts'],
117
122
  };
118
123
 
119
124
  let functionsPaths: string[] = [];
@@ -123,6 +128,8 @@ class Combinator {
123
128
  console.log(e);
124
129
  }
125
130
 
131
+ functionsPaths = functionsPaths.filter(functionPath => !functionPath.endsWith('.d.ts'));
132
+
126
133
  // create and combine routes into the app
127
134
  for (let i = 0; i < functionsPaths.length; i++) {
128
135
  const modularFunctions = require(functionsPaths[i]);
@@ -6,67 +6,49 @@ interface DirectorySettings {
6
6
  filter?: string[];
7
7
  }
8
8
 
9
- type WalkCallback = (err: Error | null, results: string[]) => void;
10
-
11
9
  /**
12
- * Walk through a directory and its subdirectories
10
+ * Walk through a directory and its subdirectories efficiently
13
11
  * @param dir - Directory to walk
14
12
  * @param settings - Settings for filtering files
15
- * @param done - Callback function
16
13
  */
17
- function walk(dir: string, settings: DirectorySettings, done: WalkCallback): void {
14
+ async function walk(dir: string, settings: DirectorySettings): Promise<string[]> {
18
15
  let results: string[] = [];
16
+ const list = await fs.promises.readdir(dir);
19
17
 
20
- // Read director file and folders
21
- fs.readdir(dir, function (err, list) {
22
- if (err) return done(err, results);
23
-
24
- let pending = list.length;
25
- if (!pending) return done(null, results);
26
-
27
- list.forEach(function (file) {
28
- file = path.join(dir, file);
29
- fs.stat(file, function (err, stat) {
30
- if (err) {
31
- // Handle file stat error but continue with other files
32
- console.error(`Error reading file stats for ${file}:`, err);
33
- if (!--pending) done(null, results);
34
- return;
35
- }
36
-
37
- // If directory, execute a recursive call
38
- if (stat && stat.isDirectory()) {
39
- // Add directory to array [comment if you need to remove the directories from the array]
40
- // results.push(file);
41
- walk(file, settings, function (err, res) {
42
- results = results.concat(res);
43
- if (!--pending) done(null, results);
44
- });
45
- } else {
46
- // file filter
47
- const extension = path.extname(file);
48
- const fileName = path.basename(file).split('.')[0];
49
- let fileNameKey = true;
50
-
51
- // name filter
52
- if (settings.name && settings.name === fileName) fileNameKey = true;
53
- else fileNameKey = false;
18
+ for (const file of list) {
19
+ const filePath = path.join(dir, file);
20
+ const stat = await fs.promises.stat(filePath);
54
21
 
55
- // extension filter
56
- if (settings.filter && fileNameKey) {
57
- settings.filter.forEach(function (element) {
58
- if (element.toLowerCase() === extension.toLowerCase()) results.push(file);
59
- });
60
- }
22
+ if (stat && stat.isDirectory()) {
23
+ // Ignore common directories that should not be scanned
24
+ if (file === 'node_modules' || file === '.git' || file === 'dist') {
25
+ continue;
26
+ }
27
+ const res = await walk(filePath, settings);
28
+ results = results.concat(res);
29
+ } else {
30
+ const extension = path.extname(filePath);
31
+ const fileName = path.basename(filePath).split('.')[0];
32
+ let fileNameKey = true;
61
33
 
62
- // push any file if no option
63
- else if (fileNameKey) results.push(file);
34
+ // name filter
35
+ if (settings.name && settings.name !== fileName) {
36
+ fileNameKey = false;
37
+ }
64
38
 
65
- if (!--pending) done(null, results);
39
+ // extension filter
40
+ if (settings.filter && fileNameKey) {
41
+ if (settings.filter.some(ext => ext.toLowerCase() === extension.toLowerCase())) {
42
+ results.push(filePath);
66
43
  }
67
- });
68
- });
69
- });
44
+ }
45
+ // push any file if no name filter and no extension filter
46
+ else if (fileNameKey && !settings.filter) {
47
+ results.push(filePath);
48
+ }
49
+ }
50
+ }
51
+ return results;
70
52
  }
71
53
 
72
54
  /**
@@ -75,13 +57,13 @@ function walk(dir: string, settings: DirectorySettings, done: WalkCallback): voi
75
57
  * @param settings - Settings for filtering files
76
58
  * @returns Promise resolving to an array of file paths
77
59
  */
78
- function find(dir: string, settings: DirectorySettings): Promise<string[]> {
79
- return new Promise((resolve, reject) => {
80
- walk(dir, settings, (err, result) => {
81
- if (err) reject(err);
82
- else resolve(result);
83
- });
84
- });
60
+ async function find(dir: string, settings: DirectorySettings): Promise<string[]> {
61
+ try {
62
+ return await walk(dir, settings);
63
+ } catch (err) {
64
+ console.error(`Error in directory.find for ${dir}:`, err);
65
+ return [];
66
+ }
85
67
  }
86
68
 
87
69
  export { walk, find };
@@ -45,28 +45,97 @@ interface AdminCredentials {
45
45
  export async function createAdminUser({ email, password }: AdminCredentials): Promise<void> {
46
46
  const authModel = DataProvider.getCollection('cms', 'auth');
47
47
 
48
+ // Wait for connection to be ready for queries
49
+ const connection = authModel.db;
50
+ if (connection) {
51
+ if (connection.readyState !== 1) {
52
+ await new Promise<void>((resolve, reject) => {
53
+ const timeout = setTimeout(() => {
54
+ reject(new Error('Database connection timeout - connection not ready after 10s'));
55
+ }, 10000);
56
+
57
+ if (connection.readyState === 1) {
58
+ clearTimeout(timeout);
59
+ resolve();
60
+ } else {
61
+ connection.once('connected', () => {
62
+ clearTimeout(timeout);
63
+ resolve();
64
+ });
65
+ connection.once('error', err => {
66
+ clearTimeout(timeout);
67
+ reject(err);
68
+ });
69
+ }
70
+ });
71
+ }
72
+
73
+ // Test if database is actually ready by doing a simple operation
74
+ // This ensures the connection is fully initialized
75
+ let retries = 5;
76
+ let lastError: Error | null = null;
77
+ while (retries > 0) {
78
+ try {
79
+ await authModel.findOne({}).limit(1).maxTimeMS(3000).lean().exec();
80
+ // Additional small delay to ensure write operations will work
81
+ await new Promise(resolve => setTimeout(resolve, 100));
82
+ break; // Success, connection is ready
83
+ } catch (err) {
84
+ lastError = err instanceof Error ? err : new Error(String(err));
85
+ retries--;
86
+ if (retries > 0) {
87
+ await new Promise(resolve => setTimeout(resolve, 500));
88
+ }
89
+ }
90
+ }
91
+ if (retries === 0 && lastError) {
92
+ throw new Error(`Database not ready for queries: ${lastError.message}`);
93
+ }
94
+ }
95
+
48
96
  try {
49
- const isAnonymousExisted = await authModel.countDocuments({ type: 'anonymous' }).exec();
97
+ // Execute queries with explicit timeout handling
98
+ const queryOptions = { maxTimeMS: 5000 }; // 5 second timeout
99
+
100
+ const isAnonymousExisted = await authModel
101
+ .countDocuments({ type: 'anonymous' })
102
+ .maxTimeMS(5000)
103
+ .exec();
50
104
 
51
105
  const isAdministratorExisted = await authModel
52
106
  .countDocuments({ type: 'user', email: email })
107
+ .maxTimeMS(5000)
53
108
  .exec();
54
109
 
55
110
  if (isAnonymousExisted === 0) {
56
- await userManager.main.registerUser({
57
- permissionGroup: getDefaultAnonymousPermissionGroup().title,
58
- email: '',
59
- phone: '',
60
- password: '',
61
- type: 'anonymous',
62
- });
63
- // await new authModel({
64
- // permission: getDefaultAnonymousPermissionGroup().title,
65
- // email: "",
66
- // phone: "",
67
- // password: "",
68
- // type: "anonymous",
69
- // }).save();
111
+ // Wrap registerUser with timeout to prevent hanging
112
+ try {
113
+ let timeoutId: NodeJS.Timeout;
114
+ await Promise.race([
115
+ userManager.main.registerUser({
116
+ permissionGroup: getDefaultAnonymousPermissionGroup().title,
117
+ email: '',
118
+ phone: '',
119
+ password: '',
120
+ type: 'anonymous',
121
+ }),
122
+ new Promise<never>((_, reject) => {
123
+ timeoutId = setTimeout(
124
+ () => reject(new Error('registerUser timeout for anonymous user after 15s')),
125
+ 15000
126
+ );
127
+ }),
128
+ ]).finally(() => {
129
+ if (timeoutId!) clearTimeout(timeoutId);
130
+ });
131
+ } catch (err) {
132
+ // If anonymous user creation fails, log but don't fail completely
133
+ // It might already exist from a previous attempt
134
+ console.warn(
135
+ 'Failed to create anonymous user:',
136
+ err instanceof Error ? err.message : String(err)
137
+ );
138
+ }
70
139
  }
71
140
 
72
141
  if (isAdministratorExisted === 0) {
@@ -74,19 +143,24 @@ export async function createAdminUser({ email, password }: AdminCredentials): Pr
74
143
  return Promise.reject('Invalid email or password for admin user.');
75
144
  }
76
145
 
77
- await userManager.main.registerUser({
78
- permissionGroup: getDefaultAdministratorPermissionGroup().title,
79
- email: email,
80
- password: password,
81
- type: 'user',
146
+ // Wrap registerUser with timeout to prevent hanging
147
+ let timeoutId: NodeJS.Timeout;
148
+ await Promise.race([
149
+ userManager.main.registerUser({
150
+ permissionGroup: getDefaultAdministratorPermissionGroup().title,
151
+ email: email,
152
+ password: password,
153
+ type: 'user',
154
+ }),
155
+ new Promise<never>((_, reject) => {
156
+ timeoutId = setTimeout(
157
+ () => reject(new Error('registerUser timeout for admin user after 15s')),
158
+ 15000
159
+ );
160
+ }),
161
+ ]).finally(() => {
162
+ if (timeoutId!) clearTimeout(timeoutId);
82
163
  });
83
-
84
- // await new authModel({
85
- // permission: getDefaultAdministratorPermissionGroup().title,
86
- // email: email,
87
- // password: password,
88
- // type: "user",
89
- // }).save();
90
164
  }
91
165
  } catch (e) {
92
166
  return Promise.reject(e);
@@ -73,6 +73,8 @@ class ModelRegistry {
73
73
  useUnifiedTopology: true,
74
74
  useNewUrlParser: true,
75
75
  dbName: fullDbName,
76
+ serverSelectionTimeoutMS: 10000, // 10 second timeout for server selection
77
+ socketTimeoutMS: 45000, // 45 second timeout for socket operations
76
78
  });
77
79
 
78
80
  this.connections[database] = connection;
@@ -139,6 +141,8 @@ class ModelRegistry {
139
141
  useUnifiedTopology: true,
140
142
  useNewUrlParser: true,
141
143
  dbName: fullDbName,
144
+ serverSelectionTimeoutMS: 10000, // 10 second timeout for server selection
145
+ socketTimeoutMS: 45000, // 45 second timeout for socket operations
142
146
  });
143
147
 
144
148
  this.connections[dbName] = newConnection;
@@ -208,6 +212,30 @@ class ModelRegistry {
208
212
  hasModel(database: string, collection: string): boolean {
209
213
  return !!(this.models[database] && this.models[database][collection]);
210
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
+ }
211
239
  }
212
240
 
213
241
  // Export singleton instance
@@ -73,6 +73,8 @@ function connectToDatabaseByCollectionDefinitionList(
73
73
  useUnifiedTopology: true,
74
74
  useNewUrlParser: true,
75
75
  dbName: fullDbName,
76
+ serverSelectionTimeoutMS: 10000, // 10 second timeout for server selection
77
+ socketTimeoutMS: 45000, // 45 second timeout for socket operations
76
78
  });
77
79
  } else {
78
80
  const fullDbName = (mongoOption.dbPrefix || '') + dbName;
@@ -281,6 +283,10 @@ export function checkAccess(
281
283
  if (permission.accessType === 'god_access') return true;
282
284
  if (permission.accessType === 'anonymous_access' && user.type === 'anonymous') return true;
283
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
+ }
284
290
  return false;
285
291
  });
286
292
  }