docs, formatting, tests
This commit is contained in:
parent
1da74a833a
commit
42bff39895
12 changed files with 386 additions and 139 deletions
183
README.md
183
README.md
|
|
@ -1,19 +1,184 @@
|
||||||
# Elysia with Bun runtime
|
# Microservice Start
|
||||||
|
|
||||||
## Getting Started
|
A microservice starter template built with Bun, Elysia, PostgreSQL, and MinIO.
|
||||||
|
|
||||||
To get started with this template, simply paste this command into your terminal:
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Bun](https://bun.sh/) installed
|
||||||
|
- Docker and Docker Compose
|
||||||
|
- Node.js (for development tools)
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Runtime:** Bun
|
||||||
|
- **Framework:** Elysia
|
||||||
|
- **Database:** PostgreSQL
|
||||||
|
- **Object Storage:** MinIO
|
||||||
|
- **Authentication:** better-auth
|
||||||
|
- **Email Templates:** React Email
|
||||||
|
- **Database Tools:** Drizzle ORM
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun create elysia ./elysia-example
|
git clone <your-repository-url>
|
||||||
|
cd microservice-start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
2. Set up environment files:
|
||||||
|
|
||||||
To start the development server run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run dev
|
# Copy environment examples
|
||||||
|
cp .env.example .env
|
||||||
|
cp .env.services.example .env.services
|
||||||
```
|
```
|
||||||
|
|
||||||
Open http://localhost:3000/ with your browser to see the result.
|
3. Configure the environment variables:
|
||||||
|
|
||||||
|
**.env**
|
||||||
|
|
||||||
|
```env
|
||||||
|
PORT=3000
|
||||||
|
SERVICE_NAME=<your-service-name>
|
||||||
|
DATABASE_URL="postgresql://..."
|
||||||
|
MINIO_ACCESS_KEY=
|
||||||
|
MINIO_SECRET_KEY=
|
||||||
|
MINIO_ENDPOINT_URL=
|
||||||
|
MINIO_PORT=
|
||||||
|
MINIO_BUCKET_NAME=
|
||||||
|
BETTER_AUTH_SECRET=
|
||||||
|
|
||||||
|
# DO NOT CHANGE
|
||||||
|
BETTER_AUTH_URL=http://127.0.0.1:${PORT}
|
||||||
|
```
|
||||||
|
|
||||||
|
**.env.services**
|
||||||
|
|
||||||
|
```env
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_PORT=
|
||||||
|
|
||||||
|
MINIO_ROOT_USER=
|
||||||
|
MINIO_ROOT_PASSWORD=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Project
|
||||||
|
|
||||||
|
1. Start the required services (PostgreSQL, MinIO):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run services:up
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Configure MinIO:
|
||||||
|
|
||||||
|
- Access MinIO console at `http://localhost:9000`
|
||||||
|
- Log in using the credentials set in `.env.services`
|
||||||
|
- Create an access key and secret key
|
||||||
|
- Update the `.env` file with the generated MinIO credentials:
|
||||||
|
- `MINIO_ACCESS_KEY`
|
||||||
|
- `MINIO_SECRET_KEY`
|
||||||
|
|
||||||
|
3. Install dependencies and start the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Tools
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
|
||||||
|
- Open Database Studio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db:studio
|
||||||
|
```
|
||||||
|
|
||||||
|
- Check database schema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db:check
|
||||||
|
```
|
||||||
|
|
||||||
|
- Generate database migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
- Pull database schema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db:pull
|
||||||
|
```
|
||||||
|
|
||||||
|
- Push database changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db:push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Commands
|
||||||
|
|
||||||
|
- Run tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun test
|
||||||
|
```
|
||||||
|
|
||||||
|
- Format code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
```
|
||||||
|
|
||||||
|
- Generate auth configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run auth:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
- Development email server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run email
|
||||||
|
```
|
||||||
|
|
||||||
|
- Build for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
|
||||||
|
- Start services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run services:up
|
||||||
|
```
|
||||||
|
|
||||||
|
- Stop services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run services:down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
[Add your contribution guidelines here]
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Add your license information here]
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,16 @@ services:
|
||||||
COLLECTOR_ZIPKIN_HOST_PORT: 9411
|
COLLECTOR_ZIPKIN_HOST_PORT: 9411
|
||||||
COLLECTOR_OTLP_ENABLED: true
|
COLLECTOR_OTLP_ENABLED: true
|
||||||
ports:
|
ports:
|
||||||
- "6831:6831/udp" # jaeger-agent UDP accept compact thrift protocol
|
- "6831:6831/udp" # jaeger-agent UDP accept compact thrift protocol
|
||||||
- "6832:6832/udp" # jaeger-agent UDP accept binary thrift protocol
|
- "6832:6832/udp" # jaeger-agent UDP accept binary thrift protocol
|
||||||
- "5778:5778" # jaeger-agent HTTP serve configs
|
- "5778:5778" # jaeger-agent HTTP serve configs
|
||||||
- "16686:16686" # jaeger-query HTTP serve frontend/API
|
- "16686:16686" # jaeger-query HTTP serve frontend/API
|
||||||
- "4317:4317" # OTLP gRPC
|
- "4317:4317" # OTLP gRPC
|
||||||
- "4318:4318" # OTLP HTTP
|
- "4318:4318" # OTLP HTTP
|
||||||
- "14250:14250" # jaeger-collector gRPC
|
- "14250:14250" # jaeger-collector gRPC
|
||||||
- "14268:14268" # jaeger-collector HTTP accept spans
|
- "14268:14268" # jaeger-collector HTTP accept spans
|
||||||
- "14269:14269" # jaeger-collector HTTP admin
|
- "14269:14269" # jaeger-collector HTTP admin
|
||||||
- "9411:9411" # zipkin-collector HTTP accept spans
|
- "9411:9411" # zipkin-collector HTTP accept spans
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
|
|
|
||||||
|
|
@ -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": {},
|
||||||
|
|
@ -584,12 +572,8 @@
|
||||||
"name": "note_attachments_note_id_note_id_fk",
|
"name": "note_attachments_note_id_note_id_fk",
|
||||||
"tableFrom": "note_attachments",
|
"tableFrom": "note_attachments",
|
||||||
"tableTo": "note",
|
"tableTo": "note",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["note_id"],
|
||||||
"note_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
"onDelete": "no action",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -701,12 +685,8 @@
|
||||||
"tableFrom": "note",
|
"tableFrom": "note",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"schemaTo": "auth",
|
"schemaTo": "auth",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["owner_id"],
|
||||||
"owner_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
"onDelete": "no action",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,20 @@
|
||||||
"name": "app",
|
"name": "app",
|
||||||
"version": "1.0.50",
|
"version": "1.0.50",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test api",
|
"test": "bun run format && bun test api",
|
||||||
"format": "prettier . --write",
|
"format": "prettier . --write",
|
||||||
"dev": "bun run --watch src/index.ts",
|
"dev": "bun run --watch src/index.ts",
|
||||||
|
"services:up": "docker compose --env-file .env.services -f docker-compose.services.yaml up -d --build && bun run db:migrate",
|
||||||
|
"services:dowm": "docker compose --env-file .env.services -f docker-compose.services.yaml down",
|
||||||
"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",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"db:check": "drizzle-kit check",
|
"db:check": "drizzle-kit check",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:pull": "drizzle-kit pull",
|
"db:pull": "drizzle-kit pull",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts"
|
"build": "bun run test && bun build --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cors": "^1.2.0",
|
"@elysiajs/cors": "^1.2.0",
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,21 @@ import { attachment, note } from "../../../../db/schema/note";
|
||||||
import { CreateAttachmentType } from "./attachment.model";
|
import { CreateAttachmentType } from "./attachment.model";
|
||||||
import { NoteController } from "../note.controller";
|
import { NoteController } from "../note.controller";
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import { getSignedUrl, uploadFileAndGetUrl } from "../../../../lib/storage/storage";
|
import {
|
||||||
|
getSignedUrl,
|
||||||
|
uploadFileAndGetUrl,
|
||||||
|
} from "../../../../lib/storage/storage";
|
||||||
import { error } from "elysia";
|
import { error } from "elysia";
|
||||||
|
|
||||||
export class AttachmentController {
|
export class AttachmentController {
|
||||||
async createAttachment(new_attachment: CreateAttachmentType, ownerId:string) {
|
async createAttachment(
|
||||||
|
new_attachment: CreateAttachmentType,
|
||||||
|
ownerId: string,
|
||||||
|
) {
|
||||||
const note = new NoteController();
|
const note = new NoteController();
|
||||||
|
|
||||||
const existingNote = await note.getNoteById(new_attachment.noteId, ownerId);
|
const existingNote = await note.getNoteById(new_attachment.noteId, ownerId);
|
||||||
if (existingNote.data.length===0){
|
if (existingNote.data.length === 0) {
|
||||||
throw error(403, {
|
throw error(403, {
|
||||||
success: false,
|
success: false,
|
||||||
data: [],
|
data: [],
|
||||||
|
|
@ -20,9 +26,22 @@ export class AttachmentController {
|
||||||
error: "FORBIDDEN",
|
error: "FORBIDDEN",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const filePath = "attachments/"+existingNote.data[0].id+"/file_"+createId()+"-"+new_attachment.file.name;
|
const filePath =
|
||||||
const attachmentUrl = await uploadFileAndGetUrl(filePath, new_attachment.file)
|
"attachments/" +
|
||||||
const new_attachment_data = { ...new_attachment, noteId: new_attachment.noteId, filePath:filePath };
|
existingNote.data[0].id +
|
||||||
|
"/file_" +
|
||||||
|
createId() +
|
||||||
|
"-" +
|
||||||
|
new_attachment.file.name;
|
||||||
|
const attachmentUrl = await uploadFileAndGetUrl(
|
||||||
|
filePath,
|
||||||
|
new_attachment.file,
|
||||||
|
);
|
||||||
|
const new_attachment_data = {
|
||||||
|
...new_attachment,
|
||||||
|
noteId: new_attachment.noteId,
|
||||||
|
filePath: filePath,
|
||||||
|
};
|
||||||
const result = await db
|
const result = await db
|
||||||
.insert(attachment)
|
.insert(attachment)
|
||||||
.values(new_attachment_data)
|
.values(new_attachment_data)
|
||||||
|
|
@ -30,14 +49,14 @@ export class AttachmentController {
|
||||||
id: attachment.id,
|
id: attachment.id,
|
||||||
title: attachment.title,
|
title: attachment.title,
|
||||||
noteId: attachment.noteId,
|
noteId: attachment.noteId,
|
||||||
createdAt: attachment.createdAt
|
createdAt: attachment.createdAt,
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const resultWithAttachment = {
|
const resultWithAttachment = {
|
||||||
attachmentUrl:attachmentUrl,
|
attachmentUrl: attachmentUrl,
|
||||||
...result[0]
|
...result[0],
|
||||||
}
|
};
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: [resultWithAttachment],
|
data: [resultWithAttachment],
|
||||||
|
|
@ -46,7 +65,12 @@ export class AttachmentController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAttachmentsByNoteId(noteId: string, ownerId: string, limit: number = 10, offset: number = 0) {
|
async getAttachmentsByNoteId(
|
||||||
|
noteId: string,
|
||||||
|
ownerId: string,
|
||||||
|
limit: number = 10,
|
||||||
|
offset: number = 0,
|
||||||
|
) {
|
||||||
const result = await db
|
const result = await db
|
||||||
.select({
|
.select({
|
||||||
id: attachment.id,
|
id: attachment.id,
|
||||||
|
|
@ -57,22 +81,28 @@ export class AttachmentController {
|
||||||
})
|
})
|
||||||
.from(attachment)
|
.from(attachment)
|
||||||
.leftJoin(note, eq(note.id, attachment.noteId))
|
.leftJoin(note, eq(note.id, attachment.noteId))
|
||||||
.where(and(eq(attachment.noteId, noteId), isNull(attachment.deletedAt), eq(note.ownerId, ownerId)))
|
.where(
|
||||||
|
and(
|
||||||
|
eq(attachment.noteId, noteId),
|
||||||
|
isNull(attachment.deletedAt),
|
||||||
|
eq(note.ownerId, ownerId),
|
||||||
|
),
|
||||||
|
)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const allAttachments = [];
|
const allAttachments = [];
|
||||||
for (let i = 0; i<result.length; i++){
|
for (let i = 0; i < result.length; i++) {
|
||||||
if (!result[0].filePath){
|
if (!result[0].filePath) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const attachmentUrl = await getSignedUrl(result[i].filePath as string)
|
const attachmentUrl = await getSignedUrl(result[i].filePath as string);
|
||||||
const resultWithAttachment = {
|
const resultWithAttachment = {
|
||||||
attachmentUrl:attachmentUrl,
|
attachmentUrl: attachmentUrl,
|
||||||
...result[i]
|
...result[i],
|
||||||
}
|
};
|
||||||
allAttachments.push(resultWithAttachment)
|
allAttachments.push(resultWithAttachment);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -109,11 +139,11 @@ export class AttachmentController {
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const attachmentUrl = await getSignedUrl(result[0].filePath)
|
const attachmentUrl = await getSignedUrl(result[0].filePath);
|
||||||
const resultWithAttachment = {
|
const resultWithAttachment = {
|
||||||
attachmentUrl:attachmentUrl,
|
attachmentUrl: attachmentUrl,
|
||||||
...result[0]
|
...result[0],
|
||||||
}
|
};
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: [resultWithAttachment],
|
data: [resultWithAttachment],
|
||||||
|
|
@ -123,19 +153,17 @@ export class AttachmentController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAttachmentById(attachmentId: string, ownerId: string) {
|
async deleteAttachmentById(attachmentId: string, ownerId: string) {
|
||||||
const existingAttachment = await this.getAttachmentById(attachmentId, ownerId)
|
const existingAttachment = await this.getAttachmentById(
|
||||||
if (existingAttachment.data.length===0){
|
attachmentId,
|
||||||
|
ownerId,
|
||||||
|
);
|
||||||
|
if (existingAttachment.data.length === 0) {
|
||||||
throw error(403);
|
throw error(403);
|
||||||
}
|
}
|
||||||
await db
|
await db
|
||||||
.update(attachment)
|
.update(attachment)
|
||||||
.set({ deletedAt: new Date() })
|
.set({ deletedAt: new Date() })
|
||||||
.where(
|
.where(and(eq(attachment.id, attachmentId), isNull(attachment.deletedAt)))
|
||||||
and(
|
|
||||||
eq(attachment.id, attachmentId),
|
|
||||||
isNull(attachment.deletedAt),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.execute();
|
.execute();
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -146,11 +174,10 @@ export class AttachmentController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllAttachmentsByNoteId(noteId: string, ownerId: string) {
|
async deleteAllAttachmentsByNoteId(noteId: string, ownerId: string) {
|
||||||
|
const note = new NoteController();
|
||||||
const note = new NoteController()
|
|
||||||
const existingNote = await note.getNoteById(noteId, ownerId);
|
const existingNote = await note.getNoteById(noteId, ownerId);
|
||||||
|
|
||||||
if (existingNote.data.length===0){
|
if (existingNote.data.length === 0) {
|
||||||
throw error(403);
|
throw error(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,26 @@ import { commonResponses } from "../../../../lib/utils/common";
|
||||||
|
|
||||||
export const _AttachmentSchema = createSelectSchema(attachment);
|
export const _AttachmentSchema = createSelectSchema(attachment);
|
||||||
|
|
||||||
export const AttachmentSchema = t.Omit(_AttachmentSchema, ["deletedAt", "filePath"]);
|
export const AttachmentSchema = t.Omit(_AttachmentSchema, [
|
||||||
|
"deletedAt",
|
||||||
|
"filePath",
|
||||||
|
]);
|
||||||
|
|
||||||
export const AttachmentWithUrlSchema = t.Composite([ AttachmentSchema,t.Object({
|
export const AttachmentWithUrlSchema = t.Composite([
|
||||||
attachmentUrl: t.String({default:"http://example.com/attachment_abcd"}),
|
AttachmentSchema,
|
||||||
})])
|
t.Object({
|
||||||
|
attachmentUrl: t.String({ default: "http://example.com/attachment_abcd" }),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
export const createAttachmentSchema = t.Object({
|
export const createAttachmentSchema = t.Object({
|
||||||
title: t.Optional(t.String()),
|
title: t.Optional(t.String()),
|
||||||
noteId: t.String(),
|
noteId: t.String(),
|
||||||
file: t.File(),
|
file: t.File(),
|
||||||
})
|
});
|
||||||
|
|
||||||
export type CreateAttachmentType = typeof createAttachmentSchema.static;
|
export type CreateAttachmentType = typeof createAttachmentSchema.static;
|
||||||
|
|
||||||
|
|
||||||
export const getAttachmentResponses = {
|
export const getAttachmentResponses = {
|
||||||
200: t.Object(
|
200: t.Object(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const attachmentRouter = new Elysia({
|
||||||
.onError(({ path, error, code }) => {
|
.onError(({ path, error, code }) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return {
|
return {
|
||||||
message: path+" Error:"+code,
|
message: path + " Error:" + code,
|
||||||
success: false,
|
success: false,
|
||||||
data: null,
|
data: null,
|
||||||
error: code.toString(),
|
error: code.toString(),
|
||||||
|
|
@ -34,11 +34,16 @@ export const attachmentRouter = new Elysia({
|
||||||
.get(
|
.get(
|
||||||
"",
|
"",
|
||||||
async ({ attachment, user, query }) => {
|
async ({ attachment, user, query }) => {
|
||||||
return await attachment.getAttachmentsByNoteId(query.noteId, user.id, query.limit, query.offset);
|
return await attachment.getAttachmentsByNoteId(
|
||||||
|
query.noteId,
|
||||||
|
user.id,
|
||||||
|
query.limit,
|
||||||
|
query.offset,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
noteId:t.String(),
|
noteId: t.String(),
|
||||||
limit: t.Optional(t.Number()),
|
limit: t.Optional(t.Number()),
|
||||||
offset: t.Optional(t.Number()),
|
offset: t.Optional(t.Number()),
|
||||||
}),
|
}),
|
||||||
|
|
@ -98,11 +103,14 @@ export const attachmentRouter = new Elysia({
|
||||||
.delete(
|
.delete(
|
||||||
"",
|
"",
|
||||||
async ({ attachment, user, query }) => {
|
async ({ attachment, user, query }) => {
|
||||||
return await attachment.deleteAllAttachmentsByNoteId(query.noteId, user.id);
|
return await attachment.deleteAllAttachmentsByNoteId(
|
||||||
|
query.noteId,
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
noteId:t.String(),
|
noteId: t.String(),
|
||||||
}),
|
}),
|
||||||
response: deleteAttachmentResponses,
|
response: deleteAttachmentResponses,
|
||||||
detail: {
|
detail: {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ describe("Attachment", () => {
|
||||||
query: {
|
query: {
|
||||||
noteId: noteId,
|
noteId: noteId,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offset: 0
|
offset: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(data?.success).toBe(true);
|
expect(data?.success).toBe(true);
|
||||||
|
|
@ -57,7 +57,9 @@ describe("Attachment", () => {
|
||||||
|
|
||||||
// Get single attachment
|
// Get single attachment
|
||||||
it("Get Created Attachment", async () => {
|
it("Get Created Attachment", async () => {
|
||||||
const { data } = await testClientApp.api.attachment({ id: attachmentId }).get();
|
const { data } = await testClientApp.api
|
||||||
|
.attachment({ id: attachmentId })
|
||||||
|
.get();
|
||||||
expect(data?.success).toBe(true);
|
expect(data?.success).toBe(true);
|
||||||
expect(data?.data[0].id).toBe(attachmentId);
|
expect(data?.data[0].id).toBe(attachmentId);
|
||||||
expect(data?.data[0].noteId).toBe(noteId);
|
expect(data?.data[0].noteId).toBe(noteId);
|
||||||
|
|
@ -75,12 +77,14 @@ describe("Attachment", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createData?.data) {
|
if (!createData?.data) {
|
||||||
throw new Error("CreateData should not be null");
|
throw new Error("CreateData should not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAttachmentId = createData?.data[0].id;
|
const deleteAttachmentId = createData?.data[0].id;
|
||||||
if (!deleteAttachmentId) {
|
if (!deleteAttachmentId) {
|
||||||
throw new Error("Failed to receive attachmentId in delete attachment test");
|
throw new Error(
|
||||||
|
"Failed to receive attachmentId in delete attachment test",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the attachment
|
// Delete the attachment
|
||||||
|
|
@ -114,9 +118,12 @@ describe("Attachment", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete all attachments for the note
|
// Delete all attachments for the note
|
||||||
const { data: deleteData } = await testClientApp.api.attachment.delete({},{
|
const { data: deleteData } = await testClientApp.api.attachment.delete(
|
||||||
query: { noteId: noteId }
|
{},
|
||||||
});
|
{
|
||||||
|
query: { noteId: noteId },
|
||||||
|
},
|
||||||
|
);
|
||||||
expect(deleteData?.success).toBe(true);
|
expect(deleteData?.success).toBe(true);
|
||||||
|
|
||||||
// Verify all attachments are deleted
|
// Verify all attachments are deleted
|
||||||
|
|
@ -124,18 +131,18 @@ describe("Attachment", () => {
|
||||||
query: {
|
query: {
|
||||||
noteId: noteId,
|
noteId: noteId,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offset: 0
|
offset: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(verifyData?.data).toHaveLength(0);
|
expect(verifyData?.data).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Error cases
|
// Error cases
|
||||||
it("Should handle invalid attachment ID", async () => {
|
it("Should handle invalid attachment ID", async () => {
|
||||||
const invalidId = "invalid-id";
|
const invalidId = "invalid-id";
|
||||||
const { data, error } = await testClientApp.api.attachment({ id: invalidId }).get();
|
const { data, error } = await testClientApp.api
|
||||||
|
.attachment({ id: invalidId })
|
||||||
|
.get();
|
||||||
expect(data?.data?.length).toBe(0);
|
expect(data?.data?.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ export type CreateNoteType = Pick<
|
||||||
"title" | "content"
|
"title" | "content"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const createNoteSchema =t.Partial(t.Pick(NoteSchema, ["title", "content"]))
|
export const createNoteSchema = t.Partial(
|
||||||
|
t.Pick(NoteSchema, ["title", "content"]),
|
||||||
|
);
|
||||||
|
|
||||||
export const getNoteResponses = {
|
export const getNoteResponses = {
|
||||||
200: t.Object(
|
200: t.Object(
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ export const note = pgTable(
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export const attachment = pgTable(
|
export const attachment = pgTable(
|
||||||
"note_attachments",
|
"note_attachments",
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,76 @@ 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() {
|
const TEST_USER = {
|
||||||
|
email: "test@test.com",
|
||||||
|
password: "testpass123",
|
||||||
|
name: "Test User",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function createUserIfNotExists() {
|
||||||
const authUrl = getAuthConfig().BETTER_AUTH_URL;
|
const authUrl = getAuthConfig().BETTER_AUTH_URL;
|
||||||
const response = await fetch(`${authUrl}/api/auth/sign-in/email`, {
|
|
||||||
method: "POST",
|
// Try to sign in first
|
||||||
headers: {
|
try {
|
||||||
"Content-Type": "application/json",
|
const signInResponse = await fetch(`${authUrl}/api/auth/sign-in/email`, {
|
||||||
},
|
method: "POST",
|
||||||
body: JSON.stringify({
|
headers: {
|
||||||
email: "test@test.com",
|
"Content-Type": "application/json",
|
||||||
password: "testpass123",
|
},
|
||||||
}),
|
body: JSON.stringify({
|
||||||
});
|
email: TEST_USER.email,
|
||||||
const cookies = response.headers.getSetCookie()[0];
|
password: TEST_USER.password,
|
||||||
const sessionToken = cookies.split(";")[0].split("=")[1];
|
}),
|
||||||
return sessionToken;
|
});
|
||||||
|
|
||||||
|
// If sign in successful, return the session token
|
||||||
|
if (signInResponse.ok) {
|
||||||
|
const cookies = signInResponse.headers.getSetCookie()[0];
|
||||||
|
return cookies.split(";")[0].split("=")[1];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Sign in failed, attempting to create user...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If sign in fails, try to create the user
|
||||||
|
try {
|
||||||
|
const signUpResponse = await fetch(`${authUrl}/api/auth/sign-up/email`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(TEST_USER),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!signUpResponse.ok) {
|
||||||
|
throw new Error(`Failed to create user: ${signUpResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After creating user, sign in to get the token
|
||||||
|
const signInResponse = await fetch(`${authUrl}/api/auth/sign-in/email`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: TEST_USER.email,
|
||||||
|
password: TEST_USER.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!signInResponse.ok) {
|
||||||
|
throw new Error("Failed to sign in after creating user");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = signInResponse.headers.getSetCookie()[0];
|
||||||
|
return cookies.split(";")[0].split("=")[1];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in user creation/authentication:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await getAuthToken();
|
const token = await createUserIfNotExists();
|
||||||
export const testClientApp = treaty(app, {
|
export const testClientApp = treaty(app, {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `better-auth.session_token=${token}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue