Notes route

This commit is contained in:
Sanjib Sen 2025-01-16 12:37:06 +06:00
parent 0fe2b335ed
commit 9f706092a7
14 changed files with 546 additions and 141 deletions

View file

@ -3,7 +3,7 @@ import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
schema: "./src/db/schema",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,

View file

@ -1,4 +1,6 @@
CREATE TABLE "account" (
CREATE SCHEMA "auth";
--> statement-breakpoint
CREATE TABLE "auth"."account" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
@ -14,14 +16,14 @@ CREATE TABLE "account" (
"updated_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "rate_limit" (
CREATE TABLE "auth"."rate_limit" (
"id" text PRIMARY KEY NOT NULL,
"key" text,
"count" integer,
"last_request" integer
);
--> statement-breakpoint
CREATE TABLE "session" (
CREATE TABLE "auth"."session" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
@ -33,7 +35,7 @@ CREATE TABLE "session" (
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "user" (
CREATE TABLE "auth"."user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
@ -44,7 +46,7 @@ CREATE TABLE "user" (
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "verification" (
CREATE TABLE "auth"."verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
@ -53,5 +55,15 @@ CREATE TABLE "verification" (
"updated_at" timestamp
);
--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
CREATE TABLE "note" (
"id" text PRIMARY KEY NOT NULL,
"title" text,
"content" text,
"createdAt" timestamp DEFAULT now(),
"updatedAt" timestamp,
"ownerId" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "auth"."account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "auth"."session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "note" ADD CONSTRAINT "note_ownerId_user_id_fk" FOREIGN KEY ("ownerId") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;

View file

@ -1,12 +1,12 @@
{
"id": "e9e52dc2-cfab-453f-8e23-57a32259b8e9",
"id": "806f9895-fec6-43da-9c78-46e599e611e8",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"auth.account": {
"name": "account",
"schema": "",
"schema": "auth",
"columns": {
"id": {
"name": "id",
@ -93,6 +93,7 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
@ -109,9 +110,9 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.rate_limit": {
"auth.rate_limit": {
"name": "rate_limit",
"schema": "",
"schema": "auth",
"columns": {
"id": {
"name": "id",
@ -146,9 +147,9 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"auth.session": {
"name": "session",
"schema": "",
"schema": "auth",
"columns": {
"id": {
"name": "id",
@ -205,6 +206,7 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
@ -229,9 +231,9 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"auth.user": {
"name": "user",
"schema": "",
"schema": "auth",
"columns": {
"id": {
"name": "id",
@ -292,9 +294,9 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"auth.verification": {
"name": "verification",
"schema": "",
"schema": "auth",
"columns": {
"id": {
"name": "id",
@ -340,10 +342,77 @@
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.note": {
"name": "note",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"ownerId": {
"name": "ownerId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"note_ownerId_user_id_fk": {
"name": "note_ownerId_user_id_fk",
"tableFrom": "note",
"tableTo": "user",
"schemaTo": "auth",
"columnsFrom": [
"ownerId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"schemas": {
"auth": "auth"
},
"sequences": {},
"roles": {},
"policies": {},

View file

@ -5,8 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1736964339376,
"tag": "0000_bright_meltdown",
"when": 1736970092723,
"tag": "0000_pretty_banshee",
"breakpoints": true
}
]

View file

@ -1,25 +1,141 @@
import { Memo } from "./note.model";
import { and, eq, notExists } from "drizzle-orm";
import { db } from "../../db";
import { note } from "../../db/schema/note";
import { CreateNote } from "./note.model";
export class Note {
constructor(
public data: Memo[] = [
{
data: "Moonhalo",
},
]
) {}
add(note: Memo) {
this.data.push(note);
return this.data;
export class NoteController {
async createNote(new_note: CreateNote, ownerId: string) {
const new_note_data = { ...new_note, ownerId: ownerId };
const result = await db
.insert(note)
.values(new_note_data)
.returning({
id: note.id,
title: note.title,
content: note.content,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
})
.execute();
return {
success: true,
data: result,
message: "Note created successfully",
error: null,
};
}
remove(index: number) {
return this.data.splice(index, 1);
async getOwnerNotes(ownerId: string, limit:number=10, offset:number=0) {
const result = await db
.select({
id: note.id,
title: note.title,
content: note.content,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
})
.from(note)
.where(and(eq(note.ownerId, ownerId), notExists(note.deletedAt)))
.limit(limit).offset(offset)
.execute();
return {
success: true,
data: result,
message: "",
error: null,
};
}
update(index: number, note: Partial<Memo>) {
return (this.data[index] = { ...this.data[index], ...note });
async getNoteById(noteId: string, ownerId: string) {
const result = await db
.select({
id: note.id,
title: note.title,
content: note.content,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
})
.from(note)
.where(
and(
eq(note.id, noteId),
eq(note.ownerId, ownerId),
notExists(note.deletedAt)
)
)
.execute();
return {
success: true,
data: result,
message: "",
error: null,
};
}
async updateNoteById(
noteId: string,
updated_note: CreateNote,
ownerId: string
) {
const new_note_data = { ...updated_note, updatedAt: new Date() };
const result = await db
.update(note)
.set(new_note_data)
.where(
and(
eq(note.id, noteId),
eq(note.ownerId, ownerId),
notExists(note.deletedAt)
)
)
.returning({
id: note.id,
title: note.title,
content: note.content,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
})
.execute();
return {
success: true,
data: result,
message: "Note updated successfully",
error: null,
};
}
async deleteNoteById(noteId: string, ownerId: string) {
await db
.update(note)
.set({ deletedAt: new Date() })
.where(
and(
eq(note.id, noteId),
eq(note.ownerId, ownerId),
notExists(note.deletedAt)
)
)
.execute();
return {
success: true,
data: null,
message: "Note deleted successfully",
error: null,
};
}
async deleteAllNotes(ownerId: string) {
await db
.update(note)
.set({ deletedAt: new Date() })
.where(and(eq(note.ownerId, ownerId), notExists(note.deletedAt)))
.execute();
return {
success: true,
data: null,
message: "Notes deleted successfully",
error: null,
};
}
}

View file

@ -1,7 +1,37 @@
import { createSelectSchema } from "drizzle-typebox";
import { t } from "elysia";
import { note } from "../../db/schema/note";
import { InferInsertModel, InferSelectModel } from "drizzle-orm";
export const memoSchema = t.Object({
data: t.String(),
});
export type Memo = typeof memoSchema.static;
export const SelectNoteSchema = createSelectSchema(note);
export const NoteSchema = t.Omit(SelectNoteSchema, ["deletedAt", "ownerId"]);
export type Note = InferSelectModel<typeof note>;
export type CreateNote = Pick<
InferInsertModel<typeof note>,
"title" | "content"
>;
export const createNoteSchema = t.Pick(NoteSchema, [
"title",
"content",
]);
export const successGetNoteResponse = t.Object({
success:t.Boolean({default:true}),
data: t.Array(NoteSchema),
error: t.Null(),
message: t.String()
}, {
description:"Success"
})
export const successDeleteNoteResponse = t.Object({
success:t.Boolean({default:true}),
data: t.Null(),
error: t.Null(),
message: t.String({default:"Note deletion succesful"})
}, {
description:"Success"
})

View file

@ -1,44 +1,129 @@
import { Elysia, t } from "elysia";
import { Note } from "./note.controller";
import { memoSchema } from "./note.model";
import { Elysia, error, t } from "elysia";
import { createNoteSchema, NoteSchema, successDeleteNoteResponse, successGetNoteResponse } from "./note.model";
import { NoteController } from "./note.controller";
import { userMiddleware } from "../../middlewares/auth-middleware";
import { commonResponses } from "../../lib/utils/common";
export const note = new Elysia({ prefix: "/note" })
.decorate("note", new Note())
export const noteRouter = new Elysia({
prefix: "/note",
name: "CRUD Operations for Notes",
"analytic":true,
tags: ["Note"],
detail: {
description: "Notes CRUD operations",
},
})
.decorate("note", new NoteController())
.model({
memo: t.Omit(memoSchema, ["author"]),
})
.get("/", ({ note }) => note.data)
.put("/", ({ note, body: { data } }) => note.add({ data }), {
body: "memo",
note: NoteSchema,
})
.derive(({ request }) => userMiddleware(request))
.get(
"/:index",
({ note, params: { index }, error }) => {
return note.data[index] ?? error(404, "Not Found :(");
"",
async ({ note, user, query}) => {
return await note.getOwnerNotes(user.id, query.limit, query.offset);
},
{
params: t.Object({
index: t.Number(),
query:t.Object({
limit: t.Optional(t.Number()),
offset: t.Optional(t.Number())
}),
response:{
200: successGetNoteResponse,
...commonResponses
},
detail:{
"description":"Get all notes of the user",
"summary":"Get all notes"
}
}
)
.guard({
params: t.Object({
index: t.Number(),
}),
})
.delete("/:index", ({ note, params: { index }, error }) => {
if (index in note.data) return note.remove(index);
return error(422);
})
.patch(
"/:index",
({ note, params: { index }, body: { data }, error }) => {
if (index in note.data) return note.update(index, { data });
return error(422);
.get(
":id",
async ({ note, user, params:{id} }) => {
return await note.getNoteById(id, user.id);
},
{
body: "memo",
params:t.Object({
id: t.String(),
}),
response: {
200: successGetNoteResponse,
...commonResponses
},
detail:{
"description":"Get a note by Id",
"summary":"Get a note"
}
}
);
)
.post(
"",
async ({ body, note, user }) => {
return await note.createNote(body, user.id);
},
{
body: createNoteSchema,
response: {
200: successGetNoteResponse,
...commonResponses
},
detail:{
"description":"Create a new note",
"summary":"Create a note"
}
}
).patch(
":id",
async ({ body, note, user, params:{id} }) => {
return await note.updateNoteById(id, body, user.id);
},
{
body: createNoteSchema,
params:t.Object({
id: t.String(),
}),
response: {
200: successGetNoteResponse,
...commonResponses
},
detail:{
"description":"Update a note by Id",
"summary":"Update a note"
}
}
).delete(
":id",
async ({ note, user, params:{id} }) => {
return await note.deleteNoteById(id, user.id);
},
{
params:t.Object({
id: t.String(),
}),
response: {
200: successDeleteNoteResponse,
...commonResponses
},
detail:{
"description":"Delete a note by Id",
"summary":"Delete a note"
}
}
)
.delete(
"",
async ({ note, user }) => {
return await note.deleteAllNotes(user.id);
},
{
response: {
200: successDeleteNoteResponse,
...commonResponses
},
detail:{
"description":"Delete all notes of an user",
"summary":"Delete all notes"
}
}
)

View file

@ -1,54 +0,0 @@
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp('expires_at').notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id').notNull().references(()=> user.id)
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id').notNull().references(()=> user.id),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at'),
updatedAt: timestamp('updated_at')
});
export const rateLimit = pgTable("rate_limit", {
id: text("id").primaryKey(),
key: text('key'),
count: integer('count'),
lastRequest: integer('last_request')
});

56
src/db/schema/auth.ts Normal file
View file

@ -0,0 +1,56 @@
import { text, integer, timestamp, boolean, pgSchema } from "drizzle-orm/pg-core";
export const authSchema = pgSchema('auth');
export const user = authSchema.table("user", {
id: text("id").primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const session = authSchema.table("session", {
id: text("id").primaryKey(),
expiresAt: timestamp('expires_at').notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id').notNull().references(() => user.id)
});
export const account = authSchema.table("account", {
id: text("id").primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id').notNull().references(() => user.id),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const verification = authSchema.table("verification", {
id: text("id").primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at'),
updatedAt: timestamp('updated_at')
});
export const rateLimit = authSchema.table("rate_limit", {
id: text("id").primaryKey(),
key: text('key'),
count: integer('count'),
lastRequest: integer('last_request')
});

13
src/db/schema/note.ts Normal file
View file

@ -0,0 +1,13 @@
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createId } from '@paralleldrive/cuid2'
import { user } from "./auth";
export const note = pgTable("note", {
id: text("id").primaryKey().$defaultFn(()=> `note_${createId()}`),
title: text("title"),
content: text("content"),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp(),
deletedAt: timestamp(),
ownerId: text().notNull().references(() => user.id)
})

View file

@ -3,9 +3,8 @@ import { swagger } from "@elysiajs/swagger";
import { opentelemetry } from "@elysiajs/opentelemetry";
import { serverTiming } from "@elysiajs/server-timing";
import { cors } from '@elysiajs/cors'
import { note } from "./api/note/note.route";
import { noteRouter } from "./api/note/note.route";
import { betterAuthView } from "./lib/auth/auth-view";
import { userMiddleware, userInfo } from "./middlewares/auth-middleware";
import { getBaseConfig, validateEnv } from "./lib/utils/env";
const baseConfig = getBaseConfig()
@ -23,10 +22,8 @@ const app = new Elysia()
if (code === "NOT_FOUND") return "Not Found :(";
console.error(error);
})
.derive(({ request }) => userMiddleware(request))
.all("/api/auth/*", betterAuthView)
.use(note)
.get("/user", ({ user, session }) => userInfo(user, session))
.use(noteRouter)
.get("/", () => `${baseConfig.SERVICE_NAME} Server is Running`)
validateEnv();

View file

@ -2,7 +2,7 @@ import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../../db/index";
import { openAPI } from "better-auth/plugins"
import { user, account, verification, session } from "../../db/schema";
import { user, account, verification, session, rateLimit } from "../../db/schema/auth";
import { sendMail } from "../mail/mail";
import { renderToStaticMarkup } from "react-dom/server";
import { createElement } from "react";
@ -15,7 +15,8 @@ export const auth = betterAuth({
user: user,
session: session,
account: account,
verification: verification
verification: verification,
rateLimit: rateLimit,
}
}),
rateLimit: {

82
src/lib/utils/common.ts Normal file
View file

@ -0,0 +1,82 @@
import { t } from "elysia";
export const commonResponses = {
400: t.Object(
{
data: t.Null(),
success: t.Boolean({ default: false }),
message: t.String({ default: "Bad Request" }),
error: t.String({
default: "Missing parameters, or invalid parameters.",
}),
},
{
description:
"Bad Request. Usually due to missing parameters, or invalid parameters.",
}
),
401: t.Object(
{
data: t.Null(),
success: t.Boolean({ default: false }),
message: t.String({ default: "Unauthorized" }),
error: t.String({
default: "User needs to sign in to access this resource",
}),
},
{
description: "Unauthorized. Due to missing or invalid authentication.",
}
),
403: t.Object(
{
data: t.Null(),
success: t.Boolean({ default: false }),
message: t.String({ default: "Forbidden" }),
error: t.String({
default: "User does not have permission to access this resource",
}),
},
{
description:
"Forbidden. You do not have permission to access this resource or to perform this action.",
}
),
404: t.Object(
{
data: t.Null(),
success: t.Boolean({ default: false }),
message: t.String({ default: "Not Found" }),
error: t.String({ default: "Requested resource has not found" }),
},
{
description: "Not Found. The requested resource was not found.",
}
),
429: t.Object(
{
data: t.Null(),
success: t.Boolean({ default: false }),
message: t.String({ default: "Too Many Requests" }),
error: t.String({
default: "RUser has exceeded the rate limit. Try again later.",
}),
},
{
description:
"Too Many Requests. You have exceeded the rate limit. Try again later.",
}
),
500: t.Object(
{
data: t.Null(),
success: t.Boolean({ default: false }),
message: t.String({ default: "Internal Server Error" }),
error: t.String({ default: "Server faced an error" }),
},
{
description:
"Internal Server Error. This is a problem with the server that you cannot fix.",
}
),
};

View file

@ -1,15 +1,13 @@
import { Session, User } from "better-auth/types";
import { auth } from "../lib/auth/auth";
import { error } from "elysia";
// user middleware (compute user and session and pass to routes)
export const userMiddleware = async (request: Request) => {
const session = await auth.api.getSession({ headers: request.headers });
if (!session) {
return {
user: null,
session: null,
};
return error("Unauthorized", 401);
}
return {