Skip to content

Feature: implement file upload #2

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
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: 5 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -24,6 +24,11 @@ module.exports = {
'no-empty-function': 'off',
'import/no-extraneous-dependencies': 'off',
'no-extra-semi': 'off',
'no-console': 'off',
'dot-notation': 'off',
'max-len': [
'error',
150,
],
},
};
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -12,3 +12,6 @@ jspm_packages

# autogenerated files
swagger

# dotenv
.env
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"cSpell.words": [
"autoswagger",
"eventbridge",
"middyfied",
"typefiles"
]
}
18,295 changes: 12,306 additions & 5,989 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 20 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -4,26 +4,37 @@
"description": "Lambdas and DynamoDB",
"main": "serverless.ts",
"scripts": {
"deploy": "npx sls deploy",
"offline": "npx sls offline start",
"print": "npx sls print",
"test": "echo \"Error: no test specified\" && exit 1"
},
"engines": {
"node": ">=14.15.0"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.549.0",
"@aws-sdk/client-eventbridge": "^3.552.0",
"@aws-sdk/client-s3": "^3.550.0",
"@aws-sdk/client-ses": "^3.478.0",
"@aws-sdk/s3-presigned-post": "^3.550.0",
"@middy/core": "^3.4.0",
"@middy/http-json-body-parser": "^3.4.0",
"@middy/validator": "^5.1.0",
"aws-lambda": "^1.0.7",
"aws-sdk": "^2.1522.0",
"serverless-auto-swagger": "^2.12.0",
"serverless-dynamodb": "^0.2.47",
"@tokenizer/s3": "^0.2.3",
"dynamodb-toolbox": "^0.9.2",
"file-type": "^19.0.0",
"http-errors": "^2.0.0",
"install": "^0.13.0",
"npm": "^10.5.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@serverless/typescript": "^3.0.0",
"@types/aws-lambda": "^8.10.71",
"@types/glob": "^8.1.0",
"@types/http-errors": "^2.0.4",
"@types/node": "^14.14.25",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"cz-conventional-changelog": "^3.3.0",
@@ -33,6 +44,10 @@
"eslint-plugin-import": "^2.29.1",
"json-schema-to-ts": "^1.5.0",
"serverless": "^3.0.0",
"serverless-auto-swagger": "^2.12.0",
"serverless-dotenv-plugin": "^6.0.0",
"serverless-dynamodb": "^0.2.47",
"serverless-dynamodb-local": "^0.2.40",
"serverless-esbuild": "^1.23.3",
"serverless-offline": "^13.3.2",
"ts-node": "^10.9.2",
270 changes: 134 additions & 136 deletions serverless.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import type { AWS } from '@serverless/typescript';
import scanFs from '@libs/fs-scanner';
import { ref } from '@libs/ref-factory';

import { EventBridge } from '@resources/event-bridge/event-bridge';

// S3
import { Bucket } from '@resources/s3/s3';

// DynamoDB Tables
import { UsersTable } from '@resources/dynamodb/users-table';
import { ShopsTable } from '@resources/dynamodb/shops-table';
import { FilesTable } from '@resources/dynamodb/files-table';

// Authorizer
import { getUploadUrlAuthorizer } from '@functions/authorizer/upload/config';
// import { getDownloadUrlAuthorizer } from '@functions/authorizer/download/config';

// User
import {
@@ -16,149 +32,131 @@ import {
} from '@functions/shop/routes';

// Mail
import {
sendMail,
} from '@functions/mail/routes';
import { sendMail } from '@functions/mail/routes';

const serverlessConfiguration: AWS = {
service: 'NodeTeam',
frameworkVersion: '3',
plugins: ['serverless-esbuild', 'serverless-dynamodb', 'serverless-auto-swagger', 'serverless-offline'],
provider: {
name: 'aws',
runtime: 'nodejs18.x',
apiGateway: {
minimumCompressionSize: 1024,
shouldStartNameWithService: true,
},
environment: {
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
},
},
functions: {
usersGetAll,
usersCreate,
usersGetById,
usersGetByEmail,
setVerifiedUser,
shopsCreate,
shopsGetById,
sendMail,
},
package: { individually: true },
custom: {
esbuild: {
bundle: true,
minify: false,
sourcemap: true,
exclude: ['aws-sdk'],
target: 'node18',
define: { 'require.resolve': undefined },
platform: 'node',
concurrency: 10,
},
autoswagger: {
title: 'NodeTeam',
basePath: '/dev',
},
'serverless-dynamodb': {
start: {
port: 8000,
docker: false,
migrate: true,
inMemory: false,
dbPath: '../.dynamodb',
// File
import { getSignedUploadUrl, listFiles } from '@functions/file/crud/routes';
import { dispatchFileUploadedEvent } from '@functions/file/dispatch-file-uploaded-event/config';
import { onFileUploaded } from '@functions/file/on-file-uploaded/config';

const TYPE_FILE_PATTERN = './src/**/*.d.ts';

const getConfiguration = async (): Promise<AWS> => {
const typeFileNames = await scanFs(TYPE_FILE_PATTERN);

return {
service: 'NodeTeam',
frameworkVersion: '3',
plugins: [
'serverless-dotenv-plugin',
'serverless-auto-swagger',
'serverless-esbuild',
'serverless-dynamodb',
'serverless-offline',
],
provider: {
name: 'aws',
runtime: 'nodejs18.x',
apiGateway: {
minimumCompressionSize: 1024,
shouldStartNameWithService: true,
},
},
},
resources: {
Resources: {
UsersTable: {
Type: 'AWS::DynamoDB::Table',
Properties: {
TableName: 'UsersTable',
AttributeDefinitions: [{
AttributeName: 'userId',
AttributeType: 'S',
}, {
AttributeName: 'email',
AttributeType: 'S',
}],
KeySchema: [{
AttributeName: 'userId',
KeyType: 'HASH',
}],
GlobalSecondaryIndexes: [{
IndexName: 'email-index',
KeySchema: [{
AttributeName: 'email',
KeyType: 'HASH',
}, {
AttributeName: 'userId',
KeyType: 'RANGE',
}],
AttributeDefinitions: [{
AttributeName: 'email',
AttributeType: 'S',
}],
Projection: {
ProjectionType: 'ALL',
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
environment: {
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
},
iamRoleStatements: [
{
Effect: 'Allow',
Resource: [
{
'Fn::Join': ['', [{ 'Fn::GetAtt': ['Bucket', 'Arn'] }, '/*']],
},
}],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
],
Action: ['s3:PutObject', 's3:GetObject', 's3:DeleteObject'],
},
{
Effect: 'Allow',
Resource: [{ 'Fn::GetAtt': ['FilesTable', 'Arn'] }],
Action: [
'dynamodb:Query',
'dynamodb:GetItem',
'dynamodb:DeleteItem',
'dynamodb:PutItem',
],
},
{
Effect: 'Allow',
Resource: [{ 'Fn::GetAtt': ['EventBridge', 'Arn'] }],
Action: ['events:PutEvents'],
},
],
},
functions: {
dispatchFileUploadedEvent,
getUploadUrlAuthorizer,
getSignedUploadUrl,
listFiles,
onFileUploaded,
setVerifiedUser,
shopsCreate,
shopsGetById,
sendMail,
usersCreate,
usersGetAll,
usersGetById,
usersGetByEmail,
},
package: { individually: true },
custom: {
esbuild: {
bundle: true,
minify: false,
sourcemap: true,
exclude: [],
target: 'node18',
define: { 'require.resolve': undefined },
platform: 'node',
concurrency: 10,
},
ShopsTable: {
Type: 'AWS::DynamoDB::Table',
Properties: {
TableName: 'ShopsTable',
AttributeDefinitions: [{
AttributeName: 'shopId',
AttributeType: 'S',
}, {
AttributeName: 'name',
AttributeType: 'S',
}],
KeySchema: [{
AttributeName: 'shopId',
KeyType: 'HASH',
}],
GlobalSecondaryIndexes: [{
IndexName: 'name-index',
KeySchema: [{
AttributeName: 'name',
KeyType: 'HASH',
}, {
AttributeName: 'shopId',
KeyType: 'RANGE',
}],
AttributeDefinitions: [{
AttributeName: 'name',
AttributeType: 'S',
}],
Projection: {
ProjectionType: 'ALL',
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
}],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
autoswagger: {
title: 'NodeTeam',
basePath: '/dev',
typefiles: typeFileNames,
apiType: 'http',
apiKeyHeaders: ['Authorization'],
},
'serverless-dynamodb': {
start: {
port: 8000,
docker: false,
migrate: true,
inMemory: false,
dbPath: '../.dynamodb',
},
},
bucketName: ref({ Bucket }),
filesTableName: ref({ FilesTable }),
filesTableStreamArn: { 'Fn::GetAtt': ['FilesTable', 'StreamArn'] },
filesTableArn: { 'Fn::GetAtt': ['FilesTable', 'Arn'] },
eventBridgeArn: 'arn:aws:events:#{AWS::Region}:#{AWS::AccountId}:event-bus/NodeTeam',
eventBusName: ref({ EventBridge }),
getSignedDownloadUrlArn: {
'Fn::GetAtt': ['GetSignedDownloadUrlLambdaFunction', 'Arn'],
},
getSignedUploadUrlArn: {
'Fn::GetAtt': ['GetSignedUploadUrlLambdaFunction', 'Arn'],
},
},
resources: {
Resources: {
Bucket,
EventBridge,
FilesTable,
ShopsTable,
UsersTable,
},
},
},
};
};

module.exports = serverlessConfiguration;
module.exports = getConfiguration();
5 changes: 5 additions & 0 deletions src/constants/dynamodb.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const TABLE_NAMES = {
USERS: 'UsersTable',
SHOPS: 'ShopsTable',
FILES: 'FilesTable2',
} as const;
6 changes: 6 additions & 0 deletions src/constants/eventbridge.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const S4_EVENTS = {
SOURCE: 's4-events',
DETAIL_TYPES: {
FILE_UPLOADED: 'FILE_UPLOADED',
},
} as const;
23 changes: 23 additions & 0 deletions src/constants/http-method.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const CONNECT = 'CONNECT';
export const DELETE = 'DELETE';
export const GET = 'GET';
export const HEAD = 'HEAD';
export const OPTIONS = 'OPTIONS';
export const PATCH = 'PATCH';
export const POST = 'POST';
export const PUT = 'PUT';
export const TRACE = 'TRACE';

const HTTP_METHODS = {
CONNECT,
DELETE,
GET,
HEAD,
OPTIONS,
PATCH,
POST,
PUT,
TRACE,
} as const;

export default HTTP_METHODS;
40 changes: 40 additions & 0 deletions src/entities/file-table.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { Table } from 'dynamodb-toolbox';

import {
PRIMARY_KEY, SORT_KEY, LSI, KeyType,
} from '@resources/dynamodb/files-table';

const DocumentClient = new DynamoDBClient({
region: 'us-east-1',
credentials: {
accessKeyId: 'MockAccessKeyId',
secretAccessKey: 'MockSecretAccessKey',
},
});

const findKeyName = (
keySchema: { AttributeName: string; KeyType: KeyType }[],
keyType: KeyType,
) => keySchema.find(({ KeyType: CurrentKeyType }) => CurrentKeyType === keyType).AttributeName;

const INDEXES = Object.values(LSI).reduce(
(accIndexes, { IndexName, KeySchema }) => ({
...accIndexes,
[IndexName]: {
partitionKey: findKeyName(KeySchema, KeyType.HASH),
sortKey: findKeyName(KeySchema, KeyType.RANGE),
},
}),
{},
);

export const FileTableEntity = new Table({
name: process.env.FILE_TABLE_NAME,
partitionKey: PRIMARY_KEY,
sortKey: SORT_KEY,
indexes: INDEXES,
autoExecute: true,
autoParse: true,
DocumentClient,
});
9 changes: 9 additions & 0 deletions src/functions/authorizer/download/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { handlerPath } from '@libs/handler-resolver';

export const getDownloadUrlAuthorizer = {
handler: `${handlerPath(__dirname)}/handler.main`,
environment: {
// eslint-disable-next-line no-template-curly-in-string
GET_DOWNLOAD_URL_LAMBDA_ARN: '${self:custom.getSignedDownloadUrlArn}',
},
};
15 changes: 15 additions & 0 deletions src/functions/authorizer/download/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { APIGatewayRequestAuthorizerHandler } from 'aws-lambda';
import { generateInvokePolicyDocument } from '@libs/policy-generator';

export const main: APIGatewayRequestAuthorizerHandler = async (event) => {
const { Authorization: token } = event.headers;

// Naive authentication strategy
// all requests with a "token" containing the "allowMeToDownload" string are accepted
const isAuthorized = token?.includes('allowMeToDownload');

return generateInvokePolicyDocument(
event.methodArn,
isAuthorized ? 'Allow' : 'Deny',
);
};
9 changes: 9 additions & 0 deletions src/functions/authorizer/upload/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { handlerPath } from '@libs/handler-resolver';

export const getUploadUrlAuthorizer = {
handler: `${handlerPath(__dirname)}/handler.main`,
environment: {
// eslint-disable-next-line no-template-curly-in-string
GET_UPLOAD_URL_LAMBDA_ARN: '${self:custom.getSignedUploadUrlArn}',
},
};
15 changes: 15 additions & 0 deletions src/functions/authorizer/upload/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { APIGatewayRequestAuthorizerHandler } from 'aws-lambda';
import { generateInvokePolicyDocument } from '@libs/policy-generator';

export const main: APIGatewayRequestAuthorizerHandler = async (event) => {
const { Authorization: token } = event.headers;

// Naive authentication strategy
// all requests with a "token" containing the "allowMeToUpload" string are accepted
const isAllowed = token?.includes('allowMeToUpload');

return generateInvokePolicyDocument(
event.methodArn,
isAllowed ? 'Allow' : 'Deny',
);
};
67 changes: 67 additions & 0 deletions src/functions/file/crud/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { middify } from '@libs/lambda';
import { fileService } from '@services/index';
import { formatJSONResponse } from '@libs/api-gateway';
import { APIGatewayProxyEvent, APIGatewayProxyHandler } from 'aws-lambda';

/**
* @function: src/functions/file/handler.getAll
* @description: Gets all files
* @returns: { files: File[] }
* @example: curl -X GET http://localhost:3000/dev/files/
*/
const getAll = middify(async () => {
const files = await fileService.getAll();

return formatJSONResponse({
files,
});
});

/**
* @function: src/functions/file/handler.getSignedUploadUrl
* @description: Gets a file upload presigned URL
* @returns: {
* "url": "https://{bucket-name}.s3.{region}.amazon.com/",
* "fields": {
* "x-amz-storage-class": "INTELLIGENT_TIERING",
* "bucket": "{bucket-name}",
* "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
* "X-Amz-Credential": "{AWS_ACCESS_KEY_ID}/{current-date}/{region}/s3/aws4_request",
* "X-Amz-Date": "20240410T074048Z",
* "key": "182a3a48-362a-4605-a74b-b95a86b3a6bc_file.pdf",
* "Policy": "eyJ...XX0=",
* "X-Amz-Signature": "203...982"
* }
* }
* @example: curl -X POST http://localhost:3000/dev/files/signed-upload-url?fileType=application%2Fpdf&fileName=file.pdf
*/
const getSignedUploadUrl: APIGatewayProxyHandler = middify(
{
type: 'object',
properties: {
queryStringParameters: {
type: 'object',
properties: {
fileType: { type: 'string' },
fileName: { type: 'string' },
},
required: ['fileType', 'fileName'],
},
},
required: ['queryStringParameters'],
},
async (event: APIGatewayProxyEvent) => {
const files = await fileService.getSignedUploadUrl({
...event,
});

return formatJSONResponse({
files,
});
},
);

module.exports = {
getAll,
getSignedUploadUrl,
};
60 changes: 60 additions & 0 deletions src/functions/file/crud/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* eslint-disable no-template-curly-in-string */
import { Route } from '@common-types/route.type';
import { GET } from '@constants/http-method.constants';
import EventOptionsFactory from '@libs/event-options-factory';
import { handlerPath } from '@libs/handler-resolver';

const BASE_PATH = 'files';
const SWAGGER_TAG = 'Files';

const renderOptions = EventOptionsFactory.http({ path: BASE_PATH, swaggerTags: [SWAGGER_TAG] });

export const listFiles: Route & { environment: any } = {
/**
* @function: src/functions/file/handler.getAll
*/
handler: `${handlerPath(__dirname)}/handler.getAll`,
environment: {
FILE_TABLE_NAME: '${self:custom.filesTableName}',
},
events: [
{
http: renderOptions({
method: 'GET',
}),
},
],
};

export const getSignedUploadUrl = {
handler: `${handlerPath(__dirname)}/handler.getSignedUploadUrl`,
environment: {
BUCKET_NAME: '${self:custom.bucketName}',
FILE_TABLE_NAME: '${self:custom.filesTableName}',
},
events: [
{
http: renderOptions({
method: GET,
path: 'signed-upload-url',
authorizer: {
name: 'getUploadUrlAuthorizer',
type: 'request',
identitySource: 'method.request.header.Authorization',
},
queryStringParameters: {
fileType: {
required: false,
type: 'string',
description: 'Type of the uploaded file',
},
fileName: {
required: false,
type: 'string',
description: 'Uploaded file\'s name',
},
},
}),
},
],
};
19 changes: 19 additions & 0 deletions src/functions/file/dispatch-file-uploaded-event/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* eslint-disable no-template-curly-in-string */
import { handlerPath } from '@libs/handler-resolver';

export const dispatchFileUploadedEvent = {
handler: `${handlerPath(__dirname)}/handler.dispatchFileUploadedEvent`,
environment: {
EVENT_BUS_NAME: '${self:custom.eventBusName}',
FILE_TABLE_NAME: '${self:custom.filesTableName}',
},
events: [
{
s3: {
bucket: '${self:custom.bucketName}',
event: 's3:ObjectCreated:*',
existing: true,
},
},
],
};
77 changes: 77 additions & 0 deletions src/functions/file/dispatch-file-uploaded-event/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { EventBridgeClientSingleton } from '@libs/aws-clients/event-bridge-client.singleton';
import { S3ClientSingleton } from '@libs/aws-clients/s3-client.singleton';
import { S3Event, S3EventRecord } from 'aws-lambda';
import { makeTokenizer } from '@tokenizer/s3';
import { fileTypeFromTokenizer } from 'file-type';
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
import { PutEventsCommand } from '@aws-sdk/client-eventbridge';
import { S4_EVENTS } from '@constants/eventbridge.constants';

const S3Client = S3ClientSingleton.getClient();
const eventBridge = EventBridgeClientSingleton.getClient();

const urlDecode = (url: string) => decodeURIComponent(url.replace(/\+/g, ' '));

const dispatchFileUploadedEvent = async (event: S3Event) => {
const eventPayloads = await Promise.all(
event.Records.map(async (eventRecord: S3EventRecord) => {
const bucketName = eventRecord.s3.bucket.name;
const objectKey = urlDecode(eventRecord.s3.object.key);
const [filePrefix, fileName] = objectKey.split('/');
const fileSize = eventRecord.s3.object.size;

const s3Tokenizer = await makeTokenizer(S3Client, {
Bucket: bucketName,
Key: objectKey,
});

const { ext } = await fileTypeFromTokenizer(s3Tokenizer);

if (ext !== fileName.split('.').slice(-1)[0]) {
const deleteParams = {
Bucket: bucketName,
Key: objectKey,
};

try {
await S3Client.send(new DeleteObjectCommand(deleteParams));

console.log(`Found inconsistent uploaded file type, deleting ${objectKey}`);
} catch (error) {
console.error('Error deleting found inconsistent object:', error);
}

return null;
}

return {
Source: S4_EVENTS.SOURCE,
DetailType: S4_EVENTS.DETAIL_TYPES.FILE_UPLOADED,
Detail: JSON.stringify({
bucketName,
fileName,
fileSize,
filePrefix,
fileType: ext,
}),
EventBusName: process.env.EVENT_BUS_NAME,
};
}),
);

const entries = eventPayloads.filter((x) => x);

if (entries.length) {
const params = { Entries: entries };

try {
await eventBridge.send(new PutEventsCommand(params));
} catch (err) {
console.error(err, err.stack);
}
}
};

module.exports = {
dispatchFileUploadedEvent,
};
21 changes: 21 additions & 0 deletions src/functions/file/on-file-uploaded/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint-disable no-template-curly-in-string */
import { S4_EVENTS } from '@constants/eventbridge.constants';
import { handlerPath } from '@libs/handler-resolver';

export const onFileUploaded = {
handler: `${handlerPath(__dirname)}/handler.onFileUploaded`,
environment: {
FILE_TABLE_NAME: '${self:custom.filesTableName}',
},
events: [
{
eventBridge: {
eventBus: '${self:custom.eventBridgeArn}',
pattern: {
source: [S4_EVENTS.SOURCE],
'detail-type': [S4_EVENTS.DETAIL_TYPES.FILE_UPLOADED],
},
},
},
],
};
30 changes: 30 additions & 0 deletions src/functions/file/on-file-uploaded/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { EventBridgeEvent } from 'aws-lambda';
import File from '@model/File';
import { fileService } from '@services/index';
import { S4_EVENTS } from '@constants/eventbridge.constants';

if (S4_EVENTS.DETAIL_TYPES.FILE_UPLOADED !== 'FILE_UPLOADED') {
throw new Error(`Check constant "${S4_EVENTS.DETAIL_TYPES.FILE_UPLOADED}"`);
}

const onFileUploaded = async (
event: EventBridgeEvent<'FILE_UPLOADED', File>,
): Promise<void> => {
const {
filePrefix, fileName, fileSize, fileType, bucketName,
} = event.detail;

console.log(JSON.stringify({ fileName }, null, 4));

await fileService.create({
filePrefix,
fileName,
fileSize,
fileType,
bucketName,
});
};

module.exports = {
onFileUploaded,
};
6 changes: 3 additions & 3 deletions src/functions/mail/handler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { formatJSONResponse } from '@libs/api-gateway';
import { middyfy } from '@libs/lambda';
import { middify } from '@libs/lambda';

const ses = new SESClient({
region: 'us-west-2',
region: 'us-east-1',
credentials: {
accessKeyId: 'MockAccessKeyId',
secretAccessKey: 'MockSecretAccessKey',
@@ -18,7 +18,7 @@ type APIGatewayProxyEventWithBody<TBody> = Omit<APIGatewayProxyEvent, 'body'> &
* @description: Send Mail
* @example: curl -X POST http://localhost:3000/dev/mail -d '{"to": "test@test"}'
*/
const sendMail = middyfy(
const sendMail = middify(
{
type: 'object',
required: ['body'],
18 changes: 10 additions & 8 deletions src/functions/mail/routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Route } from '@common-types/route.type';
import { POST } from '@constants/http-method.constants';
import EventOptionsFactory from '@libs/event-options-factory';
import { handlerPath } from '@libs/handler-resolver';

type Route = {
handler: string;
events: any[];
}
const BASE_PATH = 'mail';
const SWAGGER_TAG = 'Mail';

const renderOptions = EventOptionsFactory.http({ path: BASE_PATH, swaggerTags: [SWAGGER_TAG] });

export const sendMail: Route = {
/**
@@ -12,10 +15,9 @@ export const sendMail: Route = {
handler: `${handlerPath(__dirname)}/handler.sendMail`,
events: [
{
http: {
method: 'post',
path: 'mail/',
},
http: renderOptions({
method: POST,
}),
},
],
};
9 changes: 5 additions & 4 deletions src/functions/shop/handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { v4 as uuidv4 } from 'uuid';
import { formatJSONResponse } from '@libs/api-gateway';
import { middyfy } from '@libs/lambda';
import { middify } from '@libs/lambda';
import { shopService } from '../../services';
import { ShopCreateInput } from './types';

type APIGatewayProxyEventWithBody<TBody> = Omit<APIGatewayProxyEvent, 'body'> & { body: TBody };

@@ -12,7 +13,7 @@ type APIGatewayProxyEventWithBody<TBody> = Omit<APIGatewayProxyEvent, 'body'> &
* @returns: { shop: Shop }
* @example: curl -X POST http://localhost:3000/dev/shop -d '{"email": "test@test"}'
*/
const create = middyfy(
const create = middify(
{
type: 'object',
required: ['body'],
@@ -30,7 +31,7 @@ const create = middyfy(
},
},
},
async (event: APIGatewayProxyEventWithBody<any>): Promise<APIGatewayProxyResult> => {
async (event: APIGatewayProxyEventWithBody<ShopCreateInput>): Promise<APIGatewayProxyResult> => {
const shop = await shopService.create({
shopId: uuidv4(),
email: event.body.email,
@@ -52,7 +53,7 @@ const create = middyfy(
* @returns: { shop: Shop }
* @example: curl -X GET http://localhost:3000/dev/shop/123
*/
const getById = middyfy(
const getById = middify(
{
type: 'object',
required: ['pathParameters'],
27 changes: 15 additions & 12 deletions src/functions/shop/routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { handlerPath } from '@libs/handler-resolver';
import { GET, POST } from '@constants/http-method.constants';
import EventOptionsFactory from '@libs/event-options-factory';
import { Route } from '@common-types/route.type';

type Route = {
handler: string;
events: any[];
}
const BASE_PATH = 'shops';
const SWAGGER_TAG = 'Shops';

const renderOptions = EventOptionsFactory.http({ path: BASE_PATH, swaggerTags: [SWAGGER_TAG] });

export const create: Route = {
/**
@@ -12,10 +15,10 @@ export const create: Route = {
handler: `${handlerPath(__dirname)}/handler.create`,
events: [
{
http: {
method: 'post',
path: 'shop',
},
http: renderOptions({
method: POST,
bodyType: 'ShopCreateInput',
}),
},
],
};
@@ -27,10 +30,10 @@ export const getById: Route = {
handler: `${handlerPath(__dirname)}/handler.getById`,
events: [
{
http: {
method: 'get',
path: 'shop/{shopId}',
},
http: renderOptions({
method: GET,
path: '{shopId}',
}),
},
],

7 changes: 7 additions & 0 deletions src/functions/shop/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ShopCreateInput = {
email: string,
phone: string,
name: string,
address: string,
location: [number, number],
};
21 changes: 11 additions & 10 deletions src/functions/user/handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { v4 as uuidv4 } from 'uuid';
import { formatJSONResponse } from '@libs/api-gateway';
import { middyfy } from '@libs/lambda';
import { userService } from '../../services';
import { middify } from '@libs/lambda';
import { userService } from '@services/index';
import { UserCreateInput } from './types';

type APIGatewayProxyEventWithBody<TBody> = Omit<APIGatewayProxyEvent, 'body'> & { body: TBody };

@@ -12,7 +13,7 @@ type APIGatewayProxyEventWithBody<TBody> = Omit<APIGatewayProxyEvent, 'body'> &
* @returns: { users: User[] }
* @example: curl -X GET http://localhost:3000/dev/user/
*/
const getAll = middyfy(async () => {
const getAll = middify(async () => {
const users = await userService.getAll();

return formatJSONResponse({
@@ -26,7 +27,7 @@ const getAll = middyfy(async () => {
* @returns: { user: User }
* @example: curl -X POST http://localhost:3000/dev/user -d '{"email": "test@test"}'
*/
const create = middyfy(
const create = middify(
{
type: 'object',
required: ['body'],
@@ -40,7 +41,7 @@ const create = middyfy(
},
},
},
async (event: APIGatewayProxyEventWithBody<any>): Promise<APIGatewayProxyResult> => {
async (event: APIGatewayProxyEventWithBody<UserCreateInput>): Promise<APIGatewayProxyResult> => {
const user = await userService.create({
email: event.body.email,
userId: uuidv4(),
@@ -59,7 +60,7 @@ const create = middyfy(
* @returns: { user: User }
* @example: curl -X GET http://localhost:3000/dev/user/123
*/
const getById = middyfy(
const getById = middify(
{
type: 'object',
required: ['pathParameters'],
@@ -88,7 +89,7 @@ const getById = middyfy(
* @returns: { user: User }
* @example: curl -X GET http://localhost:3000/dev/user/email/test@test
*/
const getByEmail = middyfy(
const getByEmail = middify(
{
type: 'object',
required: ['pathParameters'],
@@ -117,7 +118,7 @@ const getByEmail = middyfy(
* @returns: { user: User }
* @example: curl -X PUT http://localhost:3000/dev/user -d '{"email": "test@test"}'
*/
const setVerified = middyfy(
const setVerified = middify(
{
type: 'object',
required: ['body'],
@@ -138,10 +139,10 @@ const setVerified = middyfy(
},
},
},
async (event: APIGatewayProxyEventWithBody<any>): Promise<APIGatewayProxyResult> => {
async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const user = await userService.setVerified({
userId: event.pathParameters.userId,
isVerified: event.body.isVerified,
isVerified: true,
});

return formatJSONResponse({
50 changes: 26 additions & 24 deletions src/functions/user/routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { handlerPath } from '@libs/handler-resolver';
import { GET, PATCH, POST } from '@constants/http-method.constants';
import EventOptionsFactory from '@libs/event-options-factory';
import { Route } from '@common-types/route.type';

type Route = {
handler: string;
events: any[];
}
const BASE_PATH = 'users';
const SWAGGER_TAG = 'Users';

const renderOptions = EventOptionsFactory.http({ path: BASE_PATH, swaggerTags: [SWAGGER_TAG] });

export const getAll: Route = {
/**
@@ -12,10 +15,9 @@ export const getAll: Route = {
handler: `${handlerPath(__dirname)}/handler.getAll`,
events: [
{
http: {
method: 'get',
path: 'user/',
},
http: renderOptions({
method: GET,
}),
},
],
};
@@ -27,10 +29,10 @@ export const create: Route = {
handler: `${handlerPath(__dirname)}/handler.create`,
events: [
{
http: {
method: 'post',
path: 'user',
},
http: renderOptions({
method: POST,
bodyType: 'UserCreateInput',
}),
},
],
};
@@ -42,10 +44,10 @@ export const getById: Route = {
handler: `${handlerPath(__dirname)}/handler.getById`,
events: [
{
http: {
method: 'get',
path: 'user/{userId}',
},
http: renderOptions({
method: GET,
path: '{userId}',
}),
},
],
};
@@ -57,10 +59,10 @@ export const getByEmail: Route = {
handler: `${handlerPath(__dirname)}/handler.getByEmail`,
events: [
{
http: {
method: 'get',
path: 'user/email/{email}',
},
http: renderOptions({
method: GET,
path: 'email/{email}',
}),
},
],
};
@@ -72,10 +74,10 @@ export const setVerified: Route = {
handler: `${handlerPath(__dirname)}/handler.setVerified`,
events: [
{
http: {
method: 'patch',
path: 'user/{userId}',
},
http: renderOptions({
method: PATCH,
path: '{userId}',
}),
},
],
};
3 changes: 3 additions & 0 deletions src/functions/user/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type UserCreateInput = {
email: string,
};
19 changes: 19 additions & 0 deletions src/libs/aws-clients/event-bridge-client.singleton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { EventBridgeClient } from '@aws-sdk/client-eventbridge';

export class EventBridgeClientSingleton {
private static client: EventBridgeClient | null = null;

public static getClient() {
if (!EventBridgeClientSingleton.client) {
EventBridgeClientSingleton.client = new EventBridgeClient({
region: process.env.MY_AWS_REGION,
credentials: {
accessKeyId: process.env.MY_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.MY_AWS_SECRET_ACCESS_KEY,
},
});
}

return EventBridgeClientSingleton.client;
}
}
19 changes: 19 additions & 0 deletions src/libs/aws-clients/s3-client.singleton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { S3Client } from '@aws-sdk/client-s3';

export class S3ClientSingleton {
private static client: S3Client | null = null;

public static getClient() {
if (!S3ClientSingleton.client) {
S3ClientSingleton.client = new S3Client({
region: process.env.MY_AWS_REGION,
credentials: {
accessKeyId: process.env.MY_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.MY_AWS_SECRET_ACCESS_KEY,
},
});
}

return S3ClientSingleton.client;
}
}
18 changes: 18 additions & 0 deletions src/libs/event-options-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import HttpOptionsType from '@common-types/http-options.type';
import * as path from 'node:path';

function renderHttpOptions<T extends HttpOptionsType>(
commonOptions: Pick<HttpOptionsType, 'path' | 'swaggerTags'>,
) {
return (options: T) => ({
...options,
path: path.join(...[commonOptions.path, options.path].filter((x) => x)),
swaggerTags: commonOptions.swaggerTags,
});
}

const EventOptionsFactory = {
http: renderHttpOptions,
};

export default EventOptionsFactory;
16 changes: 16 additions & 0 deletions src/libs/file-entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Entity } from 'dynamodb-toolbox';

import { FileTableEntity } from '@entities/file-table.entity';

export const FileEntity = new Entity({
name: 'File',
attributes: {
pk: { partitionKey: true, hidden: true },
filePrefix: { sortKey: true },
fileName: 'string',
fileSize: 'number',
fileType: 'string',
bucketName: 'string',
},
table: FileTableEntity,
});
20 changes: 20 additions & 0 deletions src/libs/file-size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const kB = (value: number) => value * (2 ** 10);
export const mB = (value: number) => kB(value) * (2 ** 10);

const fileLimitsByFormat = [
{ regexp: /image\/(\w+)/, maxSize: mB(10) },
{ regexp: /application\/(.+)\.docx/, maxSize: mB(10) },
{ regexp: /application\/(.+)\.doc/, maxSize: mB(10) },
{ regexp: /application\/(.+)\.pptx/, maxSize: mB(10) },
{ regexp: /application\/(.+)\.ppt/, maxSize: mB(10) },
{ regexp: /application\/(.+)\.xlsx/, maxSize: mB(10) },
{ regexp: /application\/(.+)\.xls/, maxSize: mB(10) },
{ regexp: /application\/pdf/, maxSize: mB(10) },
{ regexp: /video\/(\w+)/, maxSize: mB(100) },
];

export const getFileSizeLimit = (fileType: string): number => {
const { maxSize } = fileLimitsByFormat.find(({ regexp }) => regexp.exec(fileType)) || {};

return maxSize || 0;
};
13 changes: 13 additions & 0 deletions src/libs/fs-scanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as glob from 'glob';

export default function scanFs(pattern: string): Promise<string[]> {
return new Promise((resolve, reject) => {
glob(pattern, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});
}
8 changes: 8 additions & 0 deletions src/libs/is-empty-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const isEmptyObject = (obj: Record<string, any>): boolean => !Object.keys(obj).length;

export const isEmpty = (value: unknown): value is null | undefined | '' | {} => (
value === undefined
|| value === null
|| value === ''
|| (typeof value === 'object' && isEmptyObject(value))
);
2 changes: 1 addition & 1 deletion src/libs/lambda.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import middyJsonBodyParser from '@middy/http-json-body-parser';
import validator from '@middy/validator';
import { transpileSchema } from '@middy/validator/transpile';

export const middyfy = (schema, handler?) => {
export const middify = (schema, handler?) => {
if (typeof schema === 'object') {
return middy(handler).use(middyJsonBodyParser()).use(
validator({
16 changes: 16 additions & 0 deletions src/libs/policy-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const generateInvokePolicyDocument = (
resource: string,
effect: 'Allow' | 'Deny',
) => ({
principalId: 'allowedUploader',
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource,
},
],
},
});
11 changes: 11 additions & 0 deletions src/libs/ref-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const ref = <R extends Record<string, unknown>>(
resource: R,
): Record<'Ref', keyof R> => {
if (Object.keys(resource).length !== 1) {
throw new Error('Ref can only be used on one resource');
}

const [resourceName] = Object.keys(resource) as (keyof R)[];

return { Ref: resourceName };
};
9 changes: 9 additions & 0 deletions src/model/File.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
interface File {
filePrefix: string;
fileName: string;
fileSize: number;
fileType: string;
bucketName: string;
};

export default File;
23 changes: 23 additions & 0 deletions src/resources/dynamodb/files-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TABLE_NAMES } from '@constants/dynamodb.constants';
import { AWS } from '@serverless/typescript';

export const FilesTable: AWS['resources']['Resources']['value'] = {
Type: 'AWS::DynamoDB::Table',
Properties: {
TableName: TABLE_NAMES.FILES,
AttributeDefinitions: [
{
AttributeName: 'fileId',
AttributeType: 'S',
},
],
KeySchema: [{
AttributeName: 'fileId',
KeyType: 'HASH',
}],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
},
};
50 changes: 50 additions & 0 deletions src/resources/dynamodb/shops-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { AWS } from '@serverless/typescript';

export const ShopsTable: AWS['resources']['Resources']['value'] = {
Type: 'AWS::DynamoDB::Table',
Properties: {
TableName: 'ShopsTable',
AttributeDefinitions: [
{
AttributeName: 'shopId',
AttributeType: 'S',
},
{
AttributeName: 'name',
AttributeType: 'S',
},
],
KeySchema: [
{
AttributeName: 'shopId',
KeyType: 'HASH',
},
],
GlobalSecondaryIndexes: [
{
IndexName: 'name-index',
KeySchema: [
{
AttributeName: 'name',
KeyType: 'HASH',
},
{
AttributeName: 'shopId',
KeyType: 'RANGE',
},
],
Projection: {
ProjectionType: 'ALL',
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
},
],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
},
};
48 changes: 48 additions & 0 deletions src/resources/dynamodb/users-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { AWS } from '@serverless/typescript';

export const UsersTable: AWS['resources']['Resources']['value'] = {
Type: 'AWS::DynamoDB::Table',
Properties: {
TableName: 'UsersTable',
AttributeDefinitions: [
{
AttributeName: 'userId',
AttributeType: 'S',
},
{
AttributeName: 'email',
AttributeType: 'S',
},
],
KeySchema: [{
AttributeName: 'userId',
KeyType: 'HASH',
}],
GlobalSecondaryIndexes: [
{
IndexName: 'email-index',
KeySchema: [
{
AttributeName: 'email',
KeyType: 'HASH',
},
{
AttributeName: 'userId',
KeyType: 'RANGE',
},
],
Projection: {
ProjectionType: 'ALL',
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
},
],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
},
};
6 changes: 6 additions & 0 deletions src/resources/event-bridge/event-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { AWS } from '@serverless/typescript';

export const EventBridge: AWS['resources']['Resources']['value'] = {
Type: 'AWS::Events::EventBus',
Properties: { Name: 'NodeTeam' },
};
17 changes: 17 additions & 0 deletions src/resources/s3/s3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { AWS } from '@serverless/typescript';

export const Bucket: AWS['resources']['Resources']['value'] = {
Type: 'AWS::S3::Bucket',
DeletionPolicy: 'Retain',
Properties: {
CorsConfiguration: {
CorsRules: [
{
AllowedOrigins: ['*'],
AllowedHeaders: ['*'],
AllowedMethods: ['POST'],
},
],
},
},
};
87 changes: 87 additions & 0 deletions src/services/file-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
import { v4 as uuidv4 } from 'uuid';
import * as createHttpError from 'http-errors';
import File from '@model/File';
import { getFileSizeLimit, kB } from '@libs/file-size';
import { S3ClientSingleton } from '@libs/aws-clients/s3-client.singleton';
import { TABLE_NAMES } from '@constants/dynamodb.constants';

const URL_EXPIRES_IN_MS = 300;

export default class FileService {
private TableName: string = TABLE_NAMES.FILES;

private s3Client = S3ClientSingleton.getClient();

constructor(private docClient: DocumentClient) {}

async getAll(): Promise<File[]> {
const files = await this.docClient.scan({
TableName: this.TableName,
}).promise();

return files.Items as File[];
}

async getById(fileId: string): Promise<File> {
const file = await this.docClient.get({
TableName: this.TableName,
Key: {
fileId,
},
}).promise();

return file.Item as File;
}

async create(file: File): Promise<File> {
console.log(`file: ${JSON.stringify(file, null, 4)}`);
const { filePrefix: fileId, ...rest } = file;

await this.docClient.put({
TableName: this.TableName,
Item: {
...rest,
fileId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}).promise();

return file;
}

async getSignedUploadUrl({ queryStringParameters }) {
const { fileType, fileName } = queryStringParameters;
const fileSizeLimit = getFileSizeLimit(fileType);

if (fileSizeLimit === 0) {
throw new createHttpError.BadRequest();
}

const filePrefix = uuidv4();

const presignedPost: { url: string; fields: Record<string, string> } = await createPresignedPost(
this.s3Client,
{
Bucket: process.env.BUCKET_NAME,
Key: `${filePrefix}/${fileName}`,
Fields: {
'x-amz-storage-class': 'INTELLIGENT_TIERING',
},
Expires: URL_EXPIRES_IN_MS,
Conditions: [
['starts-with', '$key', `${filePrefix}`],
['content-length-range', kB(1), fileSizeLimit],
{ 'Content-Type': fileType },
],
},
);

return {
statusCode: 200,
body: presignedPost,
};
}
}
7 changes: 5 additions & 2 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import dynamoDBClient from '../model';
import UserService from './user-service';
import ShopService from './shop-service';
import FileService from './file-service';

const userService = new UserService(dynamoDBClient());
const fileService = new FileService(dynamoDBClient());
const shopService = new ShopService(dynamoDBClient());
const userService = new UserService(dynamoDBClient());

export {
userService,
fileService,
shopService,
userService,
};
4 changes: 2 additions & 2 deletions src/services/user-service.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import User from '../model/User';
export default class UserService {
private TableName: string = 'UsersTable';

constructor(private docClient: DocumentClient) { }
constructor(private docClient: DocumentClient) {}

async getAll(): Promise<User[]> {
const users = await this.docClient.scan({
@@ -25,7 +25,7 @@ export default class UserService {
return user.Item as User;
}

async getByEmail(email: string): Promise<DocumentClient.AttributeMap[]> {
async getByEmail(email: string): Promise<DocumentClient.AttributeMap[] | undefined> {
const user = await this.docClient.query({
TableName: this.TableName,
IndexName: 'email-index',
6 changes: 6 additions & 0 deletions src/types/http-method.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import HTTP_METHODS from '@constants/http-method.constants';
import ObjectValues from '@common-types/object-values.type';

type HttpMethodType = ObjectValues<typeof HTTP_METHODS>;

export default HttpMethodType;
16 changes: 16 additions & 0 deletions src/types/http-options.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import HttpMethodType from '@common-types/http-method.type';

type HttpOptionsType = {
method: HttpMethodType,
path?: string,
bodyType?: string,
queryStringParameters?: Record<string, unknown>,
swaggerTags?: string[],
authorizer?: {
name: string,
type: string,
identitySource: string,
},
}

export default HttpOptionsType;
3 changes: 3 additions & 0 deletions src/types/object-values.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type ObjectValues<T extends Record<string, string>> = T[keyof T];

export default ObjectValues;
4 changes: 4 additions & 0 deletions src/types/route.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type Route = {
handler: string;
events: any[];
}
28 changes: 25 additions & 3 deletions tsconfig.paths.json
Original file line number Diff line number Diff line change
@@ -2,8 +2,30 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@functions/*": ["src/functions/*"],
"@libs/*": ["src/libs/*"]
"@common-types/*": [
"src/types/*"
],
"@constants/*": [
"src/constants/*"
],
"@entities/*": [
"src/entities/*"
],
"@functions/*": [
"src/functions/*"
],
"@libs/*": [
"src/libs/*"
],
"@model/*": [
"src/model/*"
],
"@resources/*": [
"src/resources/*"
],
"@services/*": [
"src/services/*"
]
}
}
}
}