saas-backend-kit 1.0.1 → 1.0.2

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/jest.config.js ADDED
@@ -0,0 +1,19 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/tests'],
5
+ testMatch: ['**/*.test.ts'],
6
+ collectCoverageFrom: [
7
+ 'src/**/*.ts',
8
+ '!src/**/*.d.ts'
9
+ ],
10
+ coverageDirectory: 'coverage',
11
+ verbose: true,
12
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
13
+ transform: {
14
+ '^.+\\.tsx?$': ['ts-jest', {
15
+ tsconfig: 'tsconfig.test.json',
16
+ diagnostics: false
17
+ }]
18
+ }
19
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saas-backend-kit",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Production-grade modular backend toolkit for building scalable SaaS applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -55,8 +55,8 @@
55
55
  "scripts": {
56
56
  "build": "tsup && node copy-dts.js",
57
57
  "dev": "tsup --watch",
58
- "test": "vitest run",
59
- "test:watch": "vitest",
58
+ "test": "jest",
59
+ "test:watch": "jest --watch",
60
60
  "lint": "eslint src --ext .ts",
61
61
  "typecheck": "tsc --noEmit",
62
62
  "prepublishOnly": "npm run build"
@@ -84,10 +84,10 @@
84
84
  "bcryptjs": "^2.4.3",
85
85
  "bullmq": "^5.1.0",
86
86
  "express": "^4.18.2",
87
- "fastify": "^4.25.0",
87
+ "fastify": "^5.0.0",
88
88
  "ioredis": "^5.3.2",
89
89
  "jsonwebtoken": "^9.0.2",
90
- "nodemailer": "^6.9.8",
90
+ "nodemailer": "^7.0.0",
91
91
  "pino": "^8.17.2",
92
92
  "pino-pretty": "^10.3.1",
93
93
  "twilio": "^4.20.1",
@@ -96,13 +96,15 @@
96
96
  "devDependencies": {
97
97
  "@types/bcryptjs": "^2.4.6",
98
98
  "@types/express": "^4.17.21",
99
+ "@types/jest": "^29.5.11",
99
100
  "@types/jsonwebtoken": "^9.0.5",
100
101
  "@types/node": "^20.10.6",
101
102
  "@types/nodemailer": "^6.4.14",
102
103
  "eslint": "^8.56.0",
104
+ "jest": "^29.7.0",
105
+ "ts-jest": "^29.1.1",
103
106
  "tsup": "^8.0.1",
104
- "typescript": "^5.3.3",
105
- "vitest": "^1.1.3"
107
+ "typescript": "^5.3.3"
106
108
  },
107
109
  "peerDependencies": {
108
110
  "express": "^4.0.0",
package/src/auth/jwt.ts CHANGED
@@ -39,7 +39,7 @@ export class JWTService {
39
39
  }
40
40
 
41
41
  refreshTokens(refreshToken: string): TokenPair {
42
- const payload = this.verifyRefreshToken(refreshToken);
42
+ const { iat, exp, nbf, ...payload } = this.verifyRefreshToken(refreshToken) as JWTPayload & { iat?: number; exp?: number; nbf?: number };
43
43
  return this.generateTokenPair(payload);
44
44
  }
45
45
  }
@@ -22,6 +22,7 @@ class InMemoryRateLimiter {
22
22
 
23
23
  constructor() {
24
24
  this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
25
+ this.cleanupInterval.unref();
25
26
  }
26
27
 
27
28
  private cleanup(): void {
@@ -13,7 +13,8 @@ export interface ApiResponse<T = unknown> {
13
13
  };
14
14
  }
15
15
 
16
- export interface PaginatedResponse<T> extends ApiResponse<T> {
16
+ export interface PaginatedResponse<T> extends Omit<ApiResponse<T[]>, 'data'> {
17
+ data: T[];
17
18
  meta: {
18
19
  page: number;
19
20
  limit: number;
@@ -141,57 +142,65 @@ declare global {
141
142
  }
142
143
  }
143
144
 
144
- Response.prototype.success = function <T>(data?: T, message?: string, statusCode: number = 200) {
145
- return ResponseHelper.success(this, data, message, statusCode);
146
- };
145
+ try {
146
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
147
+ const proto = require('express').response;
148
+ if (proto) {
149
+ proto.success = function <T>(this: Response, data?: T, message?: string, statusCode: number = 200) {
150
+ return ResponseHelper.success(this, data, message, statusCode);
151
+ };
147
152
 
148
- Response.prototype.created = function <T>(data?: T, message?: string) {
149
- return ResponseHelper.created(this, data, message);
150
- };
153
+ proto.created = function <T>(this: Response, data?: T, message?: string) {
154
+ return ResponseHelper.created(this, data, message);
155
+ };
151
156
 
152
- Response.prototype.updated = function <T>(data?: T, message?: string) {
153
- return ResponseHelper.updated(this, data, message);
154
- };
157
+ proto.updated = function <T>(this: Response, data?: T, message?: string) {
158
+ return ResponseHelper.updated(this, data, message);
159
+ };
155
160
 
156
- Response.prototype.deleted = function (message?: string) {
157
- return ResponseHelper.deleted(this, message);
158
- };
161
+ proto.deleted = function (this: Response, message?: string) {
162
+ return ResponseHelper.deleted(this, message);
163
+ };
159
164
 
160
- Response.prototype.error = function (error: string, statusCode: number = 400, code?: string, details?: Record<string, unknown>) {
161
- return ResponseHelper.error(this, error, statusCode, code, details);
162
- };
165
+ proto.error = function (this: Response, error: string, statusCode: number = 400, code?: string, details?: Record<string, unknown>) {
166
+ return ResponseHelper.error(this, error, statusCode, code, details);
167
+ };
163
168
 
164
- Response.prototype.badRequest = function (error?: string, code?: string) {
165
- return ResponseHelper.badRequest(this, error, code);
166
- };
169
+ proto.badRequest = function (this: Response, error?: string, code?: string) {
170
+ return ResponseHelper.badRequest(this, error, code);
171
+ };
167
172
 
168
- Response.prototype.unauthorized = function (error?: string, code?: string) {
169
- return ResponseHelper.unauthorized(this, error, code);
170
- };
173
+ proto.unauthorized = function (this: Response, error?: string, code?: string) {
174
+ return ResponseHelper.unauthorized(this, error, code);
175
+ };
171
176
 
172
- Response.prototype.forbidden = function (error?: string, code?: string) {
173
- return ResponseHelper.forbidden(this, error, code);
174
- };
177
+ proto.forbidden = function (this: Response, error?: string, code?: string) {
178
+ return ResponseHelper.forbidden(this, error, code);
179
+ };
175
180
 
176
- Response.prototype.notFound = function (error?: string, code?: string) {
177
- return ResponseHelper.notFound(this, error, code);
178
- };
181
+ proto.notFound = function (this: Response, error?: string, code?: string) {
182
+ return ResponseHelper.notFound(this, error, code);
183
+ };
179
184
 
180
- Response.prototype.conflict = function (error?: string, code?: string) {
181
- return ResponseHelper.conflict(this, error, code);
182
- };
185
+ proto.conflict = function (this: Response, error?: string, code?: string) {
186
+ return ResponseHelper.conflict(this, error, code);
187
+ };
183
188
 
184
- Response.prototype.validationError = function (error: string, details?: Record<string, unknown>) {
185
- return ResponseHelper.validationError(this, error, details);
186
- };
189
+ proto.validationError = function (this: Response, error: string, details?: Record<string, unknown>) {
190
+ return ResponseHelper.validationError(this, error, details);
191
+ };
187
192
 
188
- Response.prototype.internalError = function (error?: string) {
189
- return ResponseHelper.internalError(this, error);
190
- };
193
+ proto.internalError = function (this: Response, error?: string) {
194
+ return ResponseHelper.internalError(this, error);
195
+ };
191
196
 
192
- Response.prototype.paginated = function <T>(data: T[], page: number, limit: number, total: number) {
193
- return ResponseHelper.paginated(this, data, page, limit, total);
194
- };
197
+ proto.paginated = function <T>(this: Response, data: T[], page: number, limit: number, total: number) {
198
+ return ResponseHelper.paginated(this, data, page, limit, total);
199
+ };
200
+ }
201
+ } catch {
202
+ // express not available at runtime
203
+ }
195
204
 
196
205
  export const response = ResponseHelper;
197
206
  export default ResponseHelper;
@@ -0,0 +1,134 @@
1
+ describe('Auth Module', () => {
2
+ const mockUserStore = {
3
+ users: new Map(),
4
+
5
+ async findByEmail(email: string) {
6
+ for (const user of this.users.values()) {
7
+ if (user.email === email) return user;
8
+ }
9
+ return null;
10
+ },
11
+
12
+ async findById(id: string) {
13
+ return this.users.get(id) || null;
14
+ },
15
+
16
+ async create(data: any) {
17
+ const bcrypt = require('bcryptjs');
18
+ const id = Math.random().toString(36).substr(2, 9);
19
+ const hashedPassword = await bcrypt.hash(data.password, 10);
20
+ const user = { id, email: data.email, role: data.role || 'user', ...data, password: hashedPassword };
21
+ this.users.set(id, user);
22
+ return user;
23
+ },
24
+
25
+ async update(id: string, data: any) {
26
+ const user = this.users.get(id);
27
+ if (!user) throw new Error('User not found');
28
+ const updated = { ...user, ...data };
29
+ this.users.set(id, updated);
30
+ return updated;
31
+ }
32
+ };
33
+
34
+ test('should create auth service', () => {
35
+ const { createAuth } = require('../dist/auth');
36
+ const auth = createAuth({ jwtSecret: 'test-secret-key-that-is-at-least-32-chars' }, mockUserStore);
37
+
38
+ expect(auth).toBeDefined();
39
+ expect(auth.initialize).toBeDefined();
40
+ expect(auth.register).toBeDefined();
41
+ expect(auth.login).toBeDefined();
42
+ });
43
+
44
+ test('should register a new user', async () => {
45
+ const { createAuth } = require('../dist/auth');
46
+ const auth = createAuth({ jwtSecret: 'test-secret-key-that-is-at-least-32-chars' }, mockUserStore);
47
+
48
+ const result = await auth.register({
49
+ email: 'test@example.com',
50
+ password: 'password123',
51
+ name: 'Test User'
52
+ });
53
+
54
+ expect(result.user).toBeDefined();
55
+ expect(result.user.email).toBe('test@example.com');
56
+ expect(result.tokens).toHaveProperty('accessToken');
57
+ expect(result.tokens).toHaveProperty('refreshToken');
58
+ });
59
+
60
+ test('should login with valid credentials', async () => {
61
+ const { createAuth } = require('../dist/auth');
62
+ const auth = createAuth({ jwtSecret: 'test-secret-key-that-is-at-least-32-chars' }, mockUserStore);
63
+
64
+ await auth.register({
65
+ email: 'login@test.com',
66
+ password: 'password123'
67
+ });
68
+
69
+ const result = await auth.login({
70
+ email: 'login@test.com',
71
+ password: 'password123'
72
+ });
73
+
74
+ expect(result.user).toBeDefined();
75
+ expect(result.tokens).toHaveProperty('accessToken');
76
+ });
77
+
78
+ test('should fail login with invalid credentials', async () => {
79
+ const { createAuth } = require('../dist/auth');
80
+ const auth = createAuth({ jwtSecret: 'test-secret-key-that-is-at-least-32-chars' }, mockUserStore);
81
+
82
+ await auth.register({
83
+ email: 'fail@test.com',
84
+ password: 'password123'
85
+ });
86
+
87
+ await expect(auth.login({
88
+ email: 'fail@test.com',
89
+ password: 'wrongpassword'
90
+ })).rejects.toThrow('Invalid credentials');
91
+ });
92
+
93
+ test('should throw error for duplicate registration', async () => {
94
+ const { createAuth } = require('../dist/auth');
95
+ const auth = createAuth({ jwtSecret: 'test-secret-key-that-is-at-least-32-chars' }, mockUserStore);
96
+
97
+ await auth.register({
98
+ email: 'duplicate@test.com',
99
+ password: 'password123'
100
+ });
101
+
102
+ await expect(auth.register({
103
+ email: 'duplicate@test.com',
104
+ password: 'password123'
105
+ })).rejects.toThrow('User already exists');
106
+ });
107
+
108
+ test('should generate middleware', () => {
109
+ const { createAuth } = require('../dist/auth');
110
+ const auth = createAuth({ jwtSecret: 'test-secret-key-that-is-at-least-32-chars' }, mockUserStore);
111
+
112
+ const middleware = auth.getMiddleware();
113
+ expect(middleware).toBeDefined();
114
+ expect(typeof middleware).toBe('function');
115
+ });
116
+
117
+ test('should create requireUser middleware', () => {
118
+ const { createAuth } = require('../dist/auth');
119
+ const auth = createAuth({ jwtSecret: 'test-secret-key-that-is-at-least-32-chars' }, mockUserStore);
120
+
121
+ const middleware = auth.requireUser();
122
+ expect(middleware).toBeDefined();
123
+ expect(typeof middleware).toBe('function');
124
+ });
125
+
126
+ test('should create requireRole middleware', () => {
127
+ const { createAuth } = require('../dist/auth');
128
+ const auth = createAuth({ jwtSecret: 'test-secret-key-that-is-at-least-32-chars' }, mockUserStore);
129
+
130
+ const middleware = auth.requireRole('admin');
131
+ expect(middleware).toBeDefined();
132
+ expect(typeof middleware).toBe('function');
133
+ });
134
+ });
@@ -0,0 +1,36 @@
1
+ describe('Config Module', () => {
2
+ test('should load config with defaults', async () => {
3
+ const { config } = require('../dist/config');
4
+ const cfg = config.load();
5
+
6
+ expect(cfg.NODE_ENV).toBe('test');
7
+ expect(cfg.PORT).toBe('3000');
8
+ expect(cfg.REDIS_URL).toBe('redis://localhost:6379');
9
+ });
10
+
11
+ test('should get config value', async () => {
12
+ const { config } = require('../dist/config');
13
+ config.load();
14
+
15
+ const port = config.int('PORT');
16
+ expect(port).toBe(3000);
17
+ });
18
+
19
+ test('should check environment', async () => {
20
+ const { config } = require('../dist/config');
21
+ config.load();
22
+
23
+ expect(config.isTest()).toBe(true);
24
+ expect(config.isProduction()).toBe(false);
25
+ expect(config.isDevelopment()).toBe(false);
26
+ });
27
+
28
+ test('should get all config', async () => {
29
+ const { config } = require('../dist/config');
30
+ const cfg = config.getAll();
31
+
32
+ expect(cfg).toHaveProperty('NODE_ENV');
33
+ expect(cfg).toHaveProperty('PORT');
34
+ expect(cfg).toHaveProperty('AWS_REGION');
35
+ });
36
+ });
@@ -0,0 +1,47 @@
1
+ describe('Logger Module', () => {
2
+ test('should create logger', () => {
3
+ const { logger } = require('../dist/logger');
4
+
5
+ expect(logger).toBeDefined();
6
+ expect(logger.info).toBeDefined();
7
+ expect(logger.error).toBeDefined();
8
+ expect(logger.warn).toBeDefined();
9
+ expect(logger.debug).toBeDefined();
10
+ });
11
+
12
+ test('should log info message', () => {
13
+ const { logger } = require('../dist/logger');
14
+
15
+ expect(() => logger.info('test message')).not.toThrow();
16
+ });
17
+
18
+ test('should log error with metadata', () => {
19
+ const { logger } = require('../dist/logger');
20
+
21
+ expect(() => logger.error('error occurred', { code: 500 })).not.toThrow();
22
+ });
23
+
24
+ test('should create child logger', () => {
25
+ const { logger } = require('../dist/logger');
26
+
27
+ const child = logger.child({ module: 'auth' });
28
+ expect(child).toBeDefined();
29
+ expect(child.info).toBeDefined();
30
+ });
31
+
32
+ test('should create named logger', () => {
33
+ const { logger } = require('../dist/logger');
34
+
35
+ const named = logger.create({ name: 'test-logger' });
36
+ expect(named).toBeDefined();
37
+ expect(named.info).toBeDefined();
38
+ });
39
+
40
+ test('should get logger by name', () => {
41
+ const { logger } = require('../dist/logger');
42
+
43
+ logger.create({ name: 'custom-logger' });
44
+ const custom = logger.get('custom-logger');
45
+ expect(custom).toBeDefined();
46
+ });
47
+ });
@@ -0,0 +1,19 @@
1
+ describe('Notifications Module', () => {
2
+ test('should have notify object defined', () => {
3
+ const { notify } = require('../dist/notifications');
4
+
5
+ expect(notify).toBeDefined();
6
+ expect(notify.email).toBeDefined();
7
+ expect(notify.sms).toBeDefined();
8
+ expect(notify.webhook).toBeDefined();
9
+ expect(notify.slack).toBeDefined();
10
+ });
11
+
12
+ test('should have notification export', () => {
13
+ const { notification } = require('../dist/notifications');
14
+
15
+ expect(notification).toBeDefined();
16
+ expect(notification.email).toBeDefined();
17
+ expect(notification.sms).toBeDefined();
18
+ });
19
+ });
@@ -0,0 +1,50 @@
1
+ describe('Rate Limit Module', () => {
2
+ test('should create rate limiter middleware', () => {
3
+ const { rateLimit } = require('../src/rate-limit');
4
+
5
+ const middleware = rateLimit({ window: '1m', limit: 100 });
6
+ expect(middleware).toBeDefined();
7
+ expect(typeof middleware).toBe('function');
8
+ });
9
+
10
+ test('should create rate limiter with defaults', () => {
11
+ const { rateLimit } = require('../src/rate-limit');
12
+
13
+ const middleware = rateLimit();
14
+ expect(middleware).toBeDefined();
15
+ expect(typeof middleware).toBe('function');
16
+ });
17
+
18
+ test('should create custom rate limiter', () => {
19
+ const { createRateLimiter } = require('../src/rate-limit');
20
+
21
+ const limiter = createRateLimiter({ window: '1m', limit: 10 });
22
+ expect(limiter).toBeDefined();
23
+ expect(limiter.middleware).toBeDefined();
24
+ expect(limiter.destroy).toBeDefined();
25
+ });
26
+
27
+ test('should use custom key generator', () => {
28
+ const { rateLimit } = require('../src/rate-limit');
29
+
30
+ const middleware = rateLimit({
31
+ window: '1m',
32
+ limit: 100,
33
+ keyGenerator: (req: any) => req.userId || 'anonymous'
34
+ });
35
+
36
+ expect(middleware).toBeDefined();
37
+ });
38
+
39
+ test('should use custom skip function', () => {
40
+ const { rateLimit } = require('../src/rate-limit');
41
+
42
+ const middleware = rateLimit({
43
+ window: '1m',
44
+ limit: 100,
45
+ skip: (req: any) => req.skipRateLimit === true
46
+ });
47
+
48
+ expect(middleware).toBeDefined();
49
+ });
50
+ });
@@ -0,0 +1,33 @@
1
+ describe('Upload Module (S3)', () => {
2
+ test('should have upload functions defined', () => {
3
+ const { upload } = require('../dist/upload');
4
+
5
+ expect(upload).toBeDefined();
6
+ expect(upload.initialize).toBeDefined();
7
+ expect(upload.file).toBeDefined();
8
+ expect(upload.image).toBeDefined();
9
+ expect(upload.video).toBeDefined();
10
+ expect(upload.delete).toBeDefined();
11
+ expect(upload.getSignedUrl).toBeDefined();
12
+ expect(upload.getPublicUrl).toBeDefined();
13
+ });
14
+
15
+ test('should initialize S3 service', () => {
16
+ const { upload } = require('../dist/upload');
17
+
18
+ expect(() => {
19
+ upload.initialize({
20
+ region: 'us-east-1',
21
+ accessKeyId: 'test-key',
22
+ secretAccessKey: 'test-secret',
23
+ bucket: 'test-bucket'
24
+ });
25
+ }).not.toThrow();
26
+ });
27
+
28
+ test('should have s3Service defined', () => {
29
+ const { s3Service } = require('../dist/upload');
30
+
31
+ expect(s3Service).toBeDefined();
32
+ });
33
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "types": ["jest", "node"],
6
+ "composite": false,
7
+ "declaration": false,
8
+ "declarationMap": false,
9
+ "sourceMap": false,
10
+ "noUnusedLocals": false,
11
+ "noUnusedParameters": false
12
+ },
13
+ "include": ["src/**/*", "tests/**/*"]
14
+ }