feat: flow item view#153
Conversation
WalkthroughAdds a full-stack “item views” feature for Flow: database tables/types, repository method, API endpoint, store methods/computed, page logic with auto-mark-as-viewed and avatars, and navigation badge showing unviewed count. Also updates navigation header with a conditional button, tweaks badge logic, and applies multiple presentational typography/heading changes. Minor dependency bumps. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant P as Flow Item Page<br/>([itemId]/index.vue)
participant FS as Flow Store
participant API as POST /api/flow/id/:itemId/view
participant DB as Repository/DB
U->>P: Open item page
P->>P: Compute isViewed (by userId in item.views)
alt Not viewed
P->>FS: addView(itemId)
FS->>API: POST view { userId, itemId } (auth)
API->>DB: createItemView
DB-->>API: View created
API-->>FS: { ok: true }
FS->>FS: update() refresh items
FS-->>P: items with updated views
note over P: Avatars render from item.views
else Already viewed
P-->>U: Render existing avatars
end
sequenceDiagram
participant Nav as Navigation (UI)
participant Comp as useNavigation()
participant FS as Flow Store
FS-->>Comp: nowViewedItemsCount (computed)
Comp-->>Nav: routes with home.badge = count.toString()
Nav-->>Nav: Show chip when badge && badge !== '0'
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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. Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (12)
apps/atrium-telegram/app/components/flow/OrderAmountAverage.vue (1)
7-12: Prefer locale-aware currency formatting (Intl) and simplify markupRender the value with Intl.NumberFormat and drop the separate ₽ element.
- <h3 class="text-2xl/5 font-bold"> - {{ averageToday }} - </h3> - <p class="text-lg/4"> - ₽ - </p> + <h3 class="text-2xl/5 font-bold"> + {{ formatCurrency(averageToday) }} + </h3>Add alongside your script setup:
const formatCurrency = (n: number) => new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format(n)apps/atrium-telegram/app/components/EpicCard.vue (1)
5-11: LGTM with a tiny UX noteThe new title/description styles look good. If you notice odd wrapping, consider revisiting the combo of whitespace-pre-wrap with line-clamp.
packages/database/src/repository/flow.ts (1)
10-12: Trim related data or compute per-user flag server-side to avoid payload bloat.Eager-loading full
viewsfor every item in bothfindItemandlistItemscan get large. If the UI only needs to know whether the current user has viewed an item, prefer:
- returning a boolean (e.g.,
isViewedByMe) via anEXISTSsubquery, or- limiting relation columns to just
idanduserId(if your Drizzle version supports relation column selection).This reduces memory and network costs on the hot path.
Also applies to: 24-25
apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1)
5-12: Parameter validation message clarity.Minor: say
itemIdinstead of genericIdfor consistency with the route.- message: 'Id is required', + message: 'itemId is required',apps/atrium-telegram/app/components/flow/ItemCard.vue (1)
3-15: Nice UX touch — clear “not viewed” indicator.The animated pointer + label effectively signals attention needed. Keep it lightweight; consider
aria-live="polite"on the container for SR users.- <div v-if="!isViewed" class="flex flex-row items-center gap-1.5 text-error"> + <div v-if="!isViewed" class="flex flex-row items-center gap-1.5 text-error" aria-live="polite">apps/atrium-telegram/app/stores/flow.ts (1)
34-54: Treat duplicate view POSTs as success; prefer optimistic update over full refetch.
- Swallow 400/409 from POST /api/flow/id/:id/view and avoid refetching the whole list when possible; optionally perform an optimistic local update to the item's views.
Minimal change to swallow duplicates (apps/atrium-telegram/app/stores/flow.ts):
async function addView(itemId: string) { try { await $fetch(`/api/flow/id/${itemId}/view`, { method: 'POST', headers: { Authorization: `tma ${initDataRaw.value}`, }, }) await update() } catch (error) { if (error instanceof Error) { if (error.message.includes('401')) { // No } if (error.message.includes('404')) { // Not found } + if (error.message.includes('400') || error.message.includes('409')) { + // Already viewed — treat as success + await update() + return + } } } }Optional improvement: instead of calling update(), append the current user to the item's views locally (matching existing checks against userStore.id in flow pages/components) to avoid the full refetch.
apps/atrium-telegram/app/stores/user.ts (1)
114-117: Reuse existing finder; avoid duplicate scans and keep API cohesive.Call the existing
find()helper instead of rescanningusershere.Apply:
- function getAvatarUrl(userId: string): string | undefined { - const user = users.value.find((user) => user.id === userId) - return user?.avatarUrl ?? undefined - } + function getAvatarUrl(userId: string): string | undefined { + return find(userId)?.avatarUrl ?? undefined + }apps/atrium-telegram/app/components/Navigation.vue (1)
2-17: Plus button has no action; wire a handler or hide it; add ARIA.Currently it’s inert and not accessible.
Apply:
- <UButton + <UButton v-if="isMainPage" variant="solid" color="secondary" size="xl" class="transition-all duration-200 ease-in-out motion-preset-slide-down motion-duration-200" icon="i-lucide-plus" + aria-label="Create flow item" + @click="emit('create-flow-item')" :ui="{ base: 'size-12 font-bold rounded-full', leadingIcon: 'size-6 mx-auto', }" />And in script:
-const { isNavigationShown, mainRoutes, isMainPage } = useNavigation() +const { isNavigationShown, mainRoutes, isMainPage } = useNavigation() +const emit = defineEmits<{ + (e: 'create-flow-item'): void +}>()apps/atrium-telegram/app/components/NavigationButton.vue (1)
25-26: Guard badge visibility with a numeric check.Prevents showing badges like "NaN"/"undefined" if a bad value slips through.
Apply:
- color="error" - :show="!!route.badge && route.badge !== '0'" + color="error" + :show="Number(route.badge) > 0"apps/atrium-telegram/app/composables/useNavigation.ts (1)
16-16: Coerce safely; consider clamping large counts.
String(...)avoids.toString()on a non-number by accident and you may want to cap at e.g. 99+.Apply:
- badge: flowStore.nowViewedItemsCount.toString(), + badge: String(flowStore.nowViewedItemsCount), + // optionally: badge: String(Math.min(flowStore.nowViewedItemsCount, 99)),apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (2)
17-29: Avoid O(n×m) lookups; precompute an avatar map.
getAvatarUrl()does a linear search per view. Precompute a lookup once for smoother lists.Apply:
- <UAvatar - v-for="view in item?.views" - :key="view.id" - :src="userStore.getAvatarUrl(view.userId)" - /> + <UAvatar + v-for="view in item?.views" + :key="view.id" + :src="avatarUrlByUserId.get(view.userId)" + />And in script:
const userStore = useUserStore() const flowStore = useFlowStore() const item = computed(() => flowStore.items.find((item) => item.id === params.itemId)) +const avatarUrlByUserId = computed(() => { + const map = new Map<string, string | undefined>() + for (const u of userStore.users) map.set(u.id, u.avatarUrl ?? undefined) + return map +})
45-51: View‑on‑open logic: add a small guard to avoid double POSTs.A rapid route change could fire multiple writes before refresh. Gate with a local flag.
Apply:
-const isViewed = computed(() => item.value?.views.some((view) => view.userId === userStore?.id)) +const isViewed = computed(() => Boolean(item.value?.views.some((view) => view.userId === userStore.id))) +let posting = false watch(isViewed, () => { - if (!isViewed.value && item.value?.id) { - flowStore.addView(item.value.id) + if (!isViewed.value && item.value?.id && !posting) { + posting = true + flowStore.addView(item.value.id).finally(() => { posting = false }) } }, { immediate: true })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (20)
apps/atrium-telegram/app/components/EpicCard.vue(1 hunks)apps/atrium-telegram/app/components/Navigation.vue(2 hunks)apps/atrium-telegram/app/components/NavigationButton.vue(1 hunks)apps/atrium-telegram/app/components/flow/FeedbackAverage.vue(1 hunks)apps/atrium-telegram/app/components/flow/ItemCard.vue(2 hunks)apps/atrium-telegram/app/components/flow/KitchensOnline.vue(1 hunks)apps/atrium-telegram/app/components/flow/OrderAmountAverage.vue(1 hunks)apps/atrium-telegram/app/components/flow/OrdersOnline.vue(1 hunks)apps/atrium-telegram/app/composables/useNavigation.ts(2 hunks)apps/atrium-telegram/app/pages/epic/[epicId]/index.vue(1 hunks)apps/atrium-telegram/app/pages/flow/[itemId]/index.vue(2 hunks)apps/atrium-telegram/app/pages/tasks/index.vue(1 hunks)apps/atrium-telegram/app/stores/flow.ts(2 hunks)apps/atrium-telegram/app/stores/user.ts(2 hunks)apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts(1 hunks)apps/atrium-telegram/shared/types/index.ts(2 hunks)packages/database/src/repository/flow.ts(3 hunks)packages/database/src/tables.ts(3 hunks)packages/database/src/types.ts(1 hunks)pnpm-workspace.yaml(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
apps/atrium-telegram/app/stores/flow.ts (1)
apps/atrium-telegram/app/stores/user.ts (1)
useUserStore(8-141)
apps/atrium-telegram/shared/types/index.ts (1)
packages/database/src/types.ts (1)
FlowItemView(209-209)
apps/atrium-telegram/app/composables/useNavigation.ts (1)
apps/atrium-telegram/app/stores/flow.ts (1)
useFlowStore(3-64)
apps/atrium-telegram/app/stores/user.ts (1)
packages/database/src/tables.ts (1)
users(73-91)
packages/database/src/repository/flow.ts (2)
packages/database/src/types.ts (1)
FlowItemViewDraft(210-210)packages/database/src/tables.ts (1)
flowItemViews(821-833)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (18)
apps/atrium-telegram/app/components/flow/KitchensOnline.vue (1)
13-15: Typography tweak LGTMThe size change aligns with the rest of the flow components.
apps/atrium-telegram/app/pages/tasks/index.vue (1)
11-13: Heading hierarchy improvement LGTMSwitching to h1 on a top-level page is appropriate.
apps/atrium-telegram/app/components/flow/FeedbackAverage.vue (1)
8-12: LGTMConsistent with the typography pass.
apps/atrium-telegram/app/components/flow/OrdersOnline.vue (1)
6-8: LGTMTypography aligns with the other flow widgets.
apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1)
16-19: h1 switch LGTMMatches page semantics for a primary title.
pnpm-workspace.yaml (1)
57-58: Nuxt & nuxt-auth-utils patch bumps — verify workspace refs & Node engine
- Repo shows "catalog:" workspace refs for nuxt/nuxt-auth-utils in: apps/atrium-telegram/package.json, apps/geo-vault/package.json, apps/storefront-telegram/package.json, apps/web-app/package.json, apps/web-storefront/package.json, packages/ui/package.json — semver ranges not extractable from these refs; verify resolved versions (pnpm -w list or lockfile).
- engines.node is pinned to ">=22.18.0" (package "roll-stack"); verify Node >=22 is intentional and compatible with Nuxt ^4.1.2 and nuxt-auth-utils ^0.5.24.
packages/database/src/types.ts (1)
209-210: LGTM! Type definitions follow established patterns.The
FlowItemViewandFlowItemViewDrafttype definitions are correctly implemented usingInferSelectModelandInferInsertModel, consistent with other entity types in the file.apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1)
35-39: Prefer treating duplicates as success (idempotent semantics).With
onConflictDoNothing, return{ ok: true }regardless of prior view state (HTTP 200/204). This simplifies clients and avoids leaking state via errors.If you keep duplicate detection, ensure it’s covered by tests; otherwise, remove and rely on DB uniqueness. Consider adding an integration test for double-POST behavior.
apps/atrium-telegram/app/components/flow/ItemCard.vue (1)
46-52: Fix non-reactive prop destructure in ItemCard.vue — keep userStore.id as-isFile: apps/atrium-telegram/app/components/flow/ItemCard.vue (lines ~46–52)
- Destructuring defineProps breaks reactivity — make the prop a ref (toRef) so parent updates re-render.
- userStore.id is provided by the Pinia setup store and is proxied/auto-unwrapped, so comparing userStore.id to a string is fine (no storeToRefs required).
Suggested change:
-import { format } from 'date-fns' +import { format } from 'date-fns' +import { toRef } from 'vue' import { ru } from 'date-fns/locale/ru' -const { item } = defineProps<{ - item: FlowItemWithData -}>() - -const userStore = useUserStore() -const isViewed = computed(() => item.views.some((view) => view.userId === userStore?.id)) +const props = defineProps<{ item: FlowItemWithData }>() +const item = toRef(props, 'item') + +const userStore = useUserStore() +const isViewed = computed(() => item.value.views.some((view) => view.userId === userStore.id))Likely an incorrect or invalid review comment.
apps/atrium-telegram/app/stores/user.ts (1)
139-139: LGTM: exposinggetAvatarUrlin the store API.Public surface makes sense for UI call sites.
packages/database/src/tables.ts (2)
1312-1313: LGTM: added back‑relation forviewsonflowItems.
1326-1335: LGTM: relations forflowItemViews→useranditem.apps/atrium-telegram/shared/types/index.ts (2)
1-1: LGTM: type import forFlowItemView.
13-14: LGTM, but ensure API always returnsviews: [].Schema looks good; please confirm server responses always include a concrete array to avoid null/undefined at call sites.
apps/atrium-telegram/app/components/Navigation.vue (1)
34-34: LGTM:isMainPageusage matches the new header behavior.apps/atrium-telegram/app/composables/useNavigation.ts (1)
7-7: LGTM: bringingflowStoreinto navigation.apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (2)
8-11: LGTM: heading tweak.
41-41: LGTM: store usage consistent.
| const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length) | ||
|
|
There was a problem hiding this comment.
Bug: comparing a string to a Ref — count will be wrong for all users.
useUserStore().id is a Ref. Comparing view.userId === useUserStore().id always fails, so every item looks “not viewed”.
Fix by hoisting the store and unwrapping id. Consider renaming the var (typo) to notViewedItemsCount.
- const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length)
+ const userStore = useUserStore()
+ const { id: userId } = storeToRefs(userStore)
+ const notViewedItemsCount = computed(() =>
+ items.value.filter((item) => !item.views.some((view) => view.userId === userId.value)).length
+ )Also export the renamed symbol below.
📝 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.
| const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length) | |
| const userStore = useUserStore() | |
| const { id: userId } = storeToRefs(userStore) | |
| const notViewedItemsCount = computed(() => | |
| items.value.filter((item) => !item.views.some((view) => view.userId === userId.value)).length | |
| ) |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/stores/flow.ts around lines 6-7, the computed uses
useUserStore().id (a Ref) inline so the equality always fails; hoist the store
outside the computed (const userStore = useUserStore()), compare view.userId to
userStore.id.value (unwrap the Ref), rename the variable from
nowViewedItemsCount to notViewedItemsCount, and update the export(s) below to
export the new name.
| nowViewedItemsCount, | ||
|
|
||
| update, | ||
| addView, | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Export the renamed counter.
If you adopt notViewedItemsCount, update exports:
- nowViewedItemsCount,
+ notViewedItemsCount,Grepping for usages to update call sites:
🏁 Script executed:
#!/bin/bash
rg -n "nowViewedItemsCount|notViewedItemsCount" apps/atrium-telegramLength of output: 366
Export renamed counter and update usages
If you rename nowViewedItemsCount → notViewedItemsCount, update the export in apps/atrium-telegram/app/stores/flow.ts (def: line 6, export: line 59) and replace all references — e.g. apps/atrium-telegram/app/composables/useNavigation.ts:16 (flowStore.nowViewedItemsCount → flowStore.notViewedItemsCount).
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/stores/flow.ts around lines 59-63 the store is still
exporting nowViewedItemsCount but the variable was renamed to
notViewedItemsCount; update the export to export notViewedItemsCount (replace
nowViewedItemsCount) and then update all usages across the repo to the new name
(for example change flowStore.nowViewedItemsCount to
flowStore.notViewedItemsCount in
apps/atrium-telegram/app/composables/useNavigation.ts line ~16 and any other
references).
| // Guards: | ||
| // If not exist | ||
| // If already viewed | ||
| const item = await repository.flow.findItem(itemId) | ||
| if (!item) { | ||
| throw createError({ | ||
| statusCode: 404, | ||
| message: 'Item not found', | ||
| }) | ||
| } | ||
| if (item.views.some((view) => view.userId === event.context.user.id)) { | ||
| throw createError({ | ||
| statusCode: 400, | ||
| message: 'Already viewed', | ||
| }) | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Avoid TOCTOU and remove heavy duplicate-check query.
You fetch the whole item (including views) to detect duplicates, but this is race-prone and heavier than necessary. With a DB unique constraint and idempotent insert:
- Keep the existence check for the item.
- Drop the “already viewed” scan; let the insert no-op on conflict and return success.
- // Guards:
- // If not exist
- // If already viewed
const item = await repository.flow.findItem(itemId)
if (!item) {
throw createError({ statusCode: 404, message: 'Item not found' })
}
- if (item.views.some((view) => view.userId === event.context.user.id)) {
- throw createError({ statusCode: 400, message: 'Already viewed' })
- }📝 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.
| // Guards: | |
| // If not exist | |
| // If already viewed | |
| const item = await repository.flow.findItem(itemId) | |
| if (!item) { | |
| throw createError({ | |
| statusCode: 404, | |
| message: 'Item not found', | |
| }) | |
| } | |
| if (item.views.some((view) => view.userId === event.context.user.id)) { | |
| throw createError({ | |
| statusCode: 400, | |
| message: 'Already viewed', | |
| }) | |
| } | |
| const item = await repository.flow.findItem(itemId) | |
| if (!item) { | |
| throw createError({ | |
| statusCode: 404, | |
| message: 'Item not found', | |
| }) | |
| } |
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts around lines 13
to 29, remove the in-memory duplicate check that scans item.views for the
current user; keep the existence check for the item but do not load the full
views collection. Instead perform an idempotent DB insert for the view (INSERT
... ON CONFLICT DO NOTHING / upsert) so duplicate attempts become no-ops and are
safe from TOCTOU races; if using an ORM, use its upsert/insert-ignore API or
catch unique-constraint violations and return success, ensuring the handler
always returns success when the view already exists.
| await repository.flow.createItemView({ | ||
| itemId, | ||
| userId: event.context.user.id, | ||
| }) | ||
|
|
There was a problem hiding this comment.
Add explicit auth guard before accessing event.context.user.id.
If event.context.user is absent, this will throw. Return 401 early.
+ if (!event.context.user?.id) {
+ throw createError({ statusCode: 401, message: 'Unauthorized' })
+ }
await repository.flow.createItemView({
itemId,
userId: event.context.user.id,
})📝 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.
| await repository.flow.createItemView({ | |
| itemId, | |
| userId: event.context.user.id, | |
| }) | |
| if (!event.context.user?.id) { | |
| throw createError({ statusCode: 401, message: 'Unauthorized' }) | |
| } | |
| await repository.flow.createItemView({ | |
| itemId, | |
| userId: event.context.user.id, | |
| }) |
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts around lines 30
to 34, the handler accesses event.context.user.id without guarding against a
missing user; add an explicit auth guard that checks event.context.user (or
event.context.user?.id) before calling repository.flow.createItemView and if
absent immediately return a 401 response (or throw an unauthorized HTTP error)
so the function exits early and never attempts to read .id on undefined.
| static async createItemView(data: FlowItemViewDraft) { | ||
| const [view] = await useDatabase().insert(flowItemViews).values(data).returning() | ||
| return view | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Eliminate duplicate views with DB-level uniqueness and idempotent insert.
Current duplicate-prevention happens in app code and is race-prone (TOCTOU). Enforce a unique (itemId, userId) constraint and make the insert idempotent.
Apply this diff here to guard the insert:
static async createItemView(data: FlowItemViewDraft) {
- const [view] = await useDatabase().insert(flowItemViews).values(data).returning()
+ const [view] = await useDatabase()
+ .insert(flowItemViews)
+ .values(data)
+ .onConflictDoNothing({ target: [flowItemViews.itemId, flowItemViews.userId] })
+ .returning()
return view
}And add a unique constraint in packages/database/src/tables.ts (outside this diff):
import { flowItems } from './tables' // existing imports for context
+import { uniqueIndex } from 'drizzle-orm/pg-core'
export const flowItemViews = pgTable('flow_item_views', {
// ...existing columns...
userId: cuid2('user_id').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
-})
+}, (t) => ({
+ uniq_item_user: uniqueIndex('flow_item_views_item_id_user_id_uidx').on(t.itemId, t.userId),
+}))Run to confirm the unique index doesn’t already exist and to estimate current dupes:
🏁 Script executed:
#!/bin/bash
# Check schema for an existing unique index
rg -n "flow_item_views.*unique|uniqueIndex|unique\\(" -C2 -- packages/database/src
# (Optional) If you have DB access in your env, count duplicates (replace with your psql conn):
# psql -c "SELECT item_id, user_id, COUNT(*) FROM flow_item_views GROUP BY 1,2 HAVING COUNT(*)>1 ORDER BY COUNT(*) DESC;"Length of output: 1948
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) repo-wide occurrences of 'flow_item_views' or 'flowItemViews':"
rg -n --hidden --no-ignore "flow_item_views|flowItemViews" || true
echo
echo "2) If packages/database/src/tables.ts contains the table, show 20 lines around the first match:"
match_line=$(rg -n --hidden --no-ignore "flow_item_views" packages/database/src/tables.ts -S | head -n1 || true)
if [ -n "$match_line" ]; then
file=$(echo "$match_line" | cut -d: -f1)
line=$(echo "$match_line" | cut -d: -f2)
start=$((line-12)); if [ "$start" -lt 1 ]; then start=1; fi
sed -n "${start},$((line+60))p" "$file"
else
echo "No direct 'flow_item_views' match in packages/database/src/tables.ts"
rg -n --hidden --no-ignore "pgTable\\(|export const .* = pgTable" packages/database/src/tables.ts -n -S || true
sed -n '1,300p' packages/database/src/tables.ts || true
fi
echo
echo "3) show createItemView in packages/database/src/repository/flow.ts (around the function):"
rg -n --hidden --no-ignore "createItemView|flowItemViews|flow_item_views" packages/database/src/repository/flow.ts -n -S || true
sed -n '1,240p' packages/database/src/repository/flow.ts || true
echo
echo "4) search for unique/index/onConflict usage anywhere relevant:"
rg -n --hidden --no-ignore "uniqueIndex\\(|unique\\(|index\\(|onConflictDoNothing|onConflictDoUpdate|onConflict\\(" -S || true
echo
echo "5) search migrations / sql files for 'flow_item_views':"
rg -n --hidden --no-ignore "flow_item_views" -S || trueLength of output: 1076
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== flowItemViews occurrences (files + lines) ==="
rg -n --hidden --no-ignore "flowItemViews|flow_item_views" || true
echo
echo "=== lines 760-920 of packages/database/src/tables.ts (table definition area) ==="
sed -n '760,920p' packages/database/src/tables.ts || true
echo
echo "=== Search for uniqueIndex or unique constraints in tables.ts ==="
rg -n --hidden --no-ignore "uniqueIndex|unique\\(|uniq_item_user|flow_item_views_item_id_user_id" packages/database/src/tables.ts || true
echo
echo "=== show createItemView function in packages/database/src/repository/flow.ts ==="
sed -n '1,200p' packages/database/src/repository/flow.ts || true
echo
echo "=== repo-wide search for onConflict usage ==="
rg -n --hidden --no-ignore "onConflictDoNothing|onConflictDoUpdate|onConflict\\(" || true
echo
echo "=== search migrations/sql for flow_item_views references ==="
rg -n --hidden --no-ignore "flow_item_views" || trueLength of output: 8578
Enforce DB uniqueness for flow_item_views and make createItemView idempotent.
No composite unique index exists and createItemView currently inserts without conflict handling — add a DB-level uniqueness on (itemId, userId) and use onConflictDoNothing in the insert.
Files to change:
- packages/database/src/tables.ts (flowItemViews definition, ~line 821)
- packages/database/src/repository/flow.ts (createItemView, ~line 35)
Apply these edits:
--- packages/database/src/repository/flow.ts
+++ packages/database/src/repository/flow.ts
@@
static async createItemView(data: FlowItemViewDraft) {
- const [view] = await useDatabase().insert(flowItemViews).values(data).returning()
+ const [view] = await useDatabase()
+ .insert(flowItemViews)
+ .values(data)
+ .onConflictDoNothing({ target: [flowItemViews.itemId, flowItemViews.userId] })
+ .returning()
return view
}+import { uniqueIndex } from 'drizzle-orm/pg-core'
...
export const flowItemViews = pgTable('flow_item_views', {
// ...existing columns...
userId: cuid2('user_id').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
-})
+}, (t) => ({
+ uniq_item_user: uniqueIndex('flow_item_views_item_id_user_id_uidx').on(t.itemId, t.userId),
+}))📝 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.
| static async createItemView(data: FlowItemViewDraft) { | |
| const [view] = await useDatabase().insert(flowItemViews).values(data).returning() | |
| return view | |
| } | |
| static async createItemView(data: FlowItemViewDraft) { | |
| const [view] = await useDatabase() | |
| .insert(flowItemViews) | |
| .values(data) | |
| .onConflictDoNothing({ target: [flowItemViews.itemId, flowItemViews.userId] }) | |
| .returning() | |
| return view | |
| } |
| static async createItemView(data: FlowItemViewDraft) { | |
| const [view] = await useDatabase().insert(flowItemViews).values(data).returning() | |
| return view | |
| } | |
| import { uniqueIndex } from 'drizzle-orm/pg-core' | |
| ... | |
| export const flowItemViews = pgTable('flow_item_views', { | |
| // ...existing columns... | |
| userId: cuid2('user_id').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }), | |
| itemId: cuid2('item_id').notNull().references(() => flowItems.id, { onDelete: 'cascade', onUpdate: 'cascade' }), | |
| }, (t) => ({ | |
| uniq_item_user: uniqueIndex('flow_item_views_item_id_user_id_uidx').on(t.itemId, t.userId), | |
| })) |
🤖 Prompt for AI Agents
In packages/database/src/tables.ts around line 821, add a composite unique
constraint/index on flow_item_views for (itemId, userId) to enforce DB-level
uniqueness; in packages/database/src/repository/flow.ts around line 35, make
createItemView idempotent by using insert(...).onConflictDoNothing() so insert
does not throw on duplicate, then if the insert returned no rows fetch and
return the existing row for that (itemId, userId) pair (i.e., attempt insert
with onConflictDoNothing and returning(), and if nothing is returned run a
select by itemId and userId and return that record).
| export const flowItemViews = pgTable('flow_item_views', { | ||
| id: cuid2('id').defaultRandom().primaryKey(), | ||
| createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(), | ||
| updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(), | ||
| userId: cuid2('user_id').notNull().references(() => users.id, { | ||
| onDelete: 'cascade', | ||
| onUpdate: 'cascade', | ||
| }), | ||
| itemId: cuid2('item_id').notNull().references(() => flowItems.id, { | ||
| onDelete: 'cascade', | ||
| onUpdate: 'cascade', | ||
| }), | ||
| }) | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Enforce uniqueness and add indexes for race‑safe, performant view tracking.
Without a DB‑level unique constraint, two concurrent inserts can create duplicate (itemId, userId) rows despite app‑level checks. Add a composite unique index and a read index on itemId.
Apply:
export const flowItemViews = pgTable('flow_item_views', {
id: cuid2('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
userId: cuid2('user_id').notNull().references(() => users.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
-})
+}, (t) => ({
+ byItem: index('flow_item_views_item_idx').on(t.itemId),
+ byItemUser: uniqueIndex('flow_item_views_item_user_uniq').on(t.itemId, t.userId),
+}))And import the index helpers:
-import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'
+import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar, index, uniqueIndex } from 'drizzle-orm/pg-core'To verify there isn’t already a unique/index for this table:
🏁 Script executed:
#!/bin/bash
rg -nC2 'flow_item_views|uniqueIndex|index\(' packages/database/src/tables.tsLength of output: 244
Enforce uniqueness and add indexes for race‑safe, performant view tracking.
Prevent duplicate (itemId, userId) rows on concurrent inserts — add a composite unique index and an index on itemId. Verified: no existing index/uniqueIndex for flow_item_views in packages/database/src/tables.ts.
Apply:
export const flowItemViews = pgTable('flow_item_views', {
id: cuid2('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
userId: cuid2('user_id').notNull().references(() => users.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
-})
+}, (t) => ({
+ byItem: index('flow_item_views_item_idx').on(t.itemId),
+ byItemUser: uniqueIndex('flow_item_views_item_user_uniq').on(t.itemId, t.userId),
+}))And import the index helpers:
-import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'
+import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar, index, uniqueIndex } from 'drizzle-orm/pg-core'📝 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.
| export const flowItemViews = pgTable('flow_item_views', { | |
| id: cuid2('id').defaultRandom().primaryKey(), | |
| createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(), | |
| updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(), | |
| userId: cuid2('user_id').notNull().references(() => users.id, { | |
| onDelete: 'cascade', | |
| onUpdate: 'cascade', | |
| }), | |
| itemId: cuid2('item_id').notNull().references(() => flowItems.id, { | |
| onDelete: 'cascade', | |
| onUpdate: 'cascade', | |
| }), | |
| }) | |
| import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar, index, uniqueIndex } from 'drizzle-orm/pg-core' | |
| export const flowItemViews = pgTable('flow_item_views', { | |
| id: cuid2('id').defaultRandom().primaryKey(), | |
| createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(), | |
| updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(), | |
| userId: cuid2('user_id').notNull().references(() => users.id, { | |
| onDelete: 'cascade', | |
| onUpdate: 'cascade', | |
| }), | |
| itemId: cuid2('item_id').notNull().references(() => flowItems.id, { | |
| onDelete: 'cascade', | |
| onUpdate: 'cascade', | |
| }), | |
| }, (t) => ({ | |
| byItem: index('flow_item_views_item_idx').on(t.itemId), | |
| byItemUser: uniqueIndex('flow_item_views_item_user_uniq').on(t.itemId, t.userId), | |
| })) |
🤖 Prompt for AI Agents
In packages/database/src/tables.ts around lines 821 to 834, the flow_item_views
table needs a composite unique constraint on (itemId, userId) and a separate
index on itemId to prevent duplicate rows under concurrent inserts and to speed
lookups; import the index helper(s) (e.g., uniqueIndex and index) at the top of
the file and add
uniqueIndex('flow_item_views_item_user_unique').on(flowItemViews.itemId,
flowItemViews.userId) and
index('flow_item_views_item_idx').on(flowItemViews.itemId) (or equivalent helper
usage for your schema library) immediately after the table definition.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (15)
apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1)
26-30: Always-true length check.
epic?.comments.length >= 0is always true whencommentsexists. Use> 0for intent.- <div v-if="epic?.comments && epic?.comments.length >= 0" class="flex flex-row items-center gap-2"> + <div v-if="epic?.comments && epic?.comments.length > 0" class="flex flex-row items-center gap-2">apps/atrium-telegram/app/components/flow/OrderAmountAverage.vue (1)
7-13: Prefer locale-aware currency formatting.Avoid manual "₽" concatenation; use Intl.NumberFormat to respect locale and spacing.
- <h3 class="text-2xl/5 font-bold"> - {{ averageToday }} - </h3> - <p class="text-lg/4"> - ₽ - </p> + <h3 class="text-2xl/5 font-bold"> + {{ averageTodayFormatted }} + </h3>Add in <script setup>:
const averageToday = ref(1635) const averageTodayFormatted = computed(() => new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format(averageToday.value) )apps/atrium-telegram/app/components/flow/OrdersOnline.vue (1)
6-8: Handle undefined during initial render.Render 0 (or a placeholder) before store hydration to avoid an empty h3.
- <h3 class="text-2xl/5 font-bold"> - {{ kitchenStore.todayData?.ordersForNow }} + <h3 class="text-2xl/5 font-bold"> + {{ kitchenStore.todayData?.ordersForNow ?? 0 }}apps/atrium-telegram/app/stores/user.ts (1)
114-117: Reuse existing finder to avoid duplicate lookup and keep API consistentUse the store’s
find()to DRY up and centralize user lookup.- function getAvatarUrl(userId: string): string | undefined { - const user = users.value.find((user) => user.id === userId) - return user?.avatarUrl ?? undefined - } + function getAvatarUrl(userId: string): string | undefined { + return find(userId)?.avatarUrl ?? undefined + }apps/atrium-telegram/app/pages/tasks/index.vue (1)
11-14: Heading level change: verify page-level hierarchySwitching to h1 is fine; ensure there’s a single h1 per page and surrounding sections maintain a logical heading order for a11y.
apps/atrium-telegram/app/components/NavigationButton.vue (1)
25-27: Badge visibility check and UChip color prop
- Prefer numeric check to avoid edge cases with whitespace or non-numeric strings.
- Confirm
UChipsupportscolor="error"in your UI library/version.- color="error" - :show="!!route.badge && route.badge !== '0'" + color="error" + :show="Number(route.badge) > 0"apps/atrium-telegram/app/composables/useNavigation.ts (1)
16-16: Avoid flashing a large badge before user is knownWhen
useUserStore().idis not yet available,nowViewedItemsCountcounts all items, causing a distracting badge on the home route. Gate the count until user id is set (recommend fix in flow store).In
apps/atrium-telegram/app/stores/flow.ts:const userId = computed(() => useUserStore().id) const nowViewedItemsCount = computed(() => { if (!userId.value) return 0 return items.value.filter((item) => !item.views.some((v) => v.userId === userId.value)).length })apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (2)
17-29: Viewer avatars: minor UX polishConsider showing a placeholder or “No views yet” state when
item?.views.length === 0to avoid an empty section.
47-51: Guard addView with user presence to avoid premature callsPrevents firing before user id is resolved; still safe with idempotent server.
-watch(isViewed, () => { - if (!isViewed.value && item.value?.id) { - flowStore.addView(item.value.id) - } -}, { immediate: true }) +watch(isViewed, () => { + if (userStore.id && !isViewed.value && item.value?.id) { + flowStore.addView(item.value.id) + } +}, { immediate: true })apps/atrium-telegram/app/stores/flow.ts (2)
34-54: Handle “already viewed” (400) and avoid noisy errors; consider lighter refresh.
- Client ignores 400 from server (“Already viewed”); swallow it to prevent user-visible errors.
- Optional: update the single item locally instead of refetching the whole list.
Apply:
async function addView(itemId: string) { try { await $fetch(`/api/flow/id/${itemId}/view`, { method: 'POST', headers: { Authorization: `tma ${initDataRaw.value}`, }, }) - await update() + await update() } catch (error) { - if (error instanceof Error) { - if (error.message.includes('401')) { - // No - } - if (error.message.includes('404')) { - // Not found - } - } + if (error instanceof Error) { + // Nuxt $fetch often includes status code in message; adapt if you use a typed FetchError helper. + if (error.message.includes('400')) { + // Already viewed + return + } + if (error.message.includes('401')) { + // No + return + } + if (error.message.includes('404')) { + // Not found + return + } + } + // Optional: rethrow others or log } }If you want the lighter refresh, instead of
await update()append the new view to the matching item:const myId = userId.value const idx = items.value.findIndex(i => i.id === itemId) if (idx !== -1 && myId) { items.value[idx] = { ...items.value[idx], views: [...(items.value[idx].views ?? []), { itemId, userId: myId }] } }
59-63: Naming nit: “nowViewedItemsCount” reads like “viewed now.”If it counts unviewed items, prefer
unviewedItemsCount(ornotViewedItemsCount) for clarity. Update consumers accordingly.apps/atrium-telegram/app/components/Navigation.vue (2)
4-15: Wire the “plus” button and add an accessible label.Button has no action or label; add @click and aria-label/title.
- <UButton + <UButton v-if="isMainPage" variant="solid" color="secondary" size="xl" class="transition-all duration-200 ease-in-out motion-preset-slide-down motion-duration-200" icon="i-lucide-plus" + aria-label="Create" + title="Create" + @click="/* TODO: navigate to create flow item */" :ui="{ base: 'size-12 font-bold rounded-full', leadingIcon: 'size-6 mx-auto', }" />
2-3: Verifyh-38exists in your CSS preset.If using Tailwind default scale,
h-38isn’t standard; switch toh-[9.5rem]or confirm UnoCSS preset supports it.apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1)
23-28: Race-safe duplicate prevention.Two concurrent requests can pass the check and insert twice. Enforce a DB unique (itemId,userId) and use an upsert/do-nothing; map conflict to 200/204 client response.
- if (item.views.some((view) => view.userId === event.context.user.id)) { + if (item.views.some((view) => view.userId === event.context.user.id)) { throw createError({ statusCode: 400, message: 'Already viewed', }) } @@ - await repository.flow.createItemView({ + await repository.flow.createItemView({ itemId, userId: event.context.user.id, })DB side (tables): add unique index (item_id, user_id) and make
createItemViewuseonConflictDoNothing.apps/atrium-telegram/app/components/flow/ItemCard.vue (1)
6-15: Micro‑UX: “Not viewed” block could be less jumpy.Consider reducing motion or adding
prefers-reduced-motionguards for accessibility.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (21)
apps/atrium-telegram/app/components/EpicCard.vue(1 hunks)apps/atrium-telegram/app/components/Navigation.vue(2 hunks)apps/atrium-telegram/app/components/NavigationButton.vue(1 hunks)apps/atrium-telegram/app/components/flow/FeedbackAverage.vue(1 hunks)apps/atrium-telegram/app/components/flow/ItemCard.vue(2 hunks)apps/atrium-telegram/app/components/flow/KitchensOnline.vue(1 hunks)apps/atrium-telegram/app/components/flow/OrderAmountAverage.vue(1 hunks)apps/atrium-telegram/app/components/flow/OrdersOnline.vue(1 hunks)apps/atrium-telegram/app/composables/useNavigation.ts(2 hunks)apps/atrium-telegram/app/pages/epic/[epicId]/index.vue(1 hunks)apps/atrium-telegram/app/pages/flow/[itemId]/index.vue(2 hunks)apps/atrium-telegram/app/pages/tasks/index.vue(1 hunks)apps/atrium-telegram/app/stores/flow.ts(2 hunks)apps/atrium-telegram/app/stores/user.ts(2 hunks)apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts(1 hunks)apps/atrium-telegram/shared/types/index.ts(2 hunks)package.json(1 hunks)packages/database/src/repository/flow.ts(3 hunks)packages/database/src/tables.ts(3 hunks)packages/database/src/types.ts(1 hunks)pnpm-workspace.yaml(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
apps/atrium-telegram/app/stores/flow.ts (1)
apps/atrium-telegram/app/stores/user.ts (1)
useUserStore(8-141)
apps/atrium-telegram/app/stores/user.ts (1)
packages/database/src/tables.ts (1)
users(73-91)
apps/atrium-telegram/app/composables/useNavigation.ts (1)
apps/atrium-telegram/app/stores/flow.ts (1)
useFlowStore(3-64)
apps/atrium-telegram/shared/types/index.ts (1)
packages/database/src/types.ts (1)
FlowItemView(209-209)
packages/database/src/repository/flow.ts (2)
packages/database/src/types.ts (1)
FlowItemViewDraft(210-210)packages/database/src/tables.ts (1)
flowItemViews(821-833)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Socket Security: Pull Request Alerts
- GitHub Check: build
🔇 Additional comments (18)
apps/atrium-telegram/app/components/flow/KitchensOnline.vue (1)
13-15: Typography tweak LGTM.No logic changes; visual scale aligns with adjacent Flow widgets.
apps/atrium-telegram/app/components/flow/FeedbackAverage.vue (1)
8-12: UI-only change LGTM.Consistent scale with other Flow cards; no logic impact.
apps/atrium-telegram/app/components/EpicCard.vue (2)
5-7: Title scale bump LGTM.Visual hierarchy reads better against the icon.
9-11: Description color: confirm readability intent.Found same pattern (no
text-muted) in:
- apps/atrium-telegram/app/components/EpicCard.vue:9
- apps/atrium-telegram/app/components/flow/ItemCard.vue:21
Dropping
text-mutedincreases contrast — keep if intentional; otherwise restoretext-mutedto reduce visual noise in dense lists.pnpm-workspace.yaml (1)
57-58: Regenerate pnpm lockfile and re-run full typecheck (install deps first).pnpm-lock.yaml has no "nuxt" entries; typecheck failed with multiple TS2307/TS7006 errors in packages/geo-database because node_modules are missing. Run: pnpm -w install && git add pnpm-lock.yaml && pnpm -r run typecheck. Verify a single Nuxt version is locked.
package.json (1)
49-51: Incorrect: pnpm does not ignore Yarnresolutions— preferpnpm.overridesfor explicitnesspnpm merges package.json
resolutionswithpnpm.overrides(withpnpm.overridestaking precedence). pnpm-lock.yaml already resolves rollup@4.50.2, so no immediate fix is required; add or convert to a pnpm-specific override (pnpm.overridesor top-leveloverrides) only if you want explicit pnpm configuration.Likely an incorrect or invalid review comment.
apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1)
16-18: OK to keep this— no app-level heading found in this app.
PageContainer.vue does not render an
, and no layouts/components in apps/atrium-telegram emit an
; only page files under apps/atrium-telegram/app/pages/* contain
, so this page-level heading is appropriate.
apps/atrium-telegram/app/stores/user.ts (1)
139-139: LGTM: exposinggetAvatarUrlfrom the storePublic surface looks consistent with current usage in Flow item view.
packages/database/src/tables.ts (1)
1312-1313: Relations for views look correct
flowItemRelations.viewsandflowItemViewRelationswiring is consistent with the schema.Also applies to: 1326-1335
apps/atrium-telegram/shared/types/index.ts (2)
1-1: Imports updated correctly
FlowItemViewimport aligns with DB types.
13-13: Augmenting FlowItemWithData with views is consistentType shape matches repository eager-loading changes.
packages/database/src/repository/flow.ts (2)
10-12: Eager-loadingviewsis appropriateEnsures client types are fulfilled without extra roundtrips.
34-37: Make createItemView idempotent (DB-level conflict handling)Add DB-level conflict handling on (userId, itemId) and keep returning() so callers get the created row or undefined on conflict.
File: packages/database/src/repository/flow.ts (lines 34–37)
static async createItemView(data: FlowItemViewDraft) { - const [view] = await useDatabase().insert(flowItemViews).values(data).returning() - return view + const [view] = await useDatabase() + .insert(flowItemViews) + .values(data) + .onConflictDoNothing({ target: [flowItemViews.userId, flowItemViews.itemId] }) + .returning() + return view }
- Ensure a UNIQUE constraint/index exists on (userId, itemId).
- Confirm no callers rely on a non-undefined return; if they do, change the method to return void or { created: boolean }.
apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (1)
8-11: Heading level change: fineTitle promoted to h1; consistent with the broader typography pass.
packages/database/src/types.ts (1)
209-210: Types for FlowItemView look correct and consistent with existing patterns.Matches the
FlowItemCommentpattern and will type repo/relations cleanly.apps/atrium-telegram/app/components/Navigation.vue (1)
34-35: Script import hint (if needed).If
useNavigationgainedisMainPage, ensure its export type is updated and consumers compiled. Looks fine otherwise.apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1)
35-39: Error translation dependency.Confirm
errorResolveris auto-imported server-side; otherwise import it to avoid runtime ReferenceError.[suggest_minor_issue]
apps/atrium-telegram/app/components/flow/ItemCard.vue (1)
17-18: Heading size change acknowledged.Visual change to
text-xl/5looks consistent with the new header spacing.
| const userStore = useUserStore() | ||
| const isViewed = computed(() => item.views.some((view) => view.userId === userStore?.id)) | ||
| </script> |
There was a problem hiding this comment.
Fix Ref-vs-string comparison in isViewed.
userStore.id is a Ref; compare against its value.
-const userStore = useUserStore()
-const isViewed = computed(() => item.views.some((view) => view.userId === userStore?.id))
+const userStore = useUserStore()
+const { id: userId } = storeToRefs(userStore)
+const isViewed = computed(() => item.views?.some((view) => view.userId === userId.value))📝 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.
| const userStore = useUserStore() | |
| const isViewed = computed(() => item.views.some((view) => view.userId === userStore?.id)) | |
| </script> | |
| const userStore = useUserStore() | |
| const { id: userId } = storeToRefs(userStore) | |
| const isViewed = computed(() => item.views?.some((view) => view.userId === userId.value)) | |
| </script> |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/flow/ItemCard.vue around lines 50-52, the
computed isViewed compares item.views' userId to userStore.id which is a Ref;
change the comparison to use the Ref's inner value (e.g. use userStore.id.value
or unref(userStore.id)) so the equality checks the actual id string/number
rather than the Ref object, and guard with optional chaining if userStore or id
may be undefined.
| const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length) | ||
|
|
There was a problem hiding this comment.
Fix Ref-vs-string comparison in computed (using user id).
useUserStore().id is a Ref; you’re comparing string === Ref, which will fail. Bind the store once and unref the id.
Apply:
- const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length)
+ const userStore = useUserStore()
+ const { id: userId } = storeToRefs(userStore)
+ const nowViewedItemsCount = computed(() =>
+ items.value.filter((item) => !item.views?.some((view) => view.userId === userId.value)).length
+ )📝 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.
| const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length) | |
| const userStore = useUserStore() | |
| const { id: userId } = storeToRefs(userStore) | |
| const nowViewedItemsCount = computed(() => | |
| items.value.filter((item) => !item.views?.some((view) => view.userId === userId.value)).length | |
| ) |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/stores/flow.ts around lines 6-7, the computed
predicate compares a string to a Ref (useUserStore().id) which always fails;
cache the store outside the computed (e.g., const user = useUserStore()) and
inside the computed compare against the unwrapped id (user.id.value or
unref(user.id)) so the filter checks string === string and avoid calling
useUserStore() repeatedly.
| const itemId = getRouterParam(event, 'itemId') | ||
| if (!itemId) { | ||
| throw createError({ | ||
| statusCode: 400, | ||
| message: 'Id is required', | ||
| }) | ||
| } | ||
|
|
There was a problem hiding this comment.
Add auth guard to avoid NPE and return 401 when unauthenticated.
event.context.user may be undefined; current code would 500 on property access.
const itemId = getRouterParam(event, 'itemId')
if (!itemId) {
throw createError({
statusCode: 400,
message: 'Id is required',
})
}
+ if (!event.context.user?.id) {
+ throw createError({
+ statusCode: 401,
+ message: 'Unauthorized',
+ })
+ }📝 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.
| const itemId = getRouterParam(event, 'itemId') | |
| if (!itemId) { | |
| throw createError({ | |
| statusCode: 400, | |
| message: 'Id is required', | |
| }) | |
| } | |
| const itemId = getRouterParam(event, 'itemId') | |
| if (!itemId) { | |
| throw createError({ | |
| statusCode: 400, | |
| message: 'Id is required', | |
| }) | |
| } | |
| if (!event.context.user?.id) { | |
| throw createError({ | |
| statusCode: 401, | |
| message: 'Unauthorized', | |
| }) | |
| } |
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts around lines
5-12, add an authentication guard that checks event.context?.user before any
property access and return a 401 when missing; specifically, before using itemId
or any user properties, if event.context?.user is falsy throw createError({
statusCode: 401, message: 'Unauthorized' }); then proceed with the existing
itemId validation (400) as before so you avoid null/undefined property access
and correctly signal unauthenticated requests.
| export const flowItemViews = pgTable('flow_item_views', { | ||
| id: cuid2('id').defaultRandom().primaryKey(), | ||
| createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(), | ||
| updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(), | ||
| userId: cuid2('user_id').notNull().references(() => users.id, { | ||
| onDelete: 'cascade', | ||
| onUpdate: 'cascade', | ||
| }), | ||
| itemId: cuid2('item_id').notNull().references(() => flowItems.id, { | ||
| onDelete: 'cascade', | ||
| onUpdate: 'cascade', | ||
| }), | ||
| }) | ||
|
|
There was a problem hiding this comment.
Make item views idempotent: add unique constraint (userId, itemId) and helpful indexes
Without a uniqueness guard, repeated POSTs (watchers, retries) will create duplicate views. Add a composite unique index and indexes for joins/filtering.
export const flowItemViews = pgTable('flow_item_views', {
id: cuid2('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
userId: cuid2('user_id').notNull().references(() => users.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
-})
+}, (t) => ({
+ uiUserItem: uniqueIndex('ui_flow_item_views_user_item').on(t.userId, t.itemId),
+ idxItemId: index('idx_flow_item_views_item_id').on(t.itemId),
+ idxUserId: index('idx_flow_item_views_user_id').on(t.userId),
+}))Add missing imports (outside this hunk):
// at the pg-core import
import { ..., index, uniqueIndex } from 'drizzle-orm/pg-core'🤖 Prompt for AI Agents
In packages/database/src/tables.ts around lines 821 to 834, the flow_item_views
table allows duplicate rows for the same userId and itemId and lacks helpful
indexes; add a composite unique constraint on (userId, itemId) to make views
idempotent and add non-unique indexes on userId and itemId to speed
joins/filters, and update the pg-core import to include index and uniqueIndex
from 'drizzle-orm/pg-core'.



Summary by CodeRabbit
New Features
UI/Style
Chores