exp-node-server 1.1.7 → 2.1.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/README.md +162 -20
- package/dist/cli.js +613 -0
- package/package.json +29 -9
- package/templates/mongoose/.dockerignore +10 -0
- package/templates/mongoose/.env.example +56 -7
- package/templates/mongoose/.prettierignore +4 -0
- package/templates/mongoose/.prettierrc +7 -0
- package/templates/mongoose/Dockerfile +33 -0
- package/templates/mongoose/_gitignore +35 -0
- package/templates/mongoose/docker-compose.yml +21 -0
- package/templates/mongoose/eslint.config.js +35 -0
- package/templates/mongoose/package-lock.json +6366 -0
- package/templates/mongoose/package.json +49 -11
- package/templates/mongoose/readme.md +88 -0
- package/templates/mongoose/src/app/configs/index.ts +178 -26
- package/templates/mongoose/src/app/errors/zodError.ts +16 -16
- package/templates/mongoose/src/app/lib/db.ts +18 -0
- package/templates/mongoose/src/app/lib/logger.ts +12 -0
- package/templates/mongoose/src/app/middlewares/auth.ts +48 -44
- package/templates/mongoose/src/app/middlewares/global_error_handler.ts +59 -65
- package/templates/mongoose/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/mongoose/src/app/middlewares/rate_limiter.ts +153 -0
- package/templates/mongoose/src/app/middlewares/request_validator.ts +11 -10
- package/templates/mongoose/src/app/middlewares/uploader.ts +17 -13
- package/templates/mongoose/src/app/modules/account/account.controller.ts +156 -0
- package/templates/mongoose/src/app/modules/account/account.interface.ts +65 -0
- package/templates/mongoose/src/app/modules/account/account.route.ts +101 -0
- package/templates/mongoose/src/app/modules/account/account.schema.ts +97 -0
- package/templates/mongoose/src/app/modules/account/account.service.ts +422 -0
- package/templates/mongoose/src/app/modules/account/account.swagger.ts +278 -0
- package/templates/mongoose/src/app/modules/account/account.validation.ts +63 -0
- package/templates/mongoose/src/app/modules/profile/profile.controller.ts +17 -0
- package/templates/mongoose/src/app/modules/profile/profile.interface.ts +7 -0
- package/templates/mongoose/src/app/modules/profile/profile.route.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.schema.ts +13 -0
- package/templates/mongoose/src/app/modules/profile/profile.service.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.swagger.ts +34 -0
- package/templates/mongoose/src/app/modules/profile/profile.validation.ts +8 -0
- package/templates/mongoose/src/app/passport-js/index.ts +87 -0
- package/templates/mongoose/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/mongoose/src/app/passport-js/providers.ts +258 -0
- package/templates/mongoose/src/app/passport-js/social-account.ts +141 -0
- package/templates/mongoose/src/app/queues/agenda.ts +36 -0
- package/templates/mongoose/src/app/queues/email.queue.ts +81 -0
- package/templates/mongoose/src/app/queues/index.ts +17 -0
- package/templates/mongoose/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/mongoose/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/mongoose/src/app/types/error.d.ts +7 -7
- package/templates/mongoose/src/app/types/express.d.ts +11 -0
- package/templates/mongoose/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/mongoose/src/app/utils/JWT.ts +38 -18
- package/templates/mongoose/src/app/utils/app_error.ts +10 -10
- package/templates/mongoose/src/app/utils/auth_cookie.ts +27 -0
- package/templates/mongoose/src/app/utils/catch_async.ts +8 -8
- package/templates/mongoose/src/app/utils/cloudinary.ts +26 -59
- package/templates/mongoose/src/app/utils/mail_sender.ts +26 -92
- package/templates/mongoose/src/app/utils/manage_response.ts +19 -19
- package/templates/mongoose/src/app/utils/otp.ts +20 -0
- package/templates/mongoose/src/app/utils/session.ts +55 -0
- package/templates/mongoose/src/app.ts +73 -42
- package/templates/mongoose/src/routes.ts +13 -13
- package/templates/mongoose/src/server.ts +38 -18
- package/templates/mongoose/src/swaggerOptions.ts +23 -23
- package/templates/mongoose/tests/app.test.ts +97 -0
- package/templates/mongoose/tests/global-setup.ts +15 -0
- package/templates/mongoose/tests/helpers/fixtures.ts +42 -0
- package/templates/mongoose/tests/queues/email.queue.test.ts +137 -0
- package/templates/mongoose/tests/routes/account.test.ts +614 -0
- package/templates/mongoose/tests/routes/profile.test.ts +95 -0
- package/templates/mongoose/tests/routes/social.test.ts +310 -0
- package/templates/mongoose/tests/setup.ts +25 -0
- package/templates/mongoose/tsconfig.json +16 -20
- package/templates/mongoose/vitest.config.ts +43 -0
- package/templates/prisma/.dockerignore +2 -0
- package/templates/prisma/.env.example +62 -6
- package/templates/prisma/.prettierignore +4 -0
- package/templates/prisma/.prettierrc +7 -0
- package/templates/prisma/.projectrc.json +1 -1
- package/templates/prisma/Dockerfile +21 -7
- package/templates/prisma/_gitignore +38 -0
- package/templates/prisma/docker-compose.yml +8 -7
- package/templates/prisma/eslint.config.js +35 -0
- package/templates/prisma/package-lock.json +7316 -0
- package/templates/prisma/package.json +47 -10
- package/templates/prisma/prisma/schema/account.schema.prisma +92 -10
- package/templates/prisma/prisma/schema/profile.schema.prisma +8 -3
- package/templates/prisma/prisma/schema/schema.prisma +1 -2
- package/templates/prisma/prisma.config.ts +5 -5
- package/templates/prisma/readme.md +90 -0
- package/templates/prisma/src/app/configs/index.ts +175 -26
- package/templates/prisma/src/app/errors/zodError.ts +16 -16
- package/templates/prisma/src/app/lib/logger.ts +12 -0
- package/templates/prisma/src/app/lib/prisma.ts +4 -6
- package/templates/prisma/src/app/middlewares/auth.ts +37 -16
- package/templates/prisma/src/app/middlewares/global_error_handler.ts +42 -35
- package/templates/prisma/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/prisma/src/app/middlewares/rate_limiter.ts +124 -0
- package/templates/prisma/src/app/middlewares/request_validator.ts +4 -3
- package/templates/prisma/src/app/middlewares/uploader.ts +17 -13
- package/templates/prisma/src/app/modules/account/account.controller.ts +74 -34
- package/templates/prisma/src/app/modules/account/account.route.ts +68 -22
- package/templates/prisma/src/app/modules/account/account.service.ts +329 -309
- package/templates/prisma/src/app/modules/account/account.swagger.ts +167 -76
- package/templates/prisma/src/app/modules/account/account.validation.ts +41 -19
- package/templates/prisma/src/app/modules/profile/profile.controller.ts +4 -4
- package/templates/prisma/src/app/modules/profile/profile.route.ts +18 -12
- package/templates/prisma/src/app/modules/profile/profile.service.ts +11 -18
- package/templates/prisma/src/app/modules/profile/profile.swagger.ts +11 -11
- package/templates/prisma/src/app/modules/profile/profile.validation.ts +2 -2
- package/templates/prisma/src/app/passport-js/index.ts +87 -0
- package/templates/prisma/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/prisma/src/app/passport-js/providers.ts +263 -0
- package/templates/prisma/src/app/passport-js/social-account.ts +147 -0
- package/templates/prisma/src/app/queues/boss.ts +33 -0
- package/templates/prisma/src/app/queues/email.queue.ts +71 -0
- package/templates/prisma/src/app/queues/index.ts +23 -0
- package/templates/prisma/src/app/queues/maintenance.queue.ts +30 -0
- package/templates/prisma/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/prisma/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/prisma/src/app/types/error.d.ts +7 -7
- package/templates/prisma/src/app/types/express.d.ts +11 -0
- package/templates/prisma/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/prisma/src/app/utils/JWT.ts +36 -17
- package/templates/prisma/src/app/utils/app_error.ts +10 -10
- package/templates/prisma/src/app/utils/auth_cookie.ts +27 -0
- package/templates/prisma/src/app/utils/catch_async.ts +8 -8
- package/templates/prisma/src/app/utils/cloudinary.ts +26 -59
- package/templates/prisma/src/app/utils/mail_sender.ts +26 -92
- package/templates/prisma/src/app/utils/manage_response.ts +19 -19
- package/templates/prisma/src/app/utils/otp.ts +20 -0
- package/templates/prisma/src/app/utils/session.ts +57 -0
- package/templates/prisma/src/app.ts +73 -42
- package/templates/prisma/src/routes.ts +12 -10
- package/templates/prisma/src/server.ts +37 -19
- package/templates/prisma/src/swaggerOptions.ts +22 -26
- package/templates/prisma/tests/app.test.ts +86 -0
- package/templates/prisma/tests/helpers/fixtures.ts +35 -0
- package/templates/prisma/tests/helpers/prisma.ts +32 -0
- package/templates/prisma/tests/queues/email.queue.test.ts +162 -0
- package/templates/prisma/tests/routes/account.test.ts +627 -0
- package/templates/prisma/tests/routes/profile.test.ts +91 -0
- package/templates/prisma/tests/routes/social.test.ts +317 -0
- package/templates/prisma/tests/setup.ts +10 -0
- package/templates/prisma/tsconfig.json +16 -23
- package/templates/prisma/vitest.config.ts +38 -0
- package/bin/cli.js +0 -41
- package/src/createProject.js +0 -61
- package/src/generateModule.js +0 -93
- package/src/utils/getProjectType.js +0 -13
- package/src/utils/mongoose/controllerGenerator.js +0 -70
- package/src/utils/mongoose/routeGenerator.js +0 -27
- package/src/utils/mongoose/schemaGenerator.js +0 -11
- package/src/utils/mongoose/serviceGenerator.js +0 -49
- package/src/utils/mongoose/swaggerGenerator.js +0 -112
- package/src/utils/mongoose/validationGenerator.js +0 -13
- package/src/utils/prisma/prismaSchemaGenerator.js +0 -10
- package/src/utils/prisma/prismaServiceGenerator.js +0 -47
- package/src/utils/routeInjector.js +0 -40
- package/src/utils/swaggerInjector.js +0 -52
- package/src/utils/validateModuleName.js +0 -17
- package/src/utils/validateProjectName.js +0 -15
- package/src/utils.js +0 -26
- package/templates/mongoose/src/app/errors/castError.ts +0 -23
- package/templates/mongoose/src/app/errors/duplicateError.ts +0 -26
- package/templates/mongoose/src/app/errors/validationError.ts +0 -25
- package/templates/mongoose/src/app/modules/auth/auth.controller.ts +0 -127
- package/templates/mongoose/src/app/modules/auth/auth.interface.ts +0 -24
- package/templates/mongoose/src/app/modules/auth/auth.route.ts +0 -46
- package/templates/mongoose/src/app/modules/auth/auth.schema.ts +0 -19
- package/templates/mongoose/src/app/modules/auth/auth.service.ts +0 -324
- package/templates/mongoose/src/app/modules/auth/auth.swagger.ts +0 -214
- package/templates/mongoose/src/app/modules/auth/auth.validation.ts +0 -37
- package/templates/mongoose/src/app/modules/user/user.controller.ts +0 -17
- package/templates/mongoose/src/app/modules/user/user.interface.ts +0 -15
- package/templates/mongoose/src/app/modules/user/user.route.ts +0 -20
- package/templates/mongoose/src/app/modules/user/user.schema.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.service.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.swagger.ts +0 -75
- package/templates/mongoose/src/app/modules/user/user.validation.ts +0 -18
- package/templates/mongoose/src/app/types/index.d.ts +0 -12
- package/templates/mongoose/src/app/utils/isAccountExist.ts +0 -31
- package/templates/prisma/src/app/types/index.d.ts +0 -12
- package/templates/prisma/src/app/utils/otpGenerator.ts +0 -29
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import bcrypt from 'bcrypt';
|
|
2
|
+
import { configs } from '../../src/app/configs/index.js';
|
|
3
|
+
import type { T_account } from '../../src/app/modules/account/account.interface.js';
|
|
4
|
+
import { account_model } from '../../src/app/modules/account/account.schema.js';
|
|
5
|
+
import { profile_model } from '../../src/app/modules/profile/profile.schema.js';
|
|
6
|
+
import { jwtHelpers } from '../../src/app/utils/JWT.js';
|
|
7
|
+
|
|
8
|
+
export const PASSWORD = 'correct-horse-battery';
|
|
9
|
+
const passwordHash = bcrypt.hashSync(PASSWORD, 4);
|
|
10
|
+
|
|
11
|
+
/** Inserts a verified, active account with a password, plus its profile. */
|
|
12
|
+
export const createAccount = async (overrides: Partial<T_account> = {}) => {
|
|
13
|
+
const account = await account_model.create({
|
|
14
|
+
email: 'user@example.com',
|
|
15
|
+
password: passwordHash,
|
|
16
|
+
isAccountVerified: true,
|
|
17
|
+
...overrides,
|
|
18
|
+
});
|
|
19
|
+
await profile_model.create({ accountId: account._id, fullName: 'Test User' });
|
|
20
|
+
return account;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** The account as stored, including the fields that are hidden by default. */
|
|
24
|
+
export const findAccount = (email = 'user@example.com') =>
|
|
25
|
+
account_model.findOne({ email }).select('+password +lastOtp').lean();
|
|
26
|
+
|
|
27
|
+
type TokenSubject = Pick<T_account, 'email' | 'role' | 'tokenVersion'> & { _id: unknown };
|
|
28
|
+
|
|
29
|
+
/** A signed access token, as the app would issue it for `account`. */
|
|
30
|
+
export const accessTokenFor = (account: TokenSubject) =>
|
|
31
|
+
jwtHelpers.generateToken(
|
|
32
|
+
{
|
|
33
|
+
accountId: String(account._id),
|
|
34
|
+
email: account.email,
|
|
35
|
+
role: account.role,
|
|
36
|
+
tv: account.tokenVersion,
|
|
37
|
+
},
|
|
38
|
+
configs.jwt.access_token,
|
|
39
|
+
'1h',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const bearer = (account: TokenSubject) => `Bearer ${accessTokenFor(account)}`;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Agenda } from 'agenda';
|
|
2
|
+
import mongoose from 'mongoose';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { newAgenda } from '../../src/app/queues/agenda.js';
|
|
5
|
+
import { defineEmailJob, enqueueEmail, type EmailJob } from '../../src/app/queues/email.queue.js';
|
|
6
|
+
import { startQueues, stopQueues } from '../../src/app/queues/index.js';
|
|
7
|
+
import sendMail from '../../src/app/utils/mail_sender.js';
|
|
8
|
+
|
|
9
|
+
// the real queue (tests/setup.ts mocks it for the route tests); only SMTP is faked
|
|
10
|
+
vi.unmock('../../src/app/queues/email.queue.js');
|
|
11
|
+
vi.mock('../../src/app/utils/mail_sender.js', () => ({ default: vi.fn() }));
|
|
12
|
+
|
|
13
|
+
const send = vi.mocked(sendMail);
|
|
14
|
+
const FAST = { processEvery: 50, retryDelayMs: 50, maxRetries: 2 };
|
|
15
|
+
|
|
16
|
+
const email = (to: string): EmailJob => ({
|
|
17
|
+
type: 'reset-password',
|
|
18
|
+
email: to,
|
|
19
|
+
resetLink: `https://app.test/reset?token=${to}`,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const jobs = () => mongoose.connection.db!.collection('agendaJobs');
|
|
23
|
+
|
|
24
|
+
/** Polls until `check` passes or `timeoutMs` is over. */
|
|
25
|
+
const waitFor = async (check: () => unknown | Promise<unknown>, timeoutMs = 5000) => {
|
|
26
|
+
const start = Date.now();
|
|
27
|
+
for (;;) {
|
|
28
|
+
try {
|
|
29
|
+
await check();
|
|
30
|
+
return;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (Date.now() - start > timeoutMs) throw err;
|
|
33
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Lets workers run for a while, to show that nothing else happens. */
|
|
39
|
+
const settle = (ms = 400) => new Promise((r) => setTimeout(r, ms));
|
|
40
|
+
|
|
41
|
+
const extraWorkers: Agenda[] = [];
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
await Promise.all(extraWorkers.splice(0).map((w) => w.stop()));
|
|
44
|
+
await stopQueues().catch(() => {});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('email queue (Agenda)', () => {
|
|
48
|
+
it('sends a queued email once, and finished jobs expire', async () => {
|
|
49
|
+
send.mockResolvedValue({} as never);
|
|
50
|
+
await startQueues(FAST);
|
|
51
|
+
|
|
52
|
+
await enqueueEmail(email('a@test.com'));
|
|
53
|
+
|
|
54
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
55
|
+
expect(send).toHaveBeenCalledWith(
|
|
56
|
+
expect.objectContaining({ to: 'a@test.com', subject: 'Reset Your Password' }),
|
|
57
|
+
);
|
|
58
|
+
// jobs carry reset links and OTPs: MongoDB deletes finished email jobs after an hour
|
|
59
|
+
await waitFor(async () =>
|
|
60
|
+
expect(await jobs().findOne({ name: 'email' })).toHaveProperty('lastFinishedAt'),
|
|
61
|
+
);
|
|
62
|
+
const ttl = (await jobs().indexes()).find((i) => i.name === 'email_jobs_ttl');
|
|
63
|
+
expect(ttl).toMatchObject({
|
|
64
|
+
expireAfterSeconds: 3600,
|
|
65
|
+
partialFilterExpression: { name: 'email' },
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('retries a failed send', async () => {
|
|
70
|
+
send.mockRejectedValueOnce(new Error('SMTP down')).mockResolvedValue({} as never);
|
|
71
|
+
await startQueues(FAST);
|
|
72
|
+
|
|
73
|
+
await enqueueEmail(email('retry@test.com'));
|
|
74
|
+
|
|
75
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('gives up after the retry limit', async () => {
|
|
79
|
+
send.mockRejectedValue(new Error('SMTP down'));
|
|
80
|
+
await startQueues(FAST);
|
|
81
|
+
|
|
82
|
+
await enqueueEmail(email('broken@test.com'));
|
|
83
|
+
|
|
84
|
+
// the first attempt + 2 retries, and then nothing more
|
|
85
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(3));
|
|
86
|
+
await settle();
|
|
87
|
+
expect(send).toHaveBeenCalledTimes(3);
|
|
88
|
+
const [job] = await jobs().find().toArray();
|
|
89
|
+
expect(job).toMatchObject({ failReason: 'SMTP down' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('never sends the same email twice, even with several workers', async () => {
|
|
93
|
+
send.mockResolvedValue({} as never);
|
|
94
|
+
await startQueues(FAST);
|
|
95
|
+
const second = newAgenda(FAST);
|
|
96
|
+
defineEmailJob(second, FAST);
|
|
97
|
+
extraWorkers.push(second);
|
|
98
|
+
await second.start();
|
|
99
|
+
|
|
100
|
+
const recipients = ['1', '2', '3', '4', '5', '6'].map((n) => `${n}@test.com`);
|
|
101
|
+
await Promise.all(recipients.map((to) => enqueueEmail(email(to))));
|
|
102
|
+
|
|
103
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(recipients.length));
|
|
104
|
+
await settle();
|
|
105
|
+
const sentTo = send.mock.calls.map(([mail]) => mail.to).sort();
|
|
106
|
+
expect(sentTo).toEqual(recipients.sort());
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('sends the emails that were queued while no worker was running', async () => {
|
|
110
|
+
send.mockResolvedValue({} as never);
|
|
111
|
+
await startQueues(FAST);
|
|
112
|
+
await stopQueues();
|
|
113
|
+
await enqueueEmail(email('queued@test.com'));
|
|
114
|
+
await settle();
|
|
115
|
+
expect(send).not.toHaveBeenCalled();
|
|
116
|
+
|
|
117
|
+
await startQueues(FAST); // e.g. after a deploy
|
|
118
|
+
|
|
119
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('waits for a running send to finish before shutting down', async () => {
|
|
123
|
+
let finished = false;
|
|
124
|
+
send.mockImplementation(async () => {
|
|
125
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
126
|
+
finished = true;
|
|
127
|
+
return {} as never;
|
|
128
|
+
});
|
|
129
|
+
await startQueues(FAST);
|
|
130
|
+
await enqueueEmail(email('slow@test.com'));
|
|
131
|
+
await waitFor(() => expect(send).toHaveBeenCalled());
|
|
132
|
+
|
|
133
|
+
await stopQueues();
|
|
134
|
+
|
|
135
|
+
expect(finished).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|