Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- `apps/frontend`: Web interface (Not yet implemented)
- `packages/db`: Prisma schema and generated database client
- `packages/core`: Shared business logic, DTOs, and Zod schemas
- `packages/embedding`: A standalone embedding REST service
- `packages/logger`: The application logger setup
- `packages/observability`: Observability configurations
- `packages/env`: Package for managing environment variables

## 2. Core Mental Model & Architecture

Expand Down Expand Up @@ -68,17 +72,10 @@ These rules are absolute. Violation will cause pipeline or runtime failures.
- **camelCase**: Functions (`getUserById`), Variables, and standard file names (`authHandler.ts`).
- **UPPER_SNAKE_CASE**: Constants (`MAX_RATE_LIMIT`).

### Import Hierarchy
Order imports from farthest to closest. Prioritize path aliases
1. External: Built-in and external NPM packages.
2. Workspace: Internal packages (`@cvsa/core`, `@cvsa/db`).
3. Aliases: `@/*`, `@modules/*`, `@common/*` pointing to `./src/`.
4. **[PROHIBITED]**: relative paths (`../`, `./`, `../../../`, etc.)

### Module-Specific Information
- **Backend**:
1. Our endpoints starts with `/v2` and this prefix is already configured in root handler. In all of the rest handlers, always use the complete path **without** the version prefix (e.g. `/song/:id/details`)
2. **[PROHIBITED]**: DO NOT use verbs in paths. Endpoints should strictly follows the RESTful style.
2. **[PROHIBITED]**: DO NOT use verbs in paths. Endpoints should strictly follows the RESTful pattern.

## 5. Execution Commands & Testing

Expand Down
2 changes: 2 additions & 0 deletions apps/backend/bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ coveragePathIgnorePatterns = [
"src/utils/randomId.ts",
"src/types/**",
"**/observability/src/**",
"**/packages/core/**",
"src/*.ts"
]
4 changes: 2 additions & 2 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"lint": "bunx --bun biome lint",
"dev": "NODE_ENV=development bun --watch src/index.ts",
"start": "bun src/index.ts",
"test": "NODE_ENV=test bun test",
"test:coverage": "NODE_ENV=test bun test --coverage",
"test": "bun test --concurrent",
"test:coverage": "bun test --coverage --concurrent",
"typecheck": "bunx --bun tsc --noEmit"
},
"devDependencies": {
Expand Down
12 changes: 0 additions & 12 deletions apps/backend/src/common/error/ConflictError.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/backend/src/common/error/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from "./ConflictError";
export * from "./RateLimitError";
export * from "./getErrorResponse";
6 changes: 0 additions & 6 deletions apps/backend/src/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,13 @@ import { i18nMiddleware } from "@/middlewares";
import { ip } from "elysia-ip";
import { formatGinLog, getLogLevelForRequest } from "@common/log";

const AUTH_CONFLICT_CODES = [
"USERNAME_IS_ALREADY_TAKEN",
"USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
] as const;

const AUTH_INVALID_CODES = [
"INVALID_CREDENTIALS",
"INVALID_EMAIL_OR_PASSWORD",
"USER_NOT_FOUND",
"EMAIL_NOT_VERIFIED",
] as const;

type AuthConflictCode = (typeof AUTH_CONFLICT_CODES)[number];
type AuthInvalidCode = (typeof AUTH_INVALID_CODES)[number];

interface ErrorStoreData {
Expand Down
10 changes: 7 additions & 3 deletions apps/backend/src/handlers/catalog/song/create.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Elysia } from "elysia";
import { CreateSongRequestSchema, ErrorResponseSchema, songService } from "@cvsa/core";
import {
CreateSongRequestSchema,
ErrorResponseSchema,
SongResponseSchema,
songService,
} from "@cvsa/core";
import { authMiddleware } from "@/middlewares";
import { SongSchema } from "@cvsa/db";
import { traceTask } from "@/common/trace";

export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(authMiddleware).post(
Expand All @@ -19,7 +23,7 @@ export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(a
description: "Create a new song entry in the catalog. Requires authentication.",
},
response: {
201: SongSchema,
201: SongResponseSchema,
401: ErrorResponseSchema,
},
}
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/handlers/catalog/song/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { songUpdateHandler } from "./update";
import { songDeleteHandler } from "./delete";
import { songDetailsHandler } from "./details";
import { songSearchHandler } from "./search";
import { songLyricsHandler } from "./lyrics";

export const songHandler = new Elysia({ name: "songHandler" })
.use(songDetailsHandler)
.use(songCreateHandler)
.use(songUpdateHandler)
.use(songDeleteHandler)
.use(songSearchHandler);
.use(songSearchHandler)
.use(songLyricsHandler);
38 changes: 38 additions & 0 deletions apps/backend/src/handlers/catalog/song/lyrics/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Elysia } from "elysia";
import {
ErrorResponseSchema,
SongLyricsCreateRequestSchema,
SongLyricsResponseSchema,
songService,
} from "@cvsa/core";
import { authMiddleware } from "@/middlewares";
import { traceTask } from "@/common/trace";
import z from "zod";

export const songLyricsCreateHandler = new Elysia({ name: "songLyricsCreateHandler" })
.use(authMiddleware)
.post(
"/song/:id/lyric",
async ({ body, params, status }) => {
const lyric = await traceTask("songService.createLyric", async () => {
return await songService.createLyric(params.id, body);
});
return status(201, lyric);
},
{
body: SongLyricsCreateRequestSchema,
detail: {
summary: "Create Song Lyric",
description: "Add a new lyrics item for a song. Requires authentication.",
},
response: {
201: SongLyricsResponseSchema,
400: ErrorResponseSchema,
401: ErrorResponseSchema,
404: ErrorResponseSchema,
},
params: z.object({
id: z.coerce.number().int().positive(),
}),
Comment on lines +34 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use a shared params schema from core DTOs.

The inline params schema at Line 34-36 should be defined in packages/core/src/modules/catalog/song/dto.ts and imported here.

As per coding guidelines: apps/backend/src/handlers/**/*.ts: Define all Zod schemas in packages/core/modules/<module>/dto.ts first; do not define them inline inside handlers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/handlers/catalog/song/lyrics/create.ts` around lines 34 -
36, The inline params zod schema inside the create.ts handler (the params object
with id: z.coerce.number().int().positive()) must be moved into the core DTOs
and reused: add and export a named schema (e.g., SongParamsSchema) from the
module-level DTO file, import that schema into the create.ts handler and replace
the inline params with the imported SongParamsSchema, and update any associated
type uses (z.infer or typings) to reference the exported schema to keep schemas
centralized and consistent.

}
);
34 changes: 34 additions & 0 deletions apps/backend/src/handlers/catalog/song/lyrics/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Elysia } from "elysia";
import { ErrorResponseSchema, songService } from "@cvsa/core";
import { authMiddleware } from "@/middlewares";
import { traceTask } from "@/common/trace";
import z from "zod";

export const songLyricsDeleteHandler = new Elysia({ name: "songLyricsDeleteHandler" })
.use(authMiddleware)
.delete(
"/song/:id/lyric/:lyricId",
async ({ params, set }) => {
await traceTask("songService.deleteLyric", async () => {
return await songService.deleteLyric(params.id, params.lyricId);
});
set.status = 204;
return null;
},
{
detail: {
summary: "Delete Song Lyric",
description: "Delete a specific lyrics item for a song. Requires authentication.",
},
response: {
204: z.null(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Avoid inline Zod schemas in this handler config.

z.null() at Line 24 and the inline params object at Line 29-32 should be moved to shared schemas in packages/core/src/modules/catalog/song/dto.ts and imported here.

As per coding guidelines: apps/backend/src/handlers/**/*.ts: Define all Zod schemas in packages/core/modules/<module>/dto.ts first; do not define them inline inside handlers.

Also applies to: 29-32

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/handlers/catalog/song/lyrics/delete.ts` at line 24, The
handler currently uses inline Zod schemas (z.null() response and the inline
params object) — extract these into shared DTO exports in
packages/core/src/modules/catalog/song/dto.ts (e.g., export const
DeleteLyricsResponseSchema = z.null(); export const DeleteLyricsParamsSchema =
z.object({...}) with the same shape), import those schemas into
apps/backend/src/handlers/catalog/song/lyrics/delete.ts, and replace the inline
z.null() and inline params object with the imported DeleteLyricsResponseSchema
and DeleteLyricsParamsSchema so the handler references the centralized DTOs.

400: ErrorResponseSchema,
401: ErrorResponseSchema,
404: ErrorResponseSchema,
},
params: z.object({
id: z.coerce.number().int().positive(),
lyricId: z.coerce.number().int().positive(),
}),
}
);
29 changes: 29 additions & 0 deletions apps/backend/src/handlers/catalog/song/lyrics/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Elysia } from "elysia";
import { ErrorResponseSchema, SongLyricsResponseSchema, songService } from "@cvsa/core";
import z from "zod";
import { traceTask } from "@/common/trace";

export const songLyricsGetHandler = new Elysia().get(
"/song/:id/lyric/:lyricId",
async ({ params, status }) => {
const lyric = await traceTask("songService.getLyric", async () => {
return await songService.getLyric(params.id, params.lyricId);
});
return status(200, lyric);
},
{
detail: {
summary: "Get Song Lyric",
description: "Retrieve a specific lyrics item for a song by its ID.",
},
response: {
200: SongLyricsResponseSchema,
400: ErrorResponseSchema,
404: ErrorResponseSchema,
},
params: z.object({
id: z.coerce.number().int().positive(),
lyricId: z.coerce.number().int().positive(),
}),
Comment on lines +24 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Move route param schema to core DTO module.

Inline z.object(...) at Line 24-27 should be replaced by a shared schema exported from packages/core/src/modules/catalog/song/dto.ts.

♻️ Proposed refactor
-import z from "zod";
+import { SongLyricPathParamsSchema } from "@cvsa/core";
...
-		params: z.object({
-			id: z.coerce.number().int().positive(),
-			lyricId: z.coerce.number().int().positive(),
-		}),
+		params: SongLyricPathParamsSchema,
// packages/core/src/modules/catalog/song/dto.ts
export const SongLyricPathParamsSchema = z.object({
  id: z.coerce.number().int().positive(),
  lyricId: z.coerce.number().int().positive(),
});

As per coding guidelines: apps/backend/src/handlers/**/*.ts: Define all Zod schemas in packages/core/modules/<module>/dto.ts first; do not define them inline inside handlers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/handlers/catalog/song/lyrics/get.ts` around lines 24 - 27,
Replace the inline Zod schema in the handler's params with the shared schema
exported from the core DTO; specifically, in
apps/backend/src/handlers/catalog/song/lyrics/get.ts remove the inline
z.object(...) used in the params and import SongLyricPathParamsSchema from
packages/core/src/modules/catalog/song/dto.ts, then use
SongLyricPathParamsSchema in the params field (also add/adjust the import
statement). Ensure the symbol name SongLyricPathParamsSchema is used exactly to
match the exported DTO.

}
);
13 changes: 13 additions & 0 deletions apps/backend/src/handlers/catalog/song/lyrics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Elysia } from "elysia";
import { songLyricsListHandler } from "./list";
import { songLyricsGetHandler } from "./get";
import { songLyricsCreateHandler } from "./create";
import { songLyricsUpdateHandler } from "./update";
import { songLyricsDeleteHandler } from "./delete";

export const songLyricsHandler = new Elysia({ name: "songLyricsHandler" })
.use(songLyricsListHandler)
.use(songLyricsGetHandler)
.use(songLyricsCreateHandler)
.use(songLyricsUpdateHandler)
.use(songLyricsDeleteHandler);
28 changes: 28 additions & 0 deletions apps/backend/src/handlers/catalog/song/lyrics/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Elysia } from "elysia";
import { ErrorResponseSchema, SongLyricsListResponseSchema, songService } from "@cvsa/core";
import z from "zod";
import { traceTask } from "@/common/trace";

export const songLyricsListHandler = new Elysia().get(
"/song/:id/lyrics",
async ({ params, status }) => {
const lyrics = await traceTask("songService.listLyrics", async () => {
return await songService.listLyrics(params.id);
});
return status(200, lyrics);
},
{
detail: {
summary: "List Song Lyrics",
description: "Retrieve all available lyrics for a specific song by its ID.",
},
response: {
200: SongLyricsListResponseSchema,
400: ErrorResponseSchema,
404: ErrorResponseSchema,
},
params: z.object({
id: z.coerce.number().int().positive(),
}),
Comment on lines +24 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Extract params schema into shared song DTOs.

The inline params schema at Line 24-26 should be imported from @cvsa/core instead of being declared inside the handler.

As per coding guidelines: apps/backend/src/handlers/**/*.ts: Define all Zod schemas in packages/core/modules/<module>/dto.ts first; do not define them inline inside handlers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/handlers/catalog/song/lyrics/list.ts` around lines 24 - 26,
Replace the inline params zod schema (params: z.object({ id:
z.coerce.number().int().positive() })) with the shared schema exported from
`@cvsa/core`: create/export a SongParamsDto (or songParamsSchema) in the core song
DTO file (packages/core/modules/song/dto.ts) that defines the id
coercion/validation, then import that symbol from '@cvsa/core' into the handler
and use it in place of the inline params; ensure the handler's params reference
name matches the exported DTO name and remove the inline z.object block.

}
);
39 changes: 39 additions & 0 deletions apps/backend/src/handlers/catalog/song/lyrics/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Elysia } from "elysia";
import {
ErrorResponseSchema,
SongLyricsUpdateRequestSchema,
SongLyricsResponseSchema,
songService,
} from "@cvsa/core";
import { authMiddleware } from "@/middlewares";
import { traceTask } from "@/common/trace";
import z from "zod";

export const songLyricsUpdateHandler = new Elysia({ name: "songLyricsUpdateHandler" })
.use(authMiddleware)
.patch(
"/song/:id/lyric/:lyricId",
async ({ body, params, status }) => {
const lyric = await traceTask("songService.updateLyric", async () => {
return await songService.updateLyric(params.id, params.lyricId, body);
});
return status(200, lyric);
},
{
body: SongLyricsUpdateRequestSchema,
detail: {
summary: "Update Song Lyric",
description: "Update a specific lyrics item for a song. Requires authentication.",
},
response: {
200: SongLyricsResponseSchema,
400: ErrorResponseSchema,
401: ErrorResponseSchema,
404: ErrorResponseSchema,
},
params: z.object({
id: z.coerce.number().int().positive(),
lyricId: z.coerce.number().int().positive(),
}),
Comment on lines +34 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Extract id/lyricId params schema to core DTO module.

Please replace the inline params schema at Line 34-37 with an imported shared schema from @cvsa/core.

As per coding guidelines: apps/backend/src/handlers/**/*.ts: Define all Zod schemas in packages/core/modules/<module>/dto.ts first; do not define them inline inside handlers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/handlers/catalog/song/lyrics/update.ts` around lines 34 -
37, Replace the inline params z.object({ id: z.coerce.number().int().positive(),
lyricId: z.coerce.number().int().positive() }) in the update handler with an
imported shared schema from `@cvsa/core` (e.g., export a SongLyricParamsSchema or
songLyricParamsDto from the core module DTO and import it into this handler). If
the shared DTO does not exist yet, add it to
packages/core/modules/<module>/dto.ts as a zod schema that validates id and
lyricId, export it, then update the handler to use the imported schema
(referencing the existing params, id and lyricId symbols) instead of defining
the schema inline.

}
);
10 changes: 7 additions & 3 deletions apps/backend/src/handlers/catalog/song/update.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Elysia } from "elysia";
import { z } from "zod";
import { UpdateSongRequestSchema, ErrorResponseSchema, songService } from "@cvsa/core";
import {
UpdateSongRequestSchema,
ErrorResponseSchema,
songService,
SongResponseSchema,
} from "@cvsa/core";
import { authMiddleware } from "@/middlewares";
import { SongSchema } from "@cvsa/db";
import { traceTask } from "@/common/trace";

export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" })
Expand All @@ -26,7 +30,7 @@ export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" })
"Update metadata of an existing song identified by its ID. Requires authentication.",
},
response: {
200: SongSchema,
200: SongResponseSchema,
400: ErrorResponseSchema,
401: ErrorResponseSchema,
404: ErrorResponseSchema,
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/tests/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { treaty } from "@elysiajs/eden";
import { app } from "@/index";
import { prisma } from "@cvsa/db";
Expand Down
Loading