added prettier formatting

This commit is contained in:
Sanjib Sen 2025-01-20 22:37:03 +06:00
parent cbb30d05a6
commit 9b4cbdd0e3
24 changed files with 413 additions and 344 deletions

View file

@ -1,15 +1,19 @@
# Elysia with Bun runtime # Elysia with Bun runtime
## Getting Started ## Getting Started
To get started with this template, simply paste this command into your terminal: To get started with this template, simply paste this command into your terminal:
```bash ```bash
bun create elysia ./elysia-example bun create elysia ./elysia-example
``` ```
## Development ## Development
To start the development server run: To start the development server run:
```bash ```bash
bun run dev bun run dev
``` ```
Open http://localhost:3000/ with your browser to see the result. Open http://localhost:3000/ with your browser to see the result.

BIN
bun.lockb

Binary file not shown.

View file

@ -10,4 +10,3 @@ services:
OTEL_EXPORTER_OTLP_ENDPOINT: http://tracing:4318 OTEL_EXPORTER_OTLP_ENDPOINT: http://tracing:4318
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
DATABASE_URL: ${DATABASE_URL} DATABASE_URL: ${DATABASE_URL}

View file

@ -140,12 +140,8 @@
"tableFrom": "account", "tableFrom": "account",
"tableTo": "user", "tableTo": "user",
"schemaTo": "auth", "schemaTo": "auth",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "no action", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -337,12 +333,8 @@
"tableFrom": "session", "tableFrom": "session",
"tableTo": "user", "tableTo": "user",
"schemaTo": "auth", "schemaTo": "auth",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "no action", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -352,9 +344,7 @@
"session_token_unique": { "session_token_unique": {
"name": "session_token_unique", "name": "session_token_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["token"]
"token"
]
} }
}, },
"policies": {}, "policies": {},
@ -415,9 +405,7 @@
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["email"]
"email"
]
} }
}, },
"policies": {}, "policies": {},
@ -605,12 +593,8 @@
"tableFrom": "note", "tableFrom": "note",
"tableTo": "user", "tableTo": "user",
"schemaTo": "auth", "schemaTo": "auth",
"columnsFrom": [ "columnsFrom": ["ownerId"],
"ownerId" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "no action", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -635,4 +619,4 @@
"schemas": {}, "schemas": {},
"tables": {} "tables": {}
} }
} }

View file

@ -10,4 +10,4 @@
"breakpoints": true "breakpoints": true
} }
] ]
} }

View file

@ -3,6 +3,7 @@
"version": "1.0.50", "version": "1.0.50",
"scripts": { "scripts": {
"test": "bun test api", "test": "bun test api",
"format": "prettier . --write",
"dev": "bun run --watch src/index.ts", "dev": "bun run --watch src/index.ts",
"email": "email dev --dir src/emails", "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", "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": { "devDependencies": {
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.10",
"prettier": "^3.4.2",
"@types/react": "^19.0.7", "@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"bun-types": "latest", "bun-types": "latest",

View file

@ -50,7 +50,7 @@ export const api = new Elysia({
}, },
{ {
description: "Success", description: "Success",
} },
), ),
404: t.Object( 404: t.Object(
{ {
@ -65,8 +65,8 @@ export const api = new Elysia({
}, },
{ {
description: "Not found", description: "Not found",
} },
), ),
}, },
} },
); );

View file

@ -1,6 +1,4 @@
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { noteRouter } from "./note/note.route"; import { noteRouter } from "./note/note.route";
export const router = new Elysia().use(noteRouter);
export const router = new Elysia().use(noteRouter)

View file

@ -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 const result = await db
.select({ .select({
id: note.id, id: note.id,
@ -36,7 +36,8 @@ export class NoteController {
}) })
.from(note) .from(note)
.where(and(eq(note.ownerId, ownerId), isNull(note.deletedAt))) .where(and(eq(note.ownerId, ownerId), isNull(note.deletedAt)))
.limit(limit).offset(offset) .limit(limit)
.offset(offset)
.execute(); .execute();
return { return {
success: true, success: true,
@ -60,14 +61,14 @@ export class NoteController {
and( and(
eq(note.id, noteId), eq(note.id, noteId),
eq(note.ownerId, ownerId), eq(note.ownerId, ownerId),
isNull(note.deletedAt) isNull(note.deletedAt),
) ),
) )
.execute(); .execute();
let successStatus = true; let successStatus = true;
if(result.length===0){ if (result.length === 0) {
successStatus = false successStatus = false;
}; }
return { return {
success: successStatus, success: successStatus,
data: result, data: result,
@ -79,7 +80,7 @@ export class NoteController {
async updateNoteById( async updateNoteById(
noteId: string, noteId: string,
updated_note: CreateNoteType, updated_note: CreateNoteType,
ownerId: string ownerId: string,
) { ) {
const new_note_data = { ...updated_note, updatedAt: new Date() }; const new_note_data = { ...updated_note, updatedAt: new Date() };
const result = await db const result = await db
@ -89,8 +90,8 @@ export class NoteController {
and( and(
eq(note.id, noteId), eq(note.id, noteId),
eq(note.ownerId, ownerId), eq(note.ownerId, ownerId),
isNull(note.deletedAt) isNull(note.deletedAt),
) ),
) )
.returning({ .returning({
id: note.id, id: note.id,
@ -117,8 +118,8 @@ export class NoteController {
and( and(
eq(note.id, noteId), eq(note.id, noteId),
eq(note.ownerId, ownerId), eq(note.ownerId, ownerId),
isNull(note.deletedAt) isNull(note.deletedAt),
) ),
) )
.execute(); .execute();
return { return {

View file

@ -13,31 +13,34 @@ export type CreateNoteType = Pick<
"title" | "content" "title" | "content"
>; >;
export const createNoteSchema = t.Pick(NoteSchema, [ export const createNoteSchema = t.Pick(NoteSchema, ["title", "content"]);
"title",
"content",
]);
export const getNoteResponses = { export const getNoteResponses = {
200: t.Object({ 200: t.Object(
success:t.Boolean({default:true}), {
data: t.Array(NoteSchema), success: t.Boolean({ default: true }),
error: t.Null(), data: t.Array(NoteSchema),
message: t.String() error: t.Null(),
}, { message: t.String(),
description:"Success" },
}) , {
...commonResponses description: "Success",
} },
),
...commonResponses,
};
export const deleteNoteResponses = { export const deleteNoteResponses = {
200: t.Object({ 200: t.Object(
success:t.Boolean({default:true}), {
data: t.Null(), success: t.Boolean({ default: true }),
error: t.Null(), data: t.Null(),
message: t.String({default:"Note deletion succesful"}) error: t.Null(),
}, { message: t.String({ default: "Note deletion succesful" }),
description:"Success" },
}), {
...commonResponses description: "Success",
} },
),
...commonResponses,
};

View file

@ -1,12 +1,17 @@
import { Elysia, t } from "elysia"; 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 { NoteController } from "./note.controller";
import { userMiddleware } from "../../../middlewares/auth-middleware"; import { userMiddleware } from "../../../middlewares/auth-middleware";
export const noteRouter = new Elysia({ export const noteRouter = new Elysia({
prefix: "/note", prefix: "/note",
name: "CRUD Operations for Notes", name: "CRUD Operations for Notes",
"analytic":true, analytic: true,
tags: ["Note"], tags: ["Note"],
detail: { detail: {
description: "Notes CRUD operations", description: "Notes CRUD operations",
@ -17,47 +22,47 @@ export const noteRouter = new Elysia({
note: NoteSchema, note: NoteSchema,
}) })
.derive(({ request }) => userMiddleware(request)) .derive(({ request }) => userMiddleware(request))
.onError(({ error, code, }) => { .onError(({ error, code }) => {
console.error(error); console.error(error);
return { return {
message: "", message: "",
success: false, success: false,
data: null, data: null,
error: code.toString() error: code.toString(),
}; };
}) })
.get( .get(
"", "",
async ({ note, user, query}) => { async ({ note, user, query }) => {
return await note.getOwnerNotes(user.id, query.limit, query.offset); return await note.getOwnerNotes(user.id, query.limit, query.offset);
}, },
{ {
query:t.Object({ query: t.Object({
limit: t.Optional(t.Number()), limit: t.Optional(t.Number()),
offset: t.Optional(t.Number()) offset: t.Optional(t.Number()),
}), }),
response:getNoteResponses, response: getNoteResponses,
detail:{ detail: {
"description":"Get all notes of the user", description: "Get all notes of the user",
"summary":"Get all notes" summary: "Get all notes",
} },
} },
) )
.get( .get(
"/:id", "/:id",
async ({ note, user, params:{id} }) => { async ({ note, user, params: { id } }) => {
return await note.getNoteById(id, user.id); return await note.getNoteById(id, user.id);
}, },
{ {
params:t.Object({ params: t.Object({
id: t.String(), id: t.String(),
}), }),
response: getNoteResponses, response: getNoteResponses,
detail:{ detail: {
"description":"Get a note by Id", description: "Get a note by Id",
"summary":"Get a note" summary: "Get a note",
} },
} },
) )
.post( .post(
"", "",
@ -67,42 +72,44 @@ export const noteRouter = new Elysia({
{ {
body: createNoteSchema, body: createNoteSchema,
response: getNoteResponses, response: getNoteResponses,
detail:{ detail: {
"description":"Create a new note", description: "Create a new note",
"summary":"Create a note" summary: "Create a note",
} },
} },
).patch( )
.patch(
"/:id", "/:id",
async ({ body, note, user, params:{id} }) => { async ({ body, note, user, params: { id } }) => {
return await note.updateNoteById(id, body, user.id); return await note.updateNoteById(id, body, user.id);
}, },
{ {
body: createNoteSchema, body: createNoteSchema,
params:t.Object({ params: t.Object({
id: t.String(), id: t.String(),
}), }),
response: getNoteResponses, response: getNoteResponses,
detail:{ detail: {
"description":"Update a note by Id", description: "Update a note by Id",
"summary":"Update a note" summary: "Update a note",
} },
} },
).delete( )
.delete(
"/:id", "/:id",
async ({ note, user, params:{id} }) => { async ({ note, user, params: { id } }) => {
return await note.deleteNoteById(id, user.id); return await note.deleteNoteById(id, user.id);
}, },
{ {
params:t.Object({ params: t.Object({
id: t.String(), id: t.String(),
}), }),
response: deleteNoteResponses, response: deleteNoteResponses,
detail:{ detail: {
"description":"Delete a note by Id", description: "Delete a note by Id",
"summary":"Delete a note" summary: "Delete a note",
} },
} },
) )
.delete( .delete(
"", "",
@ -111,9 +118,9 @@ export const noteRouter = new Elysia({
}, },
{ {
response: deleteNoteResponses, response: deleteNoteResponses,
detail:{ detail: {
"description":"Delete all notes of an user", description: "Delete all notes of an user",
"summary":"Delete all notes" summary: "Delete all notes",
} },
} },
) );

View file

@ -1,102 +1,105 @@
import { describe, expect, it } from 'bun:test' import { describe, expect, it } from "bun:test";
import { testClientApp } from '../../../../test/client'; import { testClientApp } from "../../../../test/client";
let noteId: string; let noteId: string;
describe('Note', () => { describe("Note", () => {
// Create a note before tests // Create a note before tests
it('Create Note', async () => { it("Create Note", async () => {
const { data } = await testClientApp.api.note.post({ const { data } = await testClientApp.api.note.post({
"title": "test note", title: "test note",
"content": "description", content: "description",
}) });
if (!data?.data) { 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 noteId = data.data[0].id;
expect(data.data[0].title).toBe('test note') expect(data.data[0].title).toBe("test note");
}) });
// Get all notes // Get all notes
it('Get All Notes', async () => { it("Get All Notes", async () => {
const { data } = await testClientApp.api.note.get({ const { data } = await testClientApp.api.note.get({
query: { limit: 1, offset: 0 } query: { limit: 1, offset: 0 },
}) });
expect(data?.data[0].id).toBe(noteId) expect(data?.data[0].id).toBe(noteId);
}) });
// Get single note // Get single note
it('Get Created Note', async () => { it("Get Created Note", async () => {
const { data, error } = await testClientApp.api.note({ id: noteId }).get() const { data, error } = await testClientApp.api.note({ id: noteId }).get();
expect(data?.data[0].id).toBe(noteId) expect(data?.data[0].id).toBe(noteId);
}) });
// Update note // Update note
it('Update Note', async () => { it("Update Note", async () => {
const updatedTitle = "updated test note" const updatedTitle = "updated test note";
const updatedContent = "updated description" const updatedContent = "updated description";
const { data } = await testClientApp.api.note({ id: noteId }).patch({ const { data } = await testClientApp.api.note({ id: noteId }).patch({
title: updatedTitle, title: updatedTitle,
content: updatedContent, content: updatedContent,
}) });
expect(data?.success).toBe(true) expect(data?.success).toBe(true);
expect(data?.data[0].title).toBe(updatedTitle) expect(data?.data[0].title).toBe(updatedTitle);
expect(data?.data[0].content).toBe(updatedContent) expect(data?.data[0].content).toBe(updatedContent);
}) });
// Delete single note // Delete single note
it('Delete Single Note', async () => { it("Delete Single Note", async () => {
// First create a new note to delete // First create a new note to delete
const { data: createData } = await testClientApp.api.note.post({ const { data: createData } = await testClientApp.api.note.post({
"title": "note to delete", title: "note to delete",
"content": "this note will be deleted", content: "this note will be deleted",
}) });
const deleteNoteId = createData?.data[0].id const deleteNoteId = createData?.data[0].id;
if (!deleteNoteId) { 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 // Delete the note
const { data: deleteData } = await testClientApp.api.note({ id: deleteNoteId }).delete() const { data: deleteData } = await testClientApp.api
expect(deleteData?.success).toBe(true) .note({ id: deleteNoteId })
.delete();
expect(deleteData?.success).toBe(true);
// Verify note is deleted by trying to fetch it // Verify note is deleted by trying to fetch it
const { data: verifyData } = await testClientApp.api.note({ id: deleteNoteId }).get() const { data: verifyData } = await testClientApp.api
expect(verifyData?.data).toHaveLength(0) .note({ id: deleteNoteId })
}) .get();
expect(verifyData?.data).toHaveLength(0);
});
// Delete all notes // Delete all notes
it('Delete All Notes', async () => { it("Delete All Notes", async () => {
// First create multiple notes // First create multiple notes
await testClientApp.api.note.post({ await testClientApp.api.note.post({
"title": "note 1", title: "note 1",
"content": "content 1", content: "content 1",
}) });
await testClientApp.api.note.post({ await testClientApp.api.note.post({
"title": "note 2", title: "note 2",
"content": "content 2", content: "content 2",
}) });
// Delete all notes // Delete all notes
const { data: deleteData } = await testClientApp.api.note.delete() const { data: deleteData } = await testClientApp.api.note.delete();
expect(deleteData?.success).toBe(true) expect(deleteData?.success).toBe(true);
// Verify all notes are deleted // Verify all notes are deleted
const { data: verifyData } = await testClientApp.api.note.get({ const { data: verifyData } = await testClientApp.api.note.get({
query: { limit: 10, offset: 0 } query: { limit: 10, offset: 0 },
}) });
expect(verifyData?.data).toHaveLength(0) expect(verifyData?.data).toHaveLength(0);
}) });
// Error cases // Error cases
it('Should handle invalid note ID', async () => { it("Should handle invalid note ID", async () => {
const invalidId = 'invalid-id' const invalidId = "invalid-id";
const { data } = await testClientApp.api.note({ id: invalidId }).get() const { data } = await testClientApp.api.note({ id: invalidId }).get();
expect(data?.success).toBe(false) expect(data?.success).toBe(false);
expect(data?.data).toHaveLength(0) expect(data?.data).toHaveLength(0);
}) });
}) });

View file

@ -2,6 +2,6 @@ import "dotenv/config";
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres";
import { getDbConfig } from "../lib/utils/env"; import { getDbConfig } from "../lib/utils/env";
const dbConfig = getDbConfig() const dbConfig = getDbConfig();
export const db = drizzle(dbConfig.DATABASE_URL); export const db = drizzle(dbConfig.DATABASE_URL);

View file

@ -9,74 +9,94 @@ import {
export const authSchema = pgSchema("auth"); export const authSchema = pgSchema("auth");
export const user = authSchema.table( export const user = authSchema.table("user", {
"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(), id: text("id").primaryKey(),
name: text("name").notNull(), expiresAt: timestamp("expires_at").notNull(),
email: text("email").notNull().unique(), token: text("token").notNull().unique(),
emailVerified: boolean("email_verified").notNull(), createdAt: timestamp("created_at").notNull(),
image: text("image"), 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(), createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_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", { export const verification = authSchema.table(
id: text("id").primaryKey(), "verification",
expiresAt: timestamp("expires_at").notNull(), {
token: text("token").notNull().unique(), id: text("id").primaryKey(),
createdAt: timestamp("created_at").notNull(), identifier: text("identifier").notNull(),
updatedAt: timestamp("updated_at").notNull(), value: text("value").notNull(),
ipAddress: text("ip_address"), expiresAt: timestamp("expires_at").notNull(),
userAgent: text("user_agent"), createdAt: timestamp("created_at"),
userId: text("user_id") updatedAt: timestamp("updated_at"),
.notNull() },
.references(() => user.id), (table) => [
}, index("idx_auth_verification_identifier").on(table.identifier),
(table) => [index("idx_auth_session_ip_address").on(table.ipAddress), index("idx_auth_session_userid").on(table.userId)] index("idx_auth_verification_expires_at").on(table.expiresAt),
],
); );
export const account = authSchema.table("account", { export const rateLimit = authSchema.table(
id: text("id").primaryKey(), "rate_limit",
accountId: text("account_id").notNull(), {
providerId: text("provider_id").notNull(), id: text("id").primaryKey(),
userId: text("user_id") key: text("key"),
.notNull() count: integer("count"),
.references(() => user.id), lastRequest: integer("last_request"),
accessToken: text("access_token"), },
refreshToken: text("refresh_token"), (table) => [index("idx_auth_ratelimit_key").on(table.key)],
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 jwks = authSchema.table("jwks", { export const jwks = authSchema.table("jwks", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
publicKey: text("public_key").notNull(), publicKey: text("public_key").notNull(),
privateKey: text("private_key").notNull(), privateKey: text("private_key").notNull(),
createdAt: timestamp("created_at").notNull(), createdAt: timestamp("created_at").notNull(),
}); });

View file

@ -1,16 +1,26 @@
import { index, pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { index, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createId } from '@paralleldrive/cuid2' import { createId } from "@paralleldrive/cuid2";
import { user } from "./auth"; import { user } from "./auth";
export const note = pgTable("note", { export const note = pgTable(
id: text("id").primaryKey().$defaultFn(()=> `note_${createId()}`), "note",
title: text("title"), {
content: text("content"), id: text("id")
createdAt: timestamp().notNull().defaultNow(), .primaryKey()
updatedAt: timestamp(), .$defaultFn(() => `note_${createId()}`),
deletedAt: timestamp(), title: text("title"),
ownerId: text().notNull().references(() => user.id) 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)] (table) => [
) index("idx_note_ownerid").on(table.ownerId),
index("idx_note_createdat").on(table.createdAt),
index("idx_note_deletedat").on(table.deletedAt),
],
);

View file

@ -1,7 +1,13 @@
import * as React from 'react' import * as React from "react";
import { Tailwind, Section, Text } from '@react-email/components' 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 ( return (
<Tailwind> <Tailwind>
<Section className="flex justify-center items-center w-full min-h-screen font-sans"> <Section className="flex justify-center items-center w-full min-h-screen font-sans">
@ -12,17 +18,17 @@ export default function AuthEmail({ message, link }: { message: string, link: st
<Text className="text-gray-500 my-0"> <Text className="text-gray-500 my-0">
Use the following Link to {message} Use the following Link to {message}
</Text> </Text>
<a href={link} className="text-blue-400 font-bold pt-2">Link</a> <a href={link} className="text-blue-400 font-bold pt-2">
<Text className="text-gray-600 text-xs"> Link
Thanks </a>
</Text> <Text className="text-gray-600 text-xs">Thanks</Text>
</Section> </Section>
</Section> </Section>
</Tailwind> </Tailwind>
) );
} }
AuthEmail.PreviewProps = { AuthEmail.PreviewProps = {
link: "https://example.com", link: "https://example.com",
message: "Verify your email address" message: "Verify your email address",
} };

View file

@ -15,7 +15,7 @@ export const app = new Elysia()
.use( .use(
opentelemetry({ opentelemetry({
serviceName: baseConfig.SERVICE_NAME, serviceName: baseConfig.SERVICE_NAME,
}) }),
) )
.use(serverTiming()) .use(serverTiming())
.use( .use(
@ -28,7 +28,7 @@ export const app = new Elysia()
description: `API docs for ${baseConfig.SERVICE_NAME}`, description: `API docs for ${baseConfig.SERVICE_NAME}`,
}, },
}, },
}) }),
) )
.onError(({ error, code }) => { .onError(({ error, code }) => {
if (code === "NOT_FOUND") if (code === "NOT_FOUND")

View file

@ -1,8 +1,15 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../../db/index"; import { db } from "../../db/index";
import { jwt, openAPI } from "better-auth/plugins" import { jwt, openAPI } from "better-auth/plugins";
import { user, account, verification, session, rateLimit, jwks } from "../../db/schema/auth"; import {
user,
account,
verification,
session,
rateLimit,
jwks,
} from "../../db/schema/auth";
import { sendMail } from "../mail/mail"; import { sendMail } from "../mail/mail";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { createElement } from "react"; import { createElement } from "react";
@ -17,13 +24,13 @@ export const auth = betterAuth({
account: account, account: account,
verification: verification, verification: verification,
rateLimit: rateLimit, rateLimit: rateLimit,
jwks: jwks jwks: jwks,
} },
}), }),
user: { user: {
deleteUser: { deleteUser: {
enabled: true // [!Code Highlight] enabled: true, // [!Code Highlight]
} },
}, },
rateLimit: { rateLimit: {
window: 60, window: 60,
@ -40,8 +47,8 @@ export const auth = betterAuth({
return { return {
window: 3600 * 12, window: 3600 * 12,
max: 10, max: 10,
} };
} },
}, },
}, },
emailAndPassword: { emailAndPassword: {
@ -49,7 +56,9 @@ export const auth = betterAuth({
requireEmailVerification: false, requireEmailVerification: false,
sendResetPassword: async ({ user, url }, request) => { sendResetPassword: async ({ user, url }, request) => {
const subject = "Reset your password"; 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({ await sendMail({
to: user.email, to: user.email,
subject: subject, subject: subject,
@ -60,20 +69,21 @@ export const auth = betterAuth({
emailVerification: { emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => { sendVerificationEmail: async ({ user, url, token }, request) => {
const subject = "Verify your email address"; 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({ await sendMail({
to: user.email, to: user.email,
subject: subject, subject: subject,
html: html, html: html,
}); });
}, },
}, },
plugins: [ plugins: [
openAPI({ openAPI({
path: "/docs", path: "/docs",
}), }),
jwt() jwt(),
], ],
socialProviders: { socialProviders: {
/* /*

View file

@ -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({ const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST!, host: process.env.SMTP_HOST!,
port: +process.env.SMTP_PORT!, port: +process.env.SMTP_PORT!,
auth: { auth: {
user: process.env.SMTP_USER!, user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASSWORD!, pass: process.env.SMTP_PASSWORD!,
} },
}) });
await transporter.sendMail({ await transporter.sendMail({
from: process.env.SMTP_FROM!, from: process.env.SMTP_FROM!,
@ -16,5 +26,5 @@ export async function sendMail({ to, subject, text, html }: { to: string, subjec
subject, subject,
text: text, text: text,
html: html, html: html,
}) });
} }

View file

@ -1,15 +1,15 @@
import { Client } from 'minio'; import { Client } from "minio";
import { Buffer } from 'buffer'; import { Buffer } from "buffer";
import { getMinioConfig } from '../utils/env'; import { getMinioConfig } from "../utils/env";
// MinIO client configuration // MinIO client configuration
const minioConfig = getMinioConfig() const minioConfig = getMinioConfig();
const minioClient = new Client({ const minioClient = new Client({
endPoint: minioConfig.MINIO_ENDPOINT_URL, endPoint: minioConfig.MINIO_ENDPOINT_URL,
useSSL: false, useSSL: false,
accessKey: minioConfig.MINIO_ACCESS_KEY, accessKey: minioConfig.MINIO_ACCESS_KEY,
secretKey: minioConfig.MINIO_SECRET_KEY secretKey: minioConfig.MINIO_SECRET_KEY,
}); });
const BUCKET_NAME = minioConfig.MINIO_BUCKET_NAME; const BUCKET_NAME = minioConfig.MINIO_BUCKET_NAME;
@ -44,7 +44,7 @@ const fileToBuffer = async (file: File | Blob): Promise<Buffer> => {
export const uploadFileAndGetUrl = async ( export const uploadFileAndGetUrl = async (
filename: string, filename: string,
file: Buffer | File | Blob, file: Buffer | File | Blob,
contentType?: string contentType?: string,
): Promise<string> => { ): Promise<string> => {
try { try {
await ensureBucket(); await ensureBucket();
@ -54,11 +54,11 @@ export const uploadFileAndGetUrl = async (
// If file is from form-data and no contentType is provided, use its type // If file is from form-data and no contentType is provided, use its type
const metadata: Record<string, string> = {}; const metadata: Record<string, string> = {};
if (!contentType && 'type' in file) { if (!contentType && "type" in file) {
contentType = file.type; contentType = file.type;
} }
if (contentType) { if (contentType) {
metadata['Content-Type'] = contentType; metadata["Content-Type"] = contentType;
} }
// Upload the file // Upload the file
@ -67,19 +67,19 @@ export const uploadFileAndGetUrl = async (
filename, filename,
fileBuffer, fileBuffer,
fileBuffer.length, fileBuffer.length,
metadata metadata,
); );
// Generate and return signed URL // Generate and return signed URL
const url = await minioClient.presignedGetObject( const url = await minioClient.presignedGetObject(
BUCKET_NAME, BUCKET_NAME,
filename, filename,
SIGNED_URL_EXPIRY SIGNED_URL_EXPIRY,
); );
return url; return url;
} catch (error) { } catch (error) {
console.error('Error uploading file:', error); console.error("Error uploading file:", error);
if (error instanceof Error) { if (error instanceof Error) {
throw new Error(`Failed to upload file: ${error.message}`); throw new Error(`Failed to upload file: ${error.message}`);
} }
@ -100,13 +100,14 @@ export const getSignedUrl = async (filename: string): Promise<string> => {
const url = await minioClient.presignedGetObject( const url = await minioClient.presignedGetObject(
BUCKET_NAME, BUCKET_NAME,
filename, filename,
SIGNED_URL_EXPIRY SIGNED_URL_EXPIRY,
); );
return url; return url;
} catch (error) { } catch (error) {
console.error('Error generating signed URL:', error); console.error("Error generating signed URL:", error);
if (error instanceof Error) throw new Error(`Failed to generate signed URL: ${error.message}`); if (error instanceof Error)
throw new Error(`Failed to generate signed URL: ${error.message}`);
throw new Error(`Failed to generate signed URL: ${error}`); throw new Error(`Failed to generate signed URL: ${error}`);
} }
}; };
@ -120,8 +121,9 @@ export const deleteFile = async (filename: string): Promise<void> => {
try { try {
await minioClient.removeObject(BUCKET_NAME, filename); await minioClient.removeObject(BUCKET_NAME, filename);
} catch (error) { } catch (error) {
console.error('Error deleting file:', error); console.error("Error deleting file:", error);
if (error instanceof Error) throw new Error(`Failed to delete file: ${error.message}`); if (error instanceof Error)
throw new Error(`Failed to delete file: ${error.message}`);
throw new Error(`Failed to delete file: ${error}`); throw new Error(`Failed to delete file: ${error}`);
} }
}; };
@ -140,4 +142,3 @@ export const deleteFile = async (filename: string): Promise<void> => {
// Delete a file // Delete a file
// await deleteFile('hello.txt'); // await deleteFile('hello.txt');
// console.log('File deleted successfully'); // console.log('File deleted successfully');

View file

@ -11,7 +11,7 @@ export const commonResponses = {
{ {
description: description:
"Bad Request. Usually due to missing parameters, or invalid parameters.", "Bad Request. Usually due to missing parameters, or invalid parameters.",
} },
), ),
401: t.Object( 401: t.Object(
{ {
@ -22,7 +22,7 @@ export const commonResponses = {
}, },
{ {
description: "Unauthorized. Due to missing or invalid authentication.", description: "Unauthorized. Due to missing or invalid authentication.",
} },
), ),
403: t.Object( 403: t.Object(
{ {
@ -34,7 +34,7 @@ export const commonResponses = {
{ {
description: description:
"Forbidden. You do not have permission to access this resource or to perform this action.", "Forbidden. You do not have permission to access this resource or to perform this action.",
} },
), ),
404: t.Object( 404: t.Object(
{ {
@ -45,7 +45,7 @@ export const commonResponses = {
}, },
{ {
description: "Not Found. The requested resource was not found.", description: "Not Found. The requested resource was not found.",
} },
), ),
429: t.Object( 429: t.Object(
{ {
@ -57,7 +57,7 @@ export const commonResponses = {
{ {
description: description:
"Too Many Requests. You have exceeded the rate limit. Try again later.", "Too Many Requests. You have exceeded the rate limit. Try again later.",
} },
), ),
500: t.Object( 500: t.Object(
{ {
@ -69,6 +69,6 @@ export const commonResponses = {
{ {
description: description:
"Internal Server Error. This is a problem with the server that you cannot fix.", "Internal Server Error. This is a problem with the server that you cannot fix.",
} },
), ),
}; };

View file

@ -1,4 +1,4 @@
import { z } from 'zod'; import { z } from "zod";
// Define the environment schema // Define the environment schema
const envSchema = z.object({ const envSchema = z.object({
@ -34,33 +34,36 @@ export const validateEnv = (): EnvConfig => {
const config = envSchema.safeParse(process.env); const config = envSchema.safeParse(process.env);
if (!config.success) { if (!config.success) {
console.warn('\n🚨 Environment Variable Warnings:'); console.warn("\n🚨 Environment Variable Warnings:");
// Collect and categorize warnings // Collect and categorize warnings
config.error.errors.forEach((error) => { config.error.errors.forEach((error) => {
const path = error.path.join('.'); const path = error.path.join(".");
const message = error.message; const message = error.message;
let warningMessage = `${path}: ${message}`; let warningMessage = `${path}: ${message}`;
// Add specific functionality warnings // Add specific functionality warnings
if (path.startsWith('DB_') || path === 'DATABASE_URL') { if (path.startsWith("DB_") || path === "DATABASE_URL") {
warningMessage += '\n ⚠️ Database functionality may not work properly'; warningMessage +=
"\n ⚠️ Database functionality may not work properly";
} }
if (path.startsWith('MINIO_')) { if (path.startsWith("MINIO_")) {
warningMessage += '\n ⚠️ File storage functionality may not work properly'; warningMessage +=
"\n ⚠️ File storage functionality may not work properly";
} }
if (path.startsWith('BETTER_AUTH_')) { if (path.startsWith("BETTER_AUTH_")) {
warningMessage += '\n ⚠️ Authentication functionality may not work properly'; warningMessage +=
"\n ⚠️ Authentication functionality may not work properly";
} }
warnings.push(warningMessage); warnings.push(warningMessage);
}); });
// Print all warnings // Print all warnings
warnings.forEach((warning) => console.warn(warning)); 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; return config.data;
@ -74,8 +77,7 @@ export const getConfig = (): EnvConfig => {
return validateEnv(); return validateEnv();
}; };
export const getBaseConfig = (): Pick<EnvConfig, "PORT" | "SERVICE_NAME"> => {
export const getBaseConfig = (): Pick<EnvConfig, 'PORT' | 'SERVICE_NAME'> => {
const config = getConfig(); const config = getConfig();
return { return {
PORT: config.PORT, PORT: config.PORT,
@ -84,14 +86,20 @@ export const getBaseConfig = (): Pick<EnvConfig, 'PORT' | 'SERVICE_NAME'> => {
}; };
// Optional: Export individual config getters with type safety // Optional: Export individual config getters with type safety
export const getDbConfig = (): Pick<EnvConfig, 'DATABASE_URL'> => { export const getDbConfig = (): Pick<EnvConfig, "DATABASE_URL"> => {
const config = getConfig(); const config = getConfig();
return { return {
DATABASE_URL: config.DATABASE_URL, DATABASE_URL: config.DATABASE_URL,
}; };
}; };
export const getMinioConfig = (): Pick<EnvConfig, 'MINIO_ACCESS_KEY' | 'MINIO_SECRET_KEY' | 'MINIO_ENDPOINT_URL' | 'MINIO_BUCKET_NAME'> => { export const getMinioConfig = (): Pick<
EnvConfig,
| "MINIO_ACCESS_KEY"
| "MINIO_SECRET_KEY"
| "MINIO_ENDPOINT_URL"
| "MINIO_BUCKET_NAME"
> => {
const config = getConfig(); const config = getConfig();
return { return {
MINIO_ACCESS_KEY: config.MINIO_ACCESS_KEY, MINIO_ACCESS_KEY: config.MINIO_ACCESS_KEY,
@ -101,7 +109,10 @@ export const getMinioConfig = (): Pick<EnvConfig, 'MINIO_ACCESS_KEY' | 'MINIO_SE
}; };
}; };
export const getAuthConfig = (): Pick<EnvConfig, 'BETTER_AUTH_SECRET' | 'BETTER_AUTH_URL'> => { export const getAuthConfig = (): Pick<
EnvConfig,
"BETTER_AUTH_SECRET" | "BETTER_AUTH_URL"
> => {
const config = getConfig(); const config = getConfig();
return { return {
BETTER_AUTH_SECRET: config.BETTER_AUTH_SECRET, BETTER_AUTH_SECRET: config.BETTER_AUTH_SECRET,

View file

@ -1,27 +1,27 @@
import { treaty } from '@elysiajs/eden' import { treaty } from "@elysiajs/eden";
import { app } from '../src' import { app } from "../src";
import { getAuthConfig } from '../src/lib/utils/env'; import { getAuthConfig } from "../src/lib/utils/env";
async function getAuthToken() { 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`, { const response = await fetch(`${authUrl}/api/auth/sign-in/email`, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
email: "test@test.com", email: "test@test.com",
password: "testpass123" password: "testpass123",
}) }),
}); });
const cookies = response.headers.getSetCookie()[0]; const cookies = response.headers.getSetCookie()[0];
const sessionToken = cookies.split(";")[0].split("=")[1] const sessionToken = cookies.split(";")[0].split("=")[1];
return sessionToken; return sessionToken;
} }
const token = await getAuthToken(); const token = await getAuthToken();
export const testClientApp = treaty(app,{ export const testClientApp = treaty(app, {
headers: { headers: {
Cookie: `better-auth.session_token=${token}` Cookie: `better-auth.session_token=${token}`,
} },
}) });

View file

@ -9,9 +9,9 @@
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* 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. */ // "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. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "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'. */ // "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. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */ /* 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. */ // "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. */ // "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. */ // "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. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
"types": [ "types": [
"bun-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. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */ // "resolveJsonModule": true, /* Enable importing .json files. */
@ -67,11 +67,11 @@
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "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. */ // "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. */ // "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 */ /* 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. */ // "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'. */ // "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. */ // "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. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
} }
} }