Wip backend (#4)

* feat: Add new API handlers for user, project, and task management; update package dependencies

* feat: Update .gitignore, add Lambda layer configuration, and refactor DynamoDB handlers to use AWS SDK v3

* feat: Update serverless configuration and refactor API handlers to improve error handling and response structure

* feat: Add Cognito user pool name parameter and update API handlers to include CORS headers

* feat: Update task and project ID formats, add populateSeedData function, and enhance user ID handling

* feat: Update image source paths to use S3 public URL for profile and task attachments
This commit was merged in pull request #4.
This commit is contained in:
2024-11-23 18:17:00 +02:00
committed by GitHub
parent ac8455ab3a
commit 11e61829f1
39 changed files with 5438 additions and 100 deletions

View File

@@ -0,0 +1,51 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, PutCommandInput } from "@aws-sdk/lib-dynamodb";
import { v4 as uuidv4 } from "uuid";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_PROJECT_TABLE_NAME = process.env.TASKER_PROJECT_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const { name, description, startDate, endDate } = JSON.parse(event.body);
try {
const newProject = {
category: "projects",
projectId: `project_${uuidv4()}`,
name,
description,
startDate,
endDate,
};
const params: PutCommandInput = {
TableName: TASKER_PROJECT_TABLE_NAME,
Item: newProject,
};
await docClient.put(params);
return {
statusCode: 201,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(newProject),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error creating project: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,69 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, PutCommandInput } from "@aws-sdk/lib-dynamodb";
import { v4 as uuidv4 } from "uuid";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TASK_TABLE_NAME = process.env.TASKER_TASK_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const {
title,
description,
status,
priority,
tags,
startDate,
dueDate,
points,
projectId,
authorUserId,
assignedUserId,
} = JSON.parse(event.body);
try {
const newTask = {
category: "tasks",
taskId: `task_${uuidv4()}`,
title,
description,
status,
priority,
tags,
startDate,
dueDate,
points,
projectId,
authorUserId,
assignedUserId,
};
const params: PutCommandInput = {
TableName: TASKER_TASK_TABLE_NAME,
Item: newTask,
};
await docClient.put(params);
return {
statusCode: 201,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(newTask),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error creating task: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,42 @@
import { fetchRandomTeamId } from "@/lib/util";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, PutCommandInput } from "@aws-sdk/lib-dynamodb";
import { v4 as uuidv4 } from "uuid";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_USER_TABLE_NAME = process.env.TASKER_USER_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
console.info(`Event: ${JSON.stringify(event)}`);
const username =
event.request.userAttributes["preferred_username"] || event.userName;
const cognitoId = event.userName;
const teamId = await fetchRandomTeamId();
try {
const newUser = {
category: "users",
cognitoId,
userId: `user_${uuidv4()}`,
username,
profilePictureUrl: "p0.jpeg",
teamId,
};
const params: PutCommandInput = {
TableName: TASKER_USER_TABLE_NAME,
Item: newUser,
};
await docClient.put(params);
console.info(`User ${username} created with teamId ${teamId}`);
} catch (error: any) {
throw new Error(`Error creating user: ${error.message}`);
}
return event;
};

View File

@@ -0,0 +1,42 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_PROJECT_TABLE_NAME = process.env.TASKER_PROJECT_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
try {
const params: QueryCommandInput = {
TableName: TASKER_PROJECT_TABLE_NAME,
KeyConditionExpression: "category = :category",
ExpressionAttributeValues: {
":category": "projects",
},
};
const projects = await docClient.query(params);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(projects.Items),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving projects: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,75 @@
import {
fetchAttachments,
fetchComments,
fetchUserWithUserId,
} from "@/lib/util";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TASK_TABLE_NAME = process.env.TASKER_TASK_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const { projectId } = event.queryStringParameters;
try {
const params: QueryCommandInput = {
TableName: TASKER_TASK_TABLE_NAME,
KeyConditionExpression: "category = :category AND projectId = :projectId",
IndexName: "GSI-project-id",
ExpressionAttributeValues: {
":category": "tasks",
":projectId": projectId,
},
};
const result = await docClient.query(params);
const tasks = result.Items || [];
const tasksWithDetails = await Promise.all(
tasks.map(async (task: any) => {
const author = task.authorUserId
? await fetchUserWithUserId(task.authorUserId)
: null;
const assignee = task.assignedUserId
? await fetchUserWithUserId(task.assignedUserId)
: null;
const comments = await fetchComments(task.taskId);
const attachments = await fetchAttachments(task.taskId);
return {
...task,
author,
assignee,
comments,
attachments,
};
})
);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(tasksWithDetails),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving tasks: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,62 @@
import { fetchUserWithUserId } from "@/lib/util";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TEAM_TABLE_NAME = process.env.TASKER_TEAM_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
try {
const params: QueryCommandInput = {
TableName: TASKER_TEAM_TABLE_NAME,
KeyConditionExpression: "category = :category",
ExpressionAttributeValues: {
":category": "teams",
},
};
const result = await docClient.query(params);
const teams = result.Items || [];
const teamsWithUsernames = await Promise.all(
teams.map(async (team: any) => {
const productOwnerUsername = team.productOwnerUserId
? (await fetchUserWithUserId(team.productOwnerUserId))?.username
: null;
const projectManagerUsername = team.projectManagerUserId
? (await fetchUserWithUserId(team.projectManagerUserId))?.username
: null;
return {
...team,
productOwnerUsername,
projectManagerUsername,
};
})
);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(teamsWithUsernames),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving teams: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,44 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_USER_TABLE_NAME = process.env.TASKER_USER_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const { cognitoId } = event.pathParameters;
try {
const params: QueryCommandInput = {
TableName: TASKER_USER_TABLE_NAME,
KeyConditionExpression: "category = :category AND cognitoId = :cognitoId",
ExpressionAttributeValues: {
":category": "users",
":cognitoId": cognitoId,
},
};
const user = await docClient.query(params);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(user.Items?.[0] || {}),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving user: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,40 @@
import { queryTasks } from "@/lib/util";
export const handler = async (event: any): Promise<any> => {
const { userId } = event.pathParameters;
try {
const authorTasks = await queryTasks(
userId,
"GSI-author-user-id",
"authorUserId"
);
const assigneeTasks = await queryTasks(
userId,
"GSI-assigned-user-id",
"assignedUserId"
);
const userTasks = [...authorTasks, ...assigneeTasks];
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(userTasks),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving tasks for user: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,42 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_USER_TABLE_NAME = process.env.TASKER_USER_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
try {
const params: QueryCommandInput = {
TableName: TASKER_USER_TABLE_NAME,
KeyConditionExpression: "category = :category",
ExpressionAttributeValues: {
":category": "users",
},
};
const users = await docClient.query(params);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(users.Items),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving users: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,52 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, UpdateCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TASK_TABLE_NAME = process.env.TASKER_TASK_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const { taskId } = event.pathParameters;
const { status } = JSON.parse(event.body);
try {
const params: UpdateCommandInput = {
TableName: TASKER_TASK_TABLE_NAME,
Key: {
category: "tasks",
taskId,
},
UpdateExpression: "set #status = :status",
ExpressionAttributeNames: {
"#status": "status",
},
ExpressionAttributeValues: {
":status": status,
},
ReturnValues: "ALL_NEW",
};
const updatedTask = await docClient.update(params);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(updatedTask.Attributes),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error updating task: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,97 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TEAM_TABLE_NAME = process.env.TASKER_TEAM_TABLE_NAME || "";
const TASKER_USER_TABLE_NAME = process.env.TASKER_USER_TABLE_NAME || "";
const TASKER_TASK_TABLE_NAME = process.env.TASKER_TASK_TABLE_NAME || "";
const TASKER_TASK_EXTRA_TABLE_NAME =
process.env.TASKER_TASK_EXTRA_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const fetchRandomTeamId = async () => {
const params = {
TableName: TASKER_TEAM_TABLE_NAME,
KeyConditionExpression: "category = :category",
ExpressionAttributeValues: {
":category": "teams",
},
};
const teams = await docClient.query(params);
if (!teams.Items) {
return null;
}
const randomTeam =
teams.Items[Math.floor(Math.random() * teams.Items.length)];
return randomTeam.teamId;
};
export const fetchUserWithUserId = async (userId: string): Promise<any> => {
const params: QueryCommandInput = {
TableName: TASKER_USER_TABLE_NAME,
KeyConditionExpression: "category = :category AND userId = :userId",
IndexName: "GSI-user-id",
ExpressionAttributeValues: {
":category": "users",
":userId": userId,
},
};
const result = await docClient.query(params);
return result.Items?.[0];
};
export const fetchComments = async (taskId: string): Promise<any> => {
const params: QueryCommandInput = {
TableName: TASKER_TASK_EXTRA_TABLE_NAME,
KeyConditionExpression: "category = :category AND taskId = :taskId",
IndexName: "GSI-task-id",
ExpressionAttributeValues: {
":category": "comments",
":taskId": taskId,
},
};
const result = await docClient.query(params);
return result.Items;
};
export const fetchAttachments = async (taskId: string): Promise<any> => {
const params: QueryCommandInput = {
TableName: TASKER_TASK_EXTRA_TABLE_NAME,
KeyConditionExpression: "category = :category AND taskId = :taskId",
IndexName: "GSI-task-id",
ExpressionAttributeValues: {
":category": "attachments",
":taskId": taskId,
},
};
const result = await docClient.query(params);
return result.Items;
};
export const queryTasks = async (
userId: string,
indexName: string,
key: string
): Promise<any> => {
const params: QueryCommandInput = {
TableName: TASKER_TASK_TABLE_NAME,
KeyConditionExpression: "category = :category AND #key = :userId",
IndexName: indexName,
ExpressionAttributeValues: {
":category": "tasks",
":userId": userId,
},
ExpressionAttributeNames: {
"#key": key,
},
};
const result = await docClient.query(params);
return result.Items ?? [];
};