@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
@@ -28,9 +28,12 @@ interface StoredFileDetail {
28
28
  * File upload options interface
29
29
  * @interface StoreFileOptions
30
30
  * @property {Object} file - File details
31
- * @property {string} file.path - Temporary file path
32
- * @property {string} file.type - MIME type of the file
33
- * @property {string} file.name - Original filename
31
+ * @property {string} [file.path] - Temporary file path (legacy)
32
+ * @property {string} [file.filepath] - Temporary file path (koa-body v6+)
33
+ * @property {string} [file.type] - MIME type of the file (legacy)
34
+ * @property {string} [file.mimetype] - MIME type of the file (koa-body v6+)
35
+ * @property {string} [file.name] - Original filename (legacy)
36
+ * @property {string} [file.originalFilename] - Original filename (koa-body v6+)
34
37
  * @property {number} file.size - File size in bytes
35
38
  * @property {string} ownerId - ID of the file owner
36
39
  * @property {string} tag - Tag for file organization
@@ -38,9 +41,12 @@ interface StoredFileDetail {
38
41
  */
39
42
  interface StoreFileOptions {
40
43
  file: {
41
- path: string;
42
- type: string;
43
- name: string;
44
+ path?: string;
45
+ filepath?: string;
46
+ type?: string;
47
+ mimetype?: string;
48
+ name?: string;
49
+ originalFilename?: string;
44
50
  size: number;
45
51
  };
46
52
  ownerId: string;
@@ -49,11 +55,11 @@ interface StoreFileOptions {
49
55
  }
50
56
 
51
57
  /**
52
- * File service for handling file storage and retrieval.
53
- *
54
- * This service provides functionality for storing, retrieving, and managing files.
55
- * It handles file storage on disk and maintains file metadata in the database.
56
- * Files are organized by format and tag in the upload directory.
58
+ * File service class for handling file operations
59
+ * @class FileService
60
+ * @description
61
+ * This class provides methods for managing file uploads, retrieval, and deletion.
62
+ * It handles physical file storage and database metadata management.
57
63
  */
58
64
  class FileService {
59
65
  /**
@@ -107,43 +113,23 @@ class FileService {
107
113
  fs.mkdirSync(directoryOrConfig, { recursive: true });
108
114
  }
109
115
  this.directory = directoryOrConfig;
110
- this.urlPath = null; // No URL path available with legacy format
111
- return;
112
- }
113
-
114
- // New format: Extract only necessary properties from StaticPathOptions
115
- const directory = directoryOrConfig.directory || '';
116
- const urlPath = directoryOrConfig.urlPath || '/assets';
117
-
118
- if (!directory) {
119
- throw new Error('directory is required in uploadDirectoryConfig');
120
- }
121
-
122
- if (!fs.existsSync(directory)) {
123
- fs.mkdirSync(directory, { recursive: true });
116
+ this.urlPath = '/assets'; // Default urlPath for legacy
117
+ } else {
118
+ const { directory, urlPath } = directoryOrConfig;
119
+ if (!fs.existsSync(directory)) {
120
+ fs.mkdirSync(directory, { recursive: true });
121
+ }
122
+ this.directory = directory;
123
+ this.urlPath = urlPath || '/assets';
124
124
  }
125
-
126
- // Store only the necessary properties (ignore koa-static options)
127
- this.directory = directory;
128
- this.urlPath = urlPath;
129
125
  }
130
126
 
131
127
  /**
132
- * @hidden
133
- *
134
- * Creates stored file details with unique filename
128
+ * Creates a unique filename and storage details
135
129
  * @param {string} fileType - MIME type of the file
136
- * @param {string} tag - Tag for file organization
137
- * @returns {StoredFileDetail} Storage details including filename and path
138
- * @throws {Error} If upload directory is not set
139
- *
140
- * @example
141
- * ```typescript
142
- * import { fileService } from '@modular-rest/server';
143
- *
144
- * const details = fileService.createStoredDetail('image/jpeg', 'profile');
145
- * // Returns: { fileName: '1234567890.jpeg', fullPath: '/uploads/jpeg/profile/1234567890.jpeg', fileFormat: 'jpeg' }
146
- * ```
130
+ * @param {string} tag - File tag
131
+ * @returns {StoredFileDetail} Storage details including unique filename and path
132
+ * @hidden
147
133
  */
148
134
  createStoredDetail(fileType: string, tag: string): StoredFileDetail {
149
135
  const typeParts = fileType.split('/');
@@ -152,21 +138,23 @@ class FileService {
152
138
  const time = new Date().getTime();
153
139
  const fileName = `${time}.${fileFormat}`;
154
140
 
155
- if (!FileService.instance.directory) {
156
- throw new Error('Upload directory has not been set');
141
+ if (!this.directory) {
142
+ throw new Error('Upload directory not set');
157
143
  }
158
144
 
159
- const fullPath = pathModule.join(FileService.instance.directory, fileFormat, tag, fileName);
145
+ const fullPath = pathModule.join(this.directory, fileFormat, tag, fileName);
160
146
 
161
- return { fileName, fullPath, fileFormat };
147
+ return {
148
+ fileName,
149
+ fullPath,
150
+ fileFormat,
151
+ };
162
152
  }
163
153
 
164
154
  /**
165
- * @hidden
166
- *
167
- * Stores a file, removes the temporary file, and saves metadata to database
155
+ * Stores a file on disc and creates metadata in database
168
156
  * @param {StoreFileOptions} options - File storage options
169
- * @returns {Promise<IFile>} Promise resolving to stored file document
157
+ * @returns {Promise<IFile>} The created file document
170
158
  * @throws {Error} If upload directory is not set or storage fails
171
159
  * @example
172
160
  * ```typescript
@@ -190,12 +178,30 @@ class FileService {
190
178
  throw new Error('Upload directory has not been set');
191
179
  }
192
180
 
181
+ const fileType = file.mimetype || file.type || 'unknown/unknown';
182
+ const filePath = file.filepath || file.path;
183
+ const fileName = file.originalFilename || file.name || 'unknown';
184
+
185
+ if (!filePath) {
186
+ throw new Error('File path is missing');
187
+ }
188
+
193
189
  let storedFile: StoredFileDetail;
194
190
 
195
191
  return new Promise(async (done, reject) => {
196
- storedFile = FileService.instance.createStoredDetail(file.type, tag);
192
+ storedFile = FileService.instance.createStoredDetail(fileType, tag);
193
+
194
+ // Ensure destination directory exists
195
+ const destDir = pathModule.dirname(storedFile.fullPath);
196
+ try {
197
+ if (!fs.existsSync(destDir)) {
198
+ fs.mkdirSync(destDir, { recursive: true });
199
+ }
200
+ } catch (err) {
201
+ return reject(err);
202
+ }
197
203
 
198
- fs.copyFile(file.path, storedFile.fullPath, (err: Error | null) => {
204
+ fs.copyFile(filePath, storedFile.fullPath, (err: Error | null) => {
199
205
  if (err) {
200
206
  reject(err);
201
207
  } else {
@@ -204,7 +210,13 @@ class FileService {
204
210
 
205
211
  // remove temp file
206
212
  if (removeFileAfterStore) {
207
- fs.unlinkSync(file.path);
213
+ try {
214
+ if (fs.existsSync(filePath)) {
215
+ fs.unlinkSync(filePath);
216
+ }
217
+ } catch (e) {
218
+ console.warn('Failed to remove temp file:', e);
219
+ }
208
220
  }
209
221
  });
210
222
  })
@@ -218,174 +230,131 @@ class FileService {
218
230
 
219
231
  const data = {
220
232
  owner: ownerId,
233
+ tag,
234
+ originalName: fileName,
221
235
  fileName: storedFile.fileName,
222
- originalName: file.name,
223
236
  format: storedFile.fileFormat,
224
- tag,
225
237
  size: file.size,
226
238
  };
227
239
 
228
- // Create new document
229
- const doc = new CollectionModel(data);
230
-
231
- return doc.save().then(savedDoc => {
232
- triggerService.call('insert-one', 'cms', 'file', {
233
- query: null,
234
- queryResult: savedDoc,
235
- });
236
-
237
- return savedDoc;
238
- });
240
+ return CollectionModel.create(data);
239
241
  })
240
- .catch(err => {
241
- // remove stored file
242
- fs.unlinkSync(storedFile.fullPath);
243
-
244
- throw err;
245
- });
246
- }
247
-
248
- /**
249
- * @hidden
250
- *
251
- * Removes a file from the disk
252
- * @param {string} path - File path to remove
253
- * @returns {Promise<void>} Promise resolving when file is removed
254
- * @throws {Error} If file removal fails
255
- * @example
256
- * ```typescript
257
- * import { fileService } from '@modular-rest/server';
258
- *
259
- * await fileService.removeFromDisc('/uploads/jpeg/profile/1234567890.jpeg');
260
- * ```
261
- */
262
- removeFromDisc(path: string): Promise<void> {
263
- return new Promise((done, reject) => {
264
- fs.unlink(path, (err: Error | null) => {
265
- if (err) reject(err);
266
- else done();
242
+ .then(async (doc: IFile) => {
243
+ triggerService.call('insert-one', 'cms', 'file', { queryResult: doc });
244
+ return doc;
267
245
  });
268
- });
269
246
  }
270
247
 
271
248
  /**
272
- * Removes a file from both database and disk
273
- *
274
- * @param {string} fileId - File ID to remove
275
- * @returns {Promise<void>} Promise resolving when file is removed
276
- * @throws {Error} If file is not found or removal fails
249
+ * Deletes a file from disc and database
250
+ * @param {string} fileId - ID of the file to delete
251
+ * @returns {Promise<boolean>} True if deletion was successful
252
+ * @throws {Error} If file is not found or deletion fails
277
253
  * @example
278
254
  * ```typescript
279
255
  * import { fileService } from '@modular-rest/server';
280
256
  *
281
- * try {
282
- * await fileService.removeFile('file123');
283
- * console.log('File removed successfully');
284
- * } catch (error) {
285
- * console.error('Failed to remove file:', error);
286
- * }
257
+ * await fileService.removeFile('file123');
287
258
  * ```
288
259
  */
289
- removeFile(fileId: string): Promise<void> {
260
+ async removeFile(fileId: string): Promise<boolean> {
290
261
  if (!FileService.instance.directory) {
291
262
  throw new Error('Upload directory has not been set');
292
263
  }
293
264
 
294
- return new Promise(async (done, reject) => {
295
- const CollectionModel = DataProvider.getCollection<IFile>('cms', 'file');
265
+ const CollectionModel = DataProvider.getCollection<IFile>('cms', 'file');
296
266
 
297
- if (!CollectionModel) {
298
- return reject(new Error('Collection model not found'));
299
- }
267
+ if (!CollectionModel) {
268
+ throw new Error('Collection model not found');
269
+ }
300
270
 
301
- const fileDoc = await CollectionModel.findOne({ _id: fileId }).exec();
271
+ const doc = await CollectionModel.findById(fileId);
302
272
 
303
- if (!fileDoc) {
304
- return reject(new Error('File not found'));
273
+ if (!doc) {
274
+ throw new Error('File not found');
275
+ }
276
+
277
+ const filePath = pathModule.join(
278
+ FileService.instance.directory as string,
279
+ doc.format,
280
+ doc.tag,
281
+ doc.fileName
282
+ );
283
+
284
+ if (fs.existsSync(filePath)) {
285
+ try {
286
+ await FileService.instance.removeFromDisc(filePath);
287
+ } catch (err: any) {
288
+ // If the file is not found on disc, we can still proceed with deleting metadata
289
+ if (err.code !== 'ENOENT') {
290
+ throw err;
291
+ }
305
292
  }
293
+ }
294
+
295
+ await CollectionModel.findByIdAndDelete(fileId);
296
+ triggerService.call('remove-one', 'cms', 'file', { queryResult: doc });
297
+ return true;
298
+ }
306
299
 
307
- await CollectionModel.deleteOne({ _id: fileId })
308
- .exec()
309
- .then(() => {
310
- // create file path
311
- const filePath = pathModule.join(
312
- FileService.instance.directory as string,
313
- fileDoc.format,
314
- fileDoc.tag,
315
- fileDoc.fileName
316
- );
317
-
318
- // Remove file from disc
319
- return FileService.instance.removeFromDisc(filePath).catch(async err => {
320
- // Recreate fileDoc if removing file operation has error
321
- await new CollectionModel(fileDoc).save();
322
-
323
- throw err;
324
- });
325
- })
326
- .then(() => {
327
- triggerService.call('remove-one', 'cms', 'file', {
328
- query: { _id: fileId },
329
- queryResult: null,
330
- });
331
- })
332
- .then(done)
333
- .catch(reject);
300
+ /**
301
+ * Deletes a file from physical storage
302
+ * @param {string} path - Physical path to the file
303
+ * @returns {Promise<boolean>} True if deletion was successful
304
+ * @hidden
305
+ */
306
+ removeFromDisc(path: string): Promise<boolean> {
307
+ return new Promise((done, reject) => {
308
+ fs.unlink(path, err => {
309
+ if (err) {
310
+ reject(err);
311
+ } else {
312
+ done(true);
313
+ }
314
+ });
334
315
  });
335
316
  }
336
317
 
337
318
  /**
338
- * Retrieves a file document from the database
339
- *
340
- * @param {string} fileId - File ID to retrieve
341
- * @returns {Promise<IFile>} Promise resolving to file document
342
- * @throws {Error} If collection model is not found or file is not found
343
- * @example
344
- * ```typescript
345
- * import { fileService } from '@modular-rest/server';
346
- *
347
- * const fileDoc = await fileService.getFile('file123');
348
- * console.log('File details:', fileDoc);
349
- * ```
319
+ * Retrieves a file document from database
320
+ * @param {string} fileId - ID of the file
321
+ * @returns {Promise<IFile>} The file document
322
+ * @throws {Error} If file is not found
323
+ * @hidden
350
324
  */
351
- getFile(fileId: string): Promise<IFile> {
325
+ async getFile(fileId: string): Promise<IFile> {
352
326
  const CollectionModel = DataProvider.getCollection<IFile>('cms', 'file');
353
327
 
354
328
  if (!CollectionModel) {
355
329
  throw new Error('Collection model not found');
356
330
  }
357
331
 
358
- return CollectionModel.findOne({ _id: fileId })
359
- .exec()
360
- .then(doc => {
361
- if (!doc) {
362
- throw new Error('File not found');
363
- }
364
- return doc;
365
- });
332
+ const doc = await CollectionModel.findById(fileId);
333
+
334
+ if (!doc) {
335
+ throw new Error('File not found');
336
+ }
337
+
338
+ return doc;
366
339
  }
367
340
 
368
341
  /**
369
- * Retrieves the public URL link for a file
370
- *
371
- * @param {string} fileId - File ID to get link for
372
- * @returns {Promise<string>} Promise resolving to file URL
373
- * @throws {Error} If URL path is not defined or file is not found
342
+ * Gets the public URL for a file
343
+ * @param {string} fileId - ID of the file
344
+ * @returns {Promise<string>} The public URL
374
345
  * @example
375
346
  * ```typescript
376
347
  * import { fileService } from '@modular-rest/server';
377
348
  *
378
- * const link = await fileService.getFileLink('file123');
379
- * // Returns: '/uploads/jpeg/profile/1234567890.jpeg'
349
+ * const url = await fileService.getFileLink('file123');
350
+ * // Returns: '/assets/jpeg/profile/1234567890.jpeg'
380
351
  * ```
381
352
  */
382
353
  async getFileLink(fileId: string): Promise<string> {
383
354
  const fileDoc = await FileService.instance.getFile(fileId);
384
355
 
385
356
  if (!FileService.instance.urlPath) {
386
- throw new Error(
387
- 'Upload directory URL path is not defined. Please configure uploadDirectoryConfig with a urlPath property.'
388
- );
357
+ throw new Error('Upload directory config has not been set');
389
358
  }
390
359
 
391
360
  const link = `${FileService.instance.urlPath}/${fileDoc.format}/${fileDoc.tag}/${fileDoc.fileName}`;
@@ -394,11 +363,9 @@ class FileService {
394
363
  }
395
364
 
396
365
  /**
397
- * Gets the full filesystem path for a file
398
- *
399
- * @param {string} fileId - File ID to get path for
400
- * @returns {Promise<string>} Promise resolving to full file path
401
- * @throws {Error} If upload directory is not set or file is not found
366
+ * Gets the physical path for a file
367
+ * @param {string} fileId - ID of the file
368
+ * @returns {Promise<string>} The physical path
402
369
  * @example
403
370
  * ```typescript
404
371
  * import { fileService } from '@modular-rest/server';
@@ -423,3 +390,4 @@ class FileService {
423
390
  * @constant {FileService}
424
391
  */
425
392
  export const main = new FileService();
393
+ FileService.instance = main;
@@ -93,19 +93,19 @@ export interface DefinedFunction {
93
93
  */
94
94
  export function defineFunction(options: DefinedFunction): DefinedFunction {
95
95
  // Check if the function already exists
96
- const existingFunction = functions.find(f => f.name === name);
96
+ const existingFunction = functions.find(f => f.name === options.name);
97
97
  if (existingFunction) {
98
- throw new Error(`Function with name ${name} already exists`);
98
+ throw new Error(`Function with name ${options.name} already exists`);
99
99
  }
100
100
 
101
101
  // Check if the permission types provided
102
102
  if (!options.permissionTypes || !options.permissionTypes.length) {
103
- throw new Error(`Permission types not provided for function ${name}`);
103
+ throw new Error(`Permission types not provided for function ${options.name}`);
104
104
  }
105
105
 
106
106
  // Check if the callback is a function
107
107
  if (typeof options.callback !== 'function') {
108
- throw new Error(`Callback is not a function for function ${name}`);
108
+ throw new Error(`Callback is not a function for function ${options.name}`);
109
109
  }
110
110
 
111
111
  // Add the function to the list of functions
@@ -3,6 +3,7 @@ import { validateObject } from '../../class/validator';
3
3
  import { create as reply } from '../../class/reply';
4
4
  import { Context } from 'koa';
5
5
  import * as service from './service';
6
+ import * as userManager from '../user_manager/service';
6
7
 
7
8
  const name = 'verify';
8
9
  const verify = new Router();
@@ -53,7 +54,7 @@ verify.post('/checkAccess', async (ctx: Context) => {
53
54
 
54
55
  const userid = payload.id;
55
56
 
56
- await (global as any).services.userManager.main
57
+ await userManager.main
57
58
  .getUserById(userid)
58
59
  .then((user: any) => {
59
60
  const key = user.hasPermission(body.permissionField);
@@ -3,6 +3,7 @@ import { validateObject } from '../../class/validator';
3
3
  import { create as reply } from '../../class/reply';
4
4
  import { Context } from 'koa';
5
5
  import * as service from './service';
6
+ import * as dataProvider from '../data_provider/service';
6
7
 
7
8
  const name = 'user';
8
9
  const userManager = new Router();
@@ -178,7 +179,6 @@ userManager.post('/getPermission', async (ctx: Context) => {
178
179
 
179
180
  const query = { _id: body.id };
180
181
 
181
- const dataProvider = (global as any).services.dataProvider;
182
182
  const permission = await dataProvider
183
183
  .getCollection('cms', 'permission')
184
184
  .findOne(query)
@@ -470,13 +470,21 @@ class UserManager {
470
470
  throw new Error('Invalid verification code');
471
471
  }
472
472
 
473
+ const tempId = this.tempIds[id];
474
+ const idType = tempId ? tempId.type : 'email';
475
+
473
476
  const userModel = DataProvider.getCollection('cms', 'auth');
474
477
 
475
478
  if (!userModel) {
476
479
  throw new Error('User model not found');
477
480
  }
478
481
 
479
- const query = { email: id };
482
+ const query: Record<string, any> = {};
483
+ if (idType === 'phone') {
484
+ query.phone = id;
485
+ } else {
486
+ query.email = id;
487
+ }
480
488
 
481
489
  // Get from database
482
490
  let gottenFromDB;
@@ -486,25 +494,30 @@ class UserManager {
486
494
  throw error;
487
495
  }
488
496
 
489
- if (!gottenFromDB) {
490
- throw new Error('User not found');
491
- }
492
-
493
497
  try {
494
- // Load user
495
- const user = await User.loadFromModel(gottenFromDB);
496
-
497
- // Update password
498
- user.password = password;
499
-
500
- // Save to database
501
- await user.save();
498
+ let token: string;
502
499
 
503
- // Get token payload
504
- const payload = user.getBrief();
500
+ if (!gottenFromDB) {
501
+ // Registration flow: create new user
502
+ const registrationData: any = {
503
+ password,
504
+ type: 'user',
505
+ };
506
+ if (idType === 'phone') {
507
+ registrationData.phone = id;
508
+ } else {
509
+ registrationData.email = id;
510
+ }
511
+ token = await this.registerUser(registrationData);
512
+ } else {
513
+ // Password reset flow: update existing user
514
+ const user = await User.loadFromModel(gottenFromDB);
515
+ user.password = password;
516
+ await user.save();
505
517
 
506
- // Generate json web token
507
- const token = await JWT.main.sign(payload);
518
+ const payload = user.getBrief();
519
+ token = await JWT.main.sign(payload);
520
+ }
508
521
 
509
522
  // Remove temporary ID
510
523
  delete this.tempIds[id];
@@ -543,13 +556,21 @@ class UserManager {
543
556
  throw new Error('Invalid verification code');
544
557
  }
545
558
 
559
+ const tempId = this.tempIds[id];
560
+ const idType = tempId ? tempId.type : 'email';
561
+
546
562
  const userModel = DataProvider.getCollection('cms', 'auth');
547
563
 
548
564
  if (!userModel) {
549
565
  throw new Error('User model not found');
550
566
  }
551
567
 
552
- const query = { email: id };
568
+ const query: Record<string, any> = {};
569
+ if (idType === 'phone') {
570
+ query.phone = id;
571
+ } else {
572
+ query.email = id;
573
+ }
553
574
 
554
575
  // Get from database
555
576
  let gottenFromDB;
@@ -619,8 +640,8 @@ class UserManager {
619
640
  }
620
641
 
621
642
  try {
622
- // Create user document
623
- const userDoc = await userModel.create({
643
+ // Create user document with timeout
644
+ const createPromise = userModel.create({
624
645
  ...detail,
625
646
  type: detail.type || 'user',
626
647
  permissionGroup: detail.permissionGroup || getDefaultPermissionGroups().title,
@@ -629,6 +650,14 @@ class UserManager {
629
650
  password: detail.password || undefined,
630
651
  });
631
652
 
653
+ // Add timeout wrapper to prevent hanging
654
+ const userDoc = await Promise.race([
655
+ createPromise,
656
+ new Promise<never>((_, reject) =>
657
+ setTimeout(() => reject(new Error('User creation timeout after 10s')), 10000)
658
+ ),
659
+ ]);
660
+
632
661
  // Load user from document
633
662
  const user = await User.loadFromModel(userDoc);
634
663