3a-ecommerce-utils 1.0.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 +163 -0
- package/dist/chunk-PEAZVBSD.mjs +597 -0
- package/dist/client-DYGi_pyp.d.mts +87 -0
- package/dist/client-DYGi_pyp.d.ts +87 -0
- package/dist/index.d.mts +496 -0
- package/dist/index.d.ts +496 -0
- package/dist/index.js +17707 -0
- package/dist/index.mjs +17043 -0
- package/dist/validation/server.d.mts +50 -0
- package/dist/validation/server.d.ts +50 -0
- package/dist/validation/server.js +518 -0
- package/dist/validation/server.mjs +168 -0
- package/package.json +69 -0
- package/src/api/address.queries.ts +96 -0
- package/src/api/category.queries.ts +85 -0
- package/src/api/coupon.queries.ts +120 -0
- package/src/api/dashboard.queries.ts +35 -0
- package/src/api/errorHandler.ts +164 -0
- package/src/api/graphqlClient.ts +113 -0
- package/src/api/index.ts +10 -0
- package/src/api/logger.client.ts +89 -0
- package/src/api/logger.ts +135 -0
- package/src/api/order.queries.ts +211 -0
- package/src/api/product.queries.ts +144 -0
- package/src/api/review.queries.ts +56 -0
- package/src/api/user.queries.ts +232 -0
- package/src/assets/3A.png +0 -0
- package/src/assets/index.ts +1 -0
- package/src/assets.d.ts +29 -0
- package/src/auth.ts +176 -0
- package/src/config/jest.backend.config.js +42 -0
- package/src/config/jest.frontend.config.js +50 -0
- package/src/config/postcss.config.js +6 -0
- package/src/config/tailwind.config.ts +70 -0
- package/src/config/tsconfig.base.json +36 -0
- package/src/config/vite.config.ts +86 -0
- package/src/config/vitest.base.config.ts +74 -0
- package/src/config/webpack.base.config.ts +126 -0
- package/src/constants/index.ts +312 -0
- package/src/cookies.ts +104 -0
- package/src/helpers.ts +400 -0
- package/src/index.ts +32 -0
- package/src/validation/client.ts +287 -0
- package/src/validation/index.ts +3 -0
- package/src/validation/server.ts +32 -0
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper Utilities
|
|
3
|
+
* Commonly used utility functions across services and frontend apps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const capitalize = (str: string): string => {
|
|
7
|
+
if (!str) return '';
|
|
8
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const capitalizeWords = (str: string): string => {
|
|
12
|
+
if (!str) return '';
|
|
13
|
+
return str
|
|
14
|
+
.split(' ')
|
|
15
|
+
.map((word) => capitalize(word))
|
|
16
|
+
.join(' ');
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const toTitleCase = (str: string): string => {
|
|
20
|
+
if (!str) return '';
|
|
21
|
+
return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const slugify = (str: string): string => {
|
|
25
|
+
if (!str) return '';
|
|
26
|
+
return str
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/\s+/g, '-')
|
|
30
|
+
.replace(/[^\w-]/g, '');
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const generateRandomCode = (length: number = 6): string => {
|
|
34
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
35
|
+
let result = '';
|
|
36
|
+
for (let i = 0; i < length; i++) {
|
|
37
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const truncate = (str: string, length: number, suffix: string = '...'): string => {
|
|
43
|
+
if (!str || str.length <= length) return str;
|
|
44
|
+
return str.substring(0, length) + suffix;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// NUMBER UTILITIES
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
export const formatCurrency = (amount: number, currency: string = 'USD'): string => {
|
|
52
|
+
return new Intl.NumberFormat('en-US', {
|
|
53
|
+
style: 'currency',
|
|
54
|
+
currency: currency,
|
|
55
|
+
}).format(amount);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const formatNumber = (num: number, decimals: number = 2): number => {
|
|
59
|
+
return parseFloat(num.toFixed(decimals));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const roundUp = (num: number, decimals: number = 2): number => {
|
|
63
|
+
const factor = Math.pow(10, decimals);
|
|
64
|
+
return Math.ceil(num * factor) / factor;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const roundDown = (num: number, decimals: number = 2): number => {
|
|
68
|
+
const factor = Math.pow(10, decimals);
|
|
69
|
+
return Math.floor(num * factor) / factor;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const calculatePercentage = (value: number, percentage: number): number => {
|
|
73
|
+
return formatNumber((value * percentage) / 100);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const calculateDiscount = (price: number, discountPercent: number): number => {
|
|
77
|
+
return formatNumber(price - (price * discountPercent) / 100);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const calculateTax = (amount: number, taxRate: number): number => {
|
|
81
|
+
return formatNumber(amount * taxRate);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// ARRAY UTILITIES
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
export const chunk = <T>(arr: T[], size: number): T[][] => {
|
|
89
|
+
const chunks: T[][] = [];
|
|
90
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
91
|
+
chunks.push(arr.slice(i, i + size));
|
|
92
|
+
}
|
|
93
|
+
return chunks;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const unique = <T>(arr: T[]): T[] => {
|
|
97
|
+
return Array.from(new Set(arr));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const removeDuplicates = <T extends Record<string, any>>(arr: T[], key: keyof T): T[] => {
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
return arr.filter((item) => {
|
|
103
|
+
const value = item[key];
|
|
104
|
+
if (seen.has(value)) return false;
|
|
105
|
+
seen.add(value);
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const groupBy = <T extends Record<string, any>>(arr: T[], key: keyof T) => {
|
|
111
|
+
return arr.reduce((result, item) => {
|
|
112
|
+
const groupKey = String(item[key]);
|
|
113
|
+
if (!result[groupKey]) {
|
|
114
|
+
result[groupKey] = [];
|
|
115
|
+
}
|
|
116
|
+
result[groupKey].push(item);
|
|
117
|
+
return result;
|
|
118
|
+
}, {} as Record<string, T[]>);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const sortBy = <T extends Record<string, any>>(
|
|
122
|
+
arr: T[],
|
|
123
|
+
key: keyof T,
|
|
124
|
+
order: 'ASC' | 'DESC' = 'ASC'
|
|
125
|
+
): T[] => {
|
|
126
|
+
const sorted = [...arr].sort((a, b) => {
|
|
127
|
+
const aVal = a[key];
|
|
128
|
+
const bVal = b[key];
|
|
129
|
+
if (aVal < bVal) return order === 'ASC' ? -1 : 1;
|
|
130
|
+
if (aVal > bVal) return order === 'ASC' ? 1 : -1;
|
|
131
|
+
return 0;
|
|
132
|
+
});
|
|
133
|
+
return sorted;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const flatten = <T>(arr: T[][]): T[] => {
|
|
137
|
+
return arr.reduce((flat, item) => flat.concat(item), []);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const difference = <T>(arr1: T[], arr2: T[]): T[] => {
|
|
141
|
+
return arr1.filter((item) => !arr2.includes(item));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const intersection = <T>(arr1: T[], arr2: T[]): T[] => {
|
|
145
|
+
return arr1.filter((item) => arr2.includes(item));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// OBJECT UTILITIES
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
export const omit = <T extends Record<string, any>>(obj: T, keys: (keyof T)[]): Partial<T> => {
|
|
153
|
+
const result = { ...obj };
|
|
154
|
+
keys.forEach((key) => delete result[key]);
|
|
155
|
+
return result;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const pick = <T extends Record<string, any>>(obj: T, keys: (keyof T)[]): Partial<T> => {
|
|
159
|
+
const result: Partial<T> = {};
|
|
160
|
+
keys.forEach((key) => {
|
|
161
|
+
if (key in obj) {
|
|
162
|
+
result[key] = obj[key];
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const deepMerge = <T extends Record<string, any>>(target: T, source: Partial<T>): T => {
|
|
169
|
+
const result = { ...target } as T;
|
|
170
|
+
for (const key in source) {
|
|
171
|
+
if (source.hasOwnProperty(key)) {
|
|
172
|
+
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
|
173
|
+
result[key] = deepMerge(
|
|
174
|
+
(target[key] as Record<string, any>) || {},
|
|
175
|
+
source[key] as Record<string, any>
|
|
176
|
+
) as T[Extract<keyof T, string>];
|
|
177
|
+
} else {
|
|
178
|
+
result[key] = source[key] as T[Extract<keyof T, string>];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const isEmpty = (obj: Record<string, any>): boolean => {
|
|
186
|
+
return Object.keys(obj).length === 0;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const getNestedValue = <T = any>(obj: Record<string, any>, path: string): T | undefined => {
|
|
190
|
+
const keys = path.split('.');
|
|
191
|
+
let result: any = obj;
|
|
192
|
+
for (const key of keys) {
|
|
193
|
+
if (result && typeof result === 'object' && key in result) {
|
|
194
|
+
result = result[key];
|
|
195
|
+
} else {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const setNestedValue = (obj: Record<string, any>, path: string, value: any): void => {
|
|
203
|
+
const keys = path.split('.');
|
|
204
|
+
let current = obj;
|
|
205
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
206
|
+
const key = keys[i];
|
|
207
|
+
if (!(key in current) || typeof current[key] !== 'object') {
|
|
208
|
+
current[key] = {};
|
|
209
|
+
}
|
|
210
|
+
current = current[key];
|
|
211
|
+
}
|
|
212
|
+
current[keys[keys.length - 1]] = value;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// DATE UTILITIES
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
export const formatDate = (date: Date | string, format: string = 'MM/DD/YYYY'): string => {
|
|
220
|
+
const dateObj = new Date(date);
|
|
221
|
+
const year = dateObj.getFullYear();
|
|
222
|
+
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
|
223
|
+
const day = String(dateObj.getDate()).padStart(2, '0');
|
|
224
|
+
const hours = String(dateObj.getHours()).padStart(2, '0');
|
|
225
|
+
const minutes = String(dateObj.getMinutes()).padStart(2, '0');
|
|
226
|
+
const seconds = String(dateObj.getSeconds()).padStart(2, '0');
|
|
227
|
+
|
|
228
|
+
return format
|
|
229
|
+
.replace('YYYY', String(year))
|
|
230
|
+
.replace('MM', month)
|
|
231
|
+
.replace('DD', day)
|
|
232
|
+
.replace('HH', hours)
|
|
233
|
+
.replace('mm', minutes)
|
|
234
|
+
.replace('ss', seconds);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const addDays = (date: Date, days: number): Date => {
|
|
238
|
+
const result = new Date(date);
|
|
239
|
+
result.setDate(result.getDate() + days);
|
|
240
|
+
return result;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const addHours = (date: Date, hours: number): Date => {
|
|
244
|
+
const result = new Date(date);
|
|
245
|
+
result.setHours(result.getHours() + hours);
|
|
246
|
+
return result;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export const getDaysDifference = (date1: Date, date2: Date): number => {
|
|
250
|
+
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
|
251
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export const isDateInRange = (date: Date, startDate: Date, endDate: Date): boolean => {
|
|
255
|
+
return date >= startDate && date <= endDate;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const isFutureDate = (date: Date): boolean => {
|
|
259
|
+
return date > new Date();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const isPastDate = (date: Date): boolean => {
|
|
263
|
+
return date < new Date();
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// ============================================================================
|
|
267
|
+
// DELAY & TIMING UTILITIES
|
|
268
|
+
// ============================================================================
|
|
269
|
+
|
|
270
|
+
export const delay = (ms: number): Promise<void> => {
|
|
271
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export const debounce = <T extends (...args: any[]) => any>(
|
|
275
|
+
fn: T,
|
|
276
|
+
wait: number
|
|
277
|
+
): ((...args: Parameters<T>) => void) => {
|
|
278
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
279
|
+
return (...args: Parameters<T>) => {
|
|
280
|
+
clearTimeout(timeoutId);
|
|
281
|
+
timeoutId = setTimeout(() => fn(...args), wait);
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const throttle = <T extends (...args: any[]) => any>(
|
|
286
|
+
fn: T,
|
|
287
|
+
limit: number
|
|
288
|
+
): ((...args: Parameters<T>) => void) => {
|
|
289
|
+
let inThrottle: boolean;
|
|
290
|
+
return (...args: Parameters<T>) => {
|
|
291
|
+
if (!inThrottle) {
|
|
292
|
+
fn(...args);
|
|
293
|
+
inThrottle = true;
|
|
294
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// ============================================================================
|
|
300
|
+
// ERROR HANDLING UTILITIES
|
|
301
|
+
// ============================================================================
|
|
302
|
+
|
|
303
|
+
export const tryParse = <T = any>(json: string, fallback: T): T => {
|
|
304
|
+
try {
|
|
305
|
+
return JSON.parse(json) as T;
|
|
306
|
+
} catch {
|
|
307
|
+
return fallback;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export const withErrorHandling = async <T>(
|
|
312
|
+
fn: () => Promise<T>
|
|
313
|
+
): Promise<{ success: boolean; data?: T; error?: string }> => {
|
|
314
|
+
try {
|
|
315
|
+
const data = await fn();
|
|
316
|
+
return { success: true, data };
|
|
317
|
+
} catch (error) {
|
|
318
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
319
|
+
return { success: false, error: message };
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export const createApiResponse = <T>(
|
|
324
|
+
success: boolean,
|
|
325
|
+
data?: T,
|
|
326
|
+
message?: string,
|
|
327
|
+
statusCode: number = 200
|
|
328
|
+
) => {
|
|
329
|
+
return {
|
|
330
|
+
success,
|
|
331
|
+
statusCode,
|
|
332
|
+
message: message || (success ? 'Success' : 'Error'),
|
|
333
|
+
data: data || (success ? {} : null),
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// RETRY UTILITIES
|
|
339
|
+
// ============================================================================
|
|
340
|
+
|
|
341
|
+
export const retry = async <T>(
|
|
342
|
+
fn: () => Promise<T>,
|
|
343
|
+
maxAttempts: number = 3,
|
|
344
|
+
delayMs: number = 1000
|
|
345
|
+
): Promise<T> => {
|
|
346
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
347
|
+
try {
|
|
348
|
+
return await fn();
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (attempt === maxAttempts) throw error;
|
|
351
|
+
await delay(delayMs * attempt);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
throw new Error('Retry failed');
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// ============================================================================
|
|
358
|
+
// TYPE CHECKING UTILITIES
|
|
359
|
+
// ============================================================================
|
|
360
|
+
|
|
361
|
+
export const isString = (val: unknown): val is string => typeof val === 'string';
|
|
362
|
+
export const isNumber = (val: unknown): val is number => typeof val === 'number';
|
|
363
|
+
export const isBoolean = (val: unknown): val is boolean => typeof val === 'boolean';
|
|
364
|
+
export const isArray = (val: unknown): val is unknown[] => Array.isArray(val);
|
|
365
|
+
export const isObject = (val: unknown): val is Record<string, unknown> =>
|
|
366
|
+
typeof val === 'object' && val !== null && !Array.isArray(val);
|
|
367
|
+
export const isFunction = (val: unknown): val is Function => typeof val === 'function';
|
|
368
|
+
export const isNull = (val: unknown): val is null => val === null;
|
|
369
|
+
export const isUndefined = (val: unknown): val is undefined => val === undefined;
|
|
370
|
+
export const isNullOrUndefined = (val: unknown): val is null | undefined =>
|
|
371
|
+
val === null || val === undefined;
|
|
372
|
+
|
|
373
|
+
export const formatIndianCompact = (num: number = 0) => {
|
|
374
|
+
if (num >= 1e7) {
|
|
375
|
+
return (num / 1e7).toFixed(2).replace(/\.00$/, '') + 'Cr';
|
|
376
|
+
}
|
|
377
|
+
if (num >= 1e5) {
|
|
378
|
+
return (num / 1e5).toFixed(2).replace(/\.00$/, '') + 'L';
|
|
379
|
+
}
|
|
380
|
+
return (
|
|
381
|
+
'₹' +
|
|
382
|
+
num?.toLocaleString('en-IN', {
|
|
383
|
+
minimumFractionDigits: 2,
|
|
384
|
+
maximumFractionDigits: 2,
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export function formatPrice(price: number): string {
|
|
390
|
+
return `₹${price?.toFixed(2)}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function formatPriceShort(price: number): string {
|
|
394
|
+
if (price >= 100000) {
|
|
395
|
+
return `₹${(price / 100000).toFixed(1)}L`;
|
|
396
|
+
} else if (price >= 1000) {
|
|
397
|
+
return `₹${(price / 1000).toFixed(1)}K`;
|
|
398
|
+
}
|
|
399
|
+
return `₹${price.toFixed(0)}`;
|
|
400
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
export {
|
|
3
|
+
storeAuth,
|
|
4
|
+
getStoredAuth,
|
|
5
|
+
isTokenExpired,
|
|
6
|
+
willTokenExpireSoon,
|
|
7
|
+
clearAuth,
|
|
8
|
+
validateUserRole,
|
|
9
|
+
getCurrentUser,
|
|
10
|
+
getAccessToken,
|
|
11
|
+
getRefreshToken,
|
|
12
|
+
updateAccessToken,
|
|
13
|
+
setupAutoRefresh,
|
|
14
|
+
type AuthTokens,
|
|
15
|
+
type StoredAuth,
|
|
16
|
+
} from './auth';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
setCookie,
|
|
20
|
+
getCookie,
|
|
21
|
+
removeCookie,
|
|
22
|
+
areCookiesEnabled,
|
|
23
|
+
AUTH_COOKIE_NAMES,
|
|
24
|
+
} from './cookies';
|
|
25
|
+
|
|
26
|
+
export * from './api';
|
|
27
|
+
export * from './helpers';
|
|
28
|
+
export * from './constants';
|
|
29
|
+
export * from './validation';
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-safe validation utilities
|
|
3
|
+
* These can be used in both frontend and backend
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { REGEX_PATTERNS, JWT_CONFIG } from '../constants';
|
|
7
|
+
|
|
8
|
+
// EMAIL VALIDATION
|
|
9
|
+
export const isValidEmail = (email: string): boolean => {
|
|
10
|
+
return REGEX_PATTERNS.EMAIL.test(email);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const validateEmail = (email: string): { valid: boolean; error?: string } => {
|
|
14
|
+
if (!email || email.trim() === '') {
|
|
15
|
+
return { valid: false, error: 'Email is required' };
|
|
16
|
+
}
|
|
17
|
+
if (!isValidEmail(email)) {
|
|
18
|
+
return { valid: false, error: 'Invalid email format' };
|
|
19
|
+
}
|
|
20
|
+
if (email.length > 255) {
|
|
21
|
+
return { valid: false, error: 'Email is too long' };
|
|
22
|
+
}
|
|
23
|
+
return { valid: true };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// PASSWORD VALIDATION
|
|
27
|
+
export const isValidPassword = (password: string): boolean => {
|
|
28
|
+
return JWT_CONFIG.PASSWORD_PATTERN.test(password);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const validatePassword = (password: string): { valid: boolean; error?: string } => {
|
|
32
|
+
if (!password) {
|
|
33
|
+
return { valid: false, error: 'Password is required' };
|
|
34
|
+
}
|
|
35
|
+
if (password.length < JWT_CONFIG.PASSWORD_MIN_LENGTH) {
|
|
36
|
+
return {
|
|
37
|
+
valid: false,
|
|
38
|
+
error: `Password must be at least ${JWT_CONFIG.PASSWORD_MIN_LENGTH} characters`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (!isValidPassword(password)) {
|
|
42
|
+
return {
|
|
43
|
+
valid: false,
|
|
44
|
+
error: 'Password must contain uppercase, lowercase, number, and special character (@$!%*?&)',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { valid: true };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// USERNAME/NAME VALIDATION
|
|
51
|
+
export const validateName = (name: string): { valid: boolean; error?: string } => {
|
|
52
|
+
if (!name || name.trim() === '') {
|
|
53
|
+
return { valid: false, error: 'Name is required' };
|
|
54
|
+
}
|
|
55
|
+
if (name.length < 2) {
|
|
56
|
+
return { valid: false, error: 'Name must be at least 2 characters' };
|
|
57
|
+
}
|
|
58
|
+
if (name.length > 100) {
|
|
59
|
+
return { valid: false, error: 'Name is too long' };
|
|
60
|
+
}
|
|
61
|
+
// Check for valid characters (letters, spaces, hyphens, apostrophes)
|
|
62
|
+
if (!/^[a-zA-Z\s'-]+$/.test(name)) {
|
|
63
|
+
return {
|
|
64
|
+
valid: false,
|
|
65
|
+
error: 'Name can only contain letters, spaces, hyphens, and apostrophes',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { valid: true };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// PHONE VALIDATION
|
|
72
|
+
export const isValidPhone = (phone: string): boolean => {
|
|
73
|
+
return REGEX_PATTERNS.PHONE.test(phone);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const validatePhone = (phone: string): { valid: boolean; error?: string } => {
|
|
77
|
+
if (!phone) {
|
|
78
|
+
return { valid: false, error: 'Phone number is required' };
|
|
79
|
+
}
|
|
80
|
+
if (!isValidPhone(phone)) {
|
|
81
|
+
return { valid: false, error: 'Invalid phone number format' };
|
|
82
|
+
}
|
|
83
|
+
return { valid: true };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// POSTAL CODE VALIDATION
|
|
87
|
+
export const isValidPostalCode = (postalCode: string): boolean => {
|
|
88
|
+
return REGEX_PATTERNS.POSTAL_CODE.test(postalCode);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const validatePostalCode = (postalCode: string): { valid: boolean; error?: string } => {
|
|
92
|
+
if (!postalCode) {
|
|
93
|
+
return { valid: false, error: 'Postal code is required' };
|
|
94
|
+
}
|
|
95
|
+
if (!isValidPostalCode(postalCode)) {
|
|
96
|
+
return { valid: false, error: 'Invalid postal code format' };
|
|
97
|
+
}
|
|
98
|
+
return { valid: true };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// COUPON CODE VALIDATION
|
|
102
|
+
export const isValidCouponCode = (code: string): boolean => {
|
|
103
|
+
return REGEX_PATTERNS.COUPON_CODE.test(code);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const validateCouponCode = (code: string): { valid: boolean; error?: string } => {
|
|
107
|
+
if (!code) {
|
|
108
|
+
return { valid: false, error: 'Coupon code is required' };
|
|
109
|
+
}
|
|
110
|
+
if (!isValidCouponCode(code)) {
|
|
111
|
+
return { valid: false, error: 'Invalid coupon code format (6-20 alphanumeric characters)' };
|
|
112
|
+
}
|
|
113
|
+
return { valid: true };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// PRICE VALIDATION
|
|
117
|
+
export const validatePrice = (price: any): { valid: boolean; error?: string } => {
|
|
118
|
+
const numPrice = parseFloat(price);
|
|
119
|
+
if (isNaN(numPrice)) {
|
|
120
|
+
return { valid: false, error: 'Price must be a valid number' };
|
|
121
|
+
}
|
|
122
|
+
if (numPrice < 0) {
|
|
123
|
+
return { valid: false, error: 'Price cannot be negative' };
|
|
124
|
+
}
|
|
125
|
+
if (numPrice > 999999.99) {
|
|
126
|
+
return { valid: false, error: 'Price exceeds maximum allowed value' };
|
|
127
|
+
}
|
|
128
|
+
return { valid: true };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// QUANTITY/STOCK VALIDATION
|
|
132
|
+
export const validateQuantity = (qty: any): { valid: boolean; error?: string } => {
|
|
133
|
+
const numQty = parseInt(qty, 10);
|
|
134
|
+
if (isNaN(numQty)) {
|
|
135
|
+
return { valid: false, error: 'Quantity must be a valid number' };
|
|
136
|
+
}
|
|
137
|
+
if (numQty < 0) {
|
|
138
|
+
return { valid: false, error: 'Quantity cannot be negative' };
|
|
139
|
+
}
|
|
140
|
+
if (!Number.isInteger(numQty)) {
|
|
141
|
+
return { valid: false, error: 'Quantity must be a whole number' };
|
|
142
|
+
}
|
|
143
|
+
return { valid: true };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// RATING VALIDATION
|
|
147
|
+
export const validateRating = (rating: any): { valid: boolean; error?: string } => {
|
|
148
|
+
const numRating = parseFloat(rating);
|
|
149
|
+
if (isNaN(numRating)) {
|
|
150
|
+
return { valid: false, error: 'Rating must be a valid number' };
|
|
151
|
+
}
|
|
152
|
+
if (numRating < 0 || numRating > 5) {
|
|
153
|
+
return { valid: false, error: 'Rating must be between 0 and 5' };
|
|
154
|
+
}
|
|
155
|
+
return { valid: true };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// DISCOUNT VALIDATION
|
|
159
|
+
export const validateDiscountPercentage = (discount: any): { valid: boolean; error?: string } => {
|
|
160
|
+
const numDiscount = parseFloat(discount);
|
|
161
|
+
if (isNaN(numDiscount)) {
|
|
162
|
+
return { valid: false, error: 'Discount must be a valid number' };
|
|
163
|
+
}
|
|
164
|
+
if (numDiscount < 0 || numDiscount > 100) {
|
|
165
|
+
return { valid: false, error: 'Discount percentage must be between 0 and 100' };
|
|
166
|
+
}
|
|
167
|
+
return { valid: true };
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// PAGINATION VALIDATION
|
|
171
|
+
export const validatePagination = (
|
|
172
|
+
page: any,
|
|
173
|
+
limit: any
|
|
174
|
+
): { valid: boolean; error?: string; page?: number; limit?: number } => {
|
|
175
|
+
const numPage = parseInt(page, 10) || 1;
|
|
176
|
+
const numLimit = parseInt(limit, 10) || 10;
|
|
177
|
+
|
|
178
|
+
if (numPage < 1) {
|
|
179
|
+
return { valid: false, error: 'Page must be greater than 0' };
|
|
180
|
+
}
|
|
181
|
+
if (numLimit < 1) {
|
|
182
|
+
return { valid: false, error: 'Limit must be greater than 0' };
|
|
183
|
+
}
|
|
184
|
+
if (numLimit > 100) {
|
|
185
|
+
return { valid: false, error: 'Limit cannot exceed 100' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { valid: true, page: numPage, limit: numLimit };
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// OBJECT ID VALIDATION (MongoDB ObjectId)
|
|
192
|
+
export const isValidObjectId = (id: string): boolean => {
|
|
193
|
+
return /^[0-9a-fA-F]{24}$/.test(id);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const validateObjectId = (
|
|
197
|
+
id: string,
|
|
198
|
+
fieldName: string = 'ID'
|
|
199
|
+
): { valid: boolean; error?: string } => {
|
|
200
|
+
if (!id) {
|
|
201
|
+
return { valid: false, error: `${fieldName} is required` };
|
|
202
|
+
}
|
|
203
|
+
if (!isValidObjectId(id)) {
|
|
204
|
+
return { valid: false, error: `Invalid ${fieldName} format` };
|
|
205
|
+
}
|
|
206
|
+
return { valid: true };
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// URL VALIDATION
|
|
210
|
+
export const isValidUrl = (url: string): boolean => {
|
|
211
|
+
return REGEX_PATTERNS.URL.test(url);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const validateUrl = (url: string): { valid: boolean; error?: string } => {
|
|
215
|
+
if (!url) {
|
|
216
|
+
return { valid: false, error: 'URL is required' };
|
|
217
|
+
}
|
|
218
|
+
if (!isValidUrl(url)) {
|
|
219
|
+
return { valid: false, error: 'Invalid URL format' };
|
|
220
|
+
}
|
|
221
|
+
return { valid: true };
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// PRODUCT SKU VALIDATION
|
|
225
|
+
export const isValidSku = (sku: string): boolean => {
|
|
226
|
+
return REGEX_PATTERNS.PRODUCT_SKU.test(sku);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const validateSku = (sku: string): { valid: boolean; error?: string } => {
|
|
230
|
+
if (!sku) {
|
|
231
|
+
return { valid: false, error: 'SKU is required' };
|
|
232
|
+
}
|
|
233
|
+
if (!isValidSku(sku)) {
|
|
234
|
+
return {
|
|
235
|
+
valid: false,
|
|
236
|
+
error: 'Invalid SKU format (3-20 alphanumeric characters with hyphens)',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return { valid: true };
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// DATE VALIDATION
|
|
243
|
+
export const validateDate = (date: any): { valid: boolean; error?: string } => {
|
|
244
|
+
const dateObj = new Date(date);
|
|
245
|
+
if (isNaN(dateObj.getTime())) {
|
|
246
|
+
return { valid: false, error: 'Invalid date format' };
|
|
247
|
+
}
|
|
248
|
+
return { valid: true };
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const validateDateRange = (
|
|
252
|
+
startDate: any,
|
|
253
|
+
endDate: any
|
|
254
|
+
): { valid: boolean; error?: string } => {
|
|
255
|
+
const start = new Date(startDate);
|
|
256
|
+
const end = new Date(endDate);
|
|
257
|
+
|
|
258
|
+
if (isNaN(start.getTime())) {
|
|
259
|
+
return { valid: false, error: 'Invalid start date' };
|
|
260
|
+
}
|
|
261
|
+
if (isNaN(end.getTime())) {
|
|
262
|
+
return { valid: false, error: 'Invalid end date' };
|
|
263
|
+
}
|
|
264
|
+
if (start > end) {
|
|
265
|
+
return { valid: false, error: 'Start date must be before end date' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { valid: true };
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// BATCH VALIDATION HELPER
|
|
272
|
+
export const batchValidate = (
|
|
273
|
+
validations: Array<{ valid: boolean; error?: string }>
|
|
274
|
+
): { valid: boolean; errors: string[] } => {
|
|
275
|
+
const errors: string[] = [];
|
|
276
|
+
|
|
277
|
+
for (const validation of validations) {
|
|
278
|
+
if (!validation.valid && validation.error) {
|
|
279
|
+
errors.push(validation.error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
valid: errors.length === 0,
|
|
285
|
+
errors,
|
|
286
|
+
};
|
|
287
|
+
};
|