@adaptivestone/framework 5.0.0-beta.1 → 5.0.0-beta.10
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/CHANGELOG.md +39 -1
- package/Cli.js +3 -4
- package/cluster.js +0 -2
- package/commands/CreateUser.js +31 -0
- package/commands/DropIndex.js +14 -5
- package/commands/GenerateRandomBytes.js +2 -0
- package/commands/GetOpenApiJson.js +13 -2
- package/commands/SyncIndexes.js +1 -0
- package/commands/migration/Create.js +21 -10
- package/commands/migration/Migrate.js +1 -0
- package/controllers/Auth.js +20 -29
- package/eslint.config.js +68 -0
- package/folderConfig.js +0 -1
- package/helpers/files.js +1 -4
- package/helpers/logger.js +0 -1
- package/helpers/yup.js +2 -4
- package/jsconfig.json +4 -4
- package/models/Lock.js +107 -0
- package/models/User.js +23 -6
- package/modules/AbstractCommand.js +25 -1
- package/modules/AbstractController.js +0 -7
- package/modules/AbstractModel.js +21 -9
- package/modules/Base.js +3 -2
- package/modules/BaseCli.js +82 -9
- package/package.json +20 -16
- package/server.d.ts +9 -7
- package/server.js +44 -10
- package/services/cache/Cache.d.ts +1 -1
- package/services/cache/Cache.js +3 -3
- package/services/http/HttpServer.js +6 -13
- package/services/http/middleware/AbstractMiddleware.js +3 -3
- package/services/http/middleware/I18n.js +1 -1
- package/services/http/middleware/Pagination.js +4 -4
- package/services/http/middleware/RateLimiter.js +2 -2
- package/services/messaging/email/templates/.gitkeep +0 -0
- package/services/validate/ValidateService.js +4 -4
- package/services/validate/drivers/AbstractValidator.js +2 -2
- package/services/validate/drivers/CustomValidator.js +2 -2
- package/services/validate/drivers/YupValidator.js +3 -3
- package/tests/setup.js +8 -6
- package/tests/setupVitest.js +9 -7
- package/types/ICommandArguments.d.ts +41 -0
- package/types/TFoldersConfig.d.ts +7 -4
- package/vitest.config.js +4 -3
- package/config/mail.js +0 -29
- package/services/messaging/email/index.js +0 -217
- package/services/messaging/email/templates/emptyTemplate/html.pug +0 -9
- package/services/messaging/email/templates/emptyTemplate/subject.pug +0 -1
- package/services/messaging/email/templates/emptyTemplate/text.pug +0 -1
- package/services/messaging/index.js +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
### 5.0.0-beta.9
|
|
2
|
+
|
|
3
|
+
[BREAKING] move email module to separate package @adaptivestone/framework-module-email. Please use it if you want to send emails
|
|
4
|
+
[NEW] app now contains 'frameworkFolder' folder the framework located. Mostly for modules usage
|
|
5
|
+
[BREAKING] remove VIEWS folders at all. Should not afffect any user as this was not used internally
|
|
6
|
+
[UPDATE] update typing
|
|
7
|
+
[UPDATE] change redis -> @redis/client as we are using only client from pakage
|
|
8
|
+
[BREAKING] removed noidemailer-sendmail-transport. Not needed anymore and not recommended to use as well
|
|
9
|
+
|
|
10
|
+
### 5.0.0-beta.8
|
|
11
|
+
|
|
12
|
+
[UPDATE] update deps
|
|
13
|
+
[NEW] Lock model for working locks via mongoDB
|
|
14
|
+
|
|
15
|
+
### 5.0.0-beta.7
|
|
16
|
+
|
|
17
|
+
[UPDATE] update deps
|
|
18
|
+
[UPDATE] change vitest shutdown behavior as mongo driver v6.13 change befaviur that affect us (MongoClient.close now closes any outstanding cursors)
|
|
19
|
+
|
|
20
|
+
### 5.0.0-beta.5
|
|
21
|
+
|
|
22
|
+
[BREAKING] remove minimist CLI parsing and replace it by commandArguments parser
|
|
23
|
+
[UPDATE] migrated from eslint-plugin-import to eslint-plugin-import-x
|
|
24
|
+
|
|
25
|
+
### 5.0.0-beta.5
|
|
26
|
+
|
|
27
|
+
[UPDATE] migrate to eslint 9 and away from aibnb styles (they are abonded)
|
|
28
|
+
|
|
29
|
+
### 5.0.0-beta.4
|
|
30
|
+
|
|
31
|
+
[NEW] on shutdown event now after timeout we are forcing to shutdown
|
|
32
|
+
|
|
33
|
+
### 5.0.0-beta.2
|
|
34
|
+
|
|
35
|
+
[UPDATE] update deps
|
|
36
|
+
[NEW] add ability to skip mongo model init in CLI env
|
|
37
|
+
[NEW] now each mongo connection on CLI have own name and inslude command name there too (getMongoConnectionName in command)
|
|
38
|
+
|
|
1
39
|
### 5.0.0-beta.1
|
|
2
40
|
|
|
3
41
|
[UPDATE] update deps
|
|
@@ -452,7 +490,7 @@ await doc.populate([
|
|
|
452
490
|
[UPDATE] update deps
|
|
453
491
|
[UPDATE] winston console transport now using timestapms
|
|
454
492
|
[UPDATE] PrepareAppInfo middleware now a global one. Do not need to include it on every controller
|
|
455
|
-
[NEW] Request
|
|
493
|
+
[NEW] Request also works with req.query, but req.body have bigger priority
|
|
456
494
|
|
|
457
495
|
### 2.18.0
|
|
458
496
|
|
package/Cli.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import parseArgs from 'minimist';
|
|
2
1
|
import mongoose from 'mongoose';
|
|
3
2
|
import BaseCli from './modules/BaseCli.js';
|
|
4
3
|
import Server from './server.js';
|
|
@@ -8,14 +7,14 @@ class Cli extends BaseCli {
|
|
|
8
7
|
mongoose.set('autoIndex', false); // we do not need create indexes on CLI.
|
|
9
8
|
const server = new Server(serverConfig);
|
|
10
9
|
super(server);
|
|
11
|
-
this.args = parseArgs(process.argv.slice(3));
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
async run() {
|
|
15
|
-
await this.server.init();
|
|
13
|
+
await this.server.init({ isSkipModelInit: true, isSkipModelLoading: true });
|
|
16
14
|
const command = process.argv[2]?.toLowerCase();
|
|
17
|
-
await super.run(command
|
|
15
|
+
await super.run(command);
|
|
18
16
|
this.app.events.emit('shutdown');
|
|
17
|
+
return true;
|
|
19
18
|
}
|
|
20
19
|
}
|
|
21
20
|
|
package/cluster.js
CHANGED
|
@@ -4,7 +4,6 @@ import { cpus } from 'node:os';
|
|
|
4
4
|
const numCPUs = cpus().length;
|
|
5
5
|
|
|
6
6
|
if (cluster.isPrimary) {
|
|
7
|
-
// eslint-disable-next-line no-console
|
|
8
7
|
console.log(`Master ${process.pid} is running`);
|
|
9
8
|
// Fork workers.
|
|
10
9
|
for (let i = 0; i < numCPUs; i += 1) {
|
|
@@ -12,7 +11,6 @@ if (cluster.isPrimary) {
|
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
cluster.on('exit', (worker, code, signal) => {
|
|
15
|
-
// eslint-disable-next-line no-console
|
|
16
14
|
console.log(
|
|
17
15
|
`Worker \x1B[45m ${
|
|
18
16
|
worker.process.pid
|
package/commands/CreateUser.js
CHANGED
|
@@ -6,6 +6,37 @@ class CreateUser extends AbstractCommand {
|
|
|
6
6
|
return 'Create user in a database';
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* You able to add command arguments for parsing there.
|
|
11
|
+
* @returns {import("../types/ICommandArguments.js").ICommandArguments}
|
|
12
|
+
*/
|
|
13
|
+
static get commandArguments() {
|
|
14
|
+
return {
|
|
15
|
+
id: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'User id to find user',
|
|
18
|
+
},
|
|
19
|
+
email: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'User id to find/create user',
|
|
22
|
+
},
|
|
23
|
+
password: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'New password for user',
|
|
26
|
+
},
|
|
27
|
+
roles: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description:
|
|
30
|
+
'User roles comma separated string (--roles=user,admin,someOtherRoles)',
|
|
31
|
+
},
|
|
32
|
+
update: {
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
default: false,
|
|
35
|
+
description: 'Update user if it exists',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
9
40
|
async run() {
|
|
10
41
|
const User = this.app.getModel('User');
|
|
11
42
|
const { id, email, password, roles, update } = this.args;
|
package/commands/DropIndex.js
CHANGED
|
@@ -5,12 +5,21 @@ class DropIndex extends AbstractCommand {
|
|
|
5
5
|
return 'Drop indexes of model';
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
/**
|
|
9
|
+
* You able to add command arguments for parsing there.
|
|
10
|
+
* @returns {import("../types/ICommandArguments.js").ICommandArguments}
|
|
11
|
+
*/
|
|
12
|
+
static get commandArguments() {
|
|
13
|
+
return {
|
|
14
|
+
model: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Model name',
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
13
21
|
|
|
22
|
+
async run() {
|
|
14
23
|
const Model = this.app.getModel(this.args.model);
|
|
15
24
|
|
|
16
25
|
if (!Model) {
|
|
@@ -8,6 +8,18 @@ class GetOpenApiJson extends AbstractCommand {
|
|
|
8
8
|
static get description() {
|
|
9
9
|
return 'Generate documentation (openApi) ';
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* You able to add command arguments for parsing there.
|
|
13
|
+
* @returns {import("../types/ICommandArguments.js").ICommandArguments}
|
|
14
|
+
*/
|
|
15
|
+
static get commandArguments() {
|
|
16
|
+
return {
|
|
17
|
+
output: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Output file path',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
11
23
|
|
|
12
24
|
async run() {
|
|
13
25
|
const { myDomain } = this.app.getConfig('http');
|
|
@@ -18,7 +30,7 @@ class GetOpenApiJson extends AbstractCommand {
|
|
|
18
30
|
|
|
19
31
|
try {
|
|
20
32
|
jsonFile = JSON.parse(await fs.readFile(jsonFile, 'utf8'));
|
|
21
|
-
} catch
|
|
33
|
+
} catch {
|
|
22
34
|
this.logger.error(
|
|
23
35
|
'No npm package detected. Please start this command via NPM as it depends on package.json',
|
|
24
36
|
);
|
|
@@ -123,7 +135,6 @@ class GetOpenApiJson extends AbstractCommand {
|
|
|
123
135
|
|
|
124
136
|
let routeName = Object.keys(route)[0];
|
|
125
137
|
if (routeName === '/') {
|
|
126
|
-
// eslint-disable-next-line no-continue
|
|
127
138
|
continue;
|
|
128
139
|
}
|
|
129
140
|
|
package/commands/SyncIndexes.js
CHANGED
|
@@ -7,28 +7,39 @@ class CreateMigration extends AbstractCommand {
|
|
|
7
7
|
return 'Create new migration';
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* You able to add command arguments for parsing there.
|
|
12
|
+
* @returns {import("../../types/ICommandArguments.js").ICommandArguments}
|
|
13
|
+
*/
|
|
14
|
+
static get commandArguments() {
|
|
15
|
+
return {
|
|
16
|
+
name: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Migration name',
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
async run() {
|
|
11
|
-
if (!this.args.name) {
|
|
12
|
-
return this.logger.error(
|
|
13
|
-
'Please provide migration name with key "--name={someName}"',
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
25
|
if (this.args.name.match(/^\d/)) {
|
|
17
|
-
|
|
26
|
+
this.logger.error('Command cant start from nubmer');
|
|
27
|
+
return false;
|
|
18
28
|
}
|
|
19
|
-
const fileName = `${Date.now()}_${
|
|
29
|
+
const fileName = `${Date.now()}_${CreateMigration.camelSentence(
|
|
20
30
|
this.args.name,
|
|
21
31
|
)}.js`;
|
|
22
32
|
|
|
23
|
-
const fileContent =
|
|
24
|
-
|
|
33
|
+
const fileContent = CreateMigration.getTemplate(
|
|
34
|
+
CreateMigration.camelSentence(this.args.name),
|
|
25
35
|
);
|
|
26
36
|
|
|
27
37
|
await fs.writeFile(
|
|
28
38
|
path.join(this.app.foldersConfig.migrations, fileName),
|
|
29
39
|
fileContent,
|
|
30
40
|
);
|
|
31
|
-
|
|
41
|
+
this.logger.info(`Migration created ${fileName}`);
|
|
42
|
+
return true;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
static camelSentence(str) {
|
package/controllers/Auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { object, string } from 'yup';
|
|
2
2
|
import AbstractController from '../modules/AbstractController.js';
|
|
3
3
|
import GetUserByToken from '../services/http/middleware/GetUserByToken.js';
|
|
4
4
|
import RateLimiter from '../services/http/middleware/RateLimiter.js';
|
|
@@ -9,58 +9,52 @@ class Auth extends AbstractController {
|
|
|
9
9
|
post: {
|
|
10
10
|
'/login': {
|
|
11
11
|
handler: this.postLogin,
|
|
12
|
-
request:
|
|
13
|
-
email:
|
|
14
|
-
password:
|
|
12
|
+
request: object().shape({
|
|
13
|
+
email: string().email().required('auth.emailProvided'), // if not provided then error will be generated
|
|
14
|
+
password: string().required('auth.passwordProvided'), // possible to provide values from translation
|
|
15
15
|
}),
|
|
16
16
|
},
|
|
17
17
|
'/register': {
|
|
18
18
|
handler: this.postRegister,
|
|
19
|
-
request:
|
|
20
|
-
email:
|
|
21
|
-
.string()
|
|
19
|
+
request: object().shape({
|
|
20
|
+
email: string()
|
|
22
21
|
.email('auth.emailValid')
|
|
23
22
|
.required('auth.emailProvided'),
|
|
24
|
-
password:
|
|
25
|
-
.string()
|
|
23
|
+
password: string()
|
|
26
24
|
.matches(
|
|
27
25
|
/^[a-zA-Z0-9!@#$%ˆ^&*()_+\-{}[\]<>]+$/,
|
|
28
26
|
'auth.passwordValid',
|
|
29
27
|
)
|
|
30
28
|
.required('auth.passwordProvided'),
|
|
31
|
-
nickName:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
nickName: string().matches(
|
|
30
|
+
/^[a-zA-Z0-9_\-.]+$/,
|
|
31
|
+
'auth.nickNameValid',
|
|
32
|
+
),
|
|
33
|
+
firstName: string(),
|
|
34
|
+
lastName: string(),
|
|
36
35
|
}),
|
|
37
36
|
},
|
|
38
37
|
'/logout': this.postLogout,
|
|
39
38
|
'/verify': this.verifyUser,
|
|
40
39
|
'/send-recovery-email': {
|
|
41
40
|
handler: this.sendPasswordRecoveryEmail,
|
|
42
|
-
request:
|
|
43
|
-
.object()
|
|
44
|
-
.shape({ email: yup.string().email().required() }),
|
|
41
|
+
request: object().shape({ email: string().email().required() }),
|
|
45
42
|
},
|
|
46
43
|
'/recover-password': {
|
|
47
44
|
handler: this.recoverPassword,
|
|
48
|
-
request:
|
|
49
|
-
password:
|
|
50
|
-
.string()
|
|
45
|
+
request: object().shape({
|
|
46
|
+
password: string()
|
|
51
47
|
.matches(
|
|
52
48
|
/^[a-zA-Z0-9!@#$%ˆ^&*()_+\-{}[\]<>]+$/,
|
|
53
49
|
'auth.passwordValid',
|
|
54
50
|
)
|
|
55
51
|
.required(),
|
|
56
|
-
passwordRecoveryToken:
|
|
52
|
+
passwordRecoveryToken: string().required(),
|
|
57
53
|
}),
|
|
58
54
|
},
|
|
59
55
|
'/send-verification': {
|
|
60
56
|
handler: this.sendVerification,
|
|
61
|
-
request:
|
|
62
|
-
.object()
|
|
63
|
-
.shape({ email: yup.string().email().required() }),
|
|
57
|
+
request: object().shape({ email: string().email().required() }),
|
|
64
58
|
},
|
|
65
59
|
},
|
|
66
60
|
};
|
|
@@ -113,12 +107,9 @@ class Auth extends AbstractController {
|
|
|
113
107
|
|
|
114
108
|
const { isAuthWithVefificationFlow } = this.app.getConfig('auth');
|
|
115
109
|
if (isAuthWithVefificationFlow) {
|
|
116
|
-
|
|
110
|
+
await user.sendVerificationEmail(req.i18n).catch((e) => {
|
|
117
111
|
this.logger.error(e);
|
|
118
112
|
});
|
|
119
|
-
if (!answer) {
|
|
120
|
-
return res.status(500).json();
|
|
121
|
-
}
|
|
122
113
|
}
|
|
123
114
|
return res.status(201).json();
|
|
124
115
|
}
|
|
@@ -136,7 +127,7 @@ class Auth extends AbstractController {
|
|
|
136
127
|
user = await User.getUserByVerificationToken(
|
|
137
128
|
req.query.verification_token,
|
|
138
129
|
);
|
|
139
|
-
} catch
|
|
130
|
+
} catch {
|
|
140
131
|
return res.status(400).json({
|
|
141
132
|
message: req.i18n.t('email.alreadyVerifiedOrWrongToken'),
|
|
142
133
|
});
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import globals from 'globals';
|
|
2
|
+
import pluginJs from '@eslint/js';
|
|
3
|
+
import importPlugin from 'eslint-plugin-import-x';
|
|
4
|
+
import vitest from '@vitest/eslint-plugin';
|
|
5
|
+
import eslintConfigPrettier from 'eslint-config-prettier';
|
|
6
|
+
import prettierPlugin from 'eslint-plugin-prettier/recommended';
|
|
7
|
+
|
|
8
|
+
/** @type {import('eslint').Linter.Config[]} */
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
export default [
|
|
11
|
+
pluginJs.configs.recommended,
|
|
12
|
+
importPlugin.flatConfigs.recommended,
|
|
13
|
+
eslintConfigPrettier,
|
|
14
|
+
prettierPlugin,
|
|
15
|
+
{
|
|
16
|
+
languageOptions: {
|
|
17
|
+
sourceType: 'module',
|
|
18
|
+
ecmaVersion: 'latest',
|
|
19
|
+
globals: {
|
|
20
|
+
...globals.es2023,
|
|
21
|
+
...globals.node,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
rules: {
|
|
27
|
+
'no-await-in-loop': 'error',
|
|
28
|
+
'no-param-reassign': 'error',
|
|
29
|
+
'class-methods-use-this': 'error',
|
|
30
|
+
'no-shadow': 'error',
|
|
31
|
+
'prefer-const': 'error',
|
|
32
|
+
'import-x/no-extraneous-dependencies': ['error'],
|
|
33
|
+
'import-x/first': ['error'],
|
|
34
|
+
camelcase: ['error', { properties: 'never', ignoreDestructuring: false }],
|
|
35
|
+
'prefer-destructuring': [
|
|
36
|
+
'error',
|
|
37
|
+
{
|
|
38
|
+
VariableDeclarator: {
|
|
39
|
+
array: false,
|
|
40
|
+
object: true,
|
|
41
|
+
},
|
|
42
|
+
AssignmentExpression: {
|
|
43
|
+
array: true,
|
|
44
|
+
object: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
enforceForRenamedProperties: false,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
'no-plusplus': 'error',
|
|
52
|
+
'consistent-return': 'error',
|
|
53
|
+
'no-return-await': 'error',
|
|
54
|
+
'arrow-body-style': 'error',
|
|
55
|
+
'dot-notation': 'error',
|
|
56
|
+
curly: 'error',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
files: ['**/*.test.js'],
|
|
61
|
+
plugins: {
|
|
62
|
+
vitest,
|
|
63
|
+
},
|
|
64
|
+
rules: {
|
|
65
|
+
...vitest.configs.recommended.rules,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
];
|
package/folderConfig.js
CHANGED
|
@@ -5,7 +5,6 @@ export default {
|
|
|
5
5
|
config: path.resolve('./config'),
|
|
6
6
|
models: path.resolve('./models'),
|
|
7
7
|
controllers: path.resolve('./controllers'),
|
|
8
|
-
views: path.resolve('./views'),
|
|
9
8
|
locales: path.resolve('./locales'),
|
|
10
9
|
emails: path.resolve('./services/messaging/email/templates'),
|
|
11
10
|
commands: path.resolve('./commands'),
|
package/helpers/files.js
CHANGED
package/helpers/logger.js
CHANGED
package/helpers/yup.js
CHANGED
|
@@ -13,12 +13,10 @@ class YupFile extends Schema {
|
|
|
13
13
|
constructor() {
|
|
14
14
|
super({
|
|
15
15
|
type: 'file',
|
|
16
|
+
// @ts-ignore
|
|
16
17
|
check: (value) => value.every((item) => item instanceof PersistentFile),
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export {
|
|
22
|
-
// eslint-disable-next-line import/prefer-default-export
|
|
23
|
-
YupFile,
|
|
24
|
-
};
|
|
22
|
+
export { YupFile };
|
package/jsconfig.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"module": "
|
|
4
|
-
"target": "
|
|
5
|
-
"moduleResolution": "
|
|
3
|
+
"module": "NodeNext",
|
|
4
|
+
"target": "ES2024",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
6
|
"checkJs": true
|
|
7
7
|
},
|
|
8
|
-
"exclude": ["node_modules"]
|
|
8
|
+
"exclude": ["node_modules", "coverage"]
|
|
9
9
|
}
|
package/models/Lock.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import AbstractModel from '../modules/AbstractModel.js';
|
|
2
|
+
|
|
3
|
+
class Lock extends AbstractModel {
|
|
4
|
+
initHooks() {
|
|
5
|
+
this.mongooseSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line class-methods-use-this
|
|
9
|
+
get modelSchema() {
|
|
10
|
+
return {
|
|
11
|
+
_id: { type: String, required: true },
|
|
12
|
+
expiredAt: {
|
|
13
|
+
type: Date,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* acquire lock based on lock name
|
|
20
|
+
* @param {string} name
|
|
21
|
+
* @param {number} [ttlSeconds=30]
|
|
22
|
+
* @returns {Promise<boolean>}
|
|
23
|
+
*/
|
|
24
|
+
static async acquireLock(name, ttlSeconds = 30) {
|
|
25
|
+
try {
|
|
26
|
+
await this.create({
|
|
27
|
+
_id: name,
|
|
28
|
+
expiredAt: new Date(Date.now() + ttlSeconds * 1000),
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error.code !== 11000) {
|
|
32
|
+
// not a duplicate leys
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* release lock based on lock name
|
|
42
|
+
* @param {string} name
|
|
43
|
+
* @returns {Promise<boolean>}
|
|
44
|
+
*/
|
|
45
|
+
static async releaseLock(name) {
|
|
46
|
+
const res = await this.deleteOne({ _id: name });
|
|
47
|
+
if (res.acknowledged && res.deletedCount) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* wait lock based on lock name
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @returns {Promise}
|
|
57
|
+
*/
|
|
58
|
+
static async waitForUnlock(name) {
|
|
59
|
+
const res = await this.findOne({ _id: name });
|
|
60
|
+
if (!res) {
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const stream = this.watch([
|
|
66
|
+
{ $match: { operationType: 'delete', 'documentKey._id': name } },
|
|
67
|
+
]);
|
|
68
|
+
stream.on('change', () => {
|
|
69
|
+
stream.close();
|
|
70
|
+
resolve();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* get lock remaining time based on lock name
|
|
77
|
+
* @param {string} name
|
|
78
|
+
* @returns {Promise<{ttl: number}>}
|
|
79
|
+
*/
|
|
80
|
+
static async getLockData(name) {
|
|
81
|
+
const res = await this.findOne({ _id: name });
|
|
82
|
+
if (!res) {
|
|
83
|
+
return { ttl: 0 };
|
|
84
|
+
}
|
|
85
|
+
return { ttl: res.expiredAt.getTime() - Date.now() };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* get lock remaining time based on lock name
|
|
90
|
+
* @param {string[]} names
|
|
91
|
+
* @returns {Promise<{name: string, ttl: number}[]>}
|
|
92
|
+
*/
|
|
93
|
+
static async getLocksData(names) {
|
|
94
|
+
const res = await this.find({ _id: { $in: names } });
|
|
95
|
+
const lockMap = new Map(res.map((lock) => [lock._id, lock]));
|
|
96
|
+
|
|
97
|
+
return names.map((name) => {
|
|
98
|
+
const lock = lockMap.get(name);
|
|
99
|
+
return {
|
|
100
|
+
name,
|
|
101
|
+
ttl: lock ? lock.expiredAt.getTime() - Date.now() : 0,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default Lock;
|
package/models/User.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-param-reassign */
|
|
2
|
-
|
|
3
1
|
import { scrypt } from 'node:crypto';
|
|
4
2
|
|
|
5
3
|
import { promisify } from 'node:util';
|
|
@@ -16,6 +14,7 @@ class User extends AbstractModel {
|
|
|
16
14
|
initHooks() {
|
|
17
15
|
this.mongooseSchema.pre('save', async function userPreSaveHook() {
|
|
18
16
|
if (this.isModified('password')) {
|
|
17
|
+
// @ts-ignore
|
|
19
18
|
this.password = await this.constructor.hashPassword(this.password);
|
|
20
19
|
}
|
|
21
20
|
});
|
|
@@ -174,9 +173,18 @@ class User extends AbstractModel {
|
|
|
174
173
|
async sendPasswordRecoveryEmail(i18n) {
|
|
175
174
|
const passwordRecoveryToken =
|
|
176
175
|
await User.generateUserPasswordRecoveryToken(this);
|
|
176
|
+
let Mailer;
|
|
177
177
|
// speed optimisation
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
try {
|
|
179
|
+
// @ts-ignore
|
|
180
|
+
// eslint-disable-next-line import-x/no-unresolved
|
|
181
|
+
Mailer = (await import('@adaptivestone/framework-module-email')).default;
|
|
182
|
+
} catch {
|
|
183
|
+
const error =
|
|
184
|
+
'Mailer not found. Please install @adaptivestone/framework-module-email in order to use it';
|
|
185
|
+
this.getSuper().logger.error(error);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
180
188
|
|
|
181
189
|
const mail = new Mailer(
|
|
182
190
|
this.getSuper().app,
|
|
@@ -245,8 +253,17 @@ class User extends AbstractModel {
|
|
|
245
253
|
async sendVerificationEmail(i18n) {
|
|
246
254
|
const verificationToken = await User.generateUserVerificationToken(this);
|
|
247
255
|
// speed optimisation
|
|
248
|
-
|
|
249
|
-
|
|
256
|
+
let Mailer;
|
|
257
|
+
try {
|
|
258
|
+
// @ts-ignore
|
|
259
|
+
// eslint-disable-next-line import-x/no-unresolved
|
|
260
|
+
Mailer = (await import('@adaptivestone/framework-module-email')).default;
|
|
261
|
+
} catch {
|
|
262
|
+
const error =
|
|
263
|
+
'Mailer not found. Please install @adaptivestone/framework-module-email in order to use it';
|
|
264
|
+
this.getSuper().logger.error(error);
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
250
267
|
const mail = new Mailer(
|
|
251
268
|
this.getSuper().app,
|
|
252
269
|
'verification',
|