diff --git a/README.md b/README.md index 688c87e..bebf061 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ # Elysia with Bun runtime ## Getting Started + To get started with this template, simply paste this command into your terminal: + ```bash bun create elysia ./elysia-example ``` ## Development + To start the development server run: + ```bash bun run dev ``` -Open http://localhost:3000/ with your browser to see the result. \ No newline at end of file +Open http://localhost:3000/ with your browser to see the result. diff --git a/bun.lockb b/bun.lockb index c7a287a..c2de2b1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yaml b/docker-compose.yaml index ffbf3ab..a746a02 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,4 +10,3 @@ services: OTEL_EXPORTER_OTLP_ENDPOINT: http://tracing:4318 OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf DATABASE_URL: ${DATABASE_URL} - diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index 3ab4076..dfa6446 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -140,12 +140,8 @@ "tableFrom": "account", "tableTo": "user", "schemaTo": "auth", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -337,12 +333,8 @@ "tableFrom": "session", "tableTo": "user", "schemaTo": "auth", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -352,9 +344,7 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -415,9 +405,7 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -605,12 +593,8 @@ "tableFrom": "note", "tableTo": "user", "schemaTo": "auth", - "columnsFrom": [ - "ownerId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["ownerId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -635,4 +619,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3ab2cdc..13ebf20 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -10,4 +10,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index be978da..34c2c08 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.50", "scripts": { "test": "bun test api", + "format": "prettier . --write", "dev": "bun run --watch src/index.ts", "email": "email dev --dir src/emails", "auth:generate": "bun x @better-auth/cli generate --config src/lib/auth/auth.ts --output src/db/schema/auth.ts && drizzle-kit migrate", @@ -39,6 +40,7 @@ "devDependencies": { "@types/nodemailer": "^6.4.17", "@types/pg": "^8.11.10", + "prettier": "^3.4.2", "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", "bun-types": "latest", diff --git a/src/api/index.ts b/src/api/index.ts index 9d50552..9854a42 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -50,7 +50,7 @@ export const api = new Elysia({ }, { description: "Success", - } + }, ), 404: t.Object( { @@ -65,8 +65,8 @@ export const api = new Elysia({ }, { description: "Not found", - } + }, ), }, - } + }, ); diff --git a/src/api/routes/index.ts b/src/api/routes/index.ts index 6d3d1e2..0893eaa 100644 --- a/src/api/routes/index.ts +++ b/src/api/routes/index.ts @@ -1,6 +1,4 @@ - import { Elysia, t } from "elysia"; import { noteRouter } from "./note/note.route"; - -export const router = new Elysia().use(noteRouter) \ No newline at end of file +export const router = new Elysia().use(noteRouter); diff --git a/src/api/routes/note/note.controller.ts b/src/api/routes/note/note.controller.ts index e0b1060..89b5407 100644 --- a/src/api/routes/note/note.controller.ts +++ b/src/api/routes/note/note.controller.ts @@ -25,7 +25,7 @@ export class NoteController { }; } - async getOwnerNotes(ownerId: string, limit:number=10, offset:number=0) { + async getOwnerNotes(ownerId: string, limit: number = 10, offset: number = 0) { const result = await db .select({ id: note.id, @@ -36,7 +36,8 @@ export class NoteController { }) .from(note) .where(and(eq(note.ownerId, ownerId), isNull(note.deletedAt))) - .limit(limit).offset(offset) + .limit(limit) + .offset(offset) .execute(); return { success: true, @@ -60,14 +61,14 @@ export class NoteController { and( eq(note.id, noteId), eq(note.ownerId, ownerId), - isNull(note.deletedAt) - ) + isNull(note.deletedAt), + ), ) .execute(); let successStatus = true; - if(result.length===0){ - successStatus = false - }; + if (result.length === 0) { + successStatus = false; + } return { success: successStatus, data: result, @@ -79,7 +80,7 @@ export class NoteController { async updateNoteById( noteId: string, updated_note: CreateNoteType, - ownerId: string + ownerId: string, ) { const new_note_data = { ...updated_note, updatedAt: new Date() }; const result = await db @@ -89,8 +90,8 @@ export class NoteController { and( eq(note.id, noteId), eq(note.ownerId, ownerId), - isNull(note.deletedAt) - ) + isNull(note.deletedAt), + ), ) .returning({ id: note.id, @@ -117,8 +118,8 @@ export class NoteController { and( eq(note.id, noteId), eq(note.ownerId, ownerId), - isNull(note.deletedAt) - ) + isNull(note.deletedAt), + ), ) .execute(); return { diff --git a/src/api/routes/note/note.model.ts b/src/api/routes/note/note.model.ts index 5ca3d38..15f7d88 100644 --- a/src/api/routes/note/note.model.ts +++ b/src/api/routes/note/note.model.ts @@ -13,31 +13,34 @@ export type CreateNoteType = Pick< "title" | "content" >; -export const createNoteSchema = t.Pick(NoteSchema, [ - "title", - "content", -]); +export const createNoteSchema = t.Pick(NoteSchema, ["title", "content"]); export const getNoteResponses = { - 200: t.Object({ - success:t.Boolean({default:true}), - data: t.Array(NoteSchema), - error: t.Null(), - message: t.String() -}, { - description:"Success" -}) , - ...commonResponses -} + 200: t.Object( + { + success: t.Boolean({ default: true }), + data: t.Array(NoteSchema), + error: t.Null(), + message: t.String(), + }, + { + description: "Success", + }, + ), + ...commonResponses, +}; export const deleteNoteResponses = { - 200: t.Object({ - success:t.Boolean({default:true}), - data: t.Null(), - error: t.Null(), - message: t.String({default:"Note deletion succesful"}) -}, { - description:"Success" -}), - ...commonResponses -} \ No newline at end of file + 200: t.Object( + { + success: t.Boolean({ default: true }), + data: t.Null(), + error: t.Null(), + message: t.String({ default: "Note deletion succesful" }), + }, + { + description: "Success", + }, + ), + ...commonResponses, +}; diff --git a/src/api/routes/note/note.route.ts b/src/api/routes/note/note.route.ts index 839dacf..a677d42 100644 --- a/src/api/routes/note/note.route.ts +++ b/src/api/routes/note/note.route.ts @@ -1,12 +1,17 @@ import { Elysia, t } from "elysia"; -import { createNoteSchema, deleteNoteResponses, getNoteResponses, NoteSchema } from "./note.model"; +import { + createNoteSchema, + deleteNoteResponses, + getNoteResponses, + NoteSchema, +} from "./note.model"; import { NoteController } from "./note.controller"; import { userMiddleware } from "../../../middlewares/auth-middleware"; export const noteRouter = new Elysia({ prefix: "/note", name: "CRUD Operations for Notes", - "analytic":true, + analytic: true, tags: ["Note"], detail: { description: "Notes CRUD operations", @@ -17,47 +22,47 @@ export const noteRouter = new Elysia({ note: NoteSchema, }) .derive(({ request }) => userMiddleware(request)) - .onError(({ error, code, }) => { - console.error(error); - return { - message: "", - success: false, - data: null, - error: code.toString() - }; + .onError(({ error, code }) => { + console.error(error); + return { + message: "", + success: false, + data: null, + error: code.toString(), + }; }) .get( "", - async ({ note, user, query}) => { + async ({ note, user, query }) => { return await note.getOwnerNotes(user.id, query.limit, query.offset); }, { - query:t.Object({ + query: t.Object({ limit: t.Optional(t.Number()), - offset: t.Optional(t.Number()) + offset: t.Optional(t.Number()), }), - response:getNoteResponses, - detail:{ - "description":"Get all notes of the user", - "summary":"Get all notes" - } - } + response: getNoteResponses, + detail: { + description: "Get all notes of the user", + summary: "Get all notes", + }, + }, ) .get( "/:id", - async ({ note, user, params:{id} }) => { + async ({ note, user, params: { id } }) => { return await note.getNoteById(id, user.id); }, { - params:t.Object({ + params: t.Object({ id: t.String(), }), response: getNoteResponses, - detail:{ - "description":"Get a note by Id", - "summary":"Get a note" - } - } + detail: { + description: "Get a note by Id", + summary: "Get a note", + }, + }, ) .post( "", @@ -67,42 +72,44 @@ export const noteRouter = new Elysia({ { body: createNoteSchema, response: getNoteResponses, - detail:{ - "description":"Create a new note", - "summary":"Create a note" - } - } - ).patch( + detail: { + description: "Create a new note", + summary: "Create a note", + }, + }, + ) + .patch( "/:id", - async ({ body, note, user, params:{id} }) => { + async ({ body, note, user, params: { id } }) => { return await note.updateNoteById(id, body, user.id); }, { body: createNoteSchema, - params:t.Object({ + params: t.Object({ id: t.String(), }), response: getNoteResponses, - detail:{ - "description":"Update a note by Id", - "summary":"Update a note" - } - } - ).delete( + detail: { + description: "Update a note by Id", + summary: "Update a note", + }, + }, + ) + .delete( "/:id", - async ({ note, user, params:{id} }) => { + async ({ note, user, params: { id } }) => { return await note.deleteNoteById(id, user.id); }, { - params:t.Object({ + params: t.Object({ id: t.String(), }), response: deleteNoteResponses, - detail:{ - "description":"Delete a note by Id", - "summary":"Delete a note" - } - } + detail: { + description: "Delete a note by Id", + summary: "Delete a note", + }, + }, ) .delete( "", @@ -111,9 +118,9 @@ export const noteRouter = new Elysia({ }, { response: deleteNoteResponses, - detail:{ - "description":"Delete all notes of an user", - "summary":"Delete all notes" - } - } - ) + detail: { + description: "Delete all notes of an user", + summary: "Delete all notes", + }, + }, + ); diff --git a/src/api/routes/note/note.test.ts b/src/api/routes/note/note.test.ts index 304d241..c06f240 100644 --- a/src/api/routes/note/note.test.ts +++ b/src/api/routes/note/note.test.ts @@ -1,102 +1,105 @@ -import { describe, expect, it } from 'bun:test' -import { testClientApp } from '../../../../test/client'; +import { describe, expect, it } from "bun:test"; +import { testClientApp } from "../../../../test/client"; let noteId: string; -describe('Note', () => { +describe("Note", () => { // Create a note before tests - it('Create Note', async () => { + it("Create Note", async () => { const { data } = await testClientApp.api.note.post({ - "title": "test note", - "content": "description", - }) + title: "test note", + content: "description", + }); if (!data?.data) { - throw new Error('create note api did not return data'); + throw new Error("create note api did not return data"); } - noteId = data.data[0].id - expect(data.data[0].title).toBe('test note') - }) + noteId = data.data[0].id; + expect(data.data[0].title).toBe("test note"); + }); // Get all notes - it('Get All Notes', async () => { + it("Get All Notes", async () => { const { data } = await testClientApp.api.note.get({ - query: { limit: 1, offset: 0 } - }) - expect(data?.data[0].id).toBe(noteId) - }) + query: { limit: 1, offset: 0 }, + }); + expect(data?.data[0].id).toBe(noteId); + }); // Get single note - it('Get Created Note', async () => { - const { data, error } = await testClientApp.api.note({ id: noteId }).get() - expect(data?.data[0].id).toBe(noteId) - }) + it("Get Created Note", async () => { + const { data, error } = await testClientApp.api.note({ id: noteId }).get(); + expect(data?.data[0].id).toBe(noteId); + }); // Update note - it('Update Note', async () => { - const updatedTitle = "updated test note" - const updatedContent = "updated description" - + it("Update Note", async () => { + const updatedTitle = "updated test note"; + const updatedContent = "updated description"; + const { data } = await testClientApp.api.note({ id: noteId }).patch({ title: updatedTitle, content: updatedContent, - }) + }); - expect(data?.success).toBe(true) - expect(data?.data[0].title).toBe(updatedTitle) - expect(data?.data[0].content).toBe(updatedContent) - }) + expect(data?.success).toBe(true); + expect(data?.data[0].title).toBe(updatedTitle); + expect(data?.data[0].content).toBe(updatedContent); + }); // Delete single note - it('Delete Single Note', async () => { + it("Delete Single Note", async () => { // First create a new note to delete const { data: createData } = await testClientApp.api.note.post({ - "title": "note to delete", - "content": "this note will be deleted", - }) - const deleteNoteId = createData?.data[0].id - + title: "note to delete", + content: "this note will be deleted", + }); + const deleteNoteId = createData?.data[0].id; if (!deleteNoteId) { - throw new Error('Failed to receive noteId in delete note test'); + throw new Error("Failed to receive noteId in delete note test"); } // Delete the note - const { data: deleteData } = await testClientApp.api.note({ id: deleteNoteId }).delete() - expect(deleteData?.success).toBe(true) + const { data: deleteData } = await testClientApp.api + .note({ id: deleteNoteId }) + .delete(); + expect(deleteData?.success).toBe(true); // Verify note is deleted by trying to fetch it - const { data: verifyData } = await testClientApp.api.note({ id: deleteNoteId }).get() - expect(verifyData?.data).toHaveLength(0) - }) + const { data: verifyData } = await testClientApp.api + .note({ id: deleteNoteId }) + .get(); + expect(verifyData?.data).toHaveLength(0); + }); // Delete all notes - it('Delete All Notes', async () => { + it("Delete All Notes", async () => { // First create multiple notes await testClientApp.api.note.post({ - "title": "note 1", - "content": "content 1", - }) + title: "note 1", + content: "content 1", + }); await testClientApp.api.note.post({ - "title": "note 2", - "content": "content 2", - }) + title: "note 2", + content: "content 2", + }); // Delete all notes - const { data: deleteData } = await testClientApp.api.note.delete() - expect(deleteData?.success).toBe(true) + const { data: deleteData } = await testClientApp.api.note.delete(); + expect(deleteData?.success).toBe(true); // Verify all notes are deleted const { data: verifyData } = await testClientApp.api.note.get({ - query: { limit: 10, offset: 0 } - }) - expect(verifyData?.data).toHaveLength(0) - }) + query: { limit: 10, offset: 0 }, + }); + expect(verifyData?.data).toHaveLength(0); + }); // Error cases - it('Should handle invalid note ID', async () => { - const invalidId = 'invalid-id' - const { data } = await testClientApp.api.note({ id: invalidId }).get() - expect(data?.success).toBe(false) - expect(data?.data).toHaveLength(0) - }) -}) \ No newline at end of file + it("Should handle invalid note ID", async () => { + const invalidId = "invalid-id"; + const { data } = await testClientApp.api.note({ id: invalidId }).get(); + expect(data?.success).toBe(false); + expect(data?.data).toHaveLength(0); + }); +}); diff --git a/src/db/index.ts b/src/db/index.ts index 23aa82b..0f93f7d 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -2,6 +2,6 @@ import "dotenv/config"; import { drizzle } from "drizzle-orm/node-postgres"; import { getDbConfig } from "../lib/utils/env"; -const dbConfig = getDbConfig() +const dbConfig = getDbConfig(); export const db = drizzle(dbConfig.DATABASE_URL); diff --git a/src/db/schema/auth.ts b/src/db/schema/auth.ts index d14e28b..f6b68f3 100644 --- a/src/db/schema/auth.ts +++ b/src/db/schema/auth.ts @@ -9,74 +9,94 @@ import { export const authSchema = pgSchema("auth"); -export const user = authSchema.table( - "user", +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(), - name: text("name").notNull(), - email: text("email").notNull().unique(), - emailVerified: boolean("email_verified").notNull(), - image: text("image"), + 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), + }, + (table) => [ + index("idx_auth_session_ip_address").on(table.ipAddress), + index("idx_auth_session_userid").on(table.userId), + ], +); + +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(), }, + (table) => [ + index("idx_auth_account_userid").on(table.userId), + index("idx_auth_account_refreshtokenexpiresat").on( + table.refreshTokenExpiresAt, + ), + index("idx_auth_account_providerid").on(table.providerId), + ], ); -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), -}, -(table) => [index("idx_auth_session_ip_address").on(table.ipAddress), index("idx_auth_session_userid").on(table.userId)] +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"), + }, + (table) => [ + index("idx_auth_verification_identifier").on(table.identifier), + index("idx_auth_verification_expires_at").on(table.expiresAt), + ], ); -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(), -}, -(table) => [index("idx_auth_account_userid").on(table.userId), index("idx_auth_account_refreshtokenexpiresat").on(table.refreshTokenExpiresAt), index("idx_auth_account_providerid").on(table.providerId)]); - -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"), -}, -(table) => [index("idx_auth_verification_identifier").on(table.identifier), index("idx_auth_verification_expires_at").on(table.expiresAt)]); - -export const rateLimit = authSchema.table("rate_limit", { - id: text("id").primaryKey(), - key: text("key"), - count: integer("count"), - lastRequest: integer("last_request"), -}, -(table) => [index("idx_auth_ratelimit_key").on(table.key)]); +export const rateLimit = authSchema.table( + "rate_limit", + { + id: text("id").primaryKey(), + key: text("key"), + count: integer("count"), + lastRequest: integer("last_request"), + }, + (table) => [index("idx_auth_ratelimit_key").on(table.key)], +); export const jwks = authSchema.table("jwks", { id: text("id").primaryKey(), publicKey: text("public_key").notNull(), privateKey: text("private_key").notNull(), createdAt: timestamp("created_at").notNull(), -}); \ No newline at end of file +}); diff --git a/src/db/schema/note.ts b/src/db/schema/note.ts index fb803ac..3b23f8d 100644 --- a/src/db/schema/note.ts +++ b/src/db/schema/note.ts @@ -1,16 +1,26 @@ import { index, pgTable, text, timestamp } from "drizzle-orm/pg-core"; -import { createId } from '@paralleldrive/cuid2' +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) -}, +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), + }, -(table) => [index("idx_note_ownerid").on(table.ownerId), index("idx_note_createdat").on(table.createdAt), index("idx_note_deletedat").on(table.deletedAt)] -) \ No newline at end of file + (table) => [ + index("idx_note_ownerid").on(table.ownerId), + index("idx_note_createdat").on(table.createdAt), + index("idx_note_deletedat").on(table.deletedAt), + ], +); diff --git a/src/emails/auth.tsx b/src/emails/auth.tsx index 6db98a0..c7d6888 100644 --- a/src/emails/auth.tsx +++ b/src/emails/auth.tsx @@ -1,7 +1,13 @@ -import * as React from 'react' -import { Tailwind, Section, Text } from '@react-email/components' +import * as React from "react"; +import { Tailwind, Section, Text } from "@react-email/components"; -export default function AuthEmail({ message, link }: { message: string, link: string }) { +export default function AuthEmail({ + message, + link, +}: { + message: string; + link: string; +}) { return (
@@ -12,17 +18,17 @@ export default function AuthEmail({ message, link }: { message: string, link: st Use the following Link to {message} - Link - - Thanks - + + Link + + Thanks
- ) + ); } AuthEmail.PreviewProps = { link: "https://example.com", - message: "Verify your email address" -} + message: "Verify your email address", +}; diff --git a/src/index.ts b/src/index.ts index 98bccc2..9662d9b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ export const app = new Elysia() .use( opentelemetry({ serviceName: baseConfig.SERVICE_NAME, - }) + }), ) .use(serverTiming()) .use( @@ -28,7 +28,7 @@ export const app = new Elysia() description: `API docs for ${baseConfig.SERVICE_NAME}`, }, }, - }) + }), ) .onError(({ error, code }) => { if (code === "NOT_FOUND") diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts index 2bd5698..bb8bc35 100644 --- a/src/lib/auth/auth.ts +++ b/src/lib/auth/auth.ts @@ -1,8 +1,15 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "../../db/index"; -import { jwt, openAPI } from "better-auth/plugins" -import { user, account, verification, session, rateLimit, jwks } from "../../db/schema/auth"; +import { jwt, openAPI } from "better-auth/plugins"; +import { + user, + account, + verification, + session, + rateLimit, + jwks, +} from "../../db/schema/auth"; import { sendMail } from "../mail/mail"; import { renderToStaticMarkup } from "react-dom/server"; import { createElement } from "react"; @@ -17,13 +24,13 @@ export const auth = betterAuth({ account: account, verification: verification, rateLimit: rateLimit, - jwks: jwks - } + jwks: jwks, + }, }), user: { deleteUser: { - enabled: true // [!Code Highlight] - } + enabled: true, // [!Code Highlight] + }, }, rateLimit: { window: 60, @@ -40,8 +47,8 @@ export const auth = betterAuth({ return { window: 3600 * 12, max: 10, - } - } + }; + }, }, }, emailAndPassword: { @@ -49,7 +56,9 @@ export const auth = betterAuth({ requireEmailVerification: false, sendResetPassword: async ({ user, url }, request) => { const subject = "Reset your password"; - const html = renderToStaticMarkup(createElement(AuthEmail, { message: subject, link: url })); + const html = renderToStaticMarkup( + createElement(AuthEmail, { message: subject, link: url }), + ); await sendMail({ to: user.email, subject: subject, @@ -60,20 +69,21 @@ export const auth = betterAuth({ emailVerification: { sendVerificationEmail: async ({ user, url, token }, request) => { const subject = "Verify your email address"; - const html = renderToStaticMarkup(createElement(AuthEmail, { message: subject, link: url })); + const html = renderToStaticMarkup( + createElement(AuthEmail, { message: subject, link: url }), + ); await sendMail({ to: user.email, subject: subject, html: html, }); }, - }, plugins: [ openAPI({ path: "/docs", }), - jwt() + jwt(), ], socialProviders: { /* diff --git a/src/lib/mail/mail.ts b/src/lib/mail/mail.ts index ada3212..7be4be0 100644 --- a/src/lib/mail/mail.ts +++ b/src/lib/mail/mail.ts @@ -1,14 +1,24 @@ -import nodemailer from 'nodemailer' +import nodemailer from "nodemailer"; -export async function sendMail({ to, subject, text, html }: { to: string, subject: string, text?: string, html?: string }) { +export async function sendMail({ + to, + subject, + text, + html, +}: { + to: string; + subject: string; + text?: string; + html?: string; +}) { const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST!, port: +process.env.SMTP_PORT!, auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASSWORD!, - } - }) + }, + }); await transporter.sendMail({ from: process.env.SMTP_FROM!, @@ -16,5 +26,5 @@ export async function sendMail({ to, subject, text, html }: { to: string, subjec subject, text: text, html: html, - }) + }); } diff --git a/src/lib/storage/storage.ts b/src/lib/storage/storage.ts index c9e3648..2cdfd75 100644 --- a/src/lib/storage/storage.ts +++ b/src/lib/storage/storage.ts @@ -1,15 +1,15 @@ -import { Client } from 'minio'; -import { Buffer } from 'buffer'; -import { getMinioConfig } from '../utils/env'; +import { Client } from "minio"; +import { Buffer } from "buffer"; +import { getMinioConfig } from "../utils/env"; // MinIO client configuration -const minioConfig = getMinioConfig() +const minioConfig = getMinioConfig(); const minioClient = new Client({ endPoint: minioConfig.MINIO_ENDPOINT_URL, useSSL: false, accessKey: minioConfig.MINIO_ACCESS_KEY, - secretKey: minioConfig.MINIO_SECRET_KEY + secretKey: minioConfig.MINIO_SECRET_KEY, }); const BUCKET_NAME = minioConfig.MINIO_BUCKET_NAME; @@ -44,7 +44,7 @@ const fileToBuffer = async (file: File | Blob): Promise => { export const uploadFileAndGetUrl = async ( filename: string, file: Buffer | File | Blob, - contentType?: string + contentType?: string, ): Promise => { try { await ensureBucket(); @@ -54,11 +54,11 @@ export const uploadFileAndGetUrl = async ( // If file is from form-data and no contentType is provided, use its type const metadata: Record = {}; - if (!contentType && 'type' in file) { + if (!contentType && "type" in file) { contentType = file.type; } if (contentType) { - metadata['Content-Type'] = contentType; + metadata["Content-Type"] = contentType; } // Upload the file @@ -67,19 +67,19 @@ export const uploadFileAndGetUrl = async ( filename, fileBuffer, fileBuffer.length, - metadata + metadata, ); // Generate and return signed URL const url = await minioClient.presignedGetObject( BUCKET_NAME, filename, - SIGNED_URL_EXPIRY + SIGNED_URL_EXPIRY, ); return url; } catch (error) { - console.error('Error uploading file:', error); + console.error("Error uploading file:", error); if (error instanceof Error) { throw new Error(`Failed to upload file: ${error.message}`); } @@ -100,13 +100,14 @@ export const getSignedUrl = async (filename: string): Promise => { const url = await minioClient.presignedGetObject( BUCKET_NAME, filename, - SIGNED_URL_EXPIRY + SIGNED_URL_EXPIRY, ); return url; } catch (error) { - console.error('Error generating signed URL:', error); - if (error instanceof Error) throw new Error(`Failed to generate signed URL: ${error.message}`); + console.error("Error generating signed URL:", error); + if (error instanceof Error) + throw new Error(`Failed to generate signed URL: ${error.message}`); throw new Error(`Failed to generate signed URL: ${error}`); } }; @@ -120,8 +121,9 @@ export const deleteFile = async (filename: string): Promise => { try { await minioClient.removeObject(BUCKET_NAME, filename); } catch (error) { - console.error('Error deleting file:', error); - if (error instanceof Error) throw new Error(`Failed to delete file: ${error.message}`); + console.error("Error deleting file:", error); + if (error instanceof Error) + throw new Error(`Failed to delete file: ${error.message}`); throw new Error(`Failed to delete file: ${error}`); } }; @@ -140,4 +142,3 @@ export const deleteFile = async (filename: string): Promise => { // Delete a file // await deleteFile('hello.txt'); // console.log('File deleted successfully'); - diff --git a/src/lib/utils/common.ts b/src/lib/utils/common.ts index 3d5bf4c..a10f7e3 100644 --- a/src/lib/utils/common.ts +++ b/src/lib/utils/common.ts @@ -11,7 +11,7 @@ export const commonResponses = { { description: "Bad Request. Usually due to missing parameters, or invalid parameters.", - } + }, ), 401: t.Object( { @@ -22,7 +22,7 @@ export const commonResponses = { }, { description: "Unauthorized. Due to missing or invalid authentication.", - } + }, ), 403: t.Object( { @@ -34,7 +34,7 @@ export const commonResponses = { { description: "Forbidden. You do not have permission to access this resource or to perform this action.", - } + }, ), 404: t.Object( { @@ -45,7 +45,7 @@ export const commonResponses = { }, { description: "Not Found. The requested resource was not found.", - } + }, ), 429: t.Object( { @@ -57,7 +57,7 @@ export const commonResponses = { { description: "Too Many Requests. You have exceeded the rate limit. Try again later.", - } + }, ), 500: t.Object( { @@ -69,6 +69,6 @@ export const commonResponses = { { description: "Internal Server Error. This is a problem with the server that you cannot fix.", - } + }, ), }; diff --git a/src/lib/utils/env.ts b/src/lib/utils/env.ts index b8f7d60..43f792c 100644 --- a/src/lib/utils/env.ts +++ b/src/lib/utils/env.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import { z } from "zod"; // Define the environment schema const envSchema = z.object({ @@ -34,33 +34,36 @@ export const validateEnv = (): EnvConfig => { const config = envSchema.safeParse(process.env); if (!config.success) { - console.warn('\n🚨 Environment Variable Warnings:'); + console.warn("\n🚨 Environment Variable Warnings:"); // Collect and categorize warnings config.error.errors.forEach((error) => { - const path = error.path.join('.'); + const path = error.path.join("."); const message = error.message; let warningMessage = `❌ ${path}: ${message}`; // Add specific functionality warnings - if (path.startsWith('DB_') || path === 'DATABASE_URL') { - warningMessage += '\n ⚠️ Database functionality may not work properly'; + if (path.startsWith("DB_") || path === "DATABASE_URL") { + warningMessage += + "\n ⚠️ Database functionality may not work properly"; } - if (path.startsWith('MINIO_')) { - warningMessage += '\n ⚠️ File storage functionality may not work properly'; + if (path.startsWith("MINIO_")) { + warningMessage += + "\n ⚠️ File storage functionality may not work properly"; } - if (path.startsWith('BETTER_AUTH_')) { - warningMessage += '\n ⚠️ Authentication functionality may not work properly'; + if (path.startsWith("BETTER_AUTH_")) { + warningMessage += + "\n ⚠️ Authentication functionality may not work properly"; } warnings.push(warningMessage); }); // Print all warnings warnings.forEach((warning) => console.warn(warning)); - console.warn('\n'); + console.warn("\n"); - throw new Error('Environment validation failed. Check warnings above.'); + throw new Error("Environment validation failed. Check warnings above."); } return config.data; @@ -74,8 +77,7 @@ export const getConfig = (): EnvConfig => { return validateEnv(); }; - -export const getBaseConfig = (): Pick => { +export const getBaseConfig = (): Pick => { const config = getConfig(); return { PORT: config.PORT, @@ -84,14 +86,20 @@ export const getBaseConfig = (): Pick => { }; // Optional: Export individual config getters with type safety -export const getDbConfig = (): Pick => { +export const getDbConfig = (): Pick => { const config = getConfig(); return { DATABASE_URL: config.DATABASE_URL, }; }; -export const getMinioConfig = (): Pick => { +export const getMinioConfig = (): Pick< + EnvConfig, + | "MINIO_ACCESS_KEY" + | "MINIO_SECRET_KEY" + | "MINIO_ENDPOINT_URL" + | "MINIO_BUCKET_NAME" +> => { const config = getConfig(); return { MINIO_ACCESS_KEY: config.MINIO_ACCESS_KEY, @@ -101,7 +109,10 @@ export const getMinioConfig = (): Pick => { +export const getAuthConfig = (): Pick< + EnvConfig, + "BETTER_AUTH_SECRET" | "BETTER_AUTH_URL" +> => { const config = getConfig(); return { BETTER_AUTH_SECRET: config.BETTER_AUTH_SECRET, diff --git a/test/client.ts b/test/client.ts index 72966db..e2d6520 100644 --- a/test/client.ts +++ b/test/client.ts @@ -1,27 +1,27 @@ -import { treaty } from '@elysiajs/eden' -import { app } from '../src' -import { getAuthConfig } from '../src/lib/utils/env'; +import { treaty } from "@elysiajs/eden"; +import { app } from "../src"; +import { getAuthConfig } from "../src/lib/utils/env"; async function getAuthToken() { - const authUrl = getAuthConfig().BETTER_AUTH_URL + const authUrl = getAuthConfig().BETTER_AUTH_URL; const response = await fetch(`${authUrl}/api/auth/sign-in/email`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", }, body: JSON.stringify({ email: "test@test.com", - password: "testpass123" - }) + password: "testpass123", + }), }); const cookies = response.headers.getSetCookie()[0]; - const sessionToken = cookies.split(";")[0].split("=")[1] + const sessionToken = cookies.split(";")[0].split("=")[1]; return sessionToken; } const token = await getAuthToken(); -export const testClientApp = treaty(app,{ - headers: { - Cookie: `better-auth.session_token=${token}` - } -}) \ No newline at end of file +export const testClientApp = treaty(app, { + headers: { + Cookie: `better-auth.session_token=${token}`, + }, +}); diff --git a/tsconfig.json b/tsconfig.json index 671e128..9abf5cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,9 @@ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - "jsx": "react", /* Specify what JSX code is generated. */ + "jsx": "react" /* Specify what JSX code is generated. */, // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ @@ -22,16 +22,16 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "ES2022", /* Specify what module code is generated. */ + "module": "ES2022" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ "types": [ "bun-types" - ], /* Specify type package names to be included without being referenced in a source file. */ + ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "resolveJsonModule": true, /* Enable importing .json files. */ @@ -67,11 +67,11 @@ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ @@ -94,4 +94,4 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ } -} \ No newline at end of file +}