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
## 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.
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_PROTOCOL: http/protobuf
DATABASE_URL: ${DATABASE_URL}

View file

@ -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": {}
}
}
}

View file

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

View file

@ -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",

View file

@ -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",
}
},
),
},
}
},
);

View file

@ -1,6 +1,4 @@
import { Elysia, t } from "elysia";
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
.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 {

View file

@ -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
}
200: t.Object(
{
success: t.Boolean({ default: true }),
data: t.Null(),
error: t.Null(),
message: t.String({ default: "Note deletion succesful" }),
},
{
description: "Success",
},
),
...commonResponses,
};

View file

@ -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",
},
},
);

View file

@ -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)
})
})
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);
});
});

View file

@ -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);

View file

@ -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(),
});
});

View file

@ -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)]
)
(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 { 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 (
<Tailwind>
<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">
Use the following Link to {message}
</Text>
<a href={link} className="text-blue-400 font-bold pt-2">Link</a>
<Text className="text-gray-600 text-xs">
Thanks
</Text>
<a href={link} className="text-blue-400 font-bold pt-2">
Link
</a>
<Text className="text-gray-600 text-xs">Thanks</Text>
</Section>
</Section>
</Tailwind>
)
);
}
AuthEmail.PreviewProps = {
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(
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")

View file

@ -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: {
/*

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({
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,
})
});
}

View file

@ -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<Buffer> => {
export const uploadFileAndGetUrl = async (
filename: string,
file: Buffer | File | Blob,
contentType?: string
contentType?: string,
): Promise<string> => {
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<string, string> = {};
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<string> => {
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<void> => {
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<void> => {
// Delete a file
// await deleteFile('hello.txt');
// console.log('File deleted successfully');

View file

@ -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.",
}
},
),
};

View file

@ -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<EnvConfig, 'PORT' | 'SERVICE_NAME'> => {
export const getBaseConfig = (): Pick<EnvConfig, "PORT" | "SERVICE_NAME"> => {
const config = getConfig();
return {
PORT: config.PORT,
@ -84,14 +86,20 @@ export const getBaseConfig = (): Pick<EnvConfig, 'PORT' | 'SERVICE_NAME'> => {
};
// 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();
return {
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();
return {
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();
return {
BETTER_AUTH_SECRET: config.BETTER_AUTH_SECRET,

View file

@ -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}`
}
})
export const testClientApp = treaty(app, {
headers: {
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. */
// "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. */
}
}
}