Skip to content

feat: notifications lookup by id#668

Open
kamilwronka wants to merge 1 commit intodevelopfrom
feature/notifications-lookup-by-id
Open

feat: notifications lookup by id#668
kamilwronka wants to merge 1 commit intodevelopfrom
feature/notifications-lookup-by-id

Conversation

@kamilwronka
Copy link
Copy Markdown
Contributor

@kamilwronka kamilwronka commented Apr 6, 2026

Summary by CodeRabbit

  • Refactor
    • Migrated notification rule NPC filtering from numeric identifiers to name-based selection. Notification rules now target NPCs by name instead of ID, improving user experience and data consistency throughout the platform.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

The PR migrates NPC-targeting in notification rules from numeric IDs to string-based names across the entire system, including database migration, API DTOs, service logic, matching algorithms, form validation, and frontend components.

Changes

Cohort / File(s) Summary
Database Migration
apps/api/prisma/migrations/20260406023332_migrate_notification_filters_npc_id_to_name/migration.sql
Executes two-pass SQL UPDATE to convert existing NotificationRule.filters from npcId/npcIds (numeric) to npcName/npcNames (string), using Timer.npc->'name' as the lookup source and selecting the most recently updated Timer row per NPC ID.
Backend DTOs & Validation
apps/api/src/notifications/dto/create-notification-rule.dto.ts, apps/api/src/notifications/dto/update-notification-rule.dto.ts, apps/api/src/notifications/dto/has-at-least-one-npc.validator.ts
Replaced numeric NPC fields (npcId?: number, npcIds?: number[]) with string equivalents (npcName?: string, npcNames?: string[]); updated validator logic to check for either single name or non-empty name array.
Backend Matching & Handling
apps/api/src/notifications/notification-matching.service.ts, apps/api/src/notifications/notification-content.service.ts, apps/api/src/notifications/notification-job.service.ts, apps/api/src/notifications/notifications-events.handler.ts
Updated matching signature to accept `npcName: string
Backend Rule Management
apps/api/src/notifications/notification-rule.service.ts
Updated filter construction and NPC validation to use npcName/npcNames fields; now validates string-based names and enforces max NPC count per rule.
Shared Types
packages/types/src/common/notifications.types.ts
Updated NotificationFilters interface to replace npcId/npcIds with npcName/npcNames.
Frontend Form & Schema
apps/web/src/features/guild-notifications/utils/notification-rule-form.schema.ts, apps/web/src/features/guild-notifications/notification-rule-form-page.tsx
Replaced form field npcIds with npcNames in Zod schema and form binding; updated validation messages and form resets to reference name-based field.
Frontend Utilities & Hooks
apps/web/src/features/guild-notifications/utils/notification-settings.utils.ts, apps/web/src/features/guild-notifications/hooks/use-notification-rule-form.ts, apps/web/src/hooks/api/guilds/use-guild-notifications.ts
Renamed getGuildNotificationRuleNpcIds to getGuildNotificationRuleNpcNames to extract string names; updated form hook NPC selection logic to use name-based tracking; changed API payload types to transmit npcName/npcNames instead of numeric IDs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hopping through the code with delight,
From numbers old to names so bright!
Migration magic, matching flows anew,
NPCs dance by their proper name, it's true!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'notifications lookup by id' is misleading. The changes actually migrate FROM ID-based lookups TO name-based lookups, replacing npcId/npcIds with npcName/npcNames throughout the codebase. Revise the title to accurately reflect the change direction, such as 'feat: migrate notifications NPC filtering from ID to name-based lookup' or 'feat: refactor notifications to use NPC names instead of IDs'.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/notifications-lookup-by-id

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef7d71a16c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +20 to +22
WHERE nr."filters" IS NOT NULL
AND (nr."filters"::jsonb) ? 'npcId'
AND (nr."filters" ->> 'npcId')::int = t."npcId";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Migrate unresolved npcId filters before removing ID matching

This update only converts rows where (filters->>'npcId')::int has a matching Timer.npcId, so any rule pointing to an NPC ID with no current timer row keeps legacy npcId in JSON. After this commit, timer matching only reads npcName/npcNames, so those unmigrated rules become effectively unfiltered and can schedule notifications for unrelated NPCs in the same guild/world.

Useful? React with 👍 / 👎.

Comment on lines +30 to +31
|| jsonb_build_object('npcNames', (
SELECT jsonb_agg(DISTINCT t."npcName")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prevent npcNames: null fallback from broadening rule scope

In the array migration path, jsonb_agg returns NULL when none of the stored IDs resolve to a timer row, and this writes {"npcNames": null} while removing npcIds. The new matcher treats that as no NPC filter (filters.npcNames?.length is falsy), so affected rules match all timer updates instead of none/specific targets.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/api/src/notifications/dto/update-notification-rule.dto.ts (1)

56-67: LGTM with optional suggestion.

The DTO correctly updates NPC targeting from numeric IDs to string names with appropriate validators.

Optional: Consider adding @MaxLength(255, { each: true }) to npcNames to enforce consistent length limits on array elements, matching the single npcName field constraint.

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

In `@apps/api/src/notifications/dto/update-notification-rule.dto.ts` around lines
56 - 67, Add per-element length validation to the npcNames array to match
npcName by adding a MaxLength decorator with each: true; specifically update the
DTO where npcNames is defined (symbol: npcNames) to include `@MaxLength`(255, {
each: true }) alongside `@IsString`({ each: true }) so every string in the array
is limited to 255 characters like the npcName field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api/prisma/migrations/20260406023332_migrate_notification_filters_npc_id_to_name/migration.sql`:
- Around line 1-45: The migration currently leaves orphaned npcId/npcIds that
cause rules to match everything; update the migration to (1) make the
npcIds->npcNames aggregation safe by COALESCEing jsonb_agg to an empty array (so
npcNames becomes [] instead of null) or explicitly remove the npcNames key when
no names resolved, (2) add a final cleanup UPDATE that removes any leftover
'npcId' and 'npcIds' keys from "NotificationRule"."filters" (so the app only
sees npcName/npcNames), and (3) add a pre-migration SELECT that identifies/logs
NotificationRule rows where npcId is not found in Timer or where all npcIds fail
to resolve (so those rules can be reviewed); reference the "NotificationRule"
table, "filters" JSON keys 'npcId'/'npcIds' and new keys 'npcName'/'npcNames',
the "Timer" lookup subquery, and the matchesTimerRule behavior when making these
edits.

In `@apps/api/src/notifications/notification-matching.service.ts`:
- Around line 31-42: In matchesTimerRule, parse filters first via
this.parseFilters(filtersValue) instead of returning false immediately when
npcName is null; only require npcName when filters.npcName or filters.npcNames
are present (i.e., if neither NPC filter exists, allow matching even with
npcName null). Normalize comparisons by canonicalizing values (trim and
toLowerCase) for filters.npcName, every entry of filters.npcNames, and the
incoming npcName before comparing with !== or includes so comparisons are case-
and whitespace-insensitive; update the checks around filters.npcName and
filters.npcNames in matchesTimerRule accordingly and reuse parseFilters result.

---

Nitpick comments:
In `@apps/api/src/notifications/dto/update-notification-rule.dto.ts`:
- Around line 56-67: Add per-element length validation to the npcNames array to
match npcName by adding a MaxLength decorator with each: true; specifically
update the DTO where npcNames is defined (symbol: npcNames) to include
`@MaxLength`(255, { each: true }) alongside `@IsString`({ each: true }) so every
string in the array is limited to 255 characters like the npcName field.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 329120ad-46c3-4b7f-8859-e85ea37b6c93

📥 Commits

Reviewing files that changed from the base of the PR and between 143fdaf and ef7d71a.

📒 Files selected for processing (15)
  • apps/api/prisma/migrations/20260406023332_migrate_notification_filters_npc_id_to_name/migration.sql
  • apps/api/src/notifications/dto/create-notification-rule.dto.ts
  • apps/api/src/notifications/dto/has-at-least-one-npc.validator.ts
  • apps/api/src/notifications/dto/update-notification-rule.dto.ts
  • apps/api/src/notifications/notification-content.service.ts
  • apps/api/src/notifications/notification-job.service.ts
  • apps/api/src/notifications/notification-matching.service.ts
  • apps/api/src/notifications/notification-rule.service.ts
  • apps/api/src/notifications/notifications-events.handler.ts
  • apps/web/src/features/guild-notifications/hooks/use-notification-rule-form.ts
  • apps/web/src/features/guild-notifications/notification-rule-form-page.tsx
  • apps/web/src/features/guild-notifications/utils/notification-rule-form.schema.ts
  • apps/web/src/features/guild-notifications/utils/notification-settings.utils.ts
  • apps/web/src/hooks/api/guilds/use-guild-notifications.ts
  • packages/types/src/common/notifications.types.ts

Comment on lines +1 to +45
-- Migrate NotificationRule filters from npcId/npcIds to npcName/npcNames
-- using NPC names resolved from the Timer table.

-- Step 1: Migrate rules with filters->npcId (single NPC)
-- Find the NPC name from Timer.npc->>'name' matching the npcId.
UPDATE "NotificationRule" nr
SET "filters" = (
nr."filters"::jsonb
- 'npcId'
|| jsonb_build_object('npcName', t."npcName")
)::jsonb
FROM (
SELECT DISTINCT ON (sub."npcId")
sub."npcId",
sub."npc" ->> 'name' AS "npcName"
FROM "Timer" sub
WHERE sub."npc" ->> 'name' IS NOT NULL
ORDER BY sub."npcId", sub."updatedAt" DESC
) t
WHERE nr."filters" IS NOT NULL
AND (nr."filters"::jsonb) ? 'npcId'
AND (nr."filters" ->> 'npcId')::int = t."npcId";

-- Step 2: Migrate rules with filters->npcIds (array of NPC IDs)
-- For each rule, resolve all NPC IDs to names and build npcNames array.
UPDATE "NotificationRule" nr
SET "filters" = (
nr."filters"::jsonb
- 'npcIds'
|| jsonb_build_object('npcNames', (
SELECT jsonb_agg(DISTINCT t."npcName")
FROM jsonb_array_elements_text(nr."filters" -> 'npcIds') AS elem
JOIN (
SELECT DISTINCT ON (sub."npcId")
sub."npcId",
sub."npc" ->> 'name' AS "npcName"
FROM "Timer" sub
WHERE sub."npc" ->> 'name' IS NOT NULL
ORDER BY sub."npcId", sub."updatedAt" DESC
) t ON t."npcId" = elem::int
))
)::jsonb
WHERE nr."filters" IS NOT NULL
AND (nr."filters"::jsonb) ? 'npcIds'
AND jsonb_array_length(nr."filters" -> 'npcIds') > 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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are NotificationRule records with npcId/npcIds that won't resolve against Timer

# First, let's find the schema and understand the data model
echo "=== Checking for Timer and NotificationRule models ==="
rg -n "model Timer" -A 20 --type-add 'prisma:*.prisma' --type prisma

echo ""
echo "=== Checking for NotificationRule model ==="
rg -n "model NotificationRule" -A 30 --type-add 'prisma:*.prisma' --type prisma

echo ""
echo "=== Checking how filters are currently read in matching service ==="
rg -nC5 "filters\.npcId|filters\.npcName" --type ts

Repository: lootlog/monorepo

Length of output: 11162


🏁 Script executed:

#!/bin/bash

echo "=== Checking for foreign key constraints or relationships ==="
rg -n "npcId|npcName" apps/api/prisma/schema.prisma | head -30

echo ""
echo "=== Searching for other migrations related to npcId/npcName ==="
fd -e sql apps/api/prisma/migrations | xargs grep -l "npcId\|npcName" 2>/dev/null || echo "No other migration files found"

echo ""
echo "=== Checking for validation/logging in notification services ==="
rg -n "npcId|orphan|unresolved" apps/api/src/notifications/ --type ts -i

echo ""
echo "=== Checking the full notification-matching.service.ts to understand matching logic ==="
cat -n apps/api/src/notifications/notification-matching.service.ts | head -60

Repository: lootlog/monorepo

Length of output: 5850


🏁 Script executed:

#!/bin/bash

echo "=== Reading the actual migration file ==="
cat -n apps/api/prisma/migrations/20260406023332_migrate_notification_filters_npc_id_to_name/migration.sql

echo ""
echo "=== Checking for any rollback or recovery migrations ==="
find apps/api/prisma/migrations -name "*.sql" -type f | xargs grep -l "npcId\|npcName" 2>/dev/null | sort

echo ""
echo "=== Searching for migration validation or cleanup scripts ==="
find apps -name "*.ts" -o -name "*.js" | xargs grep -l "migrate_notification_filters_npc_id_to_name\|NotificationRule.*filters" 2>/dev/null | head -20

Repository: lootlog/monorepo

Length of output: 2962


Migration will silently orphan rules and cause unintended matching behavior.

The migration has two critical data integrity issues:

  1. Step 1 (single npcId): Rules whose npcId doesn't exist in the Timer table won't be updated; they retain { npcId: ... } in filters. Since matchesTimerRule only reads npcName and npcNames fields and returns true when both are absent, these orphaned rules will match all timers instead of none.

  2. Step 2 (array npcIds): NPC IDs not found in Timer are silently dropped from the resulting npcNames array. If all IDs fail to resolve, jsonb_agg returns NULL, resulting in npcNames: null, which also matches all timers.

Additionally, the migration does not include a cleanup step to remove the unmigrated npcId/npcIds keys, leaving stale data in filters that the new code ignores.

Add a cleanup step at the end of the migration:

Cleanup SQL
-- Remove unmigrated npcId/npcIds keys to prevent silent rule changes
UPDATE "NotificationRule"
SET "filters" = "filters"::jsonb - 'npcId' - 'npcIds'
WHERE "filters" IS NOT NULL
  AND ((("filters"::jsonb) ? 'npcId') OR (("filters"::jsonb) ? 'npcIds'));

Consider adding a pre-migration query to log/identify rules that won't be migrated, so they can be manually reviewed or handled by application logic.

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

In
`@apps/api/prisma/migrations/20260406023332_migrate_notification_filters_npc_id_to_name/migration.sql`
around lines 1 - 45, The migration currently leaves orphaned npcId/npcIds that
cause rules to match everything; update the migration to (1) make the
npcIds->npcNames aggregation safe by COALESCEing jsonb_agg to an empty array (so
npcNames becomes [] instead of null) or explicitly remove the npcNames key when
no names resolved, (2) add a final cleanup UPDATE that removes any leftover
'npcId' and 'npcIds' keys from "NotificationRule"."filters" (so the app only
sees npcName/npcNames), and (3) add a pre-migration SELECT that identifies/logs
NotificationRule rows where npcId is not found in Timer or where all npcIds fail
to resolve (so those rules can be reviewed); reference the "NotificationRule"
table, "filters" JSON keys 'npcId'/'npcIds' and new keys 'npcName'/'npcNames',
the "Timer" lookup subquery, and the matchesTimerRule behavior when making these
edits.

Comment on lines +31 to +42
matchesTimerRule(filtersValue: Prisma.JsonValue, npcName: string | null) {
if (!npcName) {
return false;
}

const filters = this.parseFilters(filtersValue);

if (filters.npcId && filters.npcId !== npcId) {
if (filters.npcName && filters.npcName !== npcName) {
return false;
}

if (filters.npcIds?.length && !filters.npcIds.includes(npcId)) {
if (filters.npcNames?.length && !filters.npcNames.includes(npcName)) {
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

Match timer NPCs against canonical names, and only when the rule filters by NPC.

The guard on Lines 31-34 makes matchesTimerRule({}, null) return false, so world-only/all-NPC timer rules stop matching whenever the caller cannot supply npc.name. On top of that, the raw !== / includes() checks are case- and whitespace-sensitive, which will silently miss logically identical names. Parse filters first, only require a name when NPC filters exist, and compare normalized values.

💡 Suggested fix
   matchesTimerRule(filtersValue: Prisma.JsonValue, npcName: string | null) {
-    if (!npcName) {
-      return false;
-    }
-
     const filters = this.parseFilters(filtersValue);
+    const hasNpcFilter =
+      typeof filters.npcName === "string" ||
+      (filters.npcNames?.length ?? 0) > 0;
+    const normalizeNpcName = (value: string) =>
+      value.trim().toLowerCase().replace(/\s+/g, " ");
+
+    if (!npcName) {
+      return !hasNpcFilter;
+    }
+
+    const normalizedNpcName = normalizeNpcName(npcName);
 
-    if (filters.npcName && filters.npcName !== npcName) {
+    if (
+      typeof filters.npcName === "string" &&
+      normalizeNpcName(filters.npcName) !== normalizedNpcName
+    ) {
       return false;
     }
 
-    if (filters.npcNames?.length && !filters.npcNames.includes(npcName)) {
+    if (
+      filters.npcNames?.length &&
+      !filters.npcNames.some(
+        (filterNpcName) =>
+          normalizeNpcName(filterNpcName) === normalizedNpcName,
+      )
+    ) {
       return false;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/notifications/notification-matching.service.ts` around lines 31
- 42, In matchesTimerRule, parse filters first via
this.parseFilters(filtersValue) instead of returning false immediately when
npcName is null; only require npcName when filters.npcName or filters.npcNames
are present (i.e., if neither NPC filter exists, allow matching even with
npcName null). Normalize comparisons by canonicalizing values (trim and
toLowerCase) for filters.npcName, every entry of filters.npcNames, and the
incoming npcName before comparing with !== or includes so comparisons are case-
and whitespace-insensitive; update the checks around filters.npcName and
filters.npcNames in matchesTimerRule accordingly and reuse parseFilters result.

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