Skip to content

feat(artistRole): CRUD API for artistRole#60

Merged
alikia2x merged 6 commits into
developfrom
feat/artist-role
May 22, 2026
Merged

feat(artistRole): CRUD API for artistRole#60
alikia2x merged 6 commits into
developfrom
feat/artist-role

Conversation

@alikia2x

@alikia2x alikia2x commented May 22, 2026

Copy link
Copy Markdown
Member

Changes Made in This PR

Core Feature Implementation

  • Added artistRole CRUD API handlers: Implemented five new Elysia route handlers for artist roles:

    • POST /artist-role - Create new artist role with authentication
    • GET /artist-role/:id - Retrieve artist role details
    • PATCH /artist-role/:id - Update artist role with authentication
    • DELETE /artist-role/:id - Soft delete artist role with authentication
    • GET /artist-roles - Search artist roles with optional query parameter
  • Implemented artistRole service layer:

    • ArtistRoleService class with getDetails(), create(), update(), and delete() methods
    • ArtistRoleRepository class handling Prisma queries with transaction support
    • ArtistRoleSearchService for full-text search with embedding support
  • Added database schema updates:

    • Extended ArtistRole Prisma model with language field (default: "zh")
    • Added three new SongType enum values: RETUNE, CONTRAFACTUM, TRANSLYRICS

Search & Indexing Enhancements

  • Refactored search service architecture:

    • Updated ISearchService base class to support localized entities with per-language indexing
    • Implemented concrete sync() and search() methods in base class instead of abstract methods
    • Added helper methods for name/description extraction and vector building
  • Added search service implementations:

    • ArtistRoleSearchService with embedding support using "potion-multilingual-128M"
    • Updated SongSearchService, ArtistSearchService, and SingerSearchService to use new architecture
  • Added search configuration:

    • Configured artistRole index with searchable attributes, ranking rules, and embedder settings

Event & Outbox Integration

  • Integrated outbox event processing:
    • Wired artistRoleSearchService into outbox processor for event handling
    • Service methods emit artistRole.created, artistRole.updated, and artistRole.deleted events

Authentication & API Changes

  • Removed rate limiting middleware:

    • Updated loginHandler to remove Elysia rate limiting (keep only ip() middleware)
    • Updated signupHandler to remove rate limiting middleware
  • Added auth logger integration: Wired appLogger into betterAuth configuration for logging

Testing & Data Import

  • Added comprehensive test coverage:

    • E2E test suite for artistRole CRUD operations (artistRole.test.ts)
    • Integration tests for ArtistRoleRepository with full CRUD verification
    • Unit tests for ArtistRoleService with mocked dependencies
    • Updated existing search service tests for new architecture
  • Implemented data import script (scripts/import.ts):

    • JSON file importer supporting singers, artists, and songs
    • Automatic entity creation/reuse with localization support
    • Song creation with performances, creations (artist-role pairs), and lyrics
    • Support for external IDs (BiliBili, YouTube, Niconico)

DTOs & Type Definitions

  • Defined artistRole request/response schemas:
    • CreateArtistRoleRequestSchema and UpdateArtistRoleRequestSchema with localization support
    • ArtistRoleResponseSchema and ArtistRoleDetailsResponseSchema for API responses
    • TypeScript DTO types for type safety

Module & Export Organization

  • Created artistRole module structure:

    • New barrel export at packages/core/src/modules/catalog/artistRole/index.ts
    • Container for dependency injection with repository, service, and search instances
    • Added to handlers and catalog module exports
  • Updated handler registration: Registered artistRoleHandler in backend app before singerHandler

Supporting Changes

  • Updated embedding manager: Fixed tokenizer invocation in EmbeddingManager.getEmbedding
  • Added TypeScript configuration: Created scripts/tsconfig.json with proper path aliases and compilation settings
  • Updated middleware: Added appLogger import to auth middleware

Review Change Stack

- Add language field to ArtistRole Prisma model
- Implement core module: DTOs, repository (with soft-delete), service (with outbox), DI container
- Add REST handlers: GET /artist-role/:id, POST /artist-role, PATCH /artist-role/:id, DELETE /artist-role/:id
- Add search service extending ISearchService with single-language indexing
- Add search handler: GET /artist-roles?q=
- Write unit tests (7), integration tests (9), E2E tests (11)
- Remove rate limiting from login handler
- Refactor search interface and update search services for artist/singer/song
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@alikia2x has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 42 minutes and 49 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: c9b36129-0ce7-4ef0-bd1e-d8dc8feaad0b

📥 Commits

Reviewing files that changed from the base of the PR and between f744c09 and a96af0d.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • package.json
  • packages/core/src/search/catalog/artistRole.ts
  • packages/core/tests/unit/SongSearchService.test.ts
  • packages/embedding/package.json
  • scripts/import.ts
📝 Walkthrough

Walkthrough

This PR introduces a complete artist-role CRUD feature with REST endpoints and search integration, refactors search services to support per-language localized indexing, removes rate-limiting from auth handlers, and adds data import tooling for bulk loading catalog entities.

Changes

Auth Middleware Cleanup

Layer / File(s) Summary
Rate limit removal from auth handlers
apps/backend/src/handlers/auth/login.ts, apps/backend/src/handlers/auth/signup.ts
Login and signup handlers no longer apply rate-limiting middleware; core authentication flow and schema validation are preserved with metadata moved to route configuration.
Auth logging infrastructure
packages/core/src/modules/auth/betterAuth.ts
BetterAuth configuration now wires application logger for authentication events.

Artist Role CRUD Feature

Layer / File(s) Summary
REST API handlers for artist role operations
apps/backend/src/handlers/catalog/artistRole/get.ts, create.ts, update.ts, delete.ts, search.ts, index.ts
Five Elysia route handlers expose GET/:id, POST (create), PATCH/:id (update), DELETE/:id (soft delete), and GET /search endpoints for artist roles; all handlers validate input via Zod and delegate to the service layer.
Backend handler registration and exports
apps/backend/src/handlers/index.ts, apps/backend/src/index.ts, apps/backend/src/middlewares/auth.ts
Main backend index wires the artist-role handler into the request pipeline; handlers barrel export adds artist role re-export; middleware imports logging support.
Data transfer objects and types for artist roles
packages/core/src/modules/catalog/artistRole/dto.ts
Zod schemas for create/update requests and response DTOs; ArtistRoleId type alias; response types derived with deletedAt omitted.
Service and repository layer for artist roles
packages/core/src/modules/catalog/artistRole/repository.interface.ts, repository.ts, service.ts, container.ts, packages/core/src/modules/catalog/index.ts
Repository interface and implementation handle soft-delete via Prisma; service coordinates transactional writes with outbox event creation for audit/search updates; container wires all dependencies.
Outbox wiring for artist role events
packages/core/src/outbox/processor.ts, packages/core/src/modules/catalog/artistRole/container.ts
Outbox processor routes artist role events to search service sync; artist role container wires service and search service together.
E2E and unit tests for artist role operations
apps/backend/tests/artistRole.test.ts, packages/core/tests/unit/ArtistRoleService.test.ts, packages/core/tests/integration/ArtistRoleRepository.test.ts
E2E test suite validates all CRUD endpoints with auth requirements, soft-delete behavior, and error handling; unit tests verify service logic, outbox event creation, and error contracts; integration tests verify repository operations.

Search Service Architecture Refactoring

Layer / File(s) Summary
Search service base interface with localization support
packages/core/src/search/interface.ts
ISearchService refactored to support localized entities and per-language indexing; concrete sync/search implementations replace abstract methods; new helper methods for names, descriptions, language collection, and vector building.
New artist role search service and Meilisearch configuration
packages/core/src/search/catalog/artistRole.ts, packages/core/src/search/config.ts
Implements ArtistRoleSearchService for embedding and indexing artist role names per language; adds artistRole index configuration with searchable name attribute and potion-multilingual embedder.
Refactor existing search services to use new base class
packages/core/src/search/catalog/artist.ts, singer.ts, song.ts
Artist, Singer, and Song search services updated to parameterize ISearchService with index type and expose entityType/getDocument as protected; public sync/search methods removed as they are now implemented in base class.
Search module barrel exports
packages/core/src/search/catalog/index.ts, packages/core/src/modules/catalog/index.ts
Catalog search index and core modules catalog index re-export artistRole search service and module exports.

Supporting Infrastructure Changes

Layer / File(s) Summary
Prisma model updates and database schema
packages/db/prisma/models/core/artistRole.prisma, song.prisma
ArtistRole model adds default language field; SongType enum extends with RETUNE, CONTRAFACTUM, and TRANSLYRICS variants.
Embedding optimization and data import script
packages/embedding/src/embedding.ts, scripts/import.ts
EmbeddingManager optimizes tokenizer call to synchronous invocation; new data import script bulk-loads singers, artists, and songs with full localization, performances, creations, and external links from JSON.
Infrastructure logging and TypeScript configuration
packages/core/src/outbox/queue.ts, scripts/tsconfig.json
Outbox queue connection logs Redis URL for debugging; new scripts/tsconfig.json enables path mappings and TypeScript compilation for utility scripts.
Search service unit test updates
packages/core/tests/unit/ArtistSearchService.test.ts, SingerSearchService.test.ts
ArtistSearchService and SingerSearchService tests updated to expect undefined hybrid option when embedding response is missing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • project-cvsa/cvsa#20: Removes elysia-rate-limit from login handler, overlapping with auth middleware cleanup in this PR.
  • project-cvsa/cvsa#32: MeiliSearch/search infrastructure that enables the localization-aware search service refactoring used by artist role feature.
  • project-cvsa/cvsa#39: Modifies auth handlers with traceTask and error handling patterns also used in artist role handlers.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/artist-role

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Nitpick comments (3)
apps/backend/src/handlers/catalog/artistRole/search.ts (2)

21-23: ⚡ Quick win

Bound and normalize q before hitting search.

Adding .trim().max(...) reduces avoidable expensive queries and keeps request validation defensive.

Suggested patch
 		query: z.object({
-			q: z.string().optional(),
+			q: z.string().trim().max(256).optional(),
 		}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/catalog/artistRole/search.ts` around lines 21 - 23,
The request schema for the search handler accepts a raw q string without bounds
or normalization; update the z.object query schema in search.ts (the query
definition that contains q) to trim and enforce a max length (e.g.
z.string().trim().max(...).optional()) so incoming q is normalized and bounded,
and in the search handler use the normalized value (the trimmed/validated q)
before calling the search routine to avoid expensive unbounded queries.

6-24: ⚡ Quick win

Replace the TODO with an explicit response schema for this endpoint.

Right now this route is undocumented on the response side, which makes client generation and contract checks weaker than the other artist-role endpoints.

If you want, I can draft a concrete ArtistRoleSearchResponseSchema and wire it into this handler.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/catalog/artistRole/search.ts` around lines 6 - 24,
Add an explicit response schema for artistRoleSearchHandler by defining and
using a DTO/validation schema (e.g., ArtistRoleSearchResponseSchema) that
describes the 200 response shape (likely an array of artist-role objects or a
wrapper with items and metadata) and attach it to the handler options under a
response key (for example response: { 200: ArtistRoleSearchResponseSchema }).
Ensure the schema matches the objects returned by artistRoleSearchService.search
and update any import/exports to wire ArtistRoleSearchResponseSchema into the
handler options so client generation and contract checks can validate the
endpoint.
packages/core/tests/unit/ArtistRoleService.test.ts (1)

81-86: ⚡ Quick win

Consolidate the NOT_FOUND assertions to avoid calling the service 3 times

Bun’s expect(promise).rejects... usage in this repo is not awaited (e.g., OutboxProcessor.test.ts / OutboxService.test.ts), so the missing await here isn’t the real problem. The tests are still invoking getDetails/update/delete multiple times; capture the promise once per test and run the three rejects matchers against it.

Suggested fix
 		test("throws NOT_FOUND error when role does not exist", async () => {
 			mockRepository.getDetailsById.mockResolvedValueOnce(null);
 
-			expect(artistRoleService.getDetails(999)).rejects.toThrow(AppError);
-			expect(artistRoleService.getDetails(999)).rejects.toThrow("error.artistRole.notfound");
-			expect(artistRoleService.getDetails(999)).rejects.toMatchObject({
+			const promise = artistRoleService.getDetails(999);
+			expect(promise).rejects.toThrow(AppError);
+			expect(promise).rejects.toThrow("error.artistRole.notfound");
+			expect(promise).rejects.toMatchObject({
 				code: "NOT_FOUND",
 				statusCode: 404,
 			});
 		});
@@
 		test("throws NOT_FOUND error when role does not exist", async () => {
 			mockRepository.getById.mockResolvedValueOnce(null);
 
-			expect(artistRoleService.update(999, updateInput)).rejects.toThrow(AppError);
-			expect(artistRoleService.update(999, updateInput)).rejects.toThrow(
+			const promise = artistRoleService.update(999, updateInput);
+			expect(promise).rejects.toThrow(AppError);
+			expect(promise).rejects.toThrow(
 				"error.artistRole.notfound"
 			);
-			expect(artistRoleService.update(999, updateInput)).rejects.toMatchObject({
+			expect(promise).rejects.toMatchObject({
 				code: "NOT_FOUND",
 				statusCode: 404,
 			});
 		});
@@
 		test("throws NOT_FOUND error when role does not exist", async () => {
 			mockRepository.getById.mockResolvedValueOnce(null);
 
-			expect(artistRoleService.delete(999)).rejects.toThrow(AppError);
-			expect(artistRoleService.delete(999)).rejects.toThrow("error.artistRole.notfound");
-			expect(artistRoleService.delete(999)).rejects.toMatchObject({
+			const promise = artistRoleService.delete(999);
+			expect(promise).rejects.toThrow(AppError);
+			expect(promise).rejects.toThrow("error.artistRole.notfound");
+			expect(promise).rejects.toMatchObject({
 				code: "NOT_FOUND",
 				statusCode: 404,
 			});
 		});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/tests/unit/ArtistRoleService.test.ts` around lines 81 - 86, The
test currently calls artistRoleService.getDetails(999) three times; capture the
single returned promise (e.g., const promise =
artistRoleService.getDetails(999)) and reuse that promise for all three rejects
assertions (expect(promise).rejects.toThrow(AppError);
expect(promise).rejects.toThrow("error.artistRole.notfound");
expect(promise).rejects.toMatchObject({ code: "NOT_FOUND", statusCode: 404 });)
so the service is invoked only once and all matchers run against the same
rejected promise.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/handlers/auth/login.ts`:
- Around line 17-62: The login endpoint (loginHandler for POST "/session")
currently lacks throttling and needs per-IP and per-identifier rate limiting
reinstated: add a rate-limit check/middleware into the request pipeline for
loginHandler (e.g., immediately before the handler where ip() is used) that
enforces a strict per-IP limit and a per-identifier (email/username) limit,
return appropriate 429 responses on exceed, and document the control in the
route detail; ensure the check uses the incoming headers/body to key
identifier-based limits and integrates with your existing rate-limit provider or
an in-memory/fallback store so the protection is enforced at the application
level if not guaranteed at the edge.

In `@apps/backend/src/handlers/auth/signup.ts`:
- Around line 18-60: The signup route lost rate-limiting—reintroduce route-level
throttling on the POST "/user" handler (signupHandler) by applying a
rate-limit/throttle middleware (or middleware factory) to the Elysia instance or
directly to the post("/user") route; configure it to key by IP (use the existing
ip() middleware or headers), set a conservative limit (e.g., small requests per
minute and a short ban/penalty window), and ensure the middleware returns
appropriate 429 responses and is included in the route options so automated
signups are rate-limited while legitimate users are unaffected.

In `@apps/backend/src/handlers/catalog/artistRole/create.ts`:
- Around line 28-31: The response map for the create route in
apps/backend/src/handlers/catalog/artistRole/create.ts is missing a 400 entry
for validation errors; update the response object (the response map used by the
create handler) to include 400: ErrorResponseSchema alongside 201:
ArtistRoleResponseSchema and 401: ErrorResponseSchema so that body validation
failures are documented in the OpenAPI contract.

In `@packages/core/src/modules/auth/betterAuth.ts`:
- Around line 56-60: The better-auth logger's log implementation forwards raw
variadic args into appLogger[level] as metadata (logger.log -> appLogger[level]
with { metadata: args }), which can leak sensitive auth fields to Pino/OTEL;
sanitize or allowlist the metadata before passing it on: inspect the incoming
...args in logger.log, strip or redact sensitive keys (authorization, cookie,
password, token, etc.) or build an explicit allowlist of safe fields, then call
appLogger[level] with { metadata: sanitizedMetadata } (and ensure any OTEL emit
path receives only the sanitized/allowlisted attributes rather than the raw
args).

In `@packages/core/src/modules/catalog/artistRole/container.ts`:
- Line 9: The embedding service address is hardcoded in the creation of
embeddingManager via treaty<EmbeddingApp>("localhost:14900"); change this to
read the endpoint from configuration/environment (e.g.,
process.env.EMBEDDING_SERVICE_URL or your app config accessor) and validate it
before use; if the value is missing or invalid, throw an error or exit early so
the process fails fast. Update the code that constructs embeddingManager to use
the resolved variable and include a clear error message referencing
embeddingManager/treaty<EmbeddingApp> when aborting.

In `@packages/core/src/modules/catalog/artistRole/dto.ts`:
- Around line 6-16: CreateArtistRoleRequestSchema and
UpdateArtistRoleRequestSchema currently allow empty/whitespace names and Update
accepts an empty object; fix by validating name to be non-empty after trimming
and preventing empty update payloads: change name in
CreateArtistRoleRequestSchema to require a non-blank string (e.g., use
z.string().refine(s => s.trim().length > 0, "name cannot be empty") or
z.string().min(1).transform(s => s.trim()).refine(s => s.length>0,...)), and in
UpdateArtistRoleRequestSchema ensure any provided name uses the same non-blank
validation and add a top-level refinement on UpdateArtistRoleRequestSchema
(e.g., .refine(obj => Object.keys(obj).length > 0, "update payload must contain
at least one field") ) so totally-empty update objects are rejected.

In `@packages/core/src/modules/catalog/artistRole/repository.ts`:
- Around line 50-67: The update and softDelete methods currently mutate rows by
id only, allowing modifications to soft-deleted records; update the where clause
in both ArtistRoleRepository.update and ArtistRoleRepository.softDelete (the
calls inside query("db.artistRole.update", ...) and
query("db.artistRole.softDelete", ...)) to include deletedAt: null so the Prisma
client calls client.artistRole.update({ where: { id, deletedAt: null }, ... })
only affect non-deleted rows (keep the existing data/payload and tx/client
usage).

In `@packages/core/src/modules/catalog/artistRole/service.ts`:
- Around line 50-88: The pre-check using repository.getById(id) should be moved
inside the same prisma.$transaction as the mutation to avoid a race; in both
update and delete wrap a transaction that first calls repository.getById(id, tx)
and throws the same AppError if null, then performs repository.update(id, input,
tx) (for update) or repository.softDelete(id, tx) (for delete), then calls
outbox.createEntry(..., tx) and returns the entry (and role for update); keep
outbox.enqueue(entry) outside the transaction as before. Ensure you call the
repository.getById method that accepts the transaction/tx parameter and preserve
the existing AppError message and types.

In `@packages/core/src/outbox/queue.ts`:
- Line 13: The debug log call using appLogger.debug(`Connecting to redis at
${env.REDIS_URL}`) can leak credentials; update the code that logs the Redis
connection to redact or avoid printing env.REDIS_URL directly — for example,
parse env.REDIS_URL and log only safe parts (hostname and port) or replace
credentials with a mask (e.g., show "redis://****`@host`:port" or use URL.hostname
and URL.port). Locate the appLogger.debug usage and change it to log the
sanitized URL or explicit host/port instead of the raw env.REDIS_URL.

In `@packages/core/src/search/catalog/artistRole.ts`:
- Around line 23-30: The embedding call in artist-role indexing
(embeddingManager.embeddings.post inside the code that builds the returned
object with _vectors via this.buildVectors) can throw and currently aborts
indexing; wrap the embedding call in a try/catch so that if the embeddings
request fails you log/debug the error and set _vectors: null (instead of calling
this.buildVectors with undefined), then continue returning the same object (id,
name, localizedNames) so the document is indexed without vectors; reference
embeddingManager.embeddings.post, this.buildVectors, and _vectors in your
change.

In `@packages/core/src/search/catalog/singer.ts`:
- Around line 33-35: The document payload for Singer is storing the source
locale via singer.language instead of the indexed locale parameter, causing
mismatch with the target index; update the object construction in the singer
document creation to use the function parameter language (not singer.language)
for the language field so the written document in the singer_${language} index
reflects the indexed locale—locate the code that sets name: this.getName(singer,
language), description: this.getDescription(singer, language) and replace the
language: singer.language entry to language: language.

In `@packages/core/src/search/interface.ts`:
- Around line 71-80: The loop only upserts current locales and leaves stale
documents in old locale indexes; update the code in the block using
collectLanguages, this.entityType, this.searchManager.getAdminIndex,
getDocument, index.addDocuments and this.searchManager.waitForTask to also
remove any documents for this entity.id that exist in locale-specific indexes no
longer present in collectLanguages(entity): enumerate relevant indexes for
this.entityType (or a predefined set of locale suffixes), for each index whose
language is not in the current languages call index.deleteDocument(entity.id)
(or the admin delete API) and await this.searchManager.waitForTask on the delete
task before/after the upserts so removed locales are cleaned up.
- Around line 89-102: Wrap the call to this.embeddingManager.embeddings.post
inside a try/catch so failures don't bubble up; on error (or if
embeddingResponse is missing) set embeddingAvailable = false and call
index.search without the vector and hybrid options (i.e., pass undefined for
vector and hybrid) to fall back to lexical search. Update the logic around
embeddingResponse/embeddingAvailable used when invoking index.search
(referencing embeddingManager.embeddings.post, embeddingResponse,
embeddingAvailable, and index.search) so a caught error results in a plain-text
search rather than throwing.

In `@scripts/import.ts`:
- Around line 166-178: The switch on data.type currently falls back to creating
a singer for unknown types; update the default branch in the switch (the block
around switch (data.type)) to NOT call importSinger for unsupported types like
"album", "series", etc.; instead log a clear warning including data.type and
file and skip processing (return/continue) so no bogus singer records are
created—use the existing importSinger/importArtist/importSong symbols to locate
the switch and ensure unknown types are safely ignored.
- Around line 181-194: The script currently logs per-file errors inside the
catch in main but always calls process.exit(0) in the finally, hiding failures;
update main and the file-level error handling so failures propagate: add a
boolean flag (e.g., importFailed) in the module scope or inside main, set
importFailed = true inside the per-file catch block where e is logged, remove
the unconditional process.exit(0) from the finally, and change the top-level
promise handlers so that main().catch(...) calls process.exit(1) and the finally
only logs completion (or calls process.exit(importFailed ? 1 : 0)); reference
the main() invocation, the per-file catch block, and the finally handler when
making these changes.
- Around line 103-108: The publishedAt value is being converted with new
Date(data.publishedAt ?? "").toISOString() inside the song creation payload,
which throws for empty/invalid dates; update the code around the
songService.create call to safely parse and validate data.publishedAt (e.g.,
create a Date object or use Date.parse and check isNaN/getTime) and only set the
publishedAt field when the parsed date is valid, otherwise leave publishedAt
undefined so invalid inputs don’t cause an exception during import.

---

Nitpick comments:
In `@apps/backend/src/handlers/catalog/artistRole/search.ts`:
- Around line 21-23: The request schema for the search handler accepts a raw q
string without bounds or normalization; update the z.object query schema in
search.ts (the query definition that contains q) to trim and enforce a max
length (e.g. z.string().trim().max(...).optional()) so incoming q is normalized
and bounded, and in the search handler use the normalized value (the
trimmed/validated q) before calling the search routine to avoid expensive
unbounded queries.
- Around line 6-24: Add an explicit response schema for artistRoleSearchHandler
by defining and using a DTO/validation schema (e.g.,
ArtistRoleSearchResponseSchema) that describes the 200 response shape (likely an
array of artist-role objects or a wrapper with items and metadata) and attach it
to the handler options under a response key (for example response: { 200:
ArtistRoleSearchResponseSchema }). Ensure the schema matches the objects
returned by artistRoleSearchService.search and update any import/exports to wire
ArtistRoleSearchResponseSchema into the handler options so client generation and
contract checks can validate the endpoint.

In `@packages/core/tests/unit/ArtistRoleService.test.ts`:
- Around line 81-86: The test currently calls artistRoleService.getDetails(999)
three times; capture the single returned promise (e.g., const promise =
artistRoleService.getDetails(999)) and reuse that promise for all three rejects
assertions (expect(promise).rejects.toThrow(AppError);
expect(promise).rejects.toThrow("error.artistRole.notfound");
expect(promise).rejects.toMatchObject({ code: "NOT_FOUND", statusCode: 404 });)
so the service is invoked only once and all matchers run against the same
rejected promise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: d8cfef22-4166-4209-ac58-aec1c6ec4504

📥 Commits

Reviewing files that changed from the base of the PR and between f7a6df8 and f744c09.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (38)
  • apps/backend/src/handlers/auth/login.ts
  • apps/backend/src/handlers/auth/signup.ts
  • apps/backend/src/handlers/catalog/artistRole/create.ts
  • apps/backend/src/handlers/catalog/artistRole/delete.ts
  • apps/backend/src/handlers/catalog/artistRole/get.ts
  • apps/backend/src/handlers/catalog/artistRole/index.ts
  • apps/backend/src/handlers/catalog/artistRole/search.ts
  • apps/backend/src/handlers/catalog/artistRole/update.ts
  • apps/backend/src/handlers/index.ts
  • apps/backend/src/index.ts
  • apps/backend/src/middlewares/auth.ts
  • apps/backend/tests/artistRole.test.ts
  • packages/core/src/modules/auth/betterAuth.ts
  • packages/core/src/modules/catalog/artistRole/container.ts
  • packages/core/src/modules/catalog/artistRole/dto.ts
  • packages/core/src/modules/catalog/artistRole/index.ts
  • packages/core/src/modules/catalog/artistRole/repository.interface.ts
  • packages/core/src/modules/catalog/artistRole/repository.ts
  • packages/core/src/modules/catalog/artistRole/service.ts
  • packages/core/src/modules/catalog/index.ts
  • packages/core/src/outbox/processor.ts
  • packages/core/src/outbox/queue.ts
  • packages/core/src/search/catalog/artist.ts
  • packages/core/src/search/catalog/artistRole.ts
  • packages/core/src/search/catalog/index.ts
  • packages/core/src/search/catalog/singer.ts
  • packages/core/src/search/catalog/song.ts
  • packages/core/src/search/config.ts
  • packages/core/src/search/interface.ts
  • packages/core/tests/integration/ArtistRoleRepository.test.ts
  • packages/core/tests/unit/ArtistRoleService.test.ts
  • packages/core/tests/unit/ArtistSearchService.test.ts
  • packages/core/tests/unit/SingerSearchService.test.ts
  • packages/db/prisma/models/core/artistRole.prisma
  • packages/db/prisma/models/core/song.prisma
  • packages/embedding/src/embedding.ts
  • scripts/import.ts
  • scripts/tsconfig.json

Comment on lines +17 to +62
export const loginHandler = new Elysia().use(ip()).post(
"/session",
async ({ body, status, headers, cookie: { token: tokenCookie } }) => {
const { user, token } = await traceTask("auth.signInEmail", async () => {
return await auth.api.signInEmail({
body: {
email: body.email,
password: body.password,
},
headers: toBetterAuthHeaders(headers),
});
});

if (!token) {
throw new AppError("error.login.failed", "INTERNAL_SERVER_ERROR", 500, {
cause: "Better Auth responded with no token",
});
}
if (!token) {
throw new AppError("error.login.failed", "INTERNAL_SERVER_ERROR", 500, {
cause: "Better Auth responded with no token",
});
}

tokenCookie.value = token;
tokenCookie.httpOnly = true;
tokenCookie.maxAge = 90 * DAY;
tokenCookie.secure = true;
tokenCookie.sameSite = "lax";
tokenCookie.value = token;
tokenCookie.httpOnly = true;
tokenCookie.maxAge = 90 * DAY;
tokenCookie.secure = true;
tokenCookie.sameSite = "lax";

const userInfo = betterAuthToLoginUserInfoDto(user, token);
const response = toLoginResponse(userInfo);
const userInfo = betterAuthToLoginUserInfoDto(user, token);
const response = toLoginResponse(userInfo);

return status(200, response);
return status(200, response);
},
{
body: LoginRequestSchema,
detail: {
summary: "User Login",
description:
"Authenticate an existing user with email and password credentials. Returns user info and sets an httpOnly authentication cookie.",
},
{
body: LoginRequestSchema,
detail: {
summary: "User Login",
description:
"Authenticate an existing user with email and password credentials. Returns user info and sets an httpOnly authentication cookie.",
},
response: {
200: LoginResponseSchema,
401: ErrorResponseSchema,
422: ErrorResponseSchema,
429: ErrorResponseSchema,
500: ErrorResponseSchema,
},
}
);
response: {
200: LoginResponseSchema,
401: ErrorResponseSchema,
422: ErrorResponseSchema,
429: ErrorResponseSchema,
500: ErrorResponseSchema,
},
}
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reintroduce throttling on the login endpoint.

POST /session is now unthrottled, which materially increases brute-force and credential-stuffing risk on a critical auth surface. Please restore per-IP/per-identifier rate limiting here (or wire an equivalent guaranteed control at the edge and document it in code).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/auth/login.ts` around lines 17 - 62, The login
endpoint (loginHandler for POST "/session") currently lacks throttling and needs
per-IP and per-identifier rate limiting reinstated: add a rate-limit
check/middleware into the request pipeline for loginHandler (e.g., immediately
before the handler where ip() is used) that enforces a strict per-IP limit and a
per-identifier (email/username) limit, return appropriate 429 responses on
exceed, and document the control in the route detail; ensure the check uses the
incoming headers/body to key identifier-based limits and integrates with your
existing rate-limit provider or an in-memory/fallback store so the protection is
enforced at the application level if not guaranteed at the edge.

Comment on lines +18 to +60
export const signupHandler = new Elysia().use(ip()).post(
"/user",
async ({ body, status, headers, cookie: { token: tokenCookie } }) => {
const { user, token } = await traceTask("auth.signUpEmail", async () => {
return await auth.api.signUpEmail({
body: signupRequestToBetterAuth(body),
headers: toBetterAuthHeaders(headers),
});
});

if (!token) {
throw new AppError("error.signup.failed", "INTERNAL_SERVER_ERROR", 500, {
cause: "Better Auth responded with no token",
});
}
if (!token) {
throw new AppError("error.signup.failed", "INTERNAL_SERVER_ERROR", 500, {
cause: "Better Auth responded with no token",
});
}

tokenCookie.value = token;
tokenCookie.httpOnly = true;
tokenCookie.maxAge = 90 * DAY;
tokenCookie.secure = true;
tokenCookie.sameSite = "lax";
tokenCookie.value = token;
tokenCookie.httpOnly = true;
tokenCookie.maxAge = 90 * DAY;
tokenCookie.secure = true;
tokenCookie.sameSite = "lax";

const userInfo = betterAuthToSignupUserInfoDto(user, token);
const response = toSignUpResponse(userInfo);
const userInfo = betterAuthToSignupUserInfoDto(user, token);
const response = toSignUpResponse(userInfo);

return status(200, response);
return status(200, response);
},
{
body: SignupRequestSchema,
detail: {
summary: "User Registration",
description:
"Register a new user account with email and password. Returns the created user info and sets an httpOnly authentication cookie.",
},
{
body: SignupRequestSchema,
detail: {
summary: "User Registration",
description:
"Register a new user account with email and password. Returns the created user info and sets an httpOnly authentication cookie.",
},
response: {
200: SignupResponseSchema,
400: ErrorResponseSchema,
422: ErrorResponseSchema,
429: ErrorResponseSchema,
500: ErrorResponseSchema,
},
}
);
response: {
200: SignupResponseSchema,
400: ErrorResponseSchema,
422: ErrorResponseSchema,
429: ErrorResponseSchema,
500: ErrorResponseSchema,
},
}
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep anti-abuse throttling on signup.

POST /user no longer has explicit rate limiting, which weakens abuse protection for automated account creation. Please add route-level throttling back (or enforce a guaranteed equivalent upstream control).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/auth/signup.ts` around lines 18 - 60, The signup
route lost rate-limiting—reintroduce route-level throttling on the POST "/user"
handler (signupHandler) by applying a rate-limit/throttle middleware (or
middleware factory) to the Elysia instance or directly to the post("/user")
route; configure it to key by IP (use the existing ip() middleware or headers),
set a conservative limit (e.g., small requests per minute and a short
ban/penalty window), and ensure the middleware returns appropriate 429 responses
and is included in the route options so automated signups are rate-limited while
legitimate users are unaffected.

Comment on lines +28 to +31
response: {
201: ArtistRoleResponseSchema,
401: ErrorResponseSchema,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document 400 validation errors in the create route response map.

This route validates body, so bad payloads can return 400 at runtime. Adding 400 here keeps the OpenAPI contract accurate.

Suggested patch
 			response: {
+				400: ErrorResponseSchema,
 			},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response: {
201: ArtistRoleResponseSchema,
401: ErrorResponseSchema,
},
response: {
201: ArtistRoleResponseSchema,
400: ErrorResponseSchema,
401: ErrorResponseSchema,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/catalog/artistRole/create.ts` around lines 28 - 31,
The response map for the create route in
apps/backend/src/handlers/catalog/artistRole/create.ts is missing a 400 entry
for validation errors; update the response object (the response map used by the
create handler) to include 400: ErrorResponseSchema alongside 201:
ArtistRoleResponseSchema and 401: ErrorResponseSchema so that body validation
failures are documented in the OpenAPI contract.

Comment on lines +56 to +60
logger: {
log: (level, message, ...args) => {
appLogger[level](message, {
metadata: args,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only check for existing redaction/sanitization controls in logger/auth paths.
rg -n -C2 --iglob '*.ts' 'redact|sanitize|mask|password|token|authorization|cookie|set-cookie'
rg -n -C2 --iglob '*.ts' 'logger:\s*\{|appLogger\[level\]|metadata:\s*args'

Repository: project-cvsa/cvsa

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the exact lines in betterAuth.ts around 56-60 (and nearby)
sed -n '40,90p' packages/core/src/modules/auth/betterAuth.ts

# 2) Inspect pinoSetup.ts where appLogger is defined/initialized and how redact is used
sed -n '1,200p' packages/logger/src/pinoSetup.ts

# 3) Find where appLogger is exported/created, and any custom redaction/formatters
rg -n "export (const|let|function) appLogger|appLogger\s*=" packages/logger/src -S
rg -n "redact\s*:" packages/logger/src/pinoSetup.ts packages/logger/src -S
rg -n "metadata:\s*args|metadata:\s*\{|\bmetadata\b" packages/core/src/modules/auth/betterAuth.ts -S

# 4) Quick check for any other logger wrapper behavior that may already sanitize
rg -n "sanitize|redact|mask|allowlist" packages/logger/src packages/core/src/modules/auth -S

Repository: project-cvsa/cvsa

Length of output: 1502


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# betterAuth.ts context around the logger block
sed -n '1,140p' packages/core/src/modules/auth/betterAuth.ts

# appLogger implementation and how metadata is handled
sed -n '1,240p' packages/logger/src/appLogger.ts

# Find all references to "metadata" usage in the logger package
rg -n "metadata" packages/logger/src -S

# Find createApplicationLogger definition details
rg -n "createApplicationLogger" packages/logger/src/appLogger.ts packages/logger/src -S

Repository: project-cvsa/cvsa

Length of output: 5575


Do not forward raw auth logger arguments to log/OTEL metadata.

In packages/core/src/modules/auth/betterAuth.ts, the better-auth logger passes ...args straight into { metadata: args } with no sanitization/allowlist. packages/logger/src/appLogger.ts forwards those same attrs to both Pino and OpenTelemetry emit() attributes, so any sensitive auth data in args can leak outside Pino redaction (and Pino’s redaction only targets keys like authorization, cookie, password, token).

  • Sanitize/allowlist metadata before calling appLogger[...] (and ensure the OTEL path doesn’t receive raw args).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/modules/auth/betterAuth.ts` around lines 56 - 60, The
better-auth logger's log implementation forwards raw variadic args into
appLogger[level] as metadata (logger.log -> appLogger[level] with { metadata:
args }), which can leak sensitive auth fields to Pino/OTEL; sanitize or
allowlist the metadata before passing it on: inspect the incoming ...args in
logger.log, strip or redact sensitive keys (authorization, cookie, password,
token, etc.) or build an explicit allowlist of safe fields, then call
appLogger[level] with { metadata: sanitizedMetadata } (and ensure any OTEL emit
path receives only the sanitized/allowlisted attributes rather than the raw
args).

import type { EmbeddingApp } from "@cvsa/embedding";
import { outboxService } from "../../outbox/container";

const embeddingManager = treaty<EmbeddingApp>("localhost:14900");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid hardcoded embedding service endpoint.

Line 9 hardcodes localhost:14900, which is environment-specific and brittle for deployed environments. Resolve from config/env and fail fast if missing.

Proposed fix
-const embeddingManager = treaty<EmbeddingApp>("localhost:14900");
+const embeddingServiceUrl = process.env.EMBEDDING_SERVICE_URL;
+if (!embeddingServiceUrl) {
+	throw new Error("EMBEDDING_SERVICE_URL is not configured");
+}
+const embeddingManager = treaty<EmbeddingApp>(embeddingServiceUrl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const embeddingManager = treaty<EmbeddingApp>("localhost:14900");
const embeddingServiceUrl = process.env.EMBEDDING_SERVICE_URL;
if (!embeddingServiceUrl) {
throw new Error("EMBEDDING_SERVICE_URL is not configured");
}
const embeddingManager = treaty<EmbeddingApp>(embeddingServiceUrl);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/modules/catalog/artistRole/container.ts` at line 9, The
embedding service address is hardcoded in the creation of embeddingManager via
treaty<EmbeddingApp>("localhost:14900"); change this to read the endpoint from
configuration/environment (e.g., process.env.EMBEDDING_SERVICE_URL or your app
config accessor) and validate it before use; if the value is missing or invalid,
throw an error or exit early so the process fails fast. Update the code that
constructs embeddingManager to use the resolved variable and include a clear
error message referencing embeddingManager/treaty<EmbeddingApp> when aborting.

Comment on lines +71 to +80
const languages = this.collectLanguages(entity);
for (const language of languages) {
const indexUid = `${this.entityType}_${language}`;
const index = await this.searchManager.getAdminIndex<TIndex>(indexUid);
const document = await this.getDocument(entity, language);
const task = await index.addDocuments([document], {
primaryKey: "id",
});
await this.searchManager.waitForTask(task.taskUid);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clean up documents from locales that were removed.

This loop only upserts the entity's current locales. If a translation is deleted or the base language changes, the old ${this.entityType}_${language} document is left behind and keeps showing up in search until a full reindex.

Suggested fix
 		const languages = this.collectLanguages(entity);
+		const desiredIndexes = new Set(
+			languages.map((language) => `${this.entityType}_${language}`)
+		);
+		const existingIndexes =
+			await this.searchManager.getLocalizedIndexesOfEntity(this.entityType);
+
+		for (const indexName of existingIndexes) {
+			if (!desiredIndexes.has(indexName)) {
+				const adminIndex = await this.searchManager.getAdminIndex(indexName);
+				const task = await adminIndex.deleteDocument(id);
+				await this.searchManager.waitForTask(task.taskUid);
+			}
+		}
+
 		for (const language of languages) {
 			const indexUid = `${this.entityType}_${language}`;
 			const index = await this.searchManager.getAdminIndex<TIndex>(indexUid);
 			const document = await this.getDocument(entity, language);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/search/interface.ts` around lines 71 - 80, The loop only
upserts current locales and leaves stale documents in old locale indexes; update
the code in the block using collectLanguages, this.entityType,
this.searchManager.getAdminIndex, getDocument, index.addDocuments and
this.searchManager.waitForTask to also remove any documents for this entity.id
that exist in locale-specific indexes no longer present in
collectLanguages(entity): enumerate relevant indexes for this.entityType (or a
predefined set of locale suffixes), for each index whose language is not in the
current languages call index.deleteDocument(entity.id) (or the admin delete API)
and await this.searchManager.waitForTask on the delete task before/after the
upserts so removed locales are cleaned up.

Comment on lines +89 to +102
const embeddingResponse = await this.embeddingManager.embeddings.post({
texts: [query],
});
const embeddingAvailable = (embeddingResponse?.data?.embeddings[0]?.length ?? 0) > 0;

return index.search(query, {
vector: embeddingResponse?.data?.embeddings[0],
hybrid: embeddingAvailable
? {
embedder: "potion-multilingual-128M",
semanticRatio: 0.25,
}
: undefined,
showRankingScore: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fall back to lexical search when embedding lookup fails.

An exception from embeddings.post() still takes down the whole search request. Since this method already supports a no-vector path, it should catch embedding failures and continue with plain text search instead of returning a 5xx for every transient embedding outage.

Suggested fix
 		const index = await this.searchManager.getSearchIndex(`${this.entityType}_${language}`);
-		const embeddingResponse = await this.embeddingManager.embeddings.post({
-			texts: [query],
-		});
-		const embeddingAvailable = (embeddingResponse?.data?.embeddings[0]?.length ?? 0) > 0;
+		let embedding: number[] | undefined;
+		try {
+			const embeddingResponse = await this.embeddingManager.embeddings.post({
+				texts: [query],
+			});
+			embedding = embeddingResponse?.data?.embeddings[0];
+		} catch (error) {
+			appLogger.warn(
+				{ error, entityType: this.entityType, language },
+				"Embedding lookup failed; falling back to lexical search"
+			);
+		}
+		const embeddingAvailable = (embedding?.length ?? 0) > 0;
 
 		return index.search(query, {
-			vector: embeddingResponse?.data?.embeddings[0],
+			vector: embedding,
 			hybrid: embeddingAvailable
 				? {
 						embedder: "potion-multilingual-128M",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const embeddingResponse = await this.embeddingManager.embeddings.post({
texts: [query],
});
const embeddingAvailable = (embeddingResponse?.data?.embeddings[0]?.length ?? 0) > 0;
return index.search(query, {
vector: embeddingResponse?.data?.embeddings[0],
hybrid: embeddingAvailable
? {
embedder: "potion-multilingual-128M",
semanticRatio: 0.25,
}
: undefined,
showRankingScore: true,
const index = await this.searchManager.getSearchIndex(`${this.entityType}_${language}`);
let embedding: number[] | undefined;
try {
const embeddingResponse = await this.embeddingManager.embeddings.post({
texts: [query],
});
embedding = embeddingResponse?.data?.embeddings[0];
} catch (error) {
appLogger.warn(
{ error, entityType: this.entityType, language },
"Embedding lookup failed; falling back to lexical search"
);
}
const embeddingAvailable = (embedding?.length ?? 0) > 0;
return index.search(query, {
vector: embedding,
hybrid: embeddingAvailable
? {
embedder: "potion-multilingual-128M",
semanticRatio: 0.25,
}
: undefined,
showRankingScore: true,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/search/interface.ts` around lines 89 - 102, Wrap the call
to this.embeddingManager.embeddings.post inside a try/catch so failures don't
bubble up; on error (or if embeddingResponse is missing) set embeddingAvailable
= false and call index.search without the vector and hybrid options (i.e., pass
undefined for vector and hybrid) to fall back to lexical search. Update the
logic around embeddingResponse/embeddingAvailable used when invoking
index.search (referencing embeddingManager.embeddings.post, embeddingResponse,
embeddingAvailable, and index.search) so a caught error results in a plain-text
search rather than throwing.

Comment thread scripts/import.ts
Comment on lines +103 to +108
const song = await songService.create({
name: data.name,
type: data.songType ? (songTypeMap[data.songType] as never) : undefined,
description: data.description,
publishedAt: new Date(data.publishedAt ?? "").toISOString(),
performances: performances.length ? performances : undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle optional/invalid publishedAt safely before toISOString().

Line 107 can throw on empty/invalid input (Invalid Date), which aborts that file’s song import.

Suggested fix
+	const parsedPublishedAt = data.publishedAt ? new Date(data.publishedAt) : undefined;
+	const publishedAt =
+		parsedPublishedAt && !Number.isNaN(parsedPublishedAt.getTime())
+			? parsedPublishedAt.toISOString()
+			: undefined;
+
 	const song = await songService.create({
 		name: data.name,
 		type: data.songType ? (songTypeMap[data.songType] as never) : undefined,
 		description: data.description,
-		publishedAt: new Date(data.publishedAt ?? "").toISOString(),
+		publishedAt,
 		performances: performances.length ? performances : undefined,
 		creations: creations.length ? creations : undefined,
 		lyrics: lyrics.length ? lyrics : undefined,
 	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/import.ts` around lines 103 - 108, The publishedAt value is being
converted with new Date(data.publishedAt ?? "").toISOString() inside the song
creation payload, which throws for empty/invalid dates; update the code around
the songService.create call to safely parse and validate data.publishedAt (e.g.,
create a Date object or use Date.parse and check isNaN/getTime) and only set the
publishedAt field when the parsed date is valid, otherwise leave publishedAt
undefined so invalid inputs don’t cause an exception during import.

Comment thread scripts/import.ts
Comment on lines +166 to +178
switch (data.type) {
case "singer":
await importSinger(data);
break;
case "artist":
await importArtist(data);
break;
case "song":
await importSong(data);
break;
default:
await importSinger(data);
console.warn(`unkown type ${data.type} for file ${file}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t import unknown types as singers.

Lines 176-178 create singer records for unsupported types (album, series, others), which can pollute catalog data.

Suggested fix
 			switch (data.type) {
 				case "singer":
 					await importSinger(data);
 					break;
 				case "artist":
 					await importArtist(data);
 					break;
 				case "song":
 					await importSong(data);
 					break;
+				case "album":
+				case "series":
+				case "others":
+					console.warn(`unsupported type ${data.type} for file ${file}`);
+					break;
 				default:
-					await importSinger(data);
-					console.warn(`unkown type ${data.type} for file ${file}`);
+					console.warn(`unknown type ${data.type} for file ${file}`);
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch (data.type) {
case "singer":
await importSinger(data);
break;
case "artist":
await importArtist(data);
break;
case "song":
await importSong(data);
break;
default:
await importSinger(data);
console.warn(`unkown type ${data.type} for file ${file}`);
switch (data.type) {
case "singer":
await importSinger(data);
break;
case "artist":
await importArtist(data);
break;
case "song":
await importSong(data);
break;
case "album":
case "series":
case "others":
console.warn(`unsupported type ${data.type} for file ${file}`);
break;
default:
console.warn(`unknown type ${data.type} for file ${file}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/import.ts` around lines 166 - 178, The switch on data.type currently
falls back to creating a singer for unknown types; update the default branch in
the switch (the block around switch (data.type)) to NOT call importSinger for
unsupported types like "album", "series", etc.; instead log a clear warning
including data.type and file and skip processing (return/continue) so no bogus
singer records are created—use the existing importSinger/importArtist/importSong
symbols to locate the switch and ensure unknown types are safely ignored.

Comment thread scripts/import.ts Outdated
Comment on lines +181 to +194
catch (e) {
console.error("error at: ", file);
console.error(e);
}
}
}

main().catch((err) => {
console.error("fatal:", err);
process.exit(1);
}).finally(() => {
console.log("completed");
process.exit(0);
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate import failures via non-zero exit status.

Current flow logs per-file errors but still ends with success (process.exit(0)), which hides failed imports from CI/ops.

Suggested fix
 async function main() {
+	let hadErrors = false;
 	...
 	for (const file of files) {
 		try {
 			...
 		}
 		catch (e) {
+			hadErrors = true;
 			console.error("error at: ", file);
 			console.error(e);
 		}
 	}
+
+	if (hadErrors) {
+		throw new Error("Import completed with errors");
+	}
 }
 
-main().catch((err) => {
-	console.error("fatal:", err);
-	process.exit(1);
-}).finally(() => {
-	console.log("completed");
-	process.exit(0);
-})
+main()
+	.then(() => {
+		console.log("completed");
+		process.exit(0);
+	})
+	.catch((err) => {
+		console.error("fatal:", err);
+		process.exit(1);
+	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/import.ts` around lines 181 - 194, The script currently logs per-file
errors inside the catch in main but always calls process.exit(0) in the finally,
hiding failures; update main and the file-level error handling so failures
propagate: add a boolean flag (e.g., importFailed) in the module scope or inside
main, set importFailed = true inside the per-file catch block where e is logged,
remove the unconditional process.exit(0) from the finally, and change the
top-level promise handlers so that main().catch(...) calls process.exit(1) and
the finally only logs completion (or calls process.exit(importFailed ? 1 : 0));
reference the main() invocation, the per-file catch block, and the finally
handler when making these changes.

@alikia2x alikia2x force-pushed the feat/artist-role branch from a568ac7 to 9f804ce Compare May 22, 2026 13:33
@alikia2x alikia2x force-pushed the feat/artist-role branch from 9f804ce to a96af0d Compare May 22, 2026 13:37
@alikia2x alikia2x merged commit ffe158a into develop May 22, 2026
2 checks passed
@alikia2x alikia2x deleted the feat/artist-role branch May 22, 2026 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant