Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[S3-DESTINATION] Feature: The S3 Workflow And Destination Features [v1] 📚 #3

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
26225a5
feat: the s3 feature and destination feature were implemented with al…
Edgar-Castillo-Skydropx Jul 30, 2023
d15ec7f
feat: the s3 feature and destination feature were implemented with al…
Edgar-Castillo-Skydropx Jul 30, 2023
1e30683
feat: the s3 feature and destination feature were implemented with al…
Edgar-Castillo-Skydropx Jul 30, 2023
77d07ad
feat: the s3 feature and destination feature were implemented with al…
Edgar-Castillo-Skydropx Jul 31, 2023
745ae87
feat: the s3 feature and destination feature were implemented with al…
Edgar-Castillo-Skydropx Jul 31, 2023
f44aab4
feat: the s3 feature and destination feature were implemented with al…
Edgar-Castillo-Skydropx Aug 1, 2023
19d1863
feat: the s3 feature and destination feature were implemented with al…
Edgar-Castillo-Skydropx Aug 2, 2023
e05ad45
feat: the s3 feature and destination feature were implemented with al…
Edgar-Castillo-Skydropx Aug 2, 2023
cfbca0e
feat: the s3 feature and destination feature were implemented with al…
Edgar-Castillo-Skydropx Aug 2, 2023
b3853a3
feat: new global-function in the back folder was added to get object …
Edgar-Castillo-Skydropx Aug 3, 2023
fc322e9
feat: new global-function in the back folder was added to get object …
Edgar-Castillo-Skydropx Aug 4, 2023
26a5990
feat: new global-function in the back folder was added to get object …
Edgar-Castillo-Skydropx Aug 4, 2023
8d7f2b1
feat: new global-function in the back folder was added to get object …
Edgar-Castillo-Skydropx Aug 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"DYNAMO_PORT": 4566,
"SECRET_KEY": "shhh",
"EXPIRATION": 2,
"TOPIC_ARN": "arn:aws:sns:us-east-1:000000000000:TopicUserService"
"ADMIN_EMAIL": "[email protected]",
"TOPIC_ARN": "arn:aws:sns:us-east-1:000000000000:TopicUserService",
"WATER_MARK_IMG": "watter_mark.png",
"BUCKET_NAME": "test"
}
}
18 changes: 13 additions & 5 deletions lambdas/src/functions/create-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
UserRepository,
User,
HttpStatus,
UserPrimitives
UserPrimitives,
SNSTopicService
} from '@general';
import { CreateUserDTO } from '@/functions/dtos/create-user.dto';

Expand All @@ -30,16 +31,23 @@ const createUser = async (
phone: body.phone,
password: body.password,
age: body.age,
role: body.role
role: body.role,
avatar: body.avatar || '',
verified: false
});

await UserRepository.createOrUpdate(user, true);

const data = GlobalFunctions.getNewParams<UserPrimitives>(
user.toPrimitives(),
['password']
);

await SNSTopicService.send(JSON.stringify(data));

return formatJSONResponse(HttpStatus.CREATED, {
success: true,
user: GlobalFunctions.getNewParams<UserPrimitives>(user.toPrimitives(), [
'password'
])
user: data
});
} catch (error: DomainError | any) {
console.error(error);
Expand Down
4 changes: 4 additions & 0 deletions lambdas/src/functions/dtos/create-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ export class CreateUserDTO {

@IsNumber()
age!: number;

@IsString()
@IsOptional()
avatar?: string;
}
17 changes: 16 additions & 1 deletion lambdas/src/functions/dtos/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { IsString, IsNumber, IsOptional, MinLength, UserRole } from '@general';
import {
IsString,
IsNumber,
IsOptional,
MinLength,
UserRole,
IsBoolean
} from '@general';

export class UpdateUserDTO {
@IsString()
Expand Down Expand Up @@ -37,4 +44,12 @@ export class UpdateUserDTO {
@IsNumber()
@IsOptional()
age!: number;

@IsBoolean()
@IsOptional()
verified!: number;

@IsString()
@IsOptional()
avatar?: string;
}
91 changes: 91 additions & 0 deletions lambdas/src/functions/meta-data-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Handler, S3Event, Callback, Context } from 'aws-lambda';
import { S3BucketService, config, Jimp } from '@general';

const fs = require('fs');
const path = require('path');
const directory = '/tmp/';

const metaDataValidator = async (
event: S3Event,
_context: Context,
callback: Callback
) => {
try {
const originalBucket = event.Records[0].s3.bucket.name;
const key = event.Records[0].s3.object.key;

// let getObjectResponse = await S3BucketService.get(key, originalBucket);

// getObjectResponse = await getObjectResponse.transformToString(); // transformToString('base64')

// let getWatermarkResponse = await S3BucketService.get(
// config.aws.waterMarkImg
// );

// getWatermarkResponse = await getWatermarkResponse.transformToString();

// const watermarkedImage = applyWatermark(
// getObjectResponse,
// getWatermarkResponse
// );

const [image, logo] = await Promise.all([
Jimp.read(`https://${originalBucket}.s3.amazonaws.com/${key}`),
Jimp.read(
`https://${config.aws.bucket}.s3.amazonaws.com/${config.aws.waterMarkImg}`
)
]);

logo.resize(image.bitmap.width / 10, Jimp.AUTO);

const xMargin = (image.bitmap.width * 1) / 100;
const yMargin = (image.bitmap.width * 1) / 100;

const X = image.bitmap.width - logo.bitmap.width - xMargin;
const Y = image.bitmap.height - logo.bitmap.height - yMargin;

const data = image.composite(logo, X, Y, [
{
mode: Jimp.BLEND_SCREEN,
opacitySource: 0.1,
opacityDest: 1
}
]);

const uploadKey = `watermarked-${key}`;

data.write(`${directory}${uploadKey}`);

const tmpData = fs.readFileSync(`${directory}${uploadKey}`);

await S3BucketService.send({ filePath: uploadKey, file: tmpData });

await tmpCleanup();

callback(null, {
success: true,
message: `Object written to ${config.aws.bucket}`
});
} catch (error: any) {
console.error('Error:', error);
callback(error);
}
};

const tmpCleanup = async () => {
fs.readdir(directory, (err: any, files: any[]) => {
return new Promise<void>((resolve, reject) => {
if (err) reject(err);

for (const file of files) {
const fullPath = path.join(directory, file);
fs.unlink(fullPath, (err: any) => {
if (err) reject(err);
});
}
resolve();
});
});
};

export const handler: Handler = metaDataValidator;
2 changes: 2 additions & 0 deletions lambdas/src/functions/update-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const updateUser = async (
password: body?.password,
age: body?.age || userExists.getAge(),
role: body?.role || userExists.getRole(),
verified: userExists.getVerified(),
avatar: body?.avatar || userExists.getAvatar(),
createdAt: <string>(<unknown>Date.parse(userExists.getCreatedAt()))
});

Expand Down
15 changes: 12 additions & 3 deletions lambdas/src/functions/user-message.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { SQSEvent, Handler } from 'aws-lambda';
import { DomainError } from '@general';
import { DomainError, SESEmailService, UserPrimitives } from '@general';

const notification = async (event: SQSEvent): Promise<void> => {
try {
for (const record of event.Records) {
const messageBody = record.body;
console.info(`Received message from SQS: ${messageBody}`);
const body = JSON.parse(record.body);

console.info(`Received message from SQS: ${body.Message}`);

const user: UserPrimitives = JSON.parse(body.Message);

await SESEmailService.send({
destination: user.email,
message: 'Should validate your email',
subject: 'User-Service Email-Validation'
});
}
} catch (error: DomainError | any) {
console.error(error);
Expand Down
3 changes: 3 additions & 0 deletions layers/general/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"run-checks": "yarn lint && yarn test && yarn compile"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.379.1",
"@aws-sdk/client-ses": "^3.379.1",
"@aws-sdk/client-sns": "^3.377.0",
"@middy/core": "2.5.3",
"@middy/http-cors": "2.5.3",
Expand All @@ -28,6 +30,7 @@
"dayjs": "^1.11.8",
"dynamoose": "3.2.0",
"esbuild": "0.14.14",
"jimp": "^0.22.10",
"jsonwebtoken": "^9.0.0",
"reflect-metadata": "0.1.13",
"uuid": "8.3.2"
Expand Down
32 changes: 32 additions & 0 deletions layers/general/src/domain/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export class User {
private password: string,
private age: number,
private role: UserRole,
private verified: boolean,
private avatar: string,
private createdAt: string,
private updatedAt: string
) {
Expand All @@ -38,6 +40,8 @@ export class User {
u.password,
u.age,
u.role as UserRole,
u.verified,
u.avatar,
u.createdAt || date,
u.updatedAt || date
);
Expand All @@ -61,6 +65,12 @@ export class User {
"The email isn't in the correct format."
);

if (this.isValidHttpUrl(this.avatar))
throw new InvalidPropertyError(
this.avatar,
'The avatar should have correct url format (location)'
);

if (this.age < 18)
throw new InvalidPropertyError(
this.age,
Expand All @@ -79,6 +89,8 @@ export class User {
password: this.password,
age: this.age,
role: this.role,
avatar: this.avatar,
verified: this.verified,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
Expand All @@ -92,6 +104,10 @@ export class User {
return this.firstName;
}

getAvatar(): string {
return this.avatar;
}

getLastName(): string {
return this.lastName;
}
Expand All @@ -116,6 +132,10 @@ export class User {
return this.role;
}

getVerified(): boolean {
return this.verified;
}

getCreatedAt(): string {
return this.createdAt;
}
Expand All @@ -127,4 +147,16 @@ export class User {
getPhone(): string | undefined {
return this.phone;
}

private isValidHttpUrl(uri: string) {
let url;

try {
url = new URL(uri);
} catch (_) {
return false;
}

return url.protocol === 'http:' || url.protocol === 'https:';
}
}
9 changes: 9 additions & 0 deletions layers/general/src/domain/services/bucket.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type BucketParams = {
file: any;
filePath: string;
};

export interface IBucketService<T> {
send(params: BucketParams, bucket?: string): Promise<T>;
get<S>(fileName: string, bucket?: string): Promise<S>;
}
10 changes: 10 additions & 0 deletions layers/general/src/domain/services/email.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type SESParams = {
destination: string;
message: string;
subject: string;
origin: string;
};

export interface IEmailService<T> {
send(params: SESParams): Promise<T>;
}
3 changes: 3 additions & 0 deletions layers/general/src/domain/services/topic.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ITopicService<T> {
send(message: string): Promise<T>;
}
26 changes: 24 additions & 2 deletions layers/general/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
export { IsOptional, IsString, IsNumber, MinLength } from 'class-validator';
export {
IsOptional,
IsString,
IsNumber,
MinLength,
IsBoolean
} from 'class-validator';
export { config } from '@/infrastructure/config';

import Jimp from 'jimp';
import HttpStatus from '@/domain/types/HttpStatus';
import middify from '@/infrastructure/middlewares/middify';
export { middify, HttpStatus };
import { SESEmailService as SESEmailClass } from '@/infrastructure/services/ses.email.service';
import { SNSTopicService as SNSTopicClass } from '@/infrastructure/services/sns.topic.service';
import { S3BucketService as S3BucketClass } from '@/infrastructure/services/s3.bucket.service';
const SESEmailService = SESEmailClass.getInstance();
const SNSTopicService = SNSTopicClass.getInstance();
const S3BucketService = S3BucketClass.getInstance();

export {
Jimp,
middify,
HttpStatus,
SESEmailService,
SNSTopicService,
S3BucketService
};

export { UserRole } from '@/domain/types/user.role';
export { User, UserPrimitives } from '@/domain/entities/User';
Expand Down
5 changes: 4 additions & 1 deletion layers/general/src/infrastructure/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export const config = {
}
},
region: process.env.AWS_REGION || 'us-east-1',
userTopic: process.env.TOPIC_ARN
userTopic: process.env.TOPIC_ARN,
bucket: process.env.BUCKET_NAME,
adminEmail: process.env.ADMIN_EMAIL,
waterMarkImg: process.env.WATER_MARK_IMG
},
isOffline: process.env.IS_OFFLINE == 'true' // Variable IS_OFFLINE is always set true when the app runs locally by the serverless-offline plugin (sam local start-api).
};
Loading
Loading