saas-backend-kit 1.0.2 → 1.0.4
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 +170 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +1 -0
- package/dist/auth/index.mjs.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/index.mjs +1 -0
- package/dist/config/index.mjs.map +1 -1
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +73 -1
- package/dist/index.mjs.map +1 -1
- package/dist/logger/index.js +1 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/logger/index.mjs +1 -0
- package/dist/logger/index.mjs.map +1 -1
- package/dist/notifications/index.js +1 -0
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/index.mjs +1 -0
- package/dist/notifications/index.mjs.map +1 -1
- package/dist/queue/index.js +1 -0
- package/dist/queue/index.js.map +1 -1
- package/dist/queue/index.mjs +1 -0
- package/dist/queue/index.mjs.map +1 -1
- package/dist/rate-limit/index.js +1 -0
- package/dist/rate-limit/index.js.map +1 -1
- package/dist/rate-limit/index.mjs +1 -0
- package/dist/rate-limit/index.mjs.map +1 -1
- package/dist/upload/index.js +1 -0
- package/dist/upload/index.js.map +1 -1
- package/dist/upload/index.mjs +1 -0
- package/dist/upload/index.mjs.map +1 -1
- package/package.json +18 -3
- package/CHANGELOG.md +0 -31
- package/PUBLISHING.md +0 -133
- package/copy-dts.js +0 -314
- package/examples/express/.env.example +0 -41
- package/examples/express/app.ts +0 -203
- package/jest-output.json +0 -72
- package/jest.config.js +0 -19
- package/saas-banner.svg +0 -239
- package/src/auth/express.ts +0 -250
- package/src/auth/fastify.ts +0 -65
- package/src/auth/index.ts +0 -6
- package/src/auth/jwt.ts +0 -47
- package/src/auth/oauth.ts +0 -117
- package/src/auth/rbac.ts +0 -82
- package/src/auth/types.ts +0 -69
- package/src/config/index.ts +0 -125
- package/src/index.ts +0 -18
- package/src/logger/index.ts +0 -110
- package/src/notifications/index.ts +0 -262
- package/src/plugin.ts +0 -192
- package/src/queue/index.ts +0 -208
- package/src/rate-limit/express.ts +0 -145
- package/src/rate-limit/fastify.ts +0 -47
- package/src/rate-limit/index.ts +0 -2
- package/src/response/index.ts +0 -206
- package/src/upload/index.ts +0 -268
- package/src/utils/index.ts +0 -180
- package/tests/auth.test.ts +0 -134
- package/tests/config.test.ts +0 -36
- package/tests/logger.test.ts +0 -47
- package/tests/notifications.test.ts +0 -19
- package/tests/rate-limit.test.ts +0 -50
- package/tests/upload.test.ts +0 -33
- package/tsconfig.json +0 -30
- package/tsconfig.test.json +0 -14
- package/tsup.config.ts +0 -25
package/src/queue/index.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { Queue, Worker, Job, JobsOptions, WorkerOptions } from 'bullmq';
|
|
2
|
-
import { config } from '../config';
|
|
3
|
-
import { logger } from '../logger';
|
|
4
|
-
|
|
5
|
-
export interface QueueOptions {
|
|
6
|
-
name: string;
|
|
7
|
-
defaultJobOptions?: JobsOptions;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface RedisOptions {
|
|
11
|
-
host?: string;
|
|
12
|
-
port?: number;
|
|
13
|
-
password?: string;
|
|
14
|
-
db?: number;
|
|
15
|
-
url?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface JobData {
|
|
19
|
-
[key: string]: unknown;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type JobProcessor = (job: Job<JobData>) => Promise<unknown>;
|
|
23
|
-
|
|
24
|
-
class QueueManager {
|
|
25
|
-
private queues: Map<string, Queue> = new Map();
|
|
26
|
-
private workers: Map<string, Worker> = new Map();
|
|
27
|
-
private redisOptions: RedisOptions;
|
|
28
|
-
|
|
29
|
-
constructor() {
|
|
30
|
-
const redisUrl = config.get('REDIS_URL');
|
|
31
|
-
if (redisUrl) {
|
|
32
|
-
this.redisOptions = { url: redisUrl };
|
|
33
|
-
} else {
|
|
34
|
-
this.redisOptions = {
|
|
35
|
-
host: 'localhost',
|
|
36
|
-
port: 6379,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
setRedisOptions(options: RedisOptions): void {
|
|
42
|
-
this.redisOptions = options;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
createQueue(name: string, options?: Partial<QueueOptions>): Queue {
|
|
46
|
-
if (this.queues.has(name)) {
|
|
47
|
-
return this.queues.get(name)!;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const queue = new Queue(name, {
|
|
51
|
-
connection: this.redisOptions as any,
|
|
52
|
-
defaultJobOptions: options?.defaultJobOptions || {
|
|
53
|
-
removeOnComplete: 100,
|
|
54
|
-
removeOnFail: 100,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
this.queues.set(name, queue);
|
|
59
|
-
logger.info(`Queue "${name}" created`);
|
|
60
|
-
|
|
61
|
-
return queue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
getQueue(name: string): Queue | undefined {
|
|
65
|
-
return this.queues.get(name);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async addJob(
|
|
69
|
-
queueName: string,
|
|
70
|
-
jobName: string,
|
|
71
|
-
data: JobData,
|
|
72
|
-
options?: JobsOptions
|
|
73
|
-
): Promise<Job> {
|
|
74
|
-
const queue = this.getQueue(queueName) || this.createQueue(queueName);
|
|
75
|
-
const job = await queue.add(jobName, data, options);
|
|
76
|
-
logger.debug(`Job "${jobName}" added to queue "${queueName}"`, { jobId: job.id });
|
|
77
|
-
return job;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async addBulkJobs(
|
|
81
|
-
queueName: string,
|
|
82
|
-
jobs: Array<{ name: string; data: JobData; options?: JobsOptions }>
|
|
83
|
-
): Promise<Job[]> {
|
|
84
|
-
const queue = this.getQueue(queueName) || this.createQueue(queueName);
|
|
85
|
-
const bulkJobs = jobs.map(job => ({
|
|
86
|
-
name: job.name,
|
|
87
|
-
data: job.data,
|
|
88
|
-
...job.options,
|
|
89
|
-
}));
|
|
90
|
-
const result = await queue.addBulk(bulkJobs);
|
|
91
|
-
logger.debug(`${jobs.length} jobs added to queue "${queueName}"`);
|
|
92
|
-
return result;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
processJob(
|
|
96
|
-
queueName: string,
|
|
97
|
-
processor: JobProcessor,
|
|
98
|
-
options?: WorkerOptions
|
|
99
|
-
): Worker {
|
|
100
|
-
const queue = this.getQueue(queueName) || this.createQueue(queueName);
|
|
101
|
-
|
|
102
|
-
const worker = new Worker(queueName, async (job) => {
|
|
103
|
-
logger.debug(`Processing job "${job.name}"`, { jobId: job.id, queue: queueName });
|
|
104
|
-
return await processor(job);
|
|
105
|
-
}, {
|
|
106
|
-
connection: this.redisOptions as any,
|
|
107
|
-
concurrency: options?.concurrency || 1,
|
|
108
|
-
...options,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
worker.on('completed', (job) => {
|
|
112
|
-
logger.debug(`Job completed`, { jobId: job.id, queue: queueName });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
worker.on('failed', (job, err) => {
|
|
116
|
-
logger.error(`Job failed`, { jobId: job?.id, queue: queueName, error: err.message });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
worker.on('error', (err) => {
|
|
120
|
-
logger.error(`Worker error`, { queue: queueName, error: err.message });
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
this.workers.set(queueName, worker);
|
|
124
|
-
logger.info(`Worker started for queue "${queueName}"`);
|
|
125
|
-
|
|
126
|
-
return worker;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async getJobCounts(queueName: string): Promise<Record<string, number>> {
|
|
130
|
-
const queue = this.getQueue(queueName);
|
|
131
|
-
if (!queue) {
|
|
132
|
-
return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 };
|
|
133
|
-
}
|
|
134
|
-
return await queue.getJobCounts();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async getJobs(queueName: string, start: number = 0, end: number = 10): Promise<Job[]> {
|
|
138
|
-
const queue = this.getQueue(queueName);
|
|
139
|
-
if (!queue) return [];
|
|
140
|
-
return await queue.getJobs(['waiting', 'active', 'completed', 'failed'], start, end);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async closeQueue(name: string): Promise<void> {
|
|
144
|
-
const queue = this.queues.get(name);
|
|
145
|
-
if (queue) {
|
|
146
|
-
await queue.close();
|
|
147
|
-
this.queues.delete(name);
|
|
148
|
-
logger.info(`Queue "${name}" closed`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async closeWorker(name: string): Promise<void> {
|
|
153
|
-
const worker = this.workers.get(name);
|
|
154
|
-
if (worker) {
|
|
155
|
-
await worker.close();
|
|
156
|
-
this.workers.delete(name);
|
|
157
|
-
logger.info(`Worker for queue "${name}" closed`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async closeAll(): Promise<void> {
|
|
162
|
-
await Promise.all([
|
|
163
|
-
...Array.from(this.queues.values()).map(q => q.close()),
|
|
164
|
-
...Array.from(this.workers.values()).map(w => w.close()),
|
|
165
|
-
]);
|
|
166
|
-
this.queues.clear();
|
|
167
|
-
this.workers.clear();
|
|
168
|
-
logger.info('All queues and workers closed');
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const queueManager = new QueueManager();
|
|
173
|
-
|
|
174
|
-
export const createQueue = (name: string, options?: Partial<QueueOptions>): Queue => {
|
|
175
|
-
return queueManager.createQueue(name, options);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
export const addJob = (
|
|
179
|
-
queueName: string,
|
|
180
|
-
jobName: string,
|
|
181
|
-
data: JobData,
|
|
182
|
-
options?: JobsOptions
|
|
183
|
-
): Promise<Job> => {
|
|
184
|
-
return queueManager.addJob(queueName, jobName, data, options);
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
export const processJob = (
|
|
188
|
-
queueName: string,
|
|
189
|
-
processor: JobProcessor,
|
|
190
|
-
options?: WorkerOptions
|
|
191
|
-
): Worker => {
|
|
192
|
-
return queueManager.processJob(queueName, processor, options);
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
export const queue = {
|
|
196
|
-
create: createQueue,
|
|
197
|
-
add: addJob,
|
|
198
|
-
process: processJob,
|
|
199
|
-
get: (name: string) => queueManager.getQueue(name),
|
|
200
|
-
getJobCounts: (name: string) => queueManager.getJobCounts(name),
|
|
201
|
-
getJobs: (name: string, start?: number, end?: number) => queueManager.getJobs(name, start, end),
|
|
202
|
-
close: (name: string) => queueManager.closeQueue(name),
|
|
203
|
-
closeAll: () => queueManager.closeAll(),
|
|
204
|
-
setRedisOptions: (options: RedisOptions) => queueManager.setRedisOptions(options),
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
export { QueueManager };
|
|
208
|
-
export default queue;
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
-
import { config } from '../config';
|
|
3
|
-
|
|
4
|
-
export interface RateLimitOptions {
|
|
5
|
-
window?: string;
|
|
6
|
-
limit?: number;
|
|
7
|
-
keyGenerator?: (req: Request) => string;
|
|
8
|
-
handler?: (req: Request, res: Response) => void;
|
|
9
|
-
skipSuccessfulRequests?: boolean;
|
|
10
|
-
skipFailedRequests?: boolean;
|
|
11
|
-
skip?: (req: Request) => boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface RateLimitRecord {
|
|
15
|
-
count: number;
|
|
16
|
-
resetTime: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
class InMemoryRateLimiter {
|
|
20
|
-
private store: Map<string, RateLimitRecord> = new Map();
|
|
21
|
-
private cleanupInterval: NodeJS.Timeout;
|
|
22
|
-
|
|
23
|
-
constructor() {
|
|
24
|
-
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
|
|
25
|
-
this.cleanupInterval.unref();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
private cleanup(): void {
|
|
29
|
-
const now = Date.now();
|
|
30
|
-
for (const [key, record] of this.store.entries()) {
|
|
31
|
-
if (record.resetTime < now) {
|
|
32
|
-
this.store.delete(key);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
increment(key: string, windowMs: number): RateLimitRecord {
|
|
38
|
-
const now = Date.now();
|
|
39
|
-
const record = this.store.get(key);
|
|
40
|
-
|
|
41
|
-
if (!record || record.resetTime < now) {
|
|
42
|
-
const resetTime = now + windowMs;
|
|
43
|
-
this.store.set(key, { count: 1, resetTime });
|
|
44
|
-
return { count: 1, resetTime };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
record.count++;
|
|
48
|
-
return record;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
get(key: string): RateLimitRecord | undefined {
|
|
52
|
-
return this.store.get(key);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
destroy(): void {
|
|
56
|
-
clearInterval(this.cleanupInterval);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
class RateLimiter {
|
|
61
|
-
private options: Required<RateLimitOptions>;
|
|
62
|
-
private store: InMemoryRateLimiter;
|
|
63
|
-
|
|
64
|
-
constructor(options: RateLimitOptions = {}) {
|
|
65
|
-
const defaultWindow = config.get('RATE_LIMIT_WINDOW') || '1m';
|
|
66
|
-
const defaultLimit = parseInt(config.get('RATE_LIMIT_LIMIT') || '100', 10);
|
|
67
|
-
|
|
68
|
-
this.options = {
|
|
69
|
-
window: options.window || defaultWindow,
|
|
70
|
-
limit: options.limit || defaultLimit,
|
|
71
|
-
keyGenerator: options.keyGenerator || ((req: Request) => req.ip || 'unknown'),
|
|
72
|
-
handler: options.handler || this.defaultHandler,
|
|
73
|
-
skipSuccessfulRequests: options.skipSuccessfulRequests || false,
|
|
74
|
-
skipFailedRequests: options.skipFailedRequests || false,
|
|
75
|
-
skip: options.skip || (() => false),
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
this.store = new InMemoryRateLimiter();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private getWindowMs(): number {
|
|
82
|
-
const window = this.options.window;
|
|
83
|
-
const match = window.match(/^(\d+)(s|m|h|d)$/);
|
|
84
|
-
if (!match) return 60000;
|
|
85
|
-
|
|
86
|
-
const value = parseInt(match[1], 10);
|
|
87
|
-
const unit = match[2];
|
|
88
|
-
|
|
89
|
-
switch (unit) {
|
|
90
|
-
case 's': return value * 1000;
|
|
91
|
-
case 'm': return value * 60 * 1000;
|
|
92
|
-
case 'h': return value * 60 * 60 * 1000;
|
|
93
|
-
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
94
|
-
default: return 60000;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private defaultHandler(req: Request, res: Response): void {
|
|
99
|
-
res.status(429).json({
|
|
100
|
-
error: 'Too many requests',
|
|
101
|
-
message: `Rate limit exceeded. Please try again later.`,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
middleware(): RequestHandler {
|
|
106
|
-
const windowMs = this.getWindowMs();
|
|
107
|
-
|
|
108
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
109
|
-
if (this.options.skip(req)) {
|
|
110
|
-
return next();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const key = this.options.keyGenerator(req);
|
|
114
|
-
const record = this.store.increment(key, windowMs);
|
|
115
|
-
|
|
116
|
-
const remaining = Math.max(0, this.options.limit - record.count);
|
|
117
|
-
const resetTime = new Date(record.resetTime);
|
|
118
|
-
|
|
119
|
-
res.setHeader('X-RateLimit-Limit', this.options.limit);
|
|
120
|
-
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
121
|
-
res.setHeader('X-RateLimit-Reset', Math.ceil(record.resetTime / 1000));
|
|
122
|
-
|
|
123
|
-
if (record.count > this.options.limit) {
|
|
124
|
-
return this.options.handler(req, res);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
next();
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
destroy(): void {
|
|
132
|
-
this.store.destroy();
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function rateLimit(options: RateLimitOptions = {}): RequestHandler {
|
|
137
|
-
const limiter = new RateLimiter(options);
|
|
138
|
-
return limiter.middleware();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function createRateLimiter(options: RateLimitOptions): RateLimiter {
|
|
142
|
-
return new RateLimiter(options);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export default rateLimit;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { FastifyInstance, FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
|
|
2
|
-
import { RateLimitOptions, InMemoryRateLimiter } from './express';
|
|
3
|
-
|
|
4
|
-
export function registerRateLimitPlugin(fastify: FastifyInstance, options: RateLimitOptions = {}, done: HookHandlerDoneFunction) {
|
|
5
|
-
const limiter = new (require('./express').createRateLimiter.constructor)(options);
|
|
6
|
-
|
|
7
|
-
fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
8
|
-
const key = options.keyGenerator
|
|
9
|
-
? options.keyGenerator(request.raw as any)
|
|
10
|
-
: request.ip || 'unknown';
|
|
11
|
-
|
|
12
|
-
const windowMs = parseWindowMs(options.window || '1m');
|
|
13
|
-
const record = (limiter as any).store.increment(key, windowMs);
|
|
14
|
-
const limit = options.limit || 100;
|
|
15
|
-
const remaining = Math.max(0, limit - record.count);
|
|
16
|
-
|
|
17
|
-
reply.header('X-RateLimit-Limit', limit);
|
|
18
|
-
reply.header('X-RateLimit-Remaining', remaining);
|
|
19
|
-
reply.header('X-RateLimit-Reset', Math.ceil(record.resetTime / 1000));
|
|
20
|
-
|
|
21
|
-
if (record.count > limit) {
|
|
22
|
-
reply.status(429).send({
|
|
23
|
-
error: 'Too many requests',
|
|
24
|
-
message: 'Rate limit exceeded. Please try again later.',
|
|
25
|
-
});
|
|
26
|
-
return reply;
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
done();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function parseWindowMs(window: string): number {
|
|
34
|
-
const match = window.match(/^(\d+)(s|m|h|d)$/);
|
|
35
|
-
if (!match) return 60000;
|
|
36
|
-
|
|
37
|
-
const value = parseInt(match[1], 10);
|
|
38
|
-
const unit = match[2];
|
|
39
|
-
|
|
40
|
-
switch (unit) {
|
|
41
|
-
case 's': return value * 1000;
|
|
42
|
-
case 'm': return value * 60 * 1000;
|
|
43
|
-
case 'h': return value * 60 * 60 * 1000;
|
|
44
|
-
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
45
|
-
default: return 60000;
|
|
46
|
-
}
|
|
47
|
-
}
|
package/src/rate-limit/index.ts
DELETED
package/src/response/index.ts
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { Response } from 'express';
|
|
2
|
-
|
|
3
|
-
export interface ApiResponse<T = unknown> {
|
|
4
|
-
success: boolean;
|
|
5
|
-
data?: T;
|
|
6
|
-
error?: string;
|
|
7
|
-
message?: string;
|
|
8
|
-
meta?: {
|
|
9
|
-
page?: number;
|
|
10
|
-
limit?: number;
|
|
11
|
-
total?: number;
|
|
12
|
-
totalPages?: number;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface PaginatedResponse<T> extends Omit<ApiResponse<T[]>, 'data'> {
|
|
17
|
-
data: T[];
|
|
18
|
-
meta: {
|
|
19
|
-
page: number;
|
|
20
|
-
limit: number;
|
|
21
|
-
total: number;
|
|
22
|
-
totalPages: number;
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface ErrorResponse {
|
|
27
|
-
success: false;
|
|
28
|
-
error: string;
|
|
29
|
-
code?: string;
|
|
30
|
-
details?: Record<string, unknown>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class ResponseHelper {
|
|
34
|
-
static success<T>(res: Response, data?: T, message?: string, statusCode: number = 200): Response {
|
|
35
|
-
const response: ApiResponse<T> = {
|
|
36
|
-
success: true,
|
|
37
|
-
data,
|
|
38
|
-
message,
|
|
39
|
-
};
|
|
40
|
-
return res.status(statusCode).json(response);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
static created<T>(res: Response, data?: T, message: string = 'Resource created'): Response {
|
|
44
|
-
return this.success(res, data, message, 201);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
static updated<T>(res: Response, data?: T, message: string = 'Resource updated'): Response {
|
|
48
|
-
return this.success(res, data, message, 200);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
static deleted(res: Response, message: string = 'Resource deleted'): Response {
|
|
52
|
-
return this.success(res, null, message, 200);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
static error(
|
|
56
|
-
res: Response,
|
|
57
|
-
error: string,
|
|
58
|
-
statusCode: number = 400,
|
|
59
|
-
code?: string,
|
|
60
|
-
details?: Record<string, unknown>
|
|
61
|
-
): Response {
|
|
62
|
-
const response: ErrorResponse = {
|
|
63
|
-
success: false,
|
|
64
|
-
error,
|
|
65
|
-
code,
|
|
66
|
-
...(details && { details }),
|
|
67
|
-
};
|
|
68
|
-
return res.status(statusCode).json(response);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
static badRequest(res: Response, error: string = 'Bad request', code?: string): Response {
|
|
72
|
-
return this.error(res, error, 400, code);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
static unauthorized(res: Response, error: string = 'Unauthorized', code?: string): Response {
|
|
76
|
-
return this.error(res, error, 401, code);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
static forbidden(res: Response, error: string = 'Forbidden', code?: string): Response {
|
|
80
|
-
return this.error(res, error, 403, code);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
static notFound(res: Response, error: string = 'Resource not found', code?: string): Response {
|
|
84
|
-
return this.error(res, error, 404, code);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
static conflict(res: Response, error: string = 'Conflict', code?: string): Response {
|
|
88
|
-
return this.error(res, error, 409, code);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
static validationError(res: Response, error: string, details?: Record<string, unknown>): Response {
|
|
92
|
-
return this.error(res, error, 422, 'VALIDATION_ERROR', details);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
static internalError(res: Response, error: string = 'Internal server error'): Response {
|
|
96
|
-
return this.error(res, error, 500, 'INTERNAL_ERROR');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
static paginated<T>(
|
|
100
|
-
res: Response,
|
|
101
|
-
data: T[],
|
|
102
|
-
page: number,
|
|
103
|
-
limit: number,
|
|
104
|
-
total: number
|
|
105
|
-
): Response<PaginatedResponse<T>> {
|
|
106
|
-
const totalPages = Math.ceil(total / limit);
|
|
107
|
-
const response: PaginatedResponse<T> = {
|
|
108
|
-
success: true,
|
|
109
|
-
data,
|
|
110
|
-
meta: {
|
|
111
|
-
page,
|
|
112
|
-
limit,
|
|
113
|
-
total,
|
|
114
|
-
totalPages,
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
return res.status(200).json(response);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
static noContent(res: Response): Response {
|
|
121
|
-
return res.status(204).send();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
declare global {
|
|
126
|
-
namespace Express {
|
|
127
|
-
interface Response {
|
|
128
|
-
success<T>(data?: T, message?: string, statusCode?: number): Response;
|
|
129
|
-
created<T>(data?: T, message?: string): Response;
|
|
130
|
-
updated<T>(data?: T, message?: string): Response;
|
|
131
|
-
deleted(message?: string): Response;
|
|
132
|
-
error(error: string, statusCode?: number, code?: string, details?: Record<string, unknown>): Response;
|
|
133
|
-
badRequest(error?: string, code?: string): Response;
|
|
134
|
-
unauthorized(error?: string, code?: string): Response;
|
|
135
|
-
forbidden(error?: string, code?: string): Response;
|
|
136
|
-
notFound(error?: string, code?: string): Response;
|
|
137
|
-
conflict(error?: string, code?: string): Response;
|
|
138
|
-
validationError(error: string, details?: Record<string, unknown>): Response;
|
|
139
|
-
internalError(error?: string): Response;
|
|
140
|
-
paginated<T>(data: T[], page: number, limit: number, total: number): Response;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
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
|
-
};
|
|
152
|
-
|
|
153
|
-
proto.created = function <T>(this: Response, data?: T, message?: string) {
|
|
154
|
-
return ResponseHelper.created(this, data, message);
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
proto.updated = function <T>(this: Response, data?: T, message?: string) {
|
|
158
|
-
return ResponseHelper.updated(this, data, message);
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
proto.deleted = function (this: Response, message?: string) {
|
|
162
|
-
return ResponseHelper.deleted(this, message);
|
|
163
|
-
};
|
|
164
|
-
|
|
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
|
-
};
|
|
168
|
-
|
|
169
|
-
proto.badRequest = function (this: Response, error?: string, code?: string) {
|
|
170
|
-
return ResponseHelper.badRequest(this, error, code);
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
proto.unauthorized = function (this: Response, error?: string, code?: string) {
|
|
174
|
-
return ResponseHelper.unauthorized(this, error, code);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
proto.forbidden = function (this: Response, error?: string, code?: string) {
|
|
178
|
-
return ResponseHelper.forbidden(this, error, code);
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
proto.notFound = function (this: Response, error?: string, code?: string) {
|
|
182
|
-
return ResponseHelper.notFound(this, error, code);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
proto.conflict = function (this: Response, error?: string, code?: string) {
|
|
186
|
-
return ResponseHelper.conflict(this, error, code);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
proto.validationError = function (this: Response, error: string, details?: Record<string, unknown>) {
|
|
190
|
-
return ResponseHelper.validationError(this, error, details);
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
proto.internalError = function (this: Response, error?: string) {
|
|
194
|
-
return ResponseHelper.internalError(this, error);
|
|
195
|
-
};
|
|
196
|
-
|
|
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
|
-
}
|
|
204
|
-
|
|
205
|
-
export const response = ResponseHelper;
|
|
206
|
-
export default ResponseHelper;
|