codeweaver 3.1.3 → 4.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.
- package/README.md +56 -73
- package/package.json +23 -1
- package/src/config.ts +17 -15
- package/src/constants.ts +1 -0
- package/src/core/aws/api-gateway.ts +187 -0
- package/src/core/aws/basic-types.ts +147 -0
- package/src/core/aws/dynamodb.ts +187 -0
- package/src/core/aws/index.ts +9 -0
- package/src/core/aws/lambda.ts +199 -0
- package/src/core/aws/message-broker.ts +167 -0
- package/src/core/aws/message.ts +259 -0
- package/src/core/aws/s3.ts +136 -0
- package/src/core/aws/utilities.ts +44 -0
- package/src/core/cache/basic-types.ts +17 -0
- package/src/core/cache/decorator.ts +72 -0
- package/src/core/cache/index.ts +4 -0
- package/src/core/cache/memory-cache.class.ts +119 -0
- package/src/{utilities/cache/redis-cache.ts → core/cache/redis-cache.class.ts} +58 -10
- package/src/core/container/basic-types.ts +10 -0
- package/src/{utilities → core/container}/container.ts +7 -17
- package/src/core/container/index.ts +2 -0
- package/src/{utilities → core/error}/error-handling.ts +1 -65
- package/src/core/error/index.ts +3 -0
- package/src/core/error/response-error.ts +45 -0
- package/src/core/error/send-http-error.ts +15 -0
- package/src/core/file/file-helpers.ts +166 -0
- package/src/core/file/index.ts +1 -0
- package/src/{utilities → core/helpers}/assignment.ts +2 -2
- package/src/core/helpers/comparison.ts +86 -0
- package/src/{utilities → core/helpers}/conversion.ts +2 -2
- package/src/core/helpers/decorators.ts +316 -0
- package/src/core/helpers/format.ts +9 -0
- package/src/core/helpers/index.ts +7 -0
- package/src/core/helpers/range.ts +67 -0
- package/src/core/helpers/types.ts +3 -0
- package/src/core/logger/index.ts +4 -0
- package/src/{utilities/logger/logger.config.ts → core/logger/winston-logger.config.ts} +1 -1
- package/src/{utilities → core}/logger/winston-logger.service.ts +3 -3
- package/src/core/message-broker/bullmq/basic-types.ts +67 -0
- package/src/core/message-broker/bullmq/broker.ts +141 -0
- package/src/core/message-broker/bullmq/index.ts +3 -0
- package/src/core/message-broker/bullmq/queue.ts +58 -0
- package/src/core/message-broker/bullmq/worker.ts +68 -0
- package/src/core/message-broker/kafka/basic-types.ts +45 -0
- package/src/core/message-broker/kafka/consumer.ts +95 -0
- package/src/core/message-broker/kafka/index.ts +3 -0
- package/src/core/message-broker/kafka/producer.ts +113 -0
- package/src/core/message-broker/rabitmq/basic-types.ts +44 -0
- package/src/core/message-broker/rabitmq/channel.ts +95 -0
- package/src/core/message-broker/rabitmq/consumer.ts +94 -0
- package/src/core/message-broker/rabitmq/index.ts +4 -0
- package/src/core/message-broker/rabitmq/producer.ts +100 -0
- package/src/core/message-broker/utilities.ts +50 -0
- package/src/core/middlewares/basic-types.ts +39 -0
- package/src/core/middlewares/decorators.ts +244 -0
- package/src/core/middlewares/index.ts +3 -0
- package/src/core/middlewares/middlewares.ts +246 -0
- package/src/core/parallel/index.ts +3 -0
- package/src/{utilities → core}/parallel/parallel.ts +11 -1
- package/src/core/rate-limit/basic-types.ts +43 -0
- package/src/core/rate-limit/index.ts +4 -0
- package/src/core/rate-limit/memory-store.ts +65 -0
- package/src/core/rate-limit/rate-limit.ts +134 -0
- package/src/core/rate-limit/redis-store.ts +141 -0
- package/src/core/retry/basic-types.ts +21 -0
- package/src/core/retry/decorator.ts +139 -0
- package/src/core/retry/index.ts +2 -0
- package/src/main.ts +6 -8
- package/src/routers/orders/index.router.ts +5 -1
- package/src/routers/orders/order.controller.ts +54 -64
- package/src/routers/products/index.router.ts +2 -1
- package/src/routers/products/product.controller.ts +33 -68
- package/src/routers/users/index.router.ts +1 -1
- package/src/routers/users/user.controller.ts +25 -50
- package/src/utilities/cache/memory-cache.ts +0 -74
- /package/src/{utilities → core}/logger/base-logger.interface.ts +0 -0
- /package/src/{utilities → core}/logger/logger.service.ts +0 -0
- /package/src/{utilities → core}/parallel/chanel.ts +0 -0
- /package/src/{utilities → core}/parallel/worker-pool.ts +0 -0
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Redis, RedisKey } from "ioredis";
|
|
2
|
+
import { AsyncCache } from "./basic-types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A Redis-backed cache adapter implementing AsyncCache<T | null>.
|
|
6
6
|
* This wraps a Redis instance and provides a similar API to MapAsyncCache.
|
|
7
7
|
*/
|
|
8
8
|
export class RedisCache<T> implements AsyncCache<T> {
|
|
9
|
+
private startTimeMs?: number;
|
|
10
|
+
|
|
9
11
|
public constructor(
|
|
10
|
-
private cache?: Redis,
|
|
11
12
|
private capacity?: number,
|
|
13
|
+
private durationMs?: number,
|
|
14
|
+
private cache?: Redis,
|
|
12
15
|
private namespace?: string
|
|
13
16
|
) {
|
|
17
|
+
this.startTimeMs = this.startTimeMs ?? Date.now();
|
|
18
|
+
this.durationMs = this.durationMs ?? Number.POSITIVE_INFINITY;
|
|
14
19
|
this.cache = this.cache ?? new Redis();
|
|
15
20
|
this.capacity = this.capacity ?? Number.POSITIVE_INFINITY;
|
|
16
21
|
this.namespace = this.namespace ?? "redis";
|
|
@@ -20,13 +25,55 @@ export class RedisCache<T> implements AsyncCache<T> {
|
|
|
20
25
|
return `${this.namespace}:${key}`;
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
public async size(): Promise<number> {
|
|
29
|
+
return await this.cache!.dbsize();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async keys(pattern: string): Promise<string[]> {
|
|
33
|
+
return await Array.fromAsync((await this.cache!.keys(pattern)) ?? []);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns all the field names (keys) in a Redis hash.
|
|
38
|
+
* @param key The Redis key (hash) to query.
|
|
39
|
+
* @returns An array of field names (keys) in the hash.
|
|
40
|
+
*/
|
|
41
|
+
public async hkeys(key: RedisKey): Promise<string[]> {
|
|
42
|
+
return await Array.fromAsync((await this.cache!.hkeys(key)) ?? []);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether the cache is still valid.
|
|
47
|
+
* This is a computed property that returns true if the cache
|
|
48
|
+
* has not yet expired, and false otherwise.
|
|
49
|
+
* @returns true if the cache is still valid, false otherwise
|
|
50
|
+
*/
|
|
51
|
+
public get isValid(): boolean {
|
|
52
|
+
return this.startTimeMs! + this.durationMs! > Date.now();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resets the cache if it has expired.
|
|
57
|
+
* If the cache has expired, resets the cache by clearing it and updating the start time.
|
|
58
|
+
* @returns true if the cache was reset, false otherwise.
|
|
59
|
+
*/
|
|
60
|
+
public async resetIfExpired(): Promise<boolean> {
|
|
61
|
+
if (this.isValid == false) {
|
|
62
|
+
await this.clear();
|
|
63
|
+
this.startTimeMs = Date.now();
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
23
69
|
/**
|
|
24
70
|
* Asynchronously set a value by key.
|
|
25
71
|
* If value is null, delete the key to represent "not present".
|
|
26
72
|
* Capacity is not strictly enforced in Redis on a per-key basis here; you
|
|
27
73
|
* could leverage Redis max memory and eviction policies for that.
|
|
28
74
|
*/
|
|
29
|
-
async set(key: string, value: T): Promise<void> {
|
|
75
|
+
public async set(key: string, value: T): Promise<void> {
|
|
76
|
+
if ((await this.resetIfExpired()) == true) return;
|
|
30
77
|
const k = this.keyFor(key);
|
|
31
78
|
|
|
32
79
|
if (value != null) {
|
|
@@ -50,7 +97,7 @@ export class RedisCache<T> implements AsyncCache<T> {
|
|
|
50
97
|
* - If the key exists, returns the parsed value.
|
|
51
98
|
* - If the key does not exist, returns null to satisfy Promise<T | null>.
|
|
52
99
|
*/
|
|
53
|
-
async get(key: string): Promise<T> {
|
|
100
|
+
public async get(key: string): Promise<T> {
|
|
54
101
|
const k = this.keyFor(key);
|
|
55
102
|
const raw = (await this.cache?.get(k)) ?? null;
|
|
56
103
|
if (raw == null) {
|
|
@@ -66,14 +113,15 @@ export class RedisCache<T> implements AsyncCache<T> {
|
|
|
66
113
|
/**
|
|
67
114
|
* Asynchronously delete a value by key.
|
|
68
115
|
*/
|
|
69
|
-
async delete(key: string): Promise<void> {
|
|
116
|
+
public async delete(key: string): Promise<void> {
|
|
70
117
|
await this.cache?.del(this.keyFor(key));
|
|
71
118
|
}
|
|
72
119
|
|
|
73
120
|
/**
|
|
74
121
|
* Asynchronously check if a key exists in the cache.
|
|
75
122
|
*/
|
|
76
|
-
async has(key: string): Promise<boolean> {
|
|
123
|
+
public async has(key: string): Promise<boolean> {
|
|
124
|
+
if ((await this.resetIfExpired()) == true) return false;
|
|
77
125
|
const exists = await this.cache?.exists(this.keyFor(key));
|
|
78
126
|
return exists === 1;
|
|
79
127
|
}
|
|
@@ -82,10 +130,10 @@ export class RedisCache<T> implements AsyncCache<T> {
|
|
|
82
130
|
* Asynchronously clear the cache (namespace-scoped).
|
|
83
131
|
* Use with caution in a shared Redis instance.
|
|
84
132
|
*/
|
|
85
|
-
async clear(key: string): Promise<void> {
|
|
133
|
+
public async clear(key: string = ""): Promise<void> {
|
|
86
134
|
// Optional: implement namespace-wide clear if needed
|
|
87
135
|
// This simple example uses a pattern-based approach for a full clear.
|
|
88
|
-
const pattern = `${this.namespace}
|
|
136
|
+
const pattern = `${this.namespace}:*`;
|
|
89
137
|
const stream = this.cache?.scanStream({ match: pattern });
|
|
90
138
|
const pipeline = this.cache?.pipeline();
|
|
91
139
|
stream?.on("data", (keys: string[]) => {
|
|
@@ -105,7 +153,7 @@ export class RedisCache<T> implements AsyncCache<T> {
|
|
|
105
153
|
/**
|
|
106
154
|
* Optional: gracefully close the Redis connection.
|
|
107
155
|
*/
|
|
108
|
-
async close(): Promise<void> {
|
|
156
|
+
public async close(): Promise<void> {
|
|
109
157
|
await this.cache?.quit();
|
|
110
158
|
}
|
|
111
159
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Constructor<T = any> = new (...args: any[]) => T;
|
|
2
|
+
|
|
3
|
+
export interface Provider<T = any> {
|
|
4
|
+
/** The concrete class to instantiate (may be the same as token or a different implementation). */
|
|
5
|
+
useClass: Constructor<T>;
|
|
6
|
+
/** Cached singleton instance, if one has been created. */
|
|
7
|
+
instance?: T;
|
|
8
|
+
/** Whether to treat the provider as a singleton. Defaults to true. */
|
|
9
|
+
singleton?: boolean;
|
|
10
|
+
}
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
|
+
import { Constructor, Provider } from "./basic-types";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
interface Provider<T = any> {
|
|
6
|
-
/** The concrete class to instantiate (may be the same as token or a different implementation). */
|
|
7
|
-
useClass: Constructor<T>;
|
|
8
|
-
/** Cached singleton instance, if one has been created. */
|
|
9
|
-
instance?: T;
|
|
10
|
-
/** Whether to treat the provider as a singleton. Defaults to true. */
|
|
11
|
-
singleton?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const InjectableRegistry: Array<Constructor<any>> = [];
|
|
4
|
+
const injectableRegistry: Array<Constructor<any>> = [];
|
|
15
5
|
|
|
16
6
|
/**
|
|
17
7
|
* A tiny dependency injection container.
|
|
@@ -39,7 +29,7 @@ class Container {
|
|
|
39
29
|
* - useClass?: The concrete class to instantiate for this token.
|
|
40
30
|
* - singleton?: Whether to reuse a single instance (default: true).
|
|
41
31
|
*/
|
|
42
|
-
register<T>(
|
|
32
|
+
public register<T>(
|
|
43
33
|
token: Constructor<T>,
|
|
44
34
|
options?: { useClass?: Constructor<T>; singleton?: boolean }
|
|
45
35
|
): void {
|
|
@@ -61,7 +51,7 @@ class Container {
|
|
|
61
51
|
* @param token - The token (class constructor) to resolve.
|
|
62
52
|
* @returns An instance of the requested type T.
|
|
63
53
|
*/
|
|
64
|
-
resolve<T>(token: Constructor<T>): T {
|
|
54
|
+
public resolve<T>(token: Constructor<T>): T {
|
|
65
55
|
const provider = this.registrations.get(token);
|
|
66
56
|
|
|
67
57
|
if (!provider) {
|
|
@@ -113,7 +103,7 @@ class Container {
|
|
|
113
103
|
* metadata is actually readable.
|
|
114
104
|
*/
|
|
115
105
|
function bootstrapContainer(container: Container) {
|
|
116
|
-
for (const constructorFunction of
|
|
106
|
+
for (const constructorFunction of injectableRegistry) {
|
|
117
107
|
// Read per-class options if you added metadata
|
|
118
108
|
const meta = Reflect.getMetadata("di:injectable", constructorFunction) as
|
|
119
109
|
| { singleton?: boolean }
|
|
@@ -135,14 +125,14 @@ export function Injectable(options?: {
|
|
|
135
125
|
}): ClassDecorator {
|
|
136
126
|
return (target: any) => {
|
|
137
127
|
// Push into registry for later registration
|
|
138
|
-
|
|
128
|
+
injectableRegistry.push(target);
|
|
139
129
|
// You could also attach metadata if you want to customize per-class
|
|
140
130
|
Reflect.defineMetadata("di:injectable", options ?? {}, target);
|
|
141
131
|
};
|
|
142
132
|
}
|
|
143
133
|
|
|
144
134
|
/** A single, shared DI container for the app. */
|
|
145
|
-
const container = new Container();
|
|
135
|
+
export const container = new Container();
|
|
146
136
|
bootstrapContainer(container);
|
|
147
137
|
|
|
148
138
|
/**
|
|
@@ -1,68 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Represents a standardized error response structure for API endpoints.
|
|
5
|
-
*
|
|
6
|
-
* This class models an API-friendly error, carrying a human message plus
|
|
7
|
-
* optional metadata (status, details, input, code, stack). Extends the built-in Error
|
|
8
|
-
* so it works naturally with try/catch blocks.
|
|
9
|
-
*/
|
|
10
|
-
export class ResponseError extends Error {
|
|
11
|
-
public constructor(
|
|
12
|
-
/**
|
|
13
|
-
* User-facing error message describing what went wrong.
|
|
14
|
-
*/
|
|
15
|
-
public message: string,
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Optional HTTP status code related to the error (e.g., 400, 404, 500).
|
|
19
|
-
*/
|
|
20
|
-
public status?: number,
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Optional human-readable details or context about the error.
|
|
24
|
-
*/
|
|
25
|
-
public details?: string,
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Optional input value that caused the error (useful for logging/diagnostics).
|
|
29
|
-
*/
|
|
30
|
-
public input?: string,
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Optional application-specific error code (e.g., "INVALID_INPUT").
|
|
34
|
-
*/
|
|
35
|
-
public code?: string,
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Optional stack trace string (usually provided by runtime).
|
|
39
|
-
* Note: In many environments, stack is inherited from Error; you may
|
|
40
|
-
* not need to redefine it here unless you have a specific reason.
|
|
41
|
-
*/
|
|
42
|
-
public stack?: string
|
|
43
|
-
) {
|
|
44
|
-
// Ensure the base Error class gets the message for standard properties like name, stack, etc.
|
|
45
|
-
super(message);
|
|
46
|
-
|
|
47
|
-
// If a custom stack is provided, you might assign it; otherwise, the runtime stack will be used.
|
|
48
|
-
if (stack) {
|
|
49
|
-
this.stack = stack;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Sends a standardized HTTP error response.
|
|
56
|
-
*
|
|
57
|
-
* This function sets the response status from the provided error (defaulting to 500)
|
|
58
|
-
* and serializes the error object as JSON.
|
|
59
|
-
*
|
|
60
|
-
* @param res - Express Response object to send the error on
|
|
61
|
-
* @param error - Error details to return to the client (must include status or default will be 500)
|
|
62
|
-
*/
|
|
63
|
-
export function sendHttpError(res: Response, error: ResponseError): void {
|
|
64
|
-
res.status(error.status ?? 500).json(error);
|
|
65
|
-
}
|
|
1
|
+
import { ResponseError } from "./response-error";
|
|
66
2
|
|
|
67
3
|
/**
|
|
68
4
|
* A generic alias representing a tuple of [result, error].
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a standardized error response structure for API endpoints.
|
|
3
|
+
*
|
|
4
|
+
* This class models an API-friendly error, carrying a human message plus
|
|
5
|
+
* optional metadata (status, details, input, code, stack). Extends the built-in Error
|
|
6
|
+
* so it works naturally with try/catch blocks.
|
|
7
|
+
*/
|
|
8
|
+
export class ResponseError extends Error {
|
|
9
|
+
public constructor(
|
|
10
|
+
/**
|
|
11
|
+
* User-facing error message describing what went wrong.
|
|
12
|
+
*/
|
|
13
|
+
public override message: string,
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Optional HTTP status code related to the error (e.g., 400, 404, 500).
|
|
17
|
+
*/
|
|
18
|
+
public status?: number,
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Optional human-readable details or context about the error.
|
|
22
|
+
*/
|
|
23
|
+
public details?: string,
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional input value that caused the error (useful for logging/diagnostics).
|
|
27
|
+
*/
|
|
28
|
+
public input?: string,
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Optional application-specific error code (e.g., "INVALID_INPUT").
|
|
32
|
+
*/
|
|
33
|
+
public code?: string,
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Optional stack trace string (usually provided by runtime).
|
|
37
|
+
* Note: In many environments, stack is inherited from Error; you may
|
|
38
|
+
* not need to redefine it here unless you have a specific reason.
|
|
39
|
+
*/
|
|
40
|
+
public override stack?: string
|
|
41
|
+
) {
|
|
42
|
+
// Ensure the base Error class gets the message for standard properties like name, stack, etc.
|
|
43
|
+
super(message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Response } from "express";
|
|
2
|
+
import { ResponseError } from "./response-error";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sends a standardized HTTP error response.
|
|
6
|
+
*
|
|
7
|
+
* This function sets the response status from the provided error (defaulting to 500)
|
|
8
|
+
* and serializes the error object as JSON.
|
|
9
|
+
*
|
|
10
|
+
* @param res - Express Response object to send the error on
|
|
11
|
+
* @param error - Error details to return to the client (must include status or default will be 500)
|
|
12
|
+
*/
|
|
13
|
+
export function sendHttpError(res: Response, error: ResponseError): void {
|
|
14
|
+
res.status(error.status ?? 500).json(error);
|
|
15
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A union type representing valid JSON-like values.
|
|
7
|
+
* - Primitives: string, number, boolean, null
|
|
8
|
+
* - Objects: { [key: string]: JsonValue }
|
|
9
|
+
* - Arrays: JsonValue[]
|
|
10
|
+
*/
|
|
11
|
+
export type JsonValue =
|
|
12
|
+
| string
|
|
13
|
+
| number
|
|
14
|
+
| boolean
|
|
15
|
+
| null
|
|
16
|
+
| { [key: string]: JsonValue }
|
|
17
|
+
| JsonValue[];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads a file and parses its contents as JSON or YAML depending on the file extension.
|
|
21
|
+
*
|
|
22
|
+
* - If the extension is ".json", parses with JSON.parse.
|
|
23
|
+
* - If the extension is ".yaml" or ".yml", parses with js-yaml's YAML loader.
|
|
24
|
+
* - For any other extension, throws an error indicating unsupported extension.
|
|
25
|
+
*
|
|
26
|
+
* @template T - The expected return type (defaults to JsonValue).
|
|
27
|
+
* @param {string} filePath - The path to the file to read.
|
|
28
|
+
* @returns {Promise<T>} The parsed value typed as T.
|
|
29
|
+
* @throws {Error} If the file cannot be read, the extension is unsupported, or parsing fails.
|
|
30
|
+
*
|
|
31
|
+
* Edge cases:
|
|
32
|
+
* - If the YAML/JSON content does not match the requested T, a runtime cast is performed.
|
|
33
|
+
* - If the file does not exist, an error is thrown by the underlying readFile call.
|
|
34
|
+
*/
|
|
35
|
+
export async function readJsonOrYaml<T = JsonValue>(
|
|
36
|
+
filePath: string
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const data = await fs.readFile(filePath, "utf-8");
|
|
42
|
+
if (ext === ".json") {
|
|
43
|
+
return JSON.parse(data) as T;
|
|
44
|
+
} else if (ext === ".yaml" || ext === ".yml") {
|
|
45
|
+
return yaml.load(data) as T;
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Unsupported file extension: ${ext}. Only .json, .yaml, .yml are supported.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new Error(`Failed to read ${filePath}: ${(err as Error).message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Serializes data to JSON or YAML and writes it to disk, based on the file extension.
|
|
58
|
+
*
|
|
59
|
+
* - If the extension is ".json", the data is serialized with JSON.stringify (pretty-printed with 2 spaces).
|
|
60
|
+
* - If the extension is ".yaml" or ".yml", the data is serialized with js-yaml's dump.
|
|
61
|
+
* - For any other extension, throws an error indicating unsupported extension.
|
|
62
|
+
*
|
|
63
|
+
* @template T - Type of the data to write (defaults to JsonValue).
|
|
64
|
+
* @param {string} filePath - The path to write the file to.
|
|
65
|
+
* @param {T} data - The data to serialize and write.
|
|
66
|
+
* @returns {Promise<void>} Resolves when the write completes.
|
|
67
|
+
* @throws {Error} If the extension is unsupported or the write fails.
|
|
68
|
+
*/
|
|
69
|
+
export async function writeJsonOrYaml<T = JsonValue>(
|
|
70
|
+
filePath: string,
|
|
71
|
+
data: T
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const serialized =
|
|
77
|
+
ext === ".json"
|
|
78
|
+
? JSON.stringify(data, null, 2)
|
|
79
|
+
: ext === ".yaml" || ext === ".yml"
|
|
80
|
+
? yaml.dump(data)
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
if (serialized === null) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Unsupported file extension: ${ext}. Only .json, .yaml, .yml are supported.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await fs.writeFile(filePath, serialized, "utf-8");
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new Error(`Failed to write ${filePath}: ${(err as Error).message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Updates a top-level key in a JSON/YAML file. If the file does not exist, starts with an empty object.
|
|
97
|
+
*
|
|
98
|
+
* - Loads existing data from the file (JSON or YAML) if present and valid.
|
|
99
|
+
* - If the file does not exist, initializes data as an empty object.
|
|
100
|
+
* - Sets data[key] = value and saves the updated object back to disk.
|
|
101
|
+
* - If the existing content is not an object, it will be replaced with a new object.
|
|
102
|
+
*
|
|
103
|
+
* @template K extends string - The key type (string literal type or string).
|
|
104
|
+
* @template V extends JsonValue - The value to assign to the key.
|
|
105
|
+
* @param {string} filePath - The path to the file to update.
|
|
106
|
+
* @param {K} key - The key to set on the root object.
|
|
107
|
+
* @param {V} value - The value to assign to the key.
|
|
108
|
+
* @returns {Promise<void>} Resolves when the update/write completes.
|
|
109
|
+
* @throws {Error} If reading the file fails with an unsupported extension, or writing fails.
|
|
110
|
+
*
|
|
111
|
+
* Notes:
|
|
112
|
+
* - This function operates at the top level only (not nested paths).
|
|
113
|
+
*/
|
|
114
|
+
export async function updateJsonOrYaml<K extends string, V extends JsonValue>(
|
|
115
|
+
filePath: string,
|
|
116
|
+
key: K,
|
|
117
|
+
value: V
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
120
|
+
|
|
121
|
+
// Load existing data
|
|
122
|
+
let data: any;
|
|
123
|
+
try {
|
|
124
|
+
if (ext === ".json" || ext === ".yaml" || ext === ".yml") {
|
|
125
|
+
data = await readJsonOrYaml<any>(filePath);
|
|
126
|
+
} else {
|
|
127
|
+
throw new Error(`Unsupported file extension: ${ext}.`);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
// If file doesn't exist, start with an empty object
|
|
131
|
+
if (
|
|
132
|
+
(err as Error).message.includes("Failed to read") &&
|
|
133
|
+
(await fileExists(filePath)) === false
|
|
134
|
+
) {
|
|
135
|
+
data = {};
|
|
136
|
+
} else {
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Ensure we have an object to update
|
|
142
|
+
if (typeof data !== "object" || data === null) {
|
|
143
|
+
data = {} as Record<string, any>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Update the key
|
|
147
|
+
data[key] = value;
|
|
148
|
+
|
|
149
|
+
// Save back
|
|
150
|
+
await writeJsonOrYaml(filePath, data);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Helper to check if a file exists.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} filePath - The path to check.
|
|
157
|
+
* @returns {Promise<boolean>} True if the file exists, false otherwise.
|
|
158
|
+
*/
|
|
159
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
160
|
+
try {
|
|
161
|
+
await fs.access(filePath);
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./file-helpers";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { parallelMap } from "
|
|
2
|
+
import { parallelMap } from "../parallel/parallel";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Strictly assign obj (type T1) to T2 using a Zod schema.
|
|
@@ -13,7 +13,7 @@ import { parallelMap } from "./parallel/parallel";
|
|
|
13
13
|
* @param schema - Zod schema describing the target type T2
|
|
14
14
|
* @returns T2 representing the destination after assignment
|
|
15
15
|
*/
|
|
16
|
-
export
|
|
16
|
+
export async function assign<T1 extends object, T2 extends object>(
|
|
17
17
|
destination: T2,
|
|
18
18
|
source: T1,
|
|
19
19
|
destinationSchema?: z.ZodObject<any>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two values for deep equality using parallel, recursive checks.
|
|
3
|
+
*
|
|
4
|
+
* This function performs a structural equality check between two values.
|
|
5
|
+
* It handles primitives, null, Date, RegExp, Arrays, and plain objects.
|
|
6
|
+
* Object properties are checked in parallel via Promise.all for potential
|
|
7
|
+
* performance benefits on large objects.
|
|
8
|
+
*
|
|
9
|
+
* Notes:
|
|
10
|
+
* - For objects, keys must match exactly; values are compared recursively.
|
|
11
|
+
* - For Arrays, element order and values must match.
|
|
12
|
+
* - For Dates, times (getTime) must be identical.
|
|
13
|
+
* - For RegExp, string representations (via toString) must match.
|
|
14
|
+
* - Functions are compared by reference (i.e., a === b) since they cannot be
|
|
15
|
+
* meaningfully “deep-equal” compared here.
|
|
16
|
+
* - If you enable a cycle-detection mechanism, circular references are handled
|
|
17
|
+
* by tracking seen pairs to avoid infinite recursion.
|
|
18
|
+
*
|
|
19
|
+
* @template T - Type of the first value (and, by structural typing, the second).
|
|
20
|
+
* @param a - The first value to compare.
|
|
21
|
+
* @param b - The second value to compare.
|
|
22
|
+
* @returns A Promise that resolves to true if `a` and `b` are deeply equal, otherwise false.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* true: simple primitives
|
|
26
|
+
* await equal(1, 1); // true
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* true: identical objects
|
|
30
|
+
* await equal({ x: 1, y: [2, 3] }, { x: 1, y: [2, 3] }); // true
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* false: different structure
|
|
34
|
+
* await equal({ a: 1 }, { a: 1, b: 2 }); // false
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* false: different array contents
|
|
38
|
+
* await equal([1, 2], [1, 3]); // false
|
|
39
|
+
*/
|
|
40
|
+
export async function equal<T>(
|
|
41
|
+
a: T,
|
|
42
|
+
b: T,
|
|
43
|
+
_seen = new WeakMap<object, WeakMap<object, boolean>>()
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
if (a === b) return true;
|
|
46
|
+
if (typeof a !== typeof b) return false;
|
|
47
|
+
if (a === null || b === null) return a === b;
|
|
48
|
+
|
|
49
|
+
const ta = typeof a;
|
|
50
|
+
if (ta !== "object") return a === b;
|
|
51
|
+
|
|
52
|
+
// Cycle detection for objects
|
|
53
|
+
const A = a as object;
|
|
54
|
+
const B = b as object;
|
|
55
|
+
if (_seen.has(A)) {
|
|
56
|
+
const inner = _seen.get(A)!;
|
|
57
|
+
if (inner.has(B as object)) return inner.get(B as object)!;
|
|
58
|
+
} else {
|
|
59
|
+
_seen.set(A, new WeakMap<object, boolean>());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Update trace
|
|
63
|
+
_seen.get(A)!.set(B as object, true);
|
|
64
|
+
|
|
65
|
+
if (a instanceof Date && b instanceof Date)
|
|
66
|
+
return a.getTime() === b.getTime();
|
|
67
|
+
if (a instanceof RegExp && b instanceof RegExp)
|
|
68
|
+
return a.toString() === b.toString();
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
71
|
+
if (a.length !== (b as any).length) return false;
|
|
72
|
+
const checks = a.map((_, i) => equal((a as any)[i], (b as any)[i], _seen));
|
|
73
|
+
const results = await Promise.all(checks);
|
|
74
|
+
return results.every(Boolean);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const aKeys = Object.keys(a as Object);
|
|
78
|
+
const bKeys = Object.keys(b as Object);
|
|
79
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
80
|
+
const keySet = new Set<string>(aKeys);
|
|
81
|
+
for (const k of bKeys) if (!keySet.has(k)) return false;
|
|
82
|
+
|
|
83
|
+
const checks = aKeys.map((k) => equal((a as any)[k], (b as any)[k], _seen));
|
|
84
|
+
const results = await Promise.all(checks);
|
|
85
|
+
return results.every(Boolean);
|
|
86
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z, ZodRawShape } from "zod";
|
|
2
|
-
import { parallelMap } from "
|
|
2
|
+
import { parallelMap } from "../parallel/parallel";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Helper: normalize and validate a numeric string for integer parsing.
|
|
@@ -11,7 +11,7 @@ function parseIntegerStrict(input: string): number {
|
|
|
11
11
|
|
|
12
12
|
// Empty or just sign is invalid
|
|
13
13
|
if (s.length === 0 || s === "+" || s === "-") {
|
|
14
|
-
throw new TypeError("Invalid
|
|
14
|
+
throw new TypeError("Invalid sign");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// Use a regex to ensure the entire string is an optional sign followed by digits
|