@modular-rest/server 1.19.0 → 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/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 +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/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 +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
|
@@ -47,11 +47,11 @@ const trigger_operator_1 = __importDefault(require("../../class/trigger_operator
|
|
|
47
47
|
*/
|
|
48
48
|
exports.name = 'file';
|
|
49
49
|
/**
|
|
50
|
-
* File service for handling file
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* File service class for handling file operations
|
|
51
|
+
* @class FileService
|
|
52
|
+
* @description
|
|
53
|
+
* This class provides methods for managing file uploads, retrieval, and deletion.
|
|
54
|
+
* It handles physical file storage and database metadata management.
|
|
55
55
|
*/
|
|
56
56
|
class FileService {
|
|
57
57
|
/**
|
|
@@ -95,56 +95,43 @@ class FileService {
|
|
|
95
95
|
fs_1.default.mkdirSync(directoryOrConfig, { recursive: true });
|
|
96
96
|
}
|
|
97
97
|
this.directory = directoryOrConfig;
|
|
98
|
-
this.urlPath =
|
|
99
|
-
return;
|
|
98
|
+
this.urlPath = '/assets'; // Default urlPath for legacy
|
|
100
99
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
100
|
+
else {
|
|
101
|
+
const { directory, urlPath } = directoryOrConfig;
|
|
102
|
+
if (!fs_1.default.existsSync(directory)) {
|
|
103
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
this.directory = directory;
|
|
106
|
+
this.urlPath = urlPath || '/assets';
|
|
109
107
|
}
|
|
110
|
-
// Store only the necessary properties (ignore koa-static options)
|
|
111
|
-
this.directory = directory;
|
|
112
|
-
this.urlPath = urlPath;
|
|
113
108
|
}
|
|
114
109
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* Creates stored file details with unique filename
|
|
110
|
+
* Creates a unique filename and storage details
|
|
118
111
|
* @param {string} fileType - MIME type of the file
|
|
119
|
-
* @param {string} tag -
|
|
120
|
-
* @returns {StoredFileDetail} Storage details including filename and path
|
|
121
|
-
* @
|
|
122
|
-
*
|
|
123
|
-
* @example
|
|
124
|
-
* ```typescript
|
|
125
|
-
* import { fileService } from '@modular-rest/server';
|
|
126
|
-
*
|
|
127
|
-
* const details = fileService.createStoredDetail('image/jpeg', 'profile');
|
|
128
|
-
* // Returns: { fileName: '1234567890.jpeg', fullPath: '/uploads/jpeg/profile/1234567890.jpeg', fileFormat: 'jpeg' }
|
|
129
|
-
* ```
|
|
112
|
+
* @param {string} tag - File tag
|
|
113
|
+
* @returns {StoredFileDetail} Storage details including unique filename and path
|
|
114
|
+
* @hidden
|
|
130
115
|
*/
|
|
131
116
|
createStoredDetail(fileType, tag) {
|
|
132
117
|
const typeParts = fileType.split('/');
|
|
133
118
|
const fileFormat = typeParts[1] || typeParts[0] || 'unknown';
|
|
134
119
|
const time = new Date().getTime();
|
|
135
120
|
const fileName = `${time}.${fileFormat}`;
|
|
136
|
-
if (!
|
|
137
|
-
throw new Error('Upload directory
|
|
121
|
+
if (!this.directory) {
|
|
122
|
+
throw new Error('Upload directory not set');
|
|
138
123
|
}
|
|
139
|
-
const fullPath = path_1.default.join(
|
|
140
|
-
return {
|
|
124
|
+
const fullPath = path_1.default.join(this.directory, fileFormat, tag, fileName);
|
|
125
|
+
return {
|
|
126
|
+
fileName,
|
|
127
|
+
fullPath,
|
|
128
|
+
fileFormat,
|
|
129
|
+
};
|
|
141
130
|
}
|
|
142
131
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* Stores a file, removes the temporary file, and saves metadata to database
|
|
132
|
+
* Stores a file on disc and creates metadata in database
|
|
146
133
|
* @param {StoreFileOptions} options - File storage options
|
|
147
|
-
* @returns {Promise<IFile>}
|
|
134
|
+
* @returns {Promise<IFile>} The created file document
|
|
148
135
|
* @throws {Error} If upload directory is not set or storage fails
|
|
149
136
|
* @example
|
|
150
137
|
* ```typescript
|
|
@@ -167,10 +154,16 @@ class FileService {
|
|
|
167
154
|
if (!FileService.instance.directory) {
|
|
168
155
|
throw new Error('Upload directory has not been set');
|
|
169
156
|
}
|
|
157
|
+
const fileType = file.mimetype || file.type || 'unknown/unknown';
|
|
158
|
+
const filePath = file.filepath || file.path;
|
|
159
|
+
const fileName = file.originalFilename || file.name || 'unknown';
|
|
160
|
+
if (!filePath) {
|
|
161
|
+
throw new Error('File path is missing');
|
|
162
|
+
}
|
|
170
163
|
let storedFile;
|
|
171
164
|
return new Promise(async (done, reject) => {
|
|
172
|
-
storedFile = FileService.instance.createStoredDetail(
|
|
173
|
-
fs_1.default.copyFile(
|
|
165
|
+
storedFile = FileService.instance.createStoredDetail(fileType, tag);
|
|
166
|
+
fs_1.default.copyFile(filePath, storedFile.fullPath, (err) => {
|
|
174
167
|
if (err) {
|
|
175
168
|
reject(err);
|
|
176
169
|
}
|
|
@@ -179,7 +172,14 @@ class FileService {
|
|
|
179
172
|
}
|
|
180
173
|
// remove temp file
|
|
181
174
|
if (removeFileAfterStore) {
|
|
182
|
-
|
|
175
|
+
try {
|
|
176
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
177
|
+
fs_1.default.unlinkSync(filePath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
console.warn('Failed to remove temp file:', e);
|
|
182
|
+
}
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
})
|
|
@@ -191,161 +191,119 @@ class FileService {
|
|
|
191
191
|
}
|
|
192
192
|
const data = {
|
|
193
193
|
owner: ownerId,
|
|
194
|
+
tag,
|
|
195
|
+
originalName: fileName,
|
|
194
196
|
fileName: storedFile.fileName,
|
|
195
|
-
originalName: file.name,
|
|
196
197
|
format: storedFile.fileFormat,
|
|
197
|
-
tag,
|
|
198
198
|
size: file.size,
|
|
199
199
|
};
|
|
200
|
-
|
|
201
|
-
const doc = new CollectionModel(data);
|
|
202
|
-
return doc.save().then(savedDoc => {
|
|
203
|
-
trigger_operator_1.default.call('insert-one', 'cms', 'file', {
|
|
204
|
-
query: null,
|
|
205
|
-
queryResult: savedDoc,
|
|
206
|
-
});
|
|
207
|
-
return savedDoc;
|
|
208
|
-
});
|
|
200
|
+
return CollectionModel.create(data);
|
|
209
201
|
})
|
|
210
|
-
.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
throw err;
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* @hidden
|
|
218
|
-
*
|
|
219
|
-
* Removes a file from the disk
|
|
220
|
-
* @param {string} path - File path to remove
|
|
221
|
-
* @returns {Promise<void>} Promise resolving when file is removed
|
|
222
|
-
* @throws {Error} If file removal fails
|
|
223
|
-
* @example
|
|
224
|
-
* ```typescript
|
|
225
|
-
* import { fileService } from '@modular-rest/server';
|
|
226
|
-
*
|
|
227
|
-
* await fileService.removeFromDisc('/uploads/jpeg/profile/1234567890.jpeg');
|
|
228
|
-
* ```
|
|
229
|
-
*/
|
|
230
|
-
removeFromDisc(path) {
|
|
231
|
-
return new Promise((done, reject) => {
|
|
232
|
-
fs_1.default.unlink(path, (err) => {
|
|
233
|
-
if (err)
|
|
234
|
-
reject(err);
|
|
235
|
-
else
|
|
236
|
-
done();
|
|
237
|
-
});
|
|
202
|
+
.then(async (doc) => {
|
|
203
|
+
trigger_operator_1.default.call('insert-one', 'cms', 'file', { queryResult: doc });
|
|
204
|
+
return doc;
|
|
238
205
|
});
|
|
239
206
|
}
|
|
240
207
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* @
|
|
244
|
-
* @
|
|
245
|
-
* @throws {Error} If file is not found or removal fails
|
|
208
|
+
* Deletes a file from disc and database
|
|
209
|
+
* @param {string} fileId - ID of the file to delete
|
|
210
|
+
* @returns {Promise<boolean>} True if deletion was successful
|
|
211
|
+
* @throws {Error} If file is not found or deletion fails
|
|
246
212
|
* @example
|
|
247
213
|
* ```typescript
|
|
248
214
|
* import { fileService } from '@modular-rest/server';
|
|
249
215
|
*
|
|
250
|
-
*
|
|
251
|
-
* await fileService.removeFile('file123');
|
|
252
|
-
* console.log('File removed successfully');
|
|
253
|
-
* } catch (error) {
|
|
254
|
-
* console.error('Failed to remove file:', error);
|
|
255
|
-
* }
|
|
216
|
+
* await fileService.removeFile('file123');
|
|
256
217
|
* ```
|
|
257
218
|
*/
|
|
258
|
-
removeFile(fileId) {
|
|
219
|
+
async removeFile(fileId) {
|
|
259
220
|
if (!FileService.instance.directory) {
|
|
260
221
|
throw new Error('Upload directory has not been set');
|
|
261
222
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
223
|
+
const CollectionModel = DataProvider.getCollection('cms', 'file');
|
|
224
|
+
if (!CollectionModel) {
|
|
225
|
+
throw new Error('Collection model not found');
|
|
226
|
+
}
|
|
227
|
+
const doc = await CollectionModel.findById(fileId);
|
|
228
|
+
if (!doc) {
|
|
229
|
+
throw new Error('File not found');
|
|
230
|
+
}
|
|
231
|
+
const filePath = path_1.default.join(FileService.instance.directory, doc.format, doc.tag, doc.fileName);
|
|
232
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
233
|
+
try {
|
|
234
|
+
await FileService.instance.removeFromDisc(filePath);
|
|
270
235
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
.
|
|
274
|
-
// create file path
|
|
275
|
-
const filePath = path_1.default.join(FileService.instance.directory, fileDoc.format, fileDoc.tag, fileDoc.fileName);
|
|
276
|
-
// Remove file from disc
|
|
277
|
-
return FileService.instance.removeFromDisc(filePath).catch(async (err) => {
|
|
278
|
-
// Recreate fileDoc if removing file operation has error
|
|
279
|
-
await new CollectionModel(fileDoc).save();
|
|
236
|
+
catch (err) {
|
|
237
|
+
// If the file is not found on disc, we can still proceed with deleting metadata
|
|
238
|
+
if (err.code !== 'ENOENT') {
|
|
280
239
|
throw err;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
await CollectionModel.findByIdAndDelete(fileId);
|
|
244
|
+
trigger_operator_1.default.call('remove-one', 'cms', 'file', { queryResult: doc });
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Deletes a file from physical storage
|
|
249
|
+
* @param {string} path - Physical path to the file
|
|
250
|
+
* @returns {Promise<boolean>} True if deletion was successful
|
|
251
|
+
* @hidden
|
|
252
|
+
*/
|
|
253
|
+
removeFromDisc(path) {
|
|
254
|
+
return new Promise((done, reject) => {
|
|
255
|
+
fs_1.default.unlink(path, err => {
|
|
256
|
+
if (err) {
|
|
257
|
+
reject(err);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
done(true);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
291
263
|
});
|
|
292
264
|
}
|
|
293
265
|
/**
|
|
294
|
-
* Retrieves a file document from
|
|
295
|
-
*
|
|
296
|
-
* @
|
|
297
|
-
* @
|
|
298
|
-
* @
|
|
299
|
-
* @example
|
|
300
|
-
* ```typescript
|
|
301
|
-
* import { fileService } from '@modular-rest/server';
|
|
302
|
-
*
|
|
303
|
-
* const fileDoc = await fileService.getFile('file123');
|
|
304
|
-
* console.log('File details:', fileDoc);
|
|
305
|
-
* ```
|
|
266
|
+
* Retrieves a file document from database
|
|
267
|
+
* @param {string} fileId - ID of the file
|
|
268
|
+
* @returns {Promise<IFile>} The file document
|
|
269
|
+
* @throws {Error} If file is not found
|
|
270
|
+
* @hidden
|
|
306
271
|
*/
|
|
307
|
-
getFile(fileId) {
|
|
272
|
+
async getFile(fileId) {
|
|
308
273
|
const CollectionModel = DataProvider.getCollection('cms', 'file');
|
|
309
274
|
if (!CollectionModel) {
|
|
310
275
|
throw new Error('Collection model not found');
|
|
311
276
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
return doc;
|
|
319
|
-
});
|
|
277
|
+
const doc = await CollectionModel.findById(fileId);
|
|
278
|
+
if (!doc) {
|
|
279
|
+
throw new Error('File not found');
|
|
280
|
+
}
|
|
281
|
+
return doc;
|
|
320
282
|
}
|
|
321
283
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* @
|
|
325
|
-
* @returns {Promise<string>} Promise resolving to file URL
|
|
326
|
-
* @throws {Error} If URL path is not defined or file is not found
|
|
284
|
+
* Gets the public URL for a file
|
|
285
|
+
* @param {string} fileId - ID of the file
|
|
286
|
+
* @returns {Promise<string>} The public URL
|
|
327
287
|
* @example
|
|
328
288
|
* ```typescript
|
|
329
289
|
* import { fileService } from '@modular-rest/server';
|
|
330
290
|
*
|
|
331
|
-
* const
|
|
332
|
-
* // Returns: '/
|
|
291
|
+
* const url = await fileService.getFileLink('file123');
|
|
292
|
+
* // Returns: '/assets/jpeg/profile/1234567890.jpeg'
|
|
333
293
|
* ```
|
|
334
294
|
*/
|
|
335
295
|
async getFileLink(fileId) {
|
|
336
296
|
const fileDoc = await FileService.instance.getFile(fileId);
|
|
337
297
|
if (!FileService.instance.urlPath) {
|
|
338
|
-
throw new Error('Upload directory
|
|
298
|
+
throw new Error('Upload directory config has not been set');
|
|
339
299
|
}
|
|
340
300
|
const link = `${FileService.instance.urlPath}/${fileDoc.format}/${fileDoc.tag}/${fileDoc.fileName}`;
|
|
341
301
|
return link;
|
|
342
302
|
}
|
|
343
303
|
/**
|
|
344
|
-
* Gets the
|
|
345
|
-
*
|
|
346
|
-
* @
|
|
347
|
-
* @returns {Promise<string>} Promise resolving to full file path
|
|
348
|
-
* @throws {Error} If upload directory is not set or file is not found
|
|
304
|
+
* Gets the physical path for a file
|
|
305
|
+
* @param {string} fileId - ID of the file
|
|
306
|
+
* @returns {Promise<string>} The physical path
|
|
349
307
|
* @example
|
|
350
308
|
* ```typescript
|
|
351
309
|
* import { fileService } from '@modular-rest/server';
|
|
@@ -367,3 +325,4 @@ class FileService {
|
|
|
367
325
|
* @constant {FileService}
|
|
368
326
|
*/
|
|
369
327
|
exports.main = new FileService();
|
|
328
|
+
FileService.instance = exports.main;
|
|
@@ -61,17 +61,17 @@ const functions = [];
|
|
|
61
61
|
*/
|
|
62
62
|
function defineFunction(options) {
|
|
63
63
|
// Check if the function already exists
|
|
64
|
-
const existingFunction = functions.find(f => f.name ===
|
|
64
|
+
const existingFunction = functions.find(f => f.name === options.name);
|
|
65
65
|
if (existingFunction) {
|
|
66
|
-
throw new Error(`Function with name ${
|
|
66
|
+
throw new Error(`Function with name ${options.name} already exists`);
|
|
67
67
|
}
|
|
68
68
|
// Check if the permission types provided
|
|
69
69
|
if (!options.permissionTypes || !options.permissionTypes.length) {
|
|
70
|
-
throw new Error(`Permission types not provided for function ${
|
|
70
|
+
throw new Error(`Permission types not provided for function ${options.name}`);
|
|
71
71
|
}
|
|
72
72
|
// Check if the callback is a function
|
|
73
73
|
if (typeof options.callback !== 'function') {
|
|
74
|
-
throw new Error(`Callback is not a function for function ${
|
|
74
|
+
throw new Error(`Callback is not a function for function ${options.name}`);
|
|
75
75
|
}
|
|
76
76
|
// Add the function to the list of functions
|
|
77
77
|
return options;
|
|
@@ -41,6 +41,7 @@ const koa_router_1 = __importDefault(require("koa-router"));
|
|
|
41
41
|
const validator_1 = require("../../class/validator");
|
|
42
42
|
const reply_1 = require("../../class/reply");
|
|
43
43
|
const service = __importStar(require("./service"));
|
|
44
|
+
const userManager = __importStar(require("../user_manager/service"));
|
|
44
45
|
const name = 'verify';
|
|
45
46
|
exports.name = name;
|
|
46
47
|
const verify = new koa_router_1.default();
|
|
@@ -82,7 +83,7 @@ verify.post('/checkAccess', async (ctx) => {
|
|
|
82
83
|
ctx.throw(412, err.message);
|
|
83
84
|
});
|
|
84
85
|
const userid = payload.id;
|
|
85
|
-
await
|
|
86
|
+
await userManager.main
|
|
86
87
|
.getUserById(userid)
|
|
87
88
|
.then((user) => {
|
|
88
89
|
const key = user.hasPermission(body.permissionField);
|
|
@@ -41,6 +41,7 @@ const koa_router_1 = __importDefault(require("koa-router"));
|
|
|
41
41
|
const validator_1 = require("../../class/validator");
|
|
42
42
|
const reply_1 = require("../../class/reply");
|
|
43
43
|
const service = __importStar(require("./service"));
|
|
44
|
+
const dataProvider = __importStar(require("../data_provider/service"));
|
|
44
45
|
const name = 'user';
|
|
45
46
|
exports.name = name;
|
|
46
47
|
const userManager = new koa_router_1.default();
|
|
@@ -183,7 +184,6 @@ userManager.post('/getPermission', async (ctx) => {
|
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
186
|
const query = { _id: body.id };
|
|
186
|
-
const dataProvider = global.services.dataProvider;
|
|
187
187
|
const permission = await dataProvider
|
|
188
188
|
.getCollection('cms', 'permission')
|
|
189
189
|
.findOne(query)
|
|
@@ -431,11 +431,19 @@ class UserManager {
|
|
|
431
431
|
if (!this.isCodeValid(id, code)) {
|
|
432
432
|
throw new Error('Invalid verification code');
|
|
433
433
|
}
|
|
434
|
+
const tempId = this.tempIds[id];
|
|
435
|
+
const idType = tempId ? tempId.type : 'email';
|
|
434
436
|
const userModel = DataProvider.getCollection('cms', 'auth');
|
|
435
437
|
if (!userModel) {
|
|
436
438
|
throw new Error('User model not found');
|
|
437
439
|
}
|
|
438
|
-
const query = {
|
|
440
|
+
const query = {};
|
|
441
|
+
if (idType === 'phone') {
|
|
442
|
+
query.phone = id;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
query.email = id;
|
|
446
|
+
}
|
|
439
447
|
// Get from database
|
|
440
448
|
let gottenFromDB;
|
|
441
449
|
try {
|
|
@@ -444,20 +452,30 @@ class UserManager {
|
|
|
444
452
|
catch (error) {
|
|
445
453
|
throw error;
|
|
446
454
|
}
|
|
447
|
-
if (!gottenFromDB) {
|
|
448
|
-
throw new Error('User not found');
|
|
449
|
-
}
|
|
450
455
|
try {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
456
|
+
let token;
|
|
457
|
+
if (!gottenFromDB) {
|
|
458
|
+
// Registration flow: create new user
|
|
459
|
+
const registrationData = {
|
|
460
|
+
password,
|
|
461
|
+
type: 'user',
|
|
462
|
+
};
|
|
463
|
+
if (idType === 'phone') {
|
|
464
|
+
registrationData.phone = id;
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
registrationData.email = id;
|
|
468
|
+
}
|
|
469
|
+
token = await this.registerUser(registrationData);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
// Password reset flow: update existing user
|
|
473
|
+
const user = await user_1.User.loadFromModel(gottenFromDB);
|
|
474
|
+
user.password = password;
|
|
475
|
+
await user.save();
|
|
476
|
+
const payload = user.getBrief();
|
|
477
|
+
token = await JWT.main.sign(payload);
|
|
478
|
+
}
|
|
461
479
|
// Remove temporary ID
|
|
462
480
|
delete this.tempIds[id];
|
|
463
481
|
return token;
|
|
@@ -493,11 +511,19 @@ class UserManager {
|
|
|
493
511
|
if (!this.isCodeValid(id, code)) {
|
|
494
512
|
throw new Error('Invalid verification code');
|
|
495
513
|
}
|
|
514
|
+
const tempId = this.tempIds[id];
|
|
515
|
+
const idType = tempId ? tempId.type : 'email';
|
|
496
516
|
const userModel = DataProvider.getCollection('cms', 'auth');
|
|
497
517
|
if (!userModel) {
|
|
498
518
|
throw new Error('User model not found');
|
|
499
519
|
}
|
|
500
|
-
const query = {
|
|
520
|
+
const query = {};
|
|
521
|
+
if (idType === 'phone') {
|
|
522
|
+
query.phone = id;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
query.email = id;
|
|
526
|
+
}
|
|
501
527
|
// Get from database
|
|
502
528
|
let gottenFromDB;
|
|
503
529
|
try {
|
|
@@ -557,8 +583,8 @@ class UserManager {
|
|
|
557
583
|
return reject(new Error('User model not found'));
|
|
558
584
|
}
|
|
559
585
|
try {
|
|
560
|
-
// Create user document
|
|
561
|
-
const
|
|
586
|
+
// Create user document with timeout
|
|
587
|
+
const createPromise = userModel.create({
|
|
562
588
|
...detail,
|
|
563
589
|
type: detail.type || 'user',
|
|
564
590
|
permissionGroup: detail.permissionGroup || (0, permissionManager_1.getDefaultPermissionGroups)().title,
|
|
@@ -566,6 +592,11 @@ class UserManager {
|
|
|
566
592
|
email: detail.email || undefined,
|
|
567
593
|
password: detail.password || undefined,
|
|
568
594
|
});
|
|
595
|
+
// Add timeout wrapper to prevent hanging
|
|
596
|
+
const userDoc = await Promise.race([
|
|
597
|
+
createPromise,
|
|
598
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('User creation timeout after 10s')), 10000)),
|
|
599
|
+
]);
|
|
569
600
|
// Load user from document
|
|
570
601
|
const user = await user_1.User.loadFromModel(userDoc);
|
|
571
602
|
// Get token payload
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Config } from 'jest';
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
preset: 'ts-jest',
|
|
5
|
+
testEnvironment: 'node',
|
|
6
|
+
roots: ['<rootDir>/tests', '<rootDir>/src'],
|
|
7
|
+
testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(spec|test).ts'],
|
|
8
|
+
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
9
|
+
transform: {
|
|
10
|
+
'^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }],
|
|
11
|
+
},
|
|
12
|
+
setupFilesAfterEnv: ['<rootDir>/tests/setup/jest.setup.ts'],
|
|
13
|
+
testTimeout: 60000,
|
|
14
|
+
maxWorkers: 1,
|
|
15
|
+
verbose: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default config;
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modular-rest/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "TypeScript version of a nodejs module based on KOAJS for developing Rest-APIs in a modular solution.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"dev": "tsc --watch",
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"test:watch": "jest --watch",
|
|
12
|
+
"test:router": "jest tests/router --runInBand",
|
|
13
|
+
"test:coverage": "jest --coverage --runInBand",
|
|
11
14
|
"play-test": "tsc -p tsconfig.json && node dist/play-test.js",
|
|
12
15
|
"docs": "typedoc --router kind src/index.ts"
|
|
13
16
|
},
|
|
@@ -44,6 +47,7 @@
|
|
|
44
47
|
},
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"@types/bson": "4.2.0",
|
|
50
|
+
"@types/jest": "30.0.0",
|
|
47
51
|
"@types/jsonwebtoken": "^9.0.5",
|
|
48
52
|
"@types/koa": "^2.14.0",
|
|
49
53
|
"@types/koa-mount": "^4.0.5",
|
|
@@ -52,6 +56,11 @@
|
|
|
52
56
|
"@types/koa__cors": "^5.0.0",
|
|
53
57
|
"@types/mongoose": "^5.10.5",
|
|
54
58
|
"@types/node": "^20.10.0",
|
|
59
|
+
"jest": "30.2.0",
|
|
60
|
+
"mongodb-memory-server": "11.0.1",
|
|
61
|
+
"supertest": "7.1.4",
|
|
62
|
+
"@types/supertest": "^6.0.2",
|
|
63
|
+
"ts-jest": "29.4.6",
|
|
55
64
|
"typedoc": "^0.28.1",
|
|
56
65
|
"typedoc-plugin-markdown": "^4.6.0",
|
|
57
66
|
"typedoc-plugin-missing-exports": "^4.0.0",
|
package/src/application.ts
CHANGED
|
@@ -203,17 +203,18 @@ export async function createRest(options: RestOptions): Promise<{ app: Koa; serv
|
|
|
203
203
|
extension: '.js',
|
|
204
204
|
},
|
|
205
205
|
});
|
|
206
|
+
}
|
|
206
207
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
208
|
+
// 5. Plug in additional defined functions
|
|
209
|
+
if (config.functions) {
|
|
210
|
+
Combination.addFunctionsByArray(config.functions);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
// 4. Setting up default services
|
|
214
214
|
try {
|
|
215
215
|
await require('./helper/presetup_services').setup(config);
|
|
216
216
|
} catch (e) {
|
|
217
|
+
console.error(`[createRest] Error in setup:`, e);
|
|
217
218
|
return Promise.reject(e);
|
|
218
219
|
}
|
|
219
220
|
|
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]);
|