-
Notifications
You must be signed in to change notification settings - Fork 0
feat: share todo with email #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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
EMAIL_SERVICE_SECRET_TOKEN=a470af17-4371-4362-8b51-335c243ee5eb |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -2,11 +2,78 @@ import z from "https://deno.land/x/[email protected]/index.ts"; | |||||||
import { MemoDatabase, TodoSchema } from "../database.ts"; | ||||||||
import { respond } from "../utilities.ts"; | ||||||||
|
||||||||
const sendEmails = (todo: z.infer<typeof TodoSchema>, sharedWith: string[]) => { | ||||||||
// await emailService.send({ | ||||||||
// to: sharedWith, | ||||||||
// subject: `Todo shared with you: ${todo.text}`, | ||||||||
// body: `You have been shared a todo: ${todo.text}.`, | ||||||||
// }); | ||||||||
|
||||||||
const token = Deno.env.get("EMAIL_SERVICE_SECRET_TOKEN"); | ||||||||
|
||||||||
if (!token) { | ||||||||
throw new Error("Email service token is not set"); | ||||||||
} | ||||||||
|
||||||||
console.log(`Sending email with token: ${token}`); | ||||||||
console.log(`Sending emails to: ${sharedWith.join(", ")}`); | ||||||||
console.log(`Todo details: ${JSON.stringify(todo)}`); | ||||||||
return Promise.resolve(); | ||||||||
}; | ||||||||
|
||||||||
const handleShare = async ( | ||||||||
req: Request, | ||||||||
dbClient: MemoDatabase, | ||||||||
validatedBody: z.infer<typeof TodoSchema> | ||||||||
): Promise<Response> => { | ||||||||
const pattern = new URLPattern({ pathname: "/api/todos/:id/share" }); | ||||||||
const match = pattern.exec(req.url); | ||||||||
const id = Number(match?.pathname.groups.id); | ||||||||
|
||||||||
if (Number.isNaN(id)) { | ||||||||
return respond({ error: "Invalid id" }, 400); | ||||||||
} | ||||||||
|
||||||||
try { | ||||||||
const existingTodo = dbClient.todos.getById(id); | ||||||||
if (!existingTodo) { | ||||||||
return respond({ error: "Todo not found" }, 404); | ||||||||
} | ||||||||
|
||||||||
// Important: Check that todo hasn't been shared with the same user | ||||||||
|
||||||||
Comment on lines
+43
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The deduplication check for shared recipients mentioned in the comment is not implemented. Consider adding logic to prevent sharing the same todo with duplicate email addresses.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||
const updatedTodo = dbClient.todos.update(id, { | ||||||||
...existingTodo, | ||||||||
sharedWith: validatedBody.sharedWith, | ||||||||
}); | ||||||||
|
||||||||
if (!updatedTodo) { | ||||||||
return respond({ error: "Todo not found or cannot be updated" }, 404); | ||||||||
} | ||||||||
|
||||||||
await sendEmails(updatedTodo, updatedTodo.sharedWith!); | ||||||||
return respond(updatedTodo); | ||||||||
} catch (error) { | ||||||||
if (error instanceof z.ZodError) { | ||||||||
return respond({ error: "Invalid data", issues: error.errors }, 400); | ||||||||
} | ||||||||
return respond({ error: "Unexpected error", issues: error }, 400); | ||||||||
} | ||||||||
}; | ||||||||
|
||||||||
export const handlePost = async (req: Request, dbClient: MemoDatabase): Promise<Response> => { | ||||||||
try { | ||||||||
const body = await req.json(); | ||||||||
const validatedBody = TodoSchema.parse(body); | ||||||||
|
||||||||
const url = new URL(req.url); | ||||||||
const { pathname } = url; | ||||||||
|
||||||||
if (pathname.endsWith("/share")) { | ||||||||
const result = await handleShare(req, dbClient, validatedBody); | ||||||||
return result; | ||||||||
} | ||||||||
|
||||||||
const newTodo = dbClient.todos.add(validatedBody); | ||||||||
return respond(newTodo); | ||||||||
} catch (error) { | ||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -8,6 +8,7 @@ export const TodoSchema = z.object({ | |||||||||||||||||
createdAt: z.string().nullish(), | ||||||||||||||||||
updatedAt: z.string().nullish(), | ||||||||||||||||||
archivedAt: z.string().nullish(), | ||||||||||||||||||
sharedWith: z.array(z.string()).nullish(), | ||||||||||||||||||
}); | ||||||||||||||||||
|
||||||||||||||||||
export type Todo = z.infer<typeof TodoSchema>; | ||||||||||||||||||
|
@@ -44,10 +45,10 @@ class Todos { | |||||||||||||||||
|
||||||||||||||||||
getById(id: number): Todo | undefined { | ||||||||||||||||||
if (!this.isSelectableAndMutable(id)) { | ||||||||||||||||||
return undefined | ||||||||||||||||||
return undefined; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return { id, ...this.todos[id] }; | ||||||||||||||||||
return this.todos[id]; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
add(todo: Todo): Todo { | ||||||||||||||||||
|
@@ -57,14 +58,18 @@ class Todos { | |||||||||||||||||
return newTodo; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
update(id: number, todo: Todo): Todo | undefined { | ||||||||||||||||||
if (!this.isSelectableAndMutable(id)) { | ||||||||||||||||||
return undefined | ||||||||||||||||||
update(inputId: number, inputTodo: Todo): Todo | undefined { | ||||||||||||||||||
if (!this.isSelectableAndMutable(inputId)) { | ||||||||||||||||||
return undefined; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if (this.todos[id]) { | ||||||||||||||||||
this.todos[id] = { ...this.todos[id], id, ...todo, updatedAt: new Date().toISOString() }; | ||||||||||||||||||
return this.todos[id]; | ||||||||||||||||||
if (this.todos[inputId]) { | ||||||||||||||||||
this.todos[inputId] = { | ||||||||||||||||||
...this.todos[inputId], | ||||||||||||||||||
updatedAt: new Date().toISOString(), | ||||||||||||||||||
sharedWith: inputTodo.sharedWith ?? ["[email protected]"], | ||||||||||||||||||
}; | ||||||||||||||||||
Comment on lines
+69
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A hardcoded default email value is used when sharedWith is not provided. Consider removing or parameterizing this default to avoid unexpected behavior in production.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||
return this.todos[inputId]; | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,8 +1,5 @@ | ||||||||||||||
import { assertEquals, assertExists } from "@std/assert"; | ||||||||||||||
import { router } from "./routes/router.ts"; | ||||||||||||||
import { MemoDatabase } from "./database.ts"; | ||||||||||||||
|
||||||||||||||
const dbClient = new MemoDatabase(); | ||||||||||||||
|
||||||||||||||
const mockRequest = async (method: string, url: string, body: object = {}): Promise<Response> => { | ||||||||||||||
const init: RequestInit = { | ||||||||||||||
|
@@ -11,7 +8,7 @@ const mockRequest = async (method: string, url: string, body: object = {}): Prom | |||||||||||||
}; | ||||||||||||||
if (method !== "GET") init.body = JSON.stringify(body); | ||||||||||||||
const req = new Request(url, init); | ||||||||||||||
return await router(req, dbClient); | ||||||||||||||
return await router(req); | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
// Happy case tests | ||||||||||||||
|
@@ -47,6 +44,12 @@ methods.forEach((method) => { | |||||||||||||
}); | ||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
Deno.test("POST /api/todos/share shares a todo with a user", async () => { | ||||||||||||||
const shareData = { todoId: 1, userId: 2 }; | ||||||||||||||
const response = await mockRequest("POST", "http://localhost:8080/api/todos/share", shareData); | ||||||||||||||
Comment on lines
+47
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test URL for sharing a todo is missing the required todo id parameter as per the endpoint '/api/todos/:id/share'. Please update the URL to include an id, for example, '/api/todos/1/share'.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||
await response.json(); | ||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
Deno.test("DELETE /api/todos/:id archives a specific todo", async () => { | ||||||||||||||
const response = await mockRequest("DELETE", "http://localhost:8080/api/todos/1"); | ||||||||||||||
assertEquals(response.status, 200); | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
import { MemoDatabase } from "./database.ts"; | ||
import { router } from "./routes/router.ts"; | ||
|
||
Deno.serve({ port: 8080, hostname: "127.0.0.1" }, (req) => router(req, new MemoDatabase())); | ||
Deno.serve({ port: 8080, hostname: "127.0.0.1" }, (req) => router(req)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The share endpoint path appears to be misspelled as '/shar'. Please correct it to '/share' to match the implementation.
Copilot uses AI. Check for mistakes.