codeweaver 2.0.0 → 2.1.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.
@@ -1,89 +1,35 @@
1
1
  import {
2
- User,
3
2
  ZodUserCreationDto,
4
3
  UserCreationDto,
5
4
  UserDto,
5
+ ZodUserDto,
6
6
  } from "./dto/user.dto";
7
7
  import { memoizeAsync, onError, rateLimit, timeout } from "utils-decorators";
8
- import { Validate, ZodInput } from "ts-zod4-decorators";
9
- import { ResponseError } from "@/utilities/types";
10
- import { parseId } from "@/utilities/error-handling";
8
+ import { ResponseError } from "@/utilities/error-handling";
9
+ import { convert, stringToInteger } from "@/utilities/conversion";
11
10
  import config from "@/config";
12
-
13
- // Array to store users (as a mock database)
14
- const users = [
15
- {
16
- id: 1,
17
- username: "johndoe",
18
- email: "johndoe@gmail.com",
19
- password: "S3cur3P@ssw0rd",
20
- },
21
- {
22
- id: 2,
23
- username: "janesmith",
24
- email: "janesmith@yahoo.com",
25
- password: "P@ssw0rd2024",
26
- },
27
- {
28
- id: 3,
29
- username: "michael89",
30
- email: "michael89@hotmail.com",
31
- password: "M1chael!2024",
32
- },
33
- {
34
- id: 4,
35
- username: "lisa.wong",
36
- email: "lisa.wong@example.com",
37
- password: "L1saW0ng!2024",
38
- },
39
- {
40
- id: 5,
41
- username: "alex_k",
42
- email: "alex.k@gmail.com",
43
- password: "A1ex#Key2024",
44
- },
45
- {
46
- id: 6,
47
- username: "emilyj",
48
- email: "emilyj@hotmail.com",
49
- password: "Em!ly0101",
50
- },
51
- {
52
- id: 7,
53
- username: "davidparker",
54
- email: "david.parker@yahoo.com",
55
- password: "D@v!d2024",
56
- },
57
- {
58
- id: 8,
59
- username: "sophia_m",
60
- email: "sophia.m@gmail.com",
61
- password: "Sophi@2024",
62
- },
63
- {
64
- id: 9,
65
- username: "chrisw",
66
- email: "chrisw@outlook.com",
67
- password: "Chri$Wong21",
68
- },
69
- {
70
- id: 10,
71
- username: "natalie_b",
72
- email: "natalie_b@gmail.com",
73
- password: "N@talie#B2024",
74
- },
75
- ];
11
+ import { users } from "@/db";
12
+ import { User } from "@/entities/user.entity";
13
+ import { MapAsyncCache } from "@/utilities/cache/memory-cache";
76
14
 
77
15
  function exceedHandler() {
78
16
  const message = "Too much call in allowed window";
79
17
  throw new ResponseError(message, 429);
80
18
  }
81
19
 
82
- function getUserErrorHandler(e: Error) {
20
+ function userNotFoundHandler(e: ResponseError) {
83
21
  const message = "User not found.";
84
- throw new ResponseError(message, 404, e.message);
22
+ throw new ResponseError(message, 404, e?.message);
23
+ }
24
+
25
+ function invalidInputHandler(e: ResponseError) {
26
+ const message = "Invalid input";
27
+ throw new ResponseError(message, 400, e?.message);
85
28
  }
86
29
 
30
+ const usersCache = new MapAsyncCache<UserDto[]>(config.cacheSize);
31
+ const userCache = new MapAsyncCache<UserDto>(config.cacheSize);
32
+
87
33
  /**
88
34
  * Controller for handling user-related operations
89
35
  * @class UserController
@@ -92,62 +38,93 @@ function getUserErrorHandler(e: Error) {
92
38
  export default class UserController {
93
39
  // constructor(private readonly userService: UserService) { }
94
40
 
41
+ @onError({
42
+ func: invalidInputHandler,
43
+ })
44
+ /**
45
+ * Validates a string ID and converts it to a number.
46
+ *
47
+ * @param {string} id - The ID to validate and convert.
48
+ * @returns {number} The numeric value of the provided ID.
49
+ */
50
+ public async validateId(id: string): Promise<number> {
51
+ return stringToInteger(id);
52
+ }
53
+
54
+ @onError({
55
+ func: invalidInputHandler,
56
+ })
57
+ /**
58
+ * Validates and creates a new User from the given DTO.
59
+ *
60
+ * @param {UserCreationDto} user - The incoming UserCreationDto to validate and transform.
61
+ * @returns {User} A fully formed User object ready for persistence.
62
+ */
63
+ public async validateUserCreationDto(user: UserCreationDto): Promise<User> {
64
+ const newUser = await ZodUserCreationDto.parseAsync(user);
65
+ return { ...newUser, id: users.length + 1 };
66
+ }
67
+
95
68
  @rateLimit({
96
69
  timeSpanMs: config.rateLimitTimeSpan,
97
70
  allowedCalls: config.rateLimitAllowedCalls,
98
71
  exceedHandler,
99
72
  })
100
- @Validate
101
73
  /**
102
74
  * Create a new user
103
- * @param {UserCreationDto} user - User creation data validated by Zod schema
75
+ * @param {User} user - User creation data validated by Zod schema
104
76
  * @returns {Promise<void>}
105
77
  * @throws {ResponseError} 500 - When rate limit exceeded
106
78
  * @throws {ResponseError} 400 - Invalid input data
107
79
  */
108
- public async create(@ZodInput(ZodUserCreationDto) user: UserCreationDto) {
109
- users.push({ ...user, id: users.length + 1 });
80
+ public async create(user: User): Promise<void> {
81
+ users.push(user);
82
+ await userCache.set(user.id.toString(), user as User);
83
+ await usersCache.delete("key");
110
84
  }
111
85
 
112
- @memoizeAsync(config.memoizeTime)
113
- @onError({
114
- func: getUserErrorHandler,
86
+ @memoizeAsync({
87
+ cache: usersCache,
88
+ keyResolver: () => "key",
89
+ expirationTimeMs: config.memoizeTime,
115
90
  })
91
+ @timeout(config.timeout)
116
92
  @rateLimit({
117
93
  timeSpanMs: config.rateLimitTimeSpan,
118
94
  allowedCalls: config.rateLimitAllowedCalls,
119
95
  exceedHandler,
120
96
  })
121
97
  /**
122
- * Get user by ID
123
- * @param {string} id - User ID as string
124
- * @returns {Promise<User>} User details or error object
125
- * @throws {ResponseError} 404 - User not found
126
- * @throws {ResponseError} 400 - Invalid ID format
98
+ * Get all users
99
+ * @returns {Promise<UserDto[]>} List of users with hidden password fields
100
+ * @throws {ResponseError} 500 - When rate limit exceeded
127
101
  */
128
- public async get(id: string): Promise<UserDto> {
129
- const response = parseId(id);
130
- const user = users.find((user) => user.id === response);
131
- if (user == null) throw new ResponseError("User dose not exist.", 404);
132
- return user satisfies User;
102
+ public async getAll(): Promise<UserDto[]> {
103
+ return users as UserDto[];
133
104
  }
134
105
 
135
- @memoizeAsync(config.memoizeTime)
136
- @timeout(config.timeout)
106
+ @memoizeAsync({
107
+ cache: userCache,
108
+ keyResolver: (id: number) => id.toString(),
109
+ expirationTimeMs: config.memoizeTime,
110
+ })
137
111
  @rateLimit({
138
112
  timeSpanMs: config.rateLimitTimeSpan,
139
113
  allowedCalls: config.rateLimitAllowedCalls,
140
114
  exceedHandler,
141
115
  })
142
116
  /**
143
- * Get all users with masked passwords
144
- * @returns {Promise<User[]>} List of users with hidden password fields
145
- * @throws {ResponseError} 500 - When rate limit exceeded
117
+ * Get user by ID
118
+ * @param {number} id - User ID as string
119
+ * @returns {Promise<UserDto>} User details or error object
120
+ * @throws {ResponseError} 404 - User not found
121
+ * @throws {ResponseError} 400 - Invalid ID format
146
122
  */
147
- public async getAll(): Promise<UserDto[]> {
148
- return users.map((user) => ({
149
- ...user,
150
- password: "?",
151
- }));
123
+ public async get(id: number): Promise<UserDto> {
124
+ const user = users.find((user) => user.id === id);
125
+ if (user == null) {
126
+ throw new ResponseError("Product not found");
127
+ }
128
+ return convert(user!, ZodUserDto);
152
129
  }
153
130
  }
@@ -1,52 +1,37 @@
1
- import {
2
- memoizeTime,
3
- productionEnvironment,
4
- rateLimitTimeSpan,
5
- rateLimitAllowedCalls,
6
- timeout,
7
- portNumber,
8
- } from "./constants";
9
-
10
1
  /**
11
2
  * Server configuration interface
12
- * @interface
13
- * @property {string} url - Base server URL
14
3
  */
15
4
  interface Server {
5
+ /** URL of the base server */
16
6
  url: string;
17
7
  }
18
8
 
19
9
  /**
20
10
  * API information structure
21
- * @interface
22
- * @property {string} title - API title
23
- * @property {string} version - API version
24
- * @property {string} description - API description
25
11
  */
26
12
  interface Info {
13
+ /** Title of the API */
27
14
  title: string;
15
+ /** Version of the API */
28
16
  version: string;
17
+ /** Description of the API */
29
18
  description: string;
30
19
  }
31
20
 
32
21
  /**
33
22
  * Swagger definition structure
34
- * @interface
35
- * @property {string} openApi - OpenAPI specification version
36
- * @property {Info} info - API information
37
- * @property {Server[]} servers - List of server configurations
38
23
  */
39
24
  interface SwaggerDefinition {
25
+ /** OpenAPI specification version (e.g., "3.0.0") */
40
26
  openApi: string;
27
+ /** API information object */
41
28
  info: Info;
29
+ /** List of server configurations */
42
30
  servers: Server[];
43
31
  }
44
32
 
45
33
  /**
46
34
  * Swagger configuration options
47
- * @interface
48
- * @property {SwaggerDefinition} swaggerDefinition - Swagger definition object
49
- * @property {string[]} apis - Paths to API documentation files
50
35
  */
51
36
  export interface SwaggerOptions {
52
37
  swaggerDefinition: SwaggerDefinition;
@@ -1,81 +1,66 @@
1
1
  import { z, ZodRawShape } from "zod";
2
- import { ResponseError } from "./types";
2
+ import { ResponseError } from "./error-handling";
3
3
 
4
4
  /**
5
- * Strictly convert obj (type T1) to T2 using a Zod schema.
5
+ * Strictly assign obj (type T1) to T2 using a Zod schema.
6
6
  *
7
- * - Throws if obj has extra fields beyond those defined in the schema.
7
+ * - Extras in source are ignored (no throw).
8
8
  * - Validates fields with the schema; on failure, throws with a descriptive message.
9
9
  * - Returns an object typed as T2 (inferred from the schema).
10
10
  *
11
- * @param obj - Source object of type T1
11
+ * @param source - Source object of type T1
12
+ * @param destination - Destination object to be populated (typed as T2)
12
13
  * @param schema - Zod schema describing the target type T2
13
- * @returns T2 inferred from the provided schema
14
+ * @returns T2 representing the destination after assignment
14
15
  */
15
- export function assignStrictlyFromSchema<T1 extends object, T2 extends object>(
16
- obj: T1,
17
- schema: z.ZodObject<any>
16
+ export default function assign<T1 extends object, T2 extends object>(
17
+ source: T1,
18
+ destination: T2,
19
+ schema: z.ZodObject<any>,
20
+ ignoreNullAndUndefined: false
18
21
  ): T2 {
19
- // 1) Derive the runtime keys from the schema's shape
20
- const shape = (schema as any)._def?.shape as ZodRawShape | undefined;
21
- if (!shape) {
22
- throw new ResponseError(
23
- "assignStrictlyFromSchema: provided schema has no shape.",
24
- 500
25
- );
26
- }
22
+ // 1) Validate the subset of keys defined in the schema
23
+ // Build an object that contains only the keys present in source but are part of the schema
24
+ const subsetForSchema: any = {};
27
25
 
28
- const keysSchema = Object.keys(shape) as Array<keyof any>;
29
-
30
- // 2) Extra keys check
31
- const objKeys = Object.keys(obj) as Array<keyof T1>;
32
- const extraKeys = objKeys.filter((k) => !keysSchema.includes(k as any));
33
- if (extraKeys.length > 0) {
34
- throw new ResponseError(
35
- `assignStrictlyFromSchema: source object contains extra field(s) not present on target: ${extraKeys.join(
36
- ", "
37
- )}`,
38
- 500
39
- );
26
+ // Iterate schema keys
27
+ for (const key of Object.keys(schema.shape)) {
28
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
29
+ if (ignoreNullAndUndefined) {
30
+ subsetForSchema[key] = (source as any)[key] ?? subsetForSchema[key];
31
+ } else {
32
+ subsetForSchema[key] = (source as any)[key];
33
+ }
34
+ }
40
35
  }
41
36
 
42
- // 3) Required-field check for T2 (all keys in schema must be present and non-undefined)
43
- const missingOrUndefined = keysSchema.filter((k) => {
44
- const v = (obj as any)[k];
45
- return v === undefined || v === null;
46
- });
47
- if (missingOrUndefined.length > 0) {
37
+ // Validate using the schema on the subset (this will also coerce if the schema has transforms)
38
+ const parseResult = schema.safeParse(subsetForSchema);
39
+ if (!parseResult.success) {
40
+ // Build a descriptive error message from the first issue
41
+ const issue = parseResult.error.issues?.[0];
42
+ const path = issue?.path?.length ? issue.path.join(".") : "value";
43
+ const message = issue?.message ?? "Schema validation failed";
48
44
  throw new ResponseError(
49
- `assignStrictlyFromSchema: missing required field(s): ${missingOrUndefined.join(
50
- ", "
51
- )}`,
45
+ `assignStrictlyFromSchema: Validation failed for "${path}": ${message}`,
52
46
  500
53
47
  );
54
48
  }
55
49
 
56
- // 4) Build a plain object to pass through Zod for validation
57
- const candidate: any = {};
58
- for (const k of keysSchema) {
59
- if (k in (obj as any)) {
60
- candidate[k] = (obj as any)[k];
61
- }
62
- }
50
+ // 2) Assign validated values to destination strictly
51
+ // Use the parsed result to ensure types align with the schema
52
+ const validated = parseResult.data as any;
63
53
 
64
- // 5) Validate against the schema
65
- const result = schema.safeParse(candidate);
66
- if (!result.success) {
67
- // Modern, non-format error reporting
68
- const issues = result.error.issues.map((i) => ({
69
- path: i.path, // where the issue occurred
70
- message: i.message, // human-friendly message
71
- code: i.code, // e.g., "too_small", "invalid_type"
72
- }));
73
- // You can log issues or throw a structured error
74
- throw new Error(
75
- `assignStrictlyFromSchema: validation failed: ${JSON.stringify(issues)}`
76
- );
54
+ // Copy only keys that are in the schema
55
+ for (const key of Object.keys(schema.shape)) {
56
+ if (Object.prototype.hasOwnProperty.call(validated, key)) {
57
+ if (ignoreNullAndUndefined) {
58
+ subsetForSchema[key] = (source as any)[key] ?? subsetForSchema[key];
59
+ } else {
60
+ subsetForSchema[key] = (source as any)[key];
61
+ }
62
+ }
77
63
  }
78
64
 
79
- // 6) Return the validated data typed as T2
80
- return result.data as T2;
65
+ return destination;
81
66
  }
@@ -0,0 +1,74 @@
1
+ import { AsyncCache } from "utils-decorators";
2
+
3
+ /**
4
+ * A simple in-memory map-based cache implementing AsyncCache<T>.
5
+ * This is a straightforward wrapper around a Map<string, T>.
6
+ */
7
+ export class MapAsyncCache<T> implements AsyncCache<T> {
8
+ public constructor(
9
+ private capacity?: number,
10
+ private cache?: Map<string, T>
11
+ ) {
12
+ this.capacity = this.capacity ?? Number.POSITIVE_INFINITY;
13
+ this.cache = this.cache ?? new Map<string, T>();
14
+ }
15
+
16
+ /**
17
+ * Asynchronously set a value by key.
18
+ * @param key The cache key
19
+ * @param value The value to cache
20
+ */
21
+ async set(key: string, value: T): Promise<void> {
22
+ if (value != null) {
23
+ if (this.cache?.has(key)) {
24
+ this.cache?.set(key, value);
25
+ } else {
26
+ if (
27
+ (this.capacity ?? Number.POSITIVE_INFINITY) > (this.cache?.size ?? 0)
28
+ ) {
29
+ this.cache?.set(key, value);
30
+ }
31
+ }
32
+ } else {
33
+ this.cache?.delete(key);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Asynchronously get a value by key.
39
+ * - If the key exists, returns the value.
40
+ * - If the key does not exist, returns undefined cast to T to satisfy the Promise<T> return type.
41
+ *
42
+ * Note: Returning undefined may be surprising for callers expecting a strict A/B.
43
+ * Consider returning `T | undefined` from AsyncCache if you can adjust the interface,
44
+ * or throw a ResponseError for "not found" depending on your usage.
45
+ */
46
+ async get(key: string): Promise<T> {
47
+ if (this.cache?.has(key)) {
48
+ return this.cache?.get(key) ?? (null as T);
49
+ }
50
+
51
+ return null as T;
52
+ }
53
+
54
+ /**
55
+ * Asynchronously delete a value by key.
56
+ */
57
+ async delete(key: string): Promise<void> {
58
+ this.cache?.delete(key);
59
+ }
60
+
61
+ /**
62
+ * Asynchronously check if a key exists in the cache.
63
+ */
64
+ async has(key: string): Promise<boolean> {
65
+ return this.cache?.has(key) ?? false;
66
+ }
67
+
68
+ /**
69
+ * Asynchronously clear the cache.
70
+ */
71
+ async clear(key: string): Promise<void> {
72
+ this.cache?.clear();
73
+ }
74
+ }
@@ -0,0 +1,111 @@
1
+ import { AsyncCache } from "utils-decorators";
2
+ import { Redis } from "ioredis";
3
+
4
+ /**
5
+ * A Redis-backed cache adapter implementing AsyncCache<T | null>.
6
+ * This wraps a Redis instance and provides a similar API to MapAsyncCache.
7
+ */
8
+ export class RedisCache<T> implements AsyncCache<T> {
9
+ public constructor(
10
+ private cache?: Redis,
11
+ private capacity?: number,
12
+ private namespace?: string
13
+ ) {
14
+ this.cache = this.cache ?? new Redis();
15
+ this.capacity = this.capacity ?? Number.POSITIVE_INFINITY;
16
+ this.namespace = this.namespace ?? "redis";
17
+ }
18
+
19
+ private keyFor(key: string): string {
20
+ return `${this.namespace}:${key}`;
21
+ }
22
+
23
+ /**
24
+ * Asynchronously set a value by key.
25
+ * If value is null, delete the key to represent "not present".
26
+ * Capacity is not strictly enforced in Redis on a per-key basis here; you
27
+ * could leverage Redis max memory and eviction policies for that.
28
+ */
29
+ async set(key: string, value: T): Promise<void> {
30
+ const k = this.keyFor(key);
31
+
32
+ if (value != null) {
33
+ if (await this.has(key)) {
34
+ await this.cache?.set(k, JSON.stringify(value));
35
+ } else {
36
+ if (
37
+ (this.capacity ?? Number.POSITIVE_INFINITY) >
38
+ ((await this.cache?.dbsize()) ?? 0)
39
+ ) {
40
+ await this.cache?.set(k, JSON.stringify(value));
41
+ }
42
+ }
43
+ } else {
44
+ await this.cache?.del(k);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Asynchronously get a value by key.
50
+ * - If the key exists, returns the parsed value.
51
+ * - If the key does not exist, returns null to satisfy Promise<T | null>.
52
+ */
53
+ async get(key: string): Promise<T> {
54
+ const k = this.keyFor(key);
55
+ const raw = (await this.cache?.get(k)) ?? null;
56
+ if (raw == null) {
57
+ return null as T;
58
+ }
59
+ try {
60
+ return JSON.parse(raw) as T;
61
+ } catch {
62
+ return raw as T;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Asynchronously delete a value by key.
68
+ */
69
+ async delete(key: string): Promise<void> {
70
+ await this.cache?.del(this.keyFor(key));
71
+ }
72
+
73
+ /**
74
+ * Asynchronously check if a key exists in the cache.
75
+ */
76
+ async has(key: string): Promise<boolean> {
77
+ const exists = await this.cache?.exists(this.keyFor(key));
78
+ return exists === 1;
79
+ }
80
+
81
+ /**
82
+ * Asynchronously clear the cache (namespace-scoped).
83
+ * Use with caution in a shared Redis instance.
84
+ */
85
+ async clear(key: string): Promise<void> {
86
+ // Optional: implement namespace-wide clear if needed
87
+ // This simple example uses a pattern-based approach for a full clear.
88
+ const pattern = `${this.namespace}:*${key ? "" : ""}`;
89
+ const stream = this.cache?.scanStream({ match: pattern });
90
+ const pipeline = this.cache?.pipeline();
91
+ stream?.on("data", (keys: string[]) => {
92
+ if (keys.length) {
93
+ pipeline?.del(keys);
94
+ }
95
+ });
96
+ await new Promise<void>((resolve, reject) => {
97
+ stream?.on("end", async () => {
98
+ await pipeline?.exec();
99
+ resolve();
100
+ });
101
+ stream?.on("error", (err) => reject(err));
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Optional: gracefully close the Redis connection.
107
+ */
108
+ async close(): Promise<void> {
109
+ await this.cache?.quit();
110
+ }
111
+ }