@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.
- package/.nvmrc +1 -1
- package/dist/application.js +5 -4
- 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/services/data_provider/model_registry.d.ts +5 -0
- package/dist/services/data_provider/model_registry.js +25 -0
- package/dist/services/data_provider/service.js +8 -0
- package/dist/services/file/service.d.ts +47 -78
- package/dist/services/file/service.js +124 -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/combinator.ts +10 -3
- package/src/class/directory.ts +40 -58
- package/src/helper/data_insertion.ts +101 -27
- package/src/services/data_provider/model_registry.ts +28 -0
- package/src/services/data_provider/service.ts +6 -0
- package/src/services/file/service.ts +146 -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
package/src/class/combinator.ts
CHANGED
|
@@ -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]);
|
package/src/class/directory.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
34
|
+
// name filter
|
|
35
|
+
if (settings.name && settings.name !== fileName) {
|
|
36
|
+
fileNameKey = false;
|
|
37
|
+
}
|
|
64
38
|
|
|
65
|
-
|
|
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
|
-
|
|
80
|
-
walk(dir, settings
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
}
|