@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
@@ -1,6 +1,9 @@
1
- import { Schema } from 'mongoose';
1
+ import { Schema, Model } from 'mongoose';
2
2
  import { Permission } from './security';
3
3
  import { DatabaseTrigger } from './database_trigger';
4
+ import modelRegistry from '../services/data_provider/model_registry';
5
+ import { MongoOption } from '../services/data_provider/service';
6
+ import { config } from '../config';
4
7
 
5
8
  /**
6
9
  * Configuration options for creating a collection definition.
@@ -26,6 +29,12 @@ interface CollectionDefinitionOptions {
26
29
  * @see https://mongoosejs.com/docs/5.x/docs/guide.html
27
30
  */
28
31
  schema: Schema<any>;
32
+
33
+ /**
34
+ * Optional MongoDB connection options. If not provided, will use config.mongo if available.
35
+ * This is used to pre-create the model before server startup.
36
+ */
37
+ mongoOption?: MongoOption;
29
38
  }
30
39
 
31
40
  /**
@@ -34,7 +43,7 @@ interface CollectionDefinitionOptions {
34
43
  * @param {CollectionDefinitionOptions} options - The options for the collection
35
44
  * @expandType CollectionDefinitionOptions
36
45
  *
37
- * @returns A new instance of CollectionDefinition
46
+ * @returns A CollectionDefinition instance with a model property that returns the mongoose model
38
47
  *
39
48
  * @public
40
49
  *
@@ -51,10 +60,72 @@ interface CollectionDefinitionOptions {
51
60
  * // trigger: DatabaseTrigger[]
52
61
  * })
53
62
  * ]
63
+ *
64
+ * // Access the model directly:
65
+ * const userCollection = defineCollection({...});
66
+ * const UserModel = userCollection.model;
67
+ * const users = await UserModel.find();
54
68
  * ```
55
69
  */
56
- export function defineCollection(options: CollectionDefinitionOptions) {
57
- return new CollectionDefinition(options);
70
+ export function defineCollection(
71
+ options: CollectionDefinitionOptions
72
+ ): CollectionDefinition & { model: Model<any> } {
73
+ const definition = new CollectionDefinition(options);
74
+
75
+ // Try to get mongoOption from options or config
76
+ let mongoOption: MongoOption | undefined = options.mongoOption;
77
+ if (!mongoOption && config.mongo) {
78
+ mongoOption = config.mongo as MongoOption;
79
+ }
80
+
81
+ // If mongoOption is available, register the collection and create the model
82
+ if (mongoOption) {
83
+ const model = modelRegistry.registerCollection(definition, mongoOption);
84
+ definition.setModel(model);
85
+ }
86
+
87
+ // Create a proxy object that includes both CollectionDefinition properties and model
88
+ const result = definition as CollectionDefinition & { model: Model<any> };
89
+
90
+ // Add model property getter
91
+ Object.defineProperty(result, 'model', {
92
+ get() {
93
+ // If model was already set, return it
94
+ const existingModel = definition.getModel();
95
+ if (existingModel) {
96
+ return existingModel;
97
+ }
98
+
99
+ // Otherwise, try to get from registry
100
+ const registryModel = modelRegistry.getModel(definition.database, definition.collection);
101
+ if (registryModel) {
102
+ definition.setModel(registryModel);
103
+ return registryModel;
104
+ }
105
+
106
+ // If still not found, try to get mongoOption from config and register now
107
+ let currentMongoOption: MongoOption | undefined = mongoOption;
108
+ if (!currentMongoOption && config.mongo) {
109
+ currentMongoOption = config.mongo as MongoOption;
110
+ }
111
+
112
+ if (currentMongoOption) {
113
+ const newModel = modelRegistry.registerCollection(definition, currentMongoOption);
114
+ definition.setModel(newModel);
115
+ return newModel;
116
+ }
117
+
118
+ throw new Error(
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.`
122
+ );
123
+ },
124
+ enumerable: true,
125
+ configurable: true,
126
+ });
127
+
128
+ return result;
58
129
  }
59
130
 
60
131
  /**
@@ -110,6 +181,9 @@ export class CollectionDefinition {
110
181
  /** @readonly Optional database triggers */
111
182
  triggers?: DatabaseTrigger[];
112
183
 
184
+ /** Optional mongoose model for this collection */
185
+ private _model: Model<any> | null = null;
186
+
113
187
  /**
114
188
  * Creates a new CollectionDefinition instance
115
189
  *
@@ -131,4 +205,20 @@ export class CollectionDefinition {
131
205
  this.permissions = permissions;
132
206
  this.triggers = triggers;
133
207
  }
208
+
209
+ /**
210
+ * Get the mongoose model for this collection
211
+ * @returns {Model<any> | null} The mongoose model or null if not set
212
+ */
213
+ getModel(): Model<any> | null {
214
+ return this._model;
215
+ }
216
+
217
+ /**
218
+ * Set the mongoose model for this collection
219
+ * @param {Model<any>} model - The mongoose model
220
+ */
221
+ setModel(model: Model<any>): void {
222
+ this._model = model;
223
+ }
134
224
  }
@@ -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);
package/src/index.ts CHANGED
@@ -56,7 +56,7 @@ export { createRest };
56
56
  * ],
57
57
  * uploadDirectoryConfig: {
58
58
  * directory: './uploads',
59
- * urlPath: '/assets'
59
+ * urlPath: '/assets'
60
60
  * }
61
61
  * };
62
62
  * ```
@@ -147,6 +147,18 @@ export { paginator };
147
147
  */
148
148
  export { getCollection };
149
149
 
150
+ /**
151
+ * @description Model registry for managing mongoose models and connections
152
+ * @example
153
+ * ```typescript
154
+ * import { modelRegistry } from '@modular-rest/server';
155
+ *
156
+ * // Get a model directly from registry
157
+ * const userModel = modelRegistry.getModel('myapp', 'users');
158
+ * ```
159
+ */
160
+ export { modelRegistry } from './services/data_provider/model_registry';
161
+
150
162
  /**
151
163
  * @description File handling utilities
152
164
  * @example