saas-backend-kit 1.0.0 → 1.0.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 (42) hide show
  1. package/README.md +117 -370
  2. package/copy-dts.js +59 -0
  3. package/dist/auth/index.js +6 -1
  4. package/dist/auth/index.js.map +1 -1
  5. package/dist/auth/index.mjs +6 -1
  6. package/dist/auth/index.mjs.map +1 -1
  7. package/dist/config/index.js +6 -1
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/config/index.mjs +6 -1
  10. package/dist/config/index.mjs.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +184 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.mjs +183 -2
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/logger/index.js +6 -1
  17. package/dist/logger/index.js.map +1 -1
  18. package/dist/logger/index.mjs +6 -1
  19. package/dist/logger/index.mjs.map +1 -1
  20. package/dist/notifications/index.js +6 -1
  21. package/dist/notifications/index.js.map +1 -1
  22. package/dist/notifications/index.mjs +6 -1
  23. package/dist/notifications/index.mjs.map +1 -1
  24. package/dist/queue/index.js +6 -1
  25. package/dist/queue/index.js.map +1 -1
  26. package/dist/queue/index.mjs +6 -1
  27. package/dist/queue/index.mjs.map +1 -1
  28. package/dist/rate-limit/index.js +6 -1
  29. package/dist/rate-limit/index.js.map +1 -1
  30. package/dist/rate-limit/index.mjs +6 -1
  31. package/dist/rate-limit/index.mjs.map +1 -1
  32. package/dist/upload/index.d.ts +57 -0
  33. package/dist/upload/index.js +344 -0
  34. package/dist/upload/index.js.map +1 -0
  35. package/dist/upload/index.mjs +334 -0
  36. package/dist/upload/index.mjs.map +1 -0
  37. package/package.json +12 -2
  38. package/saas-banner.svg +239 -0
  39. package/src/config/index.ts +5 -0
  40. package/src/index.ts +2 -0
  41. package/src/upload/index.ts +268 -0
  42. package/tsup.config.ts +2 -1
@@ -0,0 +1,239 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 320" width="900" height="320">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#0a0a0f"/>
5
+ <stop offset="50%" style="stop-color:#0d1117"/>
6
+ <stop offset="100%" style="stop-color:#111827"/>
7
+ </linearGradient>
8
+ <linearGradient id="pipeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
9
+ <stop offset="0%" style="stop-color:#26d97f;stop-opacity:0"/>
10
+ <stop offset="50%" style="stop-color:#26d97f;stop-opacity:1"/>
11
+ <stop offset="100%" style="stop-color:#26d97f;stop-opacity:0"/>
12
+ </linearGradient>
13
+ <linearGradient id="pipeGrad2" x1="0%" y1="0%" x2="100%" y2="0%">
14
+ <stop offset="0%" style="stop-color:#3178c6;stop-opacity:0"/>
15
+ <stop offset="50%" style="stop-color:#3178c6;stop-opacity:1"/>
16
+ <stop offset="100%" style="stop-color:#3178c6;stop-opacity:0"/>
17
+ </linearGradient>
18
+ <linearGradient id="pipeGrad3" x1="0%" y1="0%" x2="100%" y2="0%">
19
+ <stop offset="0%" style="stop-color:#ff9900;stop-opacity:0"/>
20
+ <stop offset="50%" style="stop-color:#ff9900;stop-opacity:1"/>
21
+ <stop offset="100%" style="stop-color:#ff9900;stop-opacity:0"/>
22
+ </linearGradient>
23
+ <filter id="glow-green">
24
+ <feGaussianBlur stdDeviation="4" result="blur"/>
25
+ <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
26
+ </filter>
27
+ <filter id="glow-blue">
28
+ <feGaussianBlur stdDeviation="3" result="blur"/>
29
+ <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
30
+ </filter>
31
+ <filter id="glow-soft">
32
+ <feGaussianBlur stdDeviation="2" result="blur"/>
33
+ <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
34
+ </filter>
35
+ </defs>
36
+
37
+ <!-- Background -->
38
+ <rect width="900" height="320" fill="url(#bg)" rx="14"/>
39
+
40
+ <!-- Subtle dot-grid background -->
41
+ <pattern id="dots" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
42
+ <circle cx="1" cy="1" r="0.8" fill="#1e293b" opacity="0.8"/>
43
+ </pattern>
44
+ <rect width="900" height="320" fill="url(#dots)" rx="14" opacity="0.6"/>
45
+
46
+ <!-- ===================== DATA PIPELINE VISUALIZATION ===================== -->
47
+ <!-- Horizontal pipeline backbone -->
48
+ <line x1="60" y1="160" x2="840" y2="160" stroke="#1e293b" stroke-width="2"/>
49
+
50
+ <!-- Animated data packet 1 - green -->
51
+ <rect x="0" y="154" width="36" height="12" rx="6" fill="url(#pipeGrad)" opacity="0.9">
52
+ <animate attributeName="x" values="-40;900" dur="3.2s" repeatCount="indefinite"/>
53
+ </rect>
54
+
55
+ <!-- Animated data packet 2 - blue -->
56
+ <rect x="0" y="154" width="28" height="12" rx="6" fill="url(#pipeGrad2)" opacity="0.85">
57
+ <animate attributeName="x" values="-40;900" dur="3.2s" begin="-1.1s" repeatCount="indefinite"/>
58
+ </rect>
59
+
60
+ <!-- Animated data packet 3 - orange -->
61
+ <rect x="0" y="154" width="22" height="12" rx="6" fill="url(#pipeGrad3)" opacity="0.8">
62
+ <animate attributeName="x" values="-40;900" dur="3.2s" begin="-2.0s" repeatCount="indefinite"/>
63
+ </rect>
64
+
65
+ <!-- ===================== MODULE NODES (pipeline stops) ===================== -->
66
+ <!-- Each node represents a feature of the library -->
67
+
68
+ <!-- Node 1: AUTH -->
69
+ <g filter="url(#glow-blue)">
70
+ <circle cx="130" cy="160" r="32" fill="#0d1117" stroke="#3178c6" stroke-width="1.8"/>
71
+ <circle cx="130" cy="160" r="32" fill="none" stroke="#3178c6" stroke-width="1">
72
+ <animate attributeName="r" values="32;40;32" dur="3s" repeatCount="indefinite"/>
73
+ <animate attributeName="opacity" values="0.5;0;0.5" dur="3s" repeatCount="indefinite"/>
74
+ </circle>
75
+ <text x="130" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#3178c6" font-weight="bold">🔐</text>
76
+ <text x="130" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#3178c6" font-weight="bold">AUTH</text>
77
+ <text x="130" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#3178c6" opacity="0.6">JWT · RBAC</text>
78
+ </g>
79
+
80
+ <!-- Connector line -->
81
+ <line x1="162" y1="160" x2="218" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
82
+
83
+ <!-- Node 2: QUEUE -->
84
+ <g filter="url(#glow-green)">
85
+ <circle cx="250" cy="160" r="32" fill="#0d1117" stroke="#26d97f" stroke-width="1.8"/>
86
+ <circle cx="250" cy="160" r="32" fill="none" stroke="#26d97f" stroke-width="1">
87
+ <animate attributeName="r" values="32;40;32" dur="3.5s" begin="-0.8s" repeatCount="indefinite"/>
88
+ <animate attributeName="opacity" values="0.5;0;0.5" dur="3.5s" begin="-0.8s" repeatCount="indefinite"/>
89
+ </circle>
90
+ <text x="250" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#26d97f" font-weight="bold">⚡</text>
91
+ <text x="250" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#26d97f" font-weight="bold">QUEUE</text>
92
+ <text x="250" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#26d97f" opacity="0.6">BullMQ</text>
93
+ </g>
94
+
95
+ <!-- Connector line -->
96
+ <line x1="282" y1="160" x2="338" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
97
+
98
+ <!-- Node 3: NOTIFY -->
99
+ <g filter="url(#glow-soft)">
100
+ <circle cx="370" cy="160" r="32" fill="#0d1117" stroke="#f59e0b" stroke-width="1.8"/>
101
+ <circle cx="370" cy="160" r="32" fill="none" stroke="#f59e0b" stroke-width="1">
102
+ <animate attributeName="r" values="32;40;32" dur="4s" begin="-1.5s" repeatCount="indefinite"/>
103
+ <animate attributeName="opacity" values="0.5;0;0.5" dur="4s" begin="-1.5s" repeatCount="indefinite"/>
104
+ </circle>
105
+ <text x="370" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#f59e0b" font-weight="bold">🔔</text>
106
+ <text x="370" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#f59e0b" font-weight="bold">NOTIFY</text>
107
+ <text x="370" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#f59e0b" opacity="0.6">Email · SMS</text>
108
+ </g>
109
+
110
+ <!-- Connector line -->
111
+ <line x1="402" y1="160" x2="458" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
112
+
113
+ <!-- Node 4: UPLOAD (CENTER - largest) -->
114
+ <g filter="url(#glow-soft)">
115
+ <circle cx="490" cy="160" r="36" fill="#0d1117" stroke="#ff9900" stroke-width="2"/>
116
+ <circle cx="490" cy="160" r="36" fill="none" stroke="#ff9900" stroke-width="1.2">
117
+ <animate attributeName="r" values="36;48;36" dur="3s" begin="-0.4s" repeatCount="indefinite"/>
118
+ <animate attributeName="opacity" values="0.5;0;0.5" dur="3s" begin="-0.4s" repeatCount="indefinite"/>
119
+ </circle>
120
+ <text x="490" y="154" text-anchor="middle" font-family="'Courier New',monospace" font-size="10" fill="#ff9900" font-weight="bold">☁️</text>
121
+ <text x="490" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="10" fill="#ff9900" font-weight="bold">S3</text>
122
+ <text x="490" y="208" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#ff9900" opacity="0.6">AWS Upload</text>
123
+ </g>
124
+
125
+ <!-- Connector line -->
126
+ <line x1="526" y1="160" x2="578" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
127
+
128
+ <!-- Node 5: RATE LIMIT -->
129
+ <g filter="url(#glow-soft)">
130
+ <circle cx="610" cy="160" r="32" fill="#0d1117" stroke="#e11d48" stroke-width="1.8"/>
131
+ <circle cx="610" cy="160" r="32" fill="none" stroke="#e11d48" stroke-width="1">
132
+ <animate attributeName="r" values="32;40;32" dur="2.8s" begin="-2.1s" repeatCount="indefinite"/>
133
+ <animate attributeName="opacity" values="0.5;0;0.5" dur="2.8s" begin="-2.1s" repeatCount="indefinite"/>
134
+ </circle>
135
+ <text x="610" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#e11d48" font-weight="bold">🛡️</text>
136
+ <text x="610" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#e11d48" font-weight="bold">LIMIT</text>
137
+ <text x="610" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#e11d48" opacity="0.6">Rate Guard</text>
138
+ </g>
139
+
140
+ <!-- Connector line -->
141
+ <line x1="642" y1="160" x2="698" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
142
+
143
+ <!-- Node 6: LOGGER -->
144
+ <g filter="url(#glow-soft)">
145
+ <circle cx="730" cy="160" r="32" fill="#0d1117" stroke="#8b5cf6" stroke-width="1.8"/>
146
+ <circle cx="730" cy="160" r="32" fill="none" stroke="#8b5cf6" stroke-width="1">
147
+ <animate attributeName="r" values="32;40;32" dur="3.8s" begin="-1.0s" repeatCount="indefinite"/>
148
+ <animate attributeName="opacity" values="0.5;0;0.5" dur="3.8s" begin="-1.0s" repeatCount="indefinite"/>
149
+ </circle>
150
+ <text x="730" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#8b5cf6" font-weight="bold">📋</text>
151
+ <text x="730" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#8b5cf6" font-weight="bold">LOGGER</text>
152
+ <text x="730" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#8b5cf6" opacity="0.6">Pino · JSON</text>
153
+ </g>
154
+
155
+ <!-- Connector line -->
156
+ <line x1="762" y1="160" x2="818" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
157
+
158
+ <!-- Node 7: CONFIG -->
159
+ <g filter="url(#glow-soft)">
160
+ <circle cx="845" cy="160" r="28" fill="#0d1117" stroke="#06b6d4" stroke-width="1.8"/>
161
+ <circle cx="845" cy="160" r="28" fill="none" stroke="#06b6d4" stroke-width="1">
162
+ <animate attributeName="r" values="28;36;28" dur="3.2s" begin="-2.6s" repeatCount="indefinite"/>
163
+ <animate attributeName="opacity" values="0.5;0;0.5" dur="3.2s" begin="-2.6s" repeatCount="indefinite"/>
164
+ </circle>
165
+ <text x="845" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#06b6d4" font-weight="bold">⚙️</text>
166
+ <text x="845" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#06b6d4" font-weight="bold">CFG</text>
167
+ <text x="845" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#06b6d4" opacity="0.6">Zod · Env</text>
168
+ </g>
169
+
170
+ <!-- ===================== TITLE + SUBTITLE ===================== -->
171
+ <text x="450" y="50" text-anchor="middle" font-family="'Courier New',monospace" font-size="28" font-weight="bold" fill="#f8fafc" letter-spacing="2" filter="url(#glow-soft)">
172
+ saas-backend-kit
173
+ <animate attributeName="opacity" values="0.85;1;0.85" dur="4s" repeatCount="indefinite"/>
174
+ </text>
175
+
176
+ <text x="450" y="76" text-anchor="middle" font-family="'Courier New',monospace" font-size="12" fill="#64748b">
177
+ Production-grade modular backend toolkit · Node.js · Express · Fastify
178
+ </text>
179
+
180
+ <!-- npm install command badge — animated border -->
181
+ <rect x="310" y="88" width="280" height="30" rx="6" fill="#0d1117" stroke="#26d97f" stroke-width="1" opacity="0.9">
182
+ <animate attributeName="stroke-opacity" values="0.4;1;0.4" dur="2s" repeatCount="indefinite"/>
183
+ </rect>
184
+ <text x="450" y="108" text-anchor="middle" font-family="'Courier New',monospace" font-size="12" fill="#26d97f">
185
+ $ npm install saas-backend-kit
186
+ </text>
187
+
188
+ <!-- ===================== FLOATING PARTICLES ===================== -->
189
+ <circle cx="50" cy="50" r="2" fill="#26d97f" opacity="0.5">
190
+ <animate attributeName="cy" values="50;270;50" dur="7s" repeatCount="indefinite"/>
191
+ <animate attributeName="opacity" values="0.5;0;0.5" dur="7s" repeatCount="indefinite"/>
192
+ </circle>
193
+ <circle cx="870" cy="90" r="1.5" fill="#3178c6" opacity="0.5">
194
+ <animate attributeName="cy" values="90;30;90" dur="5s" repeatCount="indefinite"/>
195
+ <animate attributeName="opacity" values="0.5;0;0.5" dur="5s" repeatCount="indefinite"/>
196
+ </circle>
197
+ <circle cx="200" cy="280" r="2" fill="#f59e0b" opacity="0.4">
198
+ <animate attributeName="cx" values="200;700;200" dur="11s" repeatCount="indefinite"/>
199
+ <animate attributeName="opacity" values="0.4;0;0.4" dur="11s" repeatCount="indefinite"/>
200
+ </circle>
201
+ <circle cx="750" cy="30" r="1.5" fill="#8b5cf6" opacity="0.45">
202
+ <animate attributeName="cx" values="750;200;750" dur="9s" repeatCount="indefinite"/>
203
+ <animate attributeName="opacity" values="0.45;0;0.45" dur="9s" repeatCount="indefinite"/>
204
+ </circle>
205
+
206
+ <!-- ===================== BOTTOM STATUS BAR ===================== -->
207
+ <rect x="0" y="285" width="900" height="35" fill="#080c10" rx="0"/>
208
+ <rect x="0" y="282" width="900" height="3" fill="#26d97f" opacity="0.25"/>
209
+
210
+ <!-- Status indicators -->
211
+ <circle cx="30" cy="302" r="5" fill="#26d97f">
212
+ <animate attributeName="opacity" values="1;0.3;1" dur="1.5s" repeatCount="indefinite"/>
213
+ </circle>
214
+ <text x="42" y="307" font-family="'Courier New',monospace" font-size="10" fill="#26d97f" opacity="0.8">LIVE</text>
215
+
216
+ <text x="90" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
217
+ <text x="100" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b" opacity="0.7">MIT Licensed</text>
218
+
219
+ <text x="210" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
220
+ <text x="220" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b" opacity="0.7">TypeScript Ready</text>
221
+
222
+ <text x="360" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
223
+ <text x="370" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b" opacity="0.7">7 Modular Plugins</text>
224
+
225
+ <text x="520" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
226
+ <text x="530" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b" opacity="0.7">Express · Fastify</text>
227
+
228
+ <text x="670" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
229
+ <text x="680" y="307" font-family="'Courier New',monospace" font-size="10" fill="#3178c6" opacity="0.85">by @AshishK-M</text>
230
+
231
+ <!-- Scrolling ticker on right -->
232
+ <clipPath id="tickerClip">
233
+ <rect x="800" y="288" width="95" height="28"/>
234
+ </clipPath>
235
+ <text font-family="'Courier New',monospace" font-size="9" fill="#26d97f" opacity="0.6" clip-path="url(#tickerClip)">
236
+ <tspan x="900" y="305">auth · queue · notify · upload · logger · rateLimit · config</tspan>
237
+ <animate attributeName="x" values="900;600" dur="8s" repeatCount="indefinite"/>
238
+ </text>
239
+ </svg>
@@ -24,6 +24,11 @@ export const envSchema = z.object({
24
24
  RATE_LIMIT_WINDOW: z.string().default('1m'),
25
25
  RATE_LIMIT_LIMIT: z.string().default('100'),
26
26
  LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
27
+ AWS_REGION: z.string().default('us-east-1'),
28
+ AWS_ACCESS_KEY_ID: z.string().optional(),
29
+ AWS_SECRET_ACCESS_KEY: z.string().optional(),
30
+ AWS_S3_BUCKET: z.string().optional(),
31
+ AWS_ENDPOINT: z.string().optional(),
27
32
  });
28
33
 
29
34
  export type EnvConfig = z.infer<typeof envSchema>;
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export { logger } from './logger';
5
5
  export { rateLimit, createRateLimiter } from './rate-limit';
6
6
  export { config } from './config';
7
7
  export { ResponseHelper, response } from './response';
8
+ export { upload, s3Service, S3Service } from './upload';
8
9
  export { createApp, createExpressApp, SaaSAppBuilder, PluginManager, Plugin, AppOptions } from './plugin';
9
10
 
10
11
  export { AuthOptions, User, JWTPayload, TokenPair, LoginCredentials, RegisterData, Role, Permission, RolePermissions } from './auth/types';
@@ -14,3 +15,4 @@ export { LoggerConfig, LogLevel } from './logger';
14
15
  export { RateLimitOptions } from './rate-limit';
15
16
  export { EnvConfig, ConfigOptions } from './config';
16
17
  export { ApiResponse, PaginatedResponse, ErrorResponse } from './response';
18
+ export { S3Config, UploadOptions, UploadResult, SignedUrlOptions, FileObject } from './upload';
@@ -0,0 +1,268 @@
1
+ import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
2
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
3
+ import { config } from '../config';
4
+ import { logger } from '../logger';
5
+
6
+ export interface S3Config {
7
+ region?: string;
8
+ accessKeyId?: string;
9
+ secretAccessKey?: string;
10
+ bucket: string;
11
+ endpoint?: string;
12
+ forcePathStyle?: boolean;
13
+ }
14
+
15
+ export interface UploadOptions {
16
+ key?: string;
17
+ contentType?: string;
18
+ expiresIn?: number;
19
+ metadata?: Record<string, string>;
20
+ }
21
+
22
+ export interface UploadResult {
23
+ key: string;
24
+ url: string;
25
+ bucket: string;
26
+ contentType?: string;
27
+ size?: number;
28
+ }
29
+
30
+ export interface SignedUrlOptions {
31
+ expiresIn?: number;
32
+ }
33
+
34
+ export interface FileObject {
35
+ key: string;
36
+ lastModified?: Date;
37
+ size?: number;
38
+ contentType?: string;
39
+ }
40
+
41
+ class S3Service {
42
+ private client: S3Client | null = null;
43
+ private bucket: string;
44
+ private initialized: boolean = false;
45
+
46
+ constructor() {
47
+ this.bucket = '';
48
+ }
49
+
50
+ initialize(config: S3Config): void {
51
+ this.client = new S3Client({
52
+ region: config.region || 'us-east-1',
53
+ credentials: config.accessKeyId && config.secretAccessKey
54
+ ? {
55
+ accessKeyId: config.accessKeyId,
56
+ secretAccessKey: config.secretAccessKey,
57
+ }
58
+ : undefined,
59
+ endpoint: config.endpoint,
60
+ forcePathStyle: config.forcePathStyle || false,
61
+ });
62
+
63
+ this.bucket = config.bucket;
64
+ this.initialized = true;
65
+ logger.info('S3 service initialized', { bucket: this.bucket });
66
+ }
67
+
68
+ isInitialized(): boolean {
69
+ return this.initialized;
70
+ }
71
+
72
+ private ensureInitialized(): void {
73
+ if (!this.initialized) {
74
+ const region = config.get('AWS_REGION') || 'us-east-1';
75
+ const bucket = config.get('AWS_S3_BUCKET') || '';
76
+
77
+ this.initialize({
78
+ region,
79
+ accessKeyId: config.get('AWS_ACCESS_KEY_ID'),
80
+ secretAccessKey: config.get('AWS_SECRET_ACCESS_KEY'),
81
+ bucket,
82
+ endpoint: config.get('AWS_ENDPOINT'),
83
+ });
84
+ }
85
+ }
86
+
87
+ async upload(
88
+ file: Buffer | Uint8Array | string,
89
+ options: UploadOptions = {}
90
+ ): Promise<UploadResult> {
91
+ this.ensureInitialized();
92
+
93
+ const key = options.key || this.generateKey();
94
+ const contentType = options.contentType || this.guessContentType(key);
95
+
96
+ const command = new PutObjectCommand({
97
+ Bucket: this.bucket,
98
+ Key: key,
99
+ Body: file,
100
+ ContentType: contentType,
101
+ Metadata: options.metadata,
102
+ });
103
+
104
+ await this.client!.send(command);
105
+
106
+ const url = await this.getSignedUrl(key, { expiresIn: options.expiresIn || 3600 });
107
+
108
+ logger.info('File uploaded to S3', { key, bucket: this.bucket, contentType });
109
+
110
+ return {
111
+ key,
112
+ url,
113
+ bucket: this.bucket,
114
+ contentType,
115
+ };
116
+ }
117
+
118
+ async uploadImage(
119
+ file: Buffer | Uint8Array | string,
120
+ filename: string,
121
+ options: UploadOptions = {}
122
+ ): Promise<UploadResult> {
123
+ const key = options.key || `images/${Date.now()}-${filename}`;
124
+
125
+ return this.upload(file, {
126
+ ...options,
127
+ key,
128
+ contentType: options.contentType || this.getImageContentType(filename),
129
+ });
130
+ }
131
+
132
+ async uploadVideo(
133
+ file: Buffer | Uint8Array | string,
134
+ filename: string,
135
+ options: UploadOptions = {}
136
+ ): Promise<UploadResult> {
137
+ const key = options.key || `videos/${Date.now()}-${filename}`;
138
+
139
+ return this.upload(file, {
140
+ ...options,
141
+ key,
142
+ contentType: options.contentType || this.getVideoContentType(filename),
143
+ });
144
+ }
145
+
146
+ async delete(key: string): Promise<void> {
147
+ this.ensureInitialized();
148
+
149
+ const command = new DeleteObjectCommand({
150
+ Bucket: this.bucket,
151
+ Key: key,
152
+ });
153
+
154
+ await this.client!.send(command);
155
+ logger.info('File deleted from S3', { key, bucket: this.bucket });
156
+ }
157
+
158
+ async getSignedUrl(key: string, options: SignedUrlOptions = {}): Promise<string> {
159
+ this.ensureInitialized();
160
+
161
+ const command = new GetObjectCommand({
162
+ Bucket: this.bucket,
163
+ Key: key,
164
+ });
165
+
166
+ return getSignedUrl(this.client!, command, {
167
+ expiresIn: options.expiresIn || 3600,
168
+ });
169
+ }
170
+
171
+ async getPublicUrl(key: string): Promise<string> {
172
+ return `https://${this.bucket}.s3.${config.get('AWS_REGION') || 'us-east-1'}.amazonaws.com/${key}`;
173
+ }
174
+
175
+ async listFiles(prefix?: string, maxKeys: number = 1000): Promise<FileObject[]> {
176
+ this.ensureInitialized();
177
+
178
+ const command = new ListObjectsV2Command({
179
+ Bucket: this.bucket,
180
+ Prefix: prefix,
181
+ MaxKeys: maxKeys,
182
+ });
183
+
184
+ const response = await this.client!.send(command);
185
+
186
+ return (response.Contents || []).map((item) => ({
187
+ key: item.Key || '',
188
+ lastModified: item.LastModified,
189
+ size: item.Size,
190
+ }));
191
+ }
192
+
193
+ private generateKey(): string {
194
+ const timestamp = Date.now();
195
+ const random = Math.random().toString(36).substring(2, 15);
196
+ return `uploads/${timestamp}-${random}`;
197
+ }
198
+
199
+ private guessContentType(key: string): string {
200
+ const ext = key.split('.').pop()?.toLowerCase();
201
+
202
+ const contentTypes: Record<string, string> = {
203
+ jpg: 'image/jpeg',
204
+ jpeg: 'image/jpeg',
205
+ png: 'image/png',
206
+ gif: 'image/gif',
207
+ webp: 'image/webp',
208
+ svg: 'image/svg+xml',
209
+ mp4: 'video/mp4',
210
+ webm: 'video/webm',
211
+ mov: 'video/quicktime',
212
+ avi: 'video/x-msvideo',
213
+ pdf: 'application/pdf',
214
+ json: 'application/json',
215
+ txt: 'text/plain',
216
+ };
217
+
218
+ return contentTypes[ext || ''] || 'application/octet-stream';
219
+ }
220
+
221
+ private getImageContentType(filename: string): string {
222
+ const ext = filename.split('.').pop()?.toLowerCase();
223
+ const imageTypes: Record<string, string> = {
224
+ jpg: 'image/jpeg',
225
+ jpeg: 'image/jpeg',
226
+ png: 'image/png',
227
+ gif: 'image/gif',
228
+ webp: 'image/webp',
229
+ svg: 'image/svg+xml',
230
+ };
231
+ return imageTypes[ext || ''] || 'image/jpeg';
232
+ }
233
+
234
+ private getVideoContentType(filename: string): string {
235
+ const ext = filename.split('.').pop()?.toLowerCase();
236
+ const videoTypes: Record<string, string> = {
237
+ mp4: 'video/mp4',
238
+ webm: 'video/webm',
239
+ mov: 'video/quicktime',
240
+ avi: 'video/x-msvideo',
241
+ mkv: 'video/x-matroska',
242
+ ogv: 'video/ogg',
243
+ };
244
+ return videoTypes[ext || ''] || 'video/mp4';
245
+ }
246
+ }
247
+
248
+ export const s3Service = new S3Service();
249
+
250
+ export const upload = {
251
+ initialize: (config: S3Config) => s3Service.initialize(config),
252
+
253
+ file: (file: Buffer | Uint8Array | string, options?: UploadOptions) =>
254
+ s3Service.upload(file, options),
255
+
256
+ image: (file: Buffer | Uint8Array | string, filename: string, options?: UploadOptions) =>
257
+ s3Service.uploadImage(file, filename, options),
258
+
259
+ video: (file: Buffer | Uint8Array | string, filename: string, options?: UploadOptions) =>
260
+ s3Service.uploadVideo(file, filename, options),
261
+
262
+ delete: (key: string) => s3Service.delete(key),
263
+ getSignedUrl: (key: string, options?: SignedUrlOptions) => s3Service.getSignedUrl(key, options),
264
+ getPublicUrl: (key: string) => s3Service.getPublicUrl(key),
265
+ listFiles: (prefix?: string, maxKeys?: number) => s3Service.listFiles(prefix, maxKeys),
266
+ };
267
+
268
+ export default upload;
package/tsup.config.ts CHANGED
@@ -10,13 +10,14 @@ export default defineConfig({
10
10
  'rate-limit/index': 'src/rate-limit/index.ts',
11
11
  'config/index': 'src/config/index.ts',
12
12
  'response/index': 'src/response/index.ts',
13
+ 'upload/index': 'src/upload/index.ts',
13
14
  },
14
15
  format: ['cjs', 'esm'],
15
16
  dts: false,
16
17
  splitting: false,
17
18
  sourcemap: true,
18
19
  clean: false,
19
- external: ['express', 'fastify', 'ioredis', 'bullmq'],
20
+ external: ['express', 'fastify', 'ioredis', 'bullmq', '@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner'],
20
21
  treeshake: true,
21
22
  exports: {
22
23
  namedExports: true,