added prettier formatting
This commit is contained in:
parent
cbb30d05a6
commit
9b4cbdd0e3
24 changed files with 413 additions and 344 deletions
|
|
@ -1,13 +1,17 @@
|
||||||
# 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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 }),
|
success: t.Boolean({ default: true }),
|
||||||
data: t.Array(NoteSchema),
|
data: t.Array(NoteSchema),
|
||||||
error: t.Null(),
|
error: t.Null(),
|
||||||
message: t.String()
|
message: t.String(),
|
||||||
}, {
|
},
|
||||||
description:"Success"
|
{
|
||||||
}) ,
|
description: "Success",
|
||||||
...commonResponses
|
},
|
||||||
}
|
),
|
||||||
|
...commonResponses,
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteNoteResponses = {
|
export const deleteNoteResponses = {
|
||||||
200: t.Object({
|
200: t.Object(
|
||||||
|
{
|
||||||
success: t.Boolean({ default: true }),
|
success: t.Boolean({ default: true }),
|
||||||
data: t.Null(),
|
data: t.Null(),
|
||||||
error: t.Null(),
|
error: t.Null(),
|
||||||
message: t.String({default:"Note deletion succesful"})
|
message: t.String({ default: "Note deletion succesful" }),
|
||||||
}, {
|
},
|
||||||
description:"Success"
|
{
|
||||||
}),
|
description: "Success",
|
||||||
...commonResponses
|
},
|
||||||
}
|
),
|
||||||
|
...commonResponses,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,13 +22,13 @@ 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(
|
||||||
|
|
@ -34,14 +39,14 @@ export const noteRouter = new Elysia({
|
||||||
{
|
{
|
||||||
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",
|
||||||
|
|
@ -54,10 +59,10 @@ export const noteRouter = new Elysia({
|
||||||
}),
|
}),
|
||||||
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(
|
||||||
"",
|
"",
|
||||||
|
|
@ -68,11 +73,12 @@ 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);
|
||||||
|
|
@ -84,11 +90,12 @@ export const noteRouter = new Elysia({
|
||||||
}),
|
}),
|
||||||
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);
|
||||||
|
|
@ -99,10 +106,10 @@ export const noteRouter = new Elysia({
|
||||||
}),
|
}),
|
||||||
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(
|
||||||
"",
|
"",
|
||||||
|
|
@ -112,8 +119,8 @@ 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",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ 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(),
|
id: text("id").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
email: text("email").notNull().unique(),
|
email: text("email").notNull().unique(),
|
||||||
|
|
@ -19,10 +17,11 @@ export const user = authSchema.table(
|
||||||
image: text("image"),
|
image: text("image"),
|
||||||
createdAt: timestamp("created_at").notNull(),
|
createdAt: timestamp("created_at").notNull(),
|
||||||
updatedAt: timestamp("updated_at").notNull(),
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const session = authSchema.table("session", {
|
export const session = authSchema.table(
|
||||||
|
"session",
|
||||||
|
{
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
expiresAt: timestamp("expires_at").notNull(),
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
token: text("token").notNull().unique(),
|
token: text("token").notNull().unique(),
|
||||||
|
|
@ -34,10 +33,15 @@ export const session = authSchema.table("session", {
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id),
|
.references(() => user.id),
|
||||||
},
|
},
|
||||||
(table) => [index("idx_auth_session_ip_address").on(table.ipAddress), index("idx_auth_session_userid").on(table.userId)]
|
(table) => [
|
||||||
|
index("idx_auth_session_ip_address").on(table.ipAddress),
|
||||||
|
index("idx_auth_session_userid").on(table.userId),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const account = authSchema.table("account", {
|
export const account = authSchema.table(
|
||||||
|
"account",
|
||||||
|
{
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
accountId: text("account_id").notNull(),
|
accountId: text("account_id").notNull(),
|
||||||
providerId: text("provider_id").notNull(),
|
providerId: text("provider_id").notNull(),
|
||||||
|
|
@ -54,9 +58,18 @@ export const account = authSchema.table("account", {
|
||||||
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)]);
|
(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", {
|
export const verification = authSchema.table(
|
||||||
|
"verification",
|
||||||
|
{
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
identifier: text("identifier").notNull(),
|
identifier: text("identifier").notNull(),
|
||||||
value: text("value").notNull(),
|
value: text("value").notNull(),
|
||||||
|
|
@ -64,15 +77,22 @@ export const verification = authSchema.table("verification", {
|
||||||
createdAt: timestamp("created_at"),
|
createdAt: timestamp("created_at"),
|
||||||
updatedAt: timestamp("updated_at"),
|
updatedAt: timestamp("updated_at"),
|
||||||
},
|
},
|
||||||
(table) => [index("idx_auth_verification_identifier").on(table.identifier), index("idx_auth_verification_expires_at").on(table.expiresAt)]);
|
(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", {
|
export const rateLimit = authSchema.table(
|
||||||
|
"rate_limit",
|
||||||
|
{
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
key: text("key"),
|
key: text("key"),
|
||||||
count: integer("count"),
|
count: integer("count"),
|
||||||
lastRequest: integer("last_request"),
|
lastRequest: integer("last_request"),
|
||||||
},
|
},
|
||||||
(table) => [index("idx_auth_ratelimit_key").on(table.key)]);
|
(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(),
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
{
|
||||||
|
id: text("id")
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => `note_${createId()}`),
|
||||||
title: text("title"),
|
title: text("title"),
|
||||||
content: text("content"),
|
content: text("content"),
|
||||||
createdAt: timestamp().notNull().defaultNow(),
|
createdAt: timestamp().notNull().defaultNow(),
|
||||||
updatedAt: timestamp(),
|
updatedAt: timestamp(),
|
||||||
deletedAt: timestamp(),
|
deletedAt: timestamp(),
|
||||||
ownerId: text().notNull().references(() => user.id)
|
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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue