Skip to content

Commit 2cb7186

Browse files
committed
feat: add more utility methods
1 parent b8b53c5 commit 2cb7186

20 files changed

Lines changed: 3602 additions & 247 deletions

src/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,18 @@ export const Config = {
2929
*/
3030
isoTimestamp: Env.getBoolean("LOGGER_ISO_TIMESTAMP", false),
3131
},
32+
33+
Http: {
34+
/**
35+
* Timeout for HTTP requests in milliseconds
36+
*/
37+
timeout: Env.getNumber("HTTP_TIMEOUT", 30000),
38+
},
39+
40+
Cache: {
41+
/**
42+
* Default TTL (time to live) for cache entries in seconds
43+
*/
44+
defaultTtl: Env.getNumber("CACHE_DEFAULT_TTL", 300),
45+
},
3246
};

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from "./utils/fs.utils";
1010
export * from "./utils/http-status-codes";
1111
export * from "./utils/id.utils";
1212
export * from "./utils/logger.utils";
13+
export * from "./utils/middleware.utils";
1314
export * from "./utils/obj.utils";
1415
export * from "./utils/response.utils";
1516
export * from "./utils/string.utils";

src/types/api-response.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,108 @@ export interface Pagination<T = any> {
5353
* Allows semantic naming like `PaginationResponse<User>` or `PaginationResponse<Post>`.
5454
*/
5555
export type PaginationResponse<T = any> = Pagination<T>;
56+
57+
/**
58+
* Error response structure with additional metadata.
59+
* Used for providing richer error information to clients.
60+
*/
61+
export interface ApiErrorResponse extends Omit<ApiResponse<never>, "data"> {
62+
/** Error always true for error responses */
63+
error: true;
64+
65+
/** HTTP status code */
66+
status: number;
67+
68+
/** Error code for client-side error handling */
69+
code?: string;
70+
71+
/** Optional detailed validation errors */
72+
details?: Record<string, string[]>;
73+
74+
/** Link to error documentation */
75+
docUrl?: string;
76+
}
77+
78+
/**
79+
* Success response structure with strongly typed data.
80+
* Used for providing successful responses to clients.
81+
*/
82+
export interface ApiSuccessResponse<T = any> extends ApiResponse<T> {
83+
/** Error always false for success responses */
84+
error: false;
85+
86+
/** HTTP status code (usually 200) */
87+
status?: number;
88+
}
89+
90+
/**
91+
* Response structure for batch operations.
92+
* Used when multiple operations are performed in a single request.
93+
*/
94+
export interface BatchResponse<T = any> {
95+
/** Overall success/failure indicator */
96+
success: boolean;
97+
98+
/** Total number of operations */
99+
total: number;
100+
101+
/** Number of successful operations */
102+
successful: number;
103+
104+
/** Number of failed operations */
105+
failed: number;
106+
107+
/** Results of individual operations */
108+
results: Array<{
109+
/** Identifier for this operation */
110+
id: string | number;
111+
112+
/** Success/failure indicator for this operation */
113+
success: boolean;
114+
115+
/** Response data for this operation */
116+
data?: T;
117+
118+
/** Error information if this operation failed */
119+
error?: {
120+
message: string;
121+
code?: string;
122+
};
123+
}>;
124+
}
125+
126+
/**
127+
* Response structure for asynchronous operations.
128+
* Used when the operation will complete in the future.
129+
*/
130+
export interface AsyncOperationResponse {
131+
/** Always true for async operations */
132+
async: true;
133+
134+
/** Job or task ID to check status later */
135+
jobId: string;
136+
137+
/** Estimated completion time in seconds (if known) */
138+
estimatedTime?: number;
139+
140+
/** URL to check status */
141+
statusUrl: string;
142+
}
143+
144+
/**
145+
* Response structure for streaming operations.
146+
* Used when data is returned as a stream rather than all at once.
147+
*/
148+
export interface StreamResponse {
149+
/** Stream identifier */
150+
streamId: string;
151+
152+
/** Stream type (e.g., 'json', 'binary') */
153+
streamType: string;
154+
155+
/** Total size in bytes (if known) */
156+
totalSize?: number;
157+
158+
/** Chunk size in bytes */
159+
chunkSize: number;
160+
}

src/utils/array.utils.ts

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import { getValueByPath } from "./obj.utils";
1010
* @throws {TypeError} If array is not an array.
1111
* @throws {Error} If chunk size is not a positive integer.
1212
*/
13-
export const chunk = <T>(array: T[], size: number): T[][] => {
13+
export function chunk<T>(array: T[], size: number): T[][] {
1414
if (!Array.isArray(array)) throw new TypeError("Expected an array");
1515
if (!array.length) return [];
1616
if (!Number.isInteger(size) || size <= 0)
1717
throw new Error("Chunk size must be a positive integer");
1818
return Array.from({ length: Math.ceil(array.length / size) }, (_, i) =>
1919
array.slice(i * size, i * size + size),
2020
);
21-
};
21+
}
2222

2323
/**
2424
* Removes duplicate values from an array.
@@ -229,3 +229,157 @@ export function mergeSort<T>(
229229
};
230230
return sort(array);
231231
}
232+
233+
/**
234+
* Combines multiple arrays into a single array of grouped elements.
235+
* Output array length equals the length of the shortest input array.
236+
*
237+
* @example zip([1, 2], ['a', 'b']) => [[1, 'a'], [2, 'b']]
238+
* @param {...Array<T>[]} arrays - Two or more arrays to zip together.
239+
* @returns {Array<T[]>} Array of grouped elements.
240+
*/
241+
export function zip<T>(...arrays: T[][]): T[][] {
242+
if (arrays.length === 0) return [];
243+
if (arrays.some((arr) => !Array.isArray(arr))) {
244+
throw new TypeError("All arguments must be arrays");
245+
}
246+
247+
const minLength = Math.min(...arrays.map((arr) => arr.length));
248+
const result: T[][] = [];
249+
250+
for (let i = 0; i < minLength; i++) {
251+
result.push(arrays.map((arr) => arr[i]));
252+
}
253+
254+
return result;
255+
}
256+
257+
/**
258+
* Splits an array into two arrays based on a predicate function.
259+
*
260+
* @template T The type of array elements.
261+
* @param {T[]} array - The input array.
262+
* @param {(item: T, index: number, array: T[]) => boolean} predicate - Function to test each element.
263+
* @returns {[T[], T[]]} A tuple of two arrays: [matched, unmatched].
264+
*/
265+
export function partition<T>(
266+
array: T[],
267+
predicate: (item: T, index: number, array: T[]) => boolean,
268+
): [T[], T[]] {
269+
if (!Array.isArray(array)) return [[], []];
270+
271+
return array.reduce(
272+
([pass, fail], item, index) => {
273+
return predicate(item, index, array)
274+
? [[...pass, item], fail]
275+
: [pass, [...fail, item]];
276+
},
277+
[[] as T[], [] as T[]],
278+
);
279+
}
280+
281+
/**
282+
* Generates an array of numbers within a specified range.
283+
*
284+
* @param {number} start - Start of range (inclusive).
285+
* @param {number} end - End of range (exclusive).
286+
* @param {number} [step=1] - Step between numbers.
287+
* @returns {number[]} Array of numbers in range.
288+
*/
289+
export function range(start: number, end: number, step: number = 1): number[] {
290+
if (
291+
!Number.isFinite(start) ||
292+
!Number.isFinite(end) ||
293+
!Number.isFinite(step)
294+
) {
295+
throw new TypeError("Arguments must be finite numbers");
296+
}
297+
if (step === 0) throw new Error("Step cannot be zero");
298+
299+
const isAscending = step > 0;
300+
if ((isAscending && start >= end) || (!isAscending && start <= end)) {
301+
return [];
302+
}
303+
304+
const length = Math.max(Math.ceil((end - start) / step), 0);
305+
const result = new Array(length);
306+
307+
for (let i = 0, value = start; i < length; i++, value += step) {
308+
result[i] = value;
309+
}
310+
311+
return result;
312+
}
313+
314+
/**
315+
* Returns the first n elements of an array.
316+
*
317+
* @template T The type of array elements.
318+
* @param {T[]} array - The input array.
319+
* @param {number} [n=1] - Number of elements to take.
320+
* @returns {T[]} New array with first n elements.
321+
*/
322+
export function take<T>(array: T[], n: number = 1): T[] {
323+
if (!Array.isArray(array) || n <= 0) return [];
324+
return array.slice(0, n);
325+
}
326+
327+
/**
328+
* Takes elements from the array while predicate returns true.
329+
*
330+
* @template T The type of array elements.
331+
* @param {T[]} array - The input array.
332+
* @param {(item: T, index: number) => boolean} predicate - Function to test each element.
333+
* @returns {T[]} New array with taken elements.
334+
*/
335+
export function takeWhile<T>(
336+
array: T[],
337+
predicate: (item: T, index: number) => boolean,
338+
): T[] {
339+
if (!Array.isArray(array)) return [];
340+
341+
const result: T[] = [];
342+
for (let i = 0; i < array.length; i++) {
343+
if (!predicate(array[i], i)) break;
344+
result.push(array[i]);
345+
}
346+
347+
return result;
348+
}
349+
350+
/**
351+
* Removes all falsy values from an array.
352+
* False, null, 0, "", undefined, and NaN are falsy.
353+
*
354+
* @template T The type of array elements.
355+
* @param {T[]} array - The input array.
356+
* @returns {NonNullable<T>[]} New array with falsy values removed.
357+
*/
358+
export function compact<T>(array: T[]): NonNullable<T>[] {
359+
if (!Array.isArray(array)) return [];
360+
return array.filter(Boolean) as NonNullable<T>[];
361+
}
362+
363+
/**
364+
* Counts array elements by a key function.
365+
*
366+
* @template T The type of array elements.
367+
* @param {T[]} array - The input array.
368+
* @param {(item: T) => string | number | symbol} keyFn - Function to generate count key.
369+
* @returns {Record<string, number>} Object with counts by key.
370+
*/
371+
export function countBy<T>(
372+
array: T[],
373+
keyFn: (item: T) => string | number | symbol,
374+
): Record<string, number> {
375+
if (!Array.isArray(array)) return {};
376+
377+
return array.reduce(
378+
(acc, item) => {
379+
const key = String(keyFn(item));
380+
acc[key] = (acc[key] || 0) + 1;
381+
return acc;
382+
},
383+
{} as Record<string, number>,
384+
);
385+
}

0 commit comments

Comments
 (0)