codeweaver 3.1.3 → 4.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 +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 +46 -59
- package/src/routers/products/index.router.ts +2 -1
- package/src/routers/products/product.controller.ts +25 -63
- package/src/routers/users/index.router.ts +1 -1
- package/src/routers/users/user.controller.ts +22 -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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SNSClient,
|
|
3
|
+
PublishCommand,
|
|
4
|
+
GetSMSAttributesCommand,
|
|
5
|
+
SetSMSAttributesCommand,
|
|
6
|
+
} from "@aws-sdk/client-sns";
|
|
7
|
+
import {
|
|
8
|
+
SESv2Client,
|
|
9
|
+
SendEmailCommand,
|
|
10
|
+
ListEmailIdentitiesCommand,
|
|
11
|
+
} from "@aws-sdk/client-sesv2";
|
|
12
|
+
import {
|
|
13
|
+
CreatePlatformApplicationCommand,
|
|
14
|
+
CreatePlatformApplicationCommandInput,
|
|
15
|
+
CreatePlatformEndpointCommand,
|
|
16
|
+
CreatePlatformEndpointCommandInput,
|
|
17
|
+
PublishCommand as SNSPublishCommand,
|
|
18
|
+
} from "@aws-sdk/client-sns";
|
|
19
|
+
import { EmailMessage, PushPlatformArns, SMSMessage } from "./basic-types";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Comprehensive TypeScript library for AWS SNS (SMS) and SES (Email) services.
|
|
23
|
+
* Provides simplified interfaces for sending messages with full TypeScript support.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```
|
|
27
|
+
* const messenger = new AWSMessenger("us-east-1");
|
|
28
|
+
* await messenger.sendSMS({ phoneNumber: '+1234567890', message: 'Hello!' });
|
|
29
|
+
* await messenger.sendEmail({
|
|
30
|
+
* to: ['user@example.com'],
|
|
31
|
+
* subject: 'Test',
|
|
32
|
+
* textBody: 'Hello from SES!',
|
|
33
|
+
* from: 'sender@verified.com'
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export class Messenger {
|
|
38
|
+
public sns!: SNSClient;
|
|
39
|
+
public ses!: SESv2Client;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates an instance of AWSMessenger for SNS and SES operations.
|
|
43
|
+
* @param options - Configuration options.
|
|
44
|
+
* @param [options.region=us-east-1] - AWS region for operations.
|
|
45
|
+
*/
|
|
46
|
+
public constructor(region?: string, ...args: any[]) {
|
|
47
|
+
if (region != null) {
|
|
48
|
+
this.sns = new SNSClient({ region, ...args });
|
|
49
|
+
this.ses = new SESv2Client({ region, ...args });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sends an SMS message via AWS SNS.
|
|
55
|
+
* @param message - SMS configuration object.
|
|
56
|
+
* @returns Promise resolving to SNS publish response with MessageId.
|
|
57
|
+
* @throws Throws if phone number is invalid, quota exceeded, or permissions missing.
|
|
58
|
+
*/
|
|
59
|
+
public async sendSMS(message: SMSMessage): Promise<any> {
|
|
60
|
+
const command = new PublishCommand({
|
|
61
|
+
PhoneNumber: message.phoneNumber,
|
|
62
|
+
Message: message.message,
|
|
63
|
+
MessageAttributes: message.attributes,
|
|
64
|
+
});
|
|
65
|
+
return this.sns.send(command);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sends a bulk SMS to multiple phone numbers.
|
|
70
|
+
* @param messages - Array of SMS configuration objects.
|
|
71
|
+
* @returns Promise resolving to array of publish responses.
|
|
72
|
+
*/
|
|
73
|
+
public async sendBulkSMS(messages: SMSMessage[]): Promise<any[]> {
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const msg of messages) {
|
|
76
|
+
results.push(await this.sendSMS(msg));
|
|
77
|
+
}
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets current SMS attributes (delivery status, monthly spend, etc.).
|
|
83
|
+
* @returns Promise resolving to SMS attributes map.
|
|
84
|
+
*/
|
|
85
|
+
public async getSMSAttributes(): Promise<any> {
|
|
86
|
+
const command = new GetSMSAttributesCommand({});
|
|
87
|
+
const response = await this.sns.send(command);
|
|
88
|
+
return response.attributes || {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sets SMS attributes like DefaultSMSType (Transactional/Promotional).
|
|
93
|
+
* @param attributes - Map of SMS attributes to set.
|
|
94
|
+
* @returns Promise resolving to updated attributes.
|
|
95
|
+
* @example
|
|
96
|
+
* await messenger.setSMSAttributes({ DefaultSMSType: 'Transactional' });
|
|
97
|
+
*/
|
|
98
|
+
public async setSMSAttributes(attributes: Record<string, string>): Promise<any> {
|
|
99
|
+
const command = new SetSMSAttributesCommand({ attributes });
|
|
100
|
+
return this.sns.send(command);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sends an email via AWS SES v2.
|
|
105
|
+
* @param message - Email configuration object.
|
|
106
|
+
* @returns Promise resolving to SES send response with MessageId.
|
|
107
|
+
* @throws Throws if sender not verified, quota exceeded, or invalid recipients.
|
|
108
|
+
*/
|
|
109
|
+
public async sendEmail(message: EmailMessage): Promise<any> {
|
|
110
|
+
const command = new SendEmailCommand({
|
|
111
|
+
FromEmailAddress: message.from,
|
|
112
|
+
Destination: {
|
|
113
|
+
ToAddresses: message.to,
|
|
114
|
+
CcAddresses: message.cc,
|
|
115
|
+
BccAddresses: message.bcc,
|
|
116
|
+
},
|
|
117
|
+
Content: {
|
|
118
|
+
Simple: {
|
|
119
|
+
Subject: { Data: message.subject, Charset: "UTF-8" },
|
|
120
|
+
Body: {
|
|
121
|
+
Text: message.textBody
|
|
122
|
+
? { Data: message.textBody, Charset: "UTF-8" }
|
|
123
|
+
: undefined,
|
|
124
|
+
Html: message.htmlBody
|
|
125
|
+
? { Data: message.htmlBody, Charset: "UTF-8" }
|
|
126
|
+
: undefined,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
ReplyToAddresses: message.replyTo,
|
|
131
|
+
});
|
|
132
|
+
return this.ses.send(command);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Lists all SES verified email addresses.
|
|
137
|
+
* @returns Promise resolving to array of verified email addresses.
|
|
138
|
+
*/
|
|
139
|
+
public async listVerifiedEmails(): Promise<string[]> {
|
|
140
|
+
const command = new ListEmailIdentitiesCommand({});
|
|
141
|
+
const response = await this.ses.send(command);
|
|
142
|
+
// Filter identities for email addresses only (exclude domains)
|
|
143
|
+
const emails = (response.EmailIdentities || [])
|
|
144
|
+
.filter((identity) => identity.IdentityType === "EMAIL_ADDRESS")
|
|
145
|
+
.map((identity) => identity.IdentityName || "");
|
|
146
|
+
return emails;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Optional: cache or store platform application ARNs by platform */
|
|
150
|
+
public pushPlatformArns: PushPlatformArns | undefined;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Creates a platform application for push notifications.
|
|
154
|
+
* You can store the resulting PlatformApplicationArn to reuse for endpoints.
|
|
155
|
+
* @param platform - 'APNS', 'APNS_SANDBOX', 'GCM' (FCM), etc.
|
|
156
|
+
* @param attributes - Optional attributes for the platform app (e.g., PlatformCredential, PlatformPrincipal)
|
|
157
|
+
* @param name - Optional logical name for internal tracking (not strictly required)
|
|
158
|
+
* @returns Promise resolving to the created PlatformApplicationArn
|
|
159
|
+
*/
|
|
160
|
+
public async createPlatformApplication(
|
|
161
|
+
platform: string,
|
|
162
|
+
attributes: Record<string, string>
|
|
163
|
+
): Promise<string> {
|
|
164
|
+
const command = new CreatePlatformApplicationCommand({
|
|
165
|
+
Name: platform + "_application_" + Date.now(),
|
|
166
|
+
Platform: platform,
|
|
167
|
+
Attributes: attributes,
|
|
168
|
+
} as CreatePlatformApplicationCommandInput);
|
|
169
|
+
const response = await this.sns.send(command);
|
|
170
|
+
const arn = response.PlatformApplicationArn;
|
|
171
|
+
// Optionally cache by platform
|
|
172
|
+
if (!this.pushPlatformArns) this.pushPlatformArns = {};
|
|
173
|
+
this.pushPlatformArns[platform] = arn || "";
|
|
174
|
+
return arn || "";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Creates an endpoint for a device token under a given platform application.
|
|
179
|
+
* @param platform - platform key, e.g., 'APNS', 'APNS_SANDBOX', 'GCM'
|
|
180
|
+
* @param token - device push token (device token for APNS, registration token for FCM)
|
|
181
|
+
* @param customUserData - optional custom data to associate with the endpoint
|
|
182
|
+
* @returns Promise resolving to the EndpointArn
|
|
183
|
+
*/
|
|
184
|
+
public async createPlatformEndpoint(
|
|
185
|
+
platform: string,
|
|
186
|
+
token: string,
|
|
187
|
+
customUserData?: string
|
|
188
|
+
): Promise<string> {
|
|
189
|
+
const appArn = this.pushPlatformArns?.[platform];
|
|
190
|
+
if (!appArn) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Platform application ARN for ${platform} not found. Create it with createPlatformApplication first.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const command = new CreatePlatformEndpointCommand({
|
|
196
|
+
PlatformApplicationArn: appArn,
|
|
197
|
+
Token: token,
|
|
198
|
+
UserData: customUserData,
|
|
199
|
+
} as CreatePlatformEndpointCommandInput);
|
|
200
|
+
const response = await this.sns.send(command);
|
|
201
|
+
return response.EndpointArn || "";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Publish a push notification to a specific endpoint.
|
|
206
|
+
* The Message structure uses platform-specific payloads for APNS/FCM, etc.
|
|
207
|
+
* @param endpointArn - The SNS EndpointArn to target
|
|
208
|
+
* @param message - The generic message payload
|
|
209
|
+
* @param subject - Optional subject (for some platforms)
|
|
210
|
+
* @returns Promise resolving to Publish response with MessageId
|
|
211
|
+
*/
|
|
212
|
+
public async publishPushToEndpoint(
|
|
213
|
+
endpointArn: string,
|
|
214
|
+
message: {
|
|
215
|
+
// Common fields; you can structure this as needed
|
|
216
|
+
title?: string;
|
|
217
|
+
body?: string;
|
|
218
|
+
// Optional Apple/Google-specific overrides
|
|
219
|
+
apns?: any; // APNS payload
|
|
220
|
+
android?: any; // Android payload
|
|
221
|
+
default?: string;
|
|
222
|
+
}
|
|
223
|
+
): Promise<any> {
|
|
224
|
+
// Build a platform-specific JSON payload. AWS SNS expects a JSON object
|
|
225
|
+
// with keys per platform, as a string under the "Message" field,
|
|
226
|
+
// and a "MessageStructure": "json"
|
|
227
|
+
|
|
228
|
+
const payload: any = {};
|
|
229
|
+
|
|
230
|
+
if (message.apns) payload.APNS = JSON.stringify(message.apns);
|
|
231
|
+
if (message.android) payload.GCM = JSON.stringify(message.android);
|
|
232
|
+
if (message.default) payload.default = message.default;
|
|
233
|
+
// Fallback to a simple default if no platform-specific payload provided
|
|
234
|
+
if (!payload.APNS && !payload.GCM && !payload.default) {
|
|
235
|
+
const simple = {
|
|
236
|
+
aps: {
|
|
237
|
+
alert: {
|
|
238
|
+
title: message.title || "",
|
|
239
|
+
body: message.body || "",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
payload.APNS = JSON.stringify(simple);
|
|
244
|
+
// Some implementations also set GCM
|
|
245
|
+
payload.GCM = JSON.stringify({
|
|
246
|
+
notification: { title: message.title, body: message.body },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const command = new SNSPublishCommand({
|
|
251
|
+
TargetArn: endpointArn,
|
|
252
|
+
MessageStructure: "json",
|
|
253
|
+
Message: JSON.stringify(payload),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Use PublishCommand alias to avoid naming confusion
|
|
257
|
+
return this.sns.send(command);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
S3Client,
|
|
3
|
+
PutObjectCommand,
|
|
4
|
+
GetObjectCommand,
|
|
5
|
+
ListObjectsV2Command,
|
|
6
|
+
DeleteObjectCommand,
|
|
7
|
+
} from "@aws-sdk/client-s3";
|
|
8
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
9
|
+
import { retry, RetryOptions } from "./utilities";
|
|
10
|
+
import { DownloadParams, UploadParams } from "./basic-types";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import util from "util";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Client for interacting with AWS S3 buckets.
|
|
16
|
+
*/
|
|
17
|
+
export class S3 {
|
|
18
|
+
private client!: S3Client;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Constructor for S3Client.
|
|
22
|
+
* @param {string} [region] - The AWS region to use for the client.
|
|
23
|
+
* If not provided, the default region will be used.
|
|
24
|
+
*/
|
|
25
|
+
public constructor(region?: string) {
|
|
26
|
+
if (region != null) {
|
|
27
|
+
this.client = new S3Client({ region });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Uploads a file to an S3 bucket.
|
|
33
|
+
* @param {UploadParams} params - The upload parameters.
|
|
34
|
+
* @param {string} params.bucket - The name of the S3 bucket to upload to.
|
|
35
|
+
* @param {string} params.key - The key of the object to create in the bucket.
|
|
36
|
+
* @param {string} params.filePath - The local path to the file to upload.
|
|
37
|
+
* @param {Record<string, any>} [params.extraArgs] - Additional parameters to pass to the PutObjectCommand.
|
|
38
|
+
* @returns {Promise<void>} A promise resolving when the upload is complete.
|
|
39
|
+
*/
|
|
40
|
+
public async upload(
|
|
41
|
+
params: UploadParams,
|
|
42
|
+
retryOptions?: RetryOptions
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const { bucket, key, filePath, extraArgs } = params;
|
|
45
|
+
const readFile = util.promisify(fs.promises.readFile);
|
|
46
|
+
|
|
47
|
+
const body = await readFile(filePath, undefined);
|
|
48
|
+
|
|
49
|
+
await retry(async () => {
|
|
50
|
+
await this.client.send(
|
|
51
|
+
new PutObjectCommand({
|
|
52
|
+
Bucket: bucket,
|
|
53
|
+
Key: key,
|
|
54
|
+
Body: body as any,
|
|
55
|
+
...extraArgs,
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}, retryOptions);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Downloads an object from an S3 bucket.
|
|
63
|
+
* @param {DownloadParams} params - The download parameters.
|
|
64
|
+
* @param {string} params.bucket - The name of the S3 bucket to download from.
|
|
65
|
+
* @param {string} params.key - The key of the object to download.
|
|
66
|
+
* @param {string} params.destPath - The local path to write the downloaded file to.
|
|
67
|
+
* @returns {Promise<void>} A promise resolving when the download is complete.
|
|
68
|
+
*/
|
|
69
|
+
public async download(params: DownloadParams): Promise<void> {
|
|
70
|
+
const { bucket, key, destPath } = params;
|
|
71
|
+
const resp = await this.client.send(
|
|
72
|
+
new GetObjectCommand({ Bucket: bucket, Key: key })
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const writeStream = fs.createWriteStream(destPath);
|
|
76
|
+
|
|
77
|
+
// resp.Body is a stream
|
|
78
|
+
if (resp.Body && "pipe" in resp.Body) {
|
|
79
|
+
await new Promise<void>((resolve, reject) => {
|
|
80
|
+
(resp.Body as any).pipe(writeStream);
|
|
81
|
+
writeStream.on("finish", () => resolve());
|
|
82
|
+
writeStream.on("error", (e) => reject(e));
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
// Fallback if Body is not streamable
|
|
86
|
+
const chunks: Buffer[] = [];
|
|
87
|
+
for await (const chunk of resp.Body as any)
|
|
88
|
+
chunks.push(Buffer.from(chunk));
|
|
89
|
+
await fs.promises.writeFile(destPath, Buffer.concat(chunks));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Lists the objects in an S3 bucket, optionally filtered by a prefix.
|
|
95
|
+
* @param {string} bucket - The name of the S3 bucket to list objects from.
|
|
96
|
+
* @param {string} [prefix] - The optional prefix to filter objects by.
|
|
97
|
+
* @returns {Promise<string[]>} A promise resolving with an array of object keys.
|
|
98
|
+
*/
|
|
99
|
+
public async listObjects(bucket: string, prefix?: string): Promise<string[]> {
|
|
100
|
+
const cmd = new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix });
|
|
101
|
+
const resp: any = await this.client.send(cmd);
|
|
102
|
+
const keys = (resp.Contents || []).map((c: any) => c.Key).filter(Boolean);
|
|
103
|
+
return keys;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Deletes an object from an S3 bucket.
|
|
108
|
+
* @param {string} bucket - The name of the S3 bucket to delete from.
|
|
109
|
+
* @param {string} key - The key of the object to delete.
|
|
110
|
+
* @returns {Promise<void>} A promise resolving when the delete is complete.
|
|
111
|
+
*/
|
|
112
|
+
public async deleteObject(bucket: string, key: string): Promise<void> {
|
|
113
|
+
await this.client.send(
|
|
114
|
+
new DeleteObjectCommand({ Bucket: bucket, Key: key })
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generates a presigned URL for downloading an object from an S3 bucket.
|
|
120
|
+
* @param bucket The name of the S3 bucket to download from.
|
|
121
|
+
* @param key The key of the object to download.
|
|
122
|
+
* @param expirationInSeconds The number of seconds until the presigned URL expires.
|
|
123
|
+
* Defaults to 3600 (1 hour).
|
|
124
|
+
* @returns A promise resolving to the presigned URL.
|
|
125
|
+
*/
|
|
126
|
+
public async presignedUrl(
|
|
127
|
+
bucket: string,
|
|
128
|
+
key: string,
|
|
129
|
+
expirationInSeconds = 3600
|
|
130
|
+
): Promise<string> {
|
|
131
|
+
const cmd = new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
132
|
+
return await getSignedUrl(this.client, cmd, {
|
|
133
|
+
expiresIn: expirationInSeconds,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type RetryOptions = {
|
|
2
|
+
retries?: number;
|
|
3
|
+
baseDelayMs?: number;
|
|
4
|
+
maxDelayMs?: number;
|
|
5
|
+
jitter?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function sleep(ms: number): Promise<void> {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Simple exponential backoff retry wrapper.
|
|
14
|
+
* Retries the provided async function according to options.
|
|
15
|
+
*/
|
|
16
|
+
export async function retry<T>(
|
|
17
|
+
fn: () => Promise<T>,
|
|
18
|
+
options: RetryOptions = {}
|
|
19
|
+
): Promise<T> {
|
|
20
|
+
const {
|
|
21
|
+
retries = 1,
|
|
22
|
+
baseDelayMs = 200,
|
|
23
|
+
maxDelayMs = 2000,
|
|
24
|
+
jitter = true,
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
let attempt = 0;
|
|
28
|
+
while (true) {
|
|
29
|
+
try {
|
|
30
|
+
return await fn();
|
|
31
|
+
} catch (err) {
|
|
32
|
+
attempt++;
|
|
33
|
+
if (attempt > retries) {
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
let delay = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, attempt - 1));
|
|
37
|
+
if (jitter) {
|
|
38
|
+
const jitterMs = Math.random() * 100;
|
|
39
|
+
delay = Math.max(0, delay + jitterMs);
|
|
40
|
+
}
|
|
41
|
+
await sleep(delay);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple cache interface.
|
|
3
|
+
*/
|
|
4
|
+
export interface AsyncCache<Entity> {
|
|
5
|
+
set: (key: string, value: Entity) => Promise<void>;
|
|
6
|
+
get: (key: string) => Promise<Entity | null>;
|
|
7
|
+
delete: (key: string) => Promise<void>;
|
|
8
|
+
has: (key: string) => Promise<boolean>;
|
|
9
|
+
clear: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A function that takes a variable number of arguments and returns a string that uniquely identifies them.
|
|
14
|
+
*/
|
|
15
|
+
export type KeyResolver = <MethodArgs extends any[] = any[]>(
|
|
16
|
+
...args: MethodArgs
|
|
17
|
+
) => string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { AsyncCache, KeyResolver } from "./basic-types";
|
|
2
|
+
import { createMethodDecorator } from "@/core/helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default key resolver for memoization.
|
|
6
|
+
* Takes a variable number of arguments and returns a string that uniquely identifies them.
|
|
7
|
+
* For objects, it uses JSON.stringify to convert the object to a string.
|
|
8
|
+
* For other types, it uses the primitive value of the argument.
|
|
9
|
+
* The resulting strings are joined with "|" to form the final key.
|
|
10
|
+
* @param {...args} MethodArgs
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function defaultKeyResolver<MethodArgs extends any[] = any[]>(
|
|
14
|
+
...args: MethodArgs
|
|
15
|
+
): string {
|
|
16
|
+
return args.length === 0
|
|
17
|
+
? "key"
|
|
18
|
+
: args
|
|
19
|
+
.map((a) => (typeof a === "object" ? JSON.stringify(a) : a))
|
|
20
|
+
.join("|");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Decorator that memoizes a method, using a cache of type AsyncCache<Entity>
|
|
25
|
+
* and a key resolver of type KeyResolver.
|
|
26
|
+
* @returns {MethodDecorator}
|
|
27
|
+
*/
|
|
28
|
+
export function Memoize<Entity, MethodArgs extends any[] = any[]>(
|
|
29
|
+
cache: AsyncCache<Entity>,
|
|
30
|
+
keyResolver?: KeyResolver
|
|
31
|
+
) {
|
|
32
|
+
return createMethodDecorator<
|
|
33
|
+
[AsyncCache<Entity>, KeyResolver | undefined],
|
|
34
|
+
MethodArgs
|
|
35
|
+
>(async ([cache, keyResolver], methodArgs, method, self) => {
|
|
36
|
+
keyResolver = keyResolver ?? defaultKeyResolver;
|
|
37
|
+
const key = keyResolver(...methodArgs);
|
|
38
|
+
const has = await cache?.has(key);
|
|
39
|
+
if (has) {
|
|
40
|
+
const value = await cache?.get(key);
|
|
41
|
+
return value;
|
|
42
|
+
} else {
|
|
43
|
+
const value = await method.apply(self, methodArgs);
|
|
44
|
+
await cache?.set(key, value as Entity);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
})(cache, keyResolver);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decorator that invalidates a cache of type AsyncCache<Entity> by key.
|
|
52
|
+
* @returns {MethodDecorator}
|
|
53
|
+
*/
|
|
54
|
+
export function Invalidate<Entity>(
|
|
55
|
+
cache: AsyncCache<Entity>,
|
|
56
|
+
shouldClearAll: boolean = true
|
|
57
|
+
) {
|
|
58
|
+
return createMethodDecorator<
|
|
59
|
+
[AsyncCache<Entity>, boolean | true],
|
|
60
|
+
[string | undefined]
|
|
61
|
+
>(undefined, async (_value, [cache, shouldClearAll], [key]) => {
|
|
62
|
+
if (shouldClearAll === true) {
|
|
63
|
+
await cache.clear();
|
|
64
|
+
} else if (typeof key === "string" || typeof key === "number") {
|
|
65
|
+
await cache.delete(key);
|
|
66
|
+
} else {
|
|
67
|
+
throw new Error("The key must be string or number.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
})(cache, shouldClearAll);
|
|
72
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { AsyncCache } from "./basic-types";
|
|
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
|
+
private startTimeMs?: number;
|
|
9
|
+
|
|
10
|
+
public constructor(
|
|
11
|
+
private capacity?: number,
|
|
12
|
+
private durationMs?: number,
|
|
13
|
+
private cache?: Map<string, T>
|
|
14
|
+
) {
|
|
15
|
+
this.startTimeMs = this.startTimeMs ?? Date.now();
|
|
16
|
+
this.durationMs = this.durationMs ?? Number.POSITIVE_INFINITY;
|
|
17
|
+
this.capacity = this.capacity ?? Number.POSITIVE_INFINITY;
|
|
18
|
+
this.cache = this.cache ?? new Map<string, T>();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async size(): Promise<number> {
|
|
22
|
+
return await this.cache!.size;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async keys(): Promise<string[]> {
|
|
26
|
+
return await Array.fromAsync(this.cache?.keys() ?? []);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async values(): Promise<T[]> {
|
|
30
|
+
return await Array.fromAsync(this.cache?.values() ?? []);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async entries(): Promise<[string, T][]> {
|
|
34
|
+
return await Array.fromAsync(this.cache?.entries() ?? []);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether the cache is still valid.
|
|
39
|
+
* This is a computed property that returns true if the cache
|
|
40
|
+
* has not yet expired, and false otherwise.
|
|
41
|
+
* @returns true if the cache is still valid, false otherwise
|
|
42
|
+
*/
|
|
43
|
+
public get isValid(): boolean {
|
|
44
|
+
return this.startTimeMs! + this.durationMs! > Date.now();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reset the cache if it has expired.
|
|
49
|
+
* @returns true if the cache was reset, false otherwise
|
|
50
|
+
*/
|
|
51
|
+
public async resetIfExpired(): Promise<boolean> {
|
|
52
|
+
if (this.isValid == false) {
|
|
53
|
+
await this.cache?.clear();
|
|
54
|
+
this.startTimeMs = Date.now();
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Asynchronously set a value by key.
|
|
62
|
+
* @param key The cache key
|
|
63
|
+
* @param value The value to cache
|
|
64
|
+
*/
|
|
65
|
+
public async set(key: string, value: T): Promise<void> {
|
|
66
|
+
if ((await this.resetIfExpired()) == true) return;
|
|
67
|
+
if (value != null) {
|
|
68
|
+
if (this.cache?.has(key)) {
|
|
69
|
+
await this.cache?.set(key, value);
|
|
70
|
+
} else {
|
|
71
|
+
if (
|
|
72
|
+
(this.capacity ?? Number.POSITIVE_INFINITY) > (this.cache?.size ?? 0)
|
|
73
|
+
) {
|
|
74
|
+
await this.cache?.set(key, value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
await this.cache?.delete(key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Asynchronously get a value by key.
|
|
84
|
+
*
|
|
85
|
+
* @param key The cache key
|
|
86
|
+
* - If the key exists, returns the value.
|
|
87
|
+
* - If the key does not exist, returns null.
|
|
88
|
+
*/
|
|
89
|
+
public async get(key: string): Promise<T | null> {
|
|
90
|
+
if (await this.cache?.has(key)) {
|
|
91
|
+
return (await this.cache?.get(key)) ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Asynchronously delete a value by key.
|
|
99
|
+
*/
|
|
100
|
+
public async delete(key: string): Promise<void> {
|
|
101
|
+
if ((await this.resetIfExpired()) == true) return;
|
|
102
|
+
await this.cache?.delete(key);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Asynchronously check if a key exists in the cache.
|
|
107
|
+
*/
|
|
108
|
+
public async has(key: string): Promise<boolean> {
|
|
109
|
+
if ((await this.resetIfExpired()) == true) return false;
|
|
110
|
+
return (await this.cache?.has(key)) ?? false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Asynchronously clear the cache.
|
|
115
|
+
*/
|
|
116
|
+
public async clear(): Promise<void> {
|
|
117
|
+
await this.cache?.clear();
|
|
118
|
+
}
|
|
119
|
+
}
|