refactor(trpc): centralize pet-identity select fragments in a repository layer

The Prisma select fragment { id, name, nickname, avatarKey } (and its
isVerified + owner-sponsor extension) was copy-pasted ~38x across 13
router files -- no repository/query layer existed to consolidate it
(Gitea #31 DRY audit).

Adds src/repositories/pet-repository.ts exporting petIdentitySelect and
petCardSelect (composed via spread) plus their inferred Prisma payload
types, and migrates every call site to import and reuse them -- either
directly or composed with the few extra fields a given query needs
(e.g. ownerId, bio, isVerified where the base fragment doesn't cover
it). A handful of structurally different, smaller field selections
(e.g. name+nickname only, no id) were deliberately left as-is rather
than force-fit into the shared shape.

Pure refactor, no behavior change -- verified via tsc, vitest, and a
production build.
This commit is contained in:
admin committed 2026-08-21 22:01:57 +02:00
1 parent 1f177c5e88
commit e0c887c71c
14 files changed
+78 -74

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
import type { Prisma } from "@prisma/client";
/**
* Minimal pet identity — id/name/nickname/avatarKey. This is the shape every
* "who posted/commented/follows this" display needs; compose on top of it
* (spread + extra fields) rather than hand-writing the fragment again.
*/
export const petIdentitySelect = {
id: true,
name: true,
nickname: true,
avatarKey: true,
} satisfies Prisma.PetSelect;
/**
* Pet identity plus the fields a feed/explore/profile card renders alongside
* the name: the verified checkmark and the owner's sponsor window (consumed
* via isActiveSponsor() in src/lib/sponsor.ts).
*/
export const petCardSelect = {
...petIdentitySelect,
isVerified: true,
owner: { select: { sponsorSince: true, sponsorPeriodEnd: true } },
} satisfies Prisma.PetSelect;
export type PetIdentity = Prisma.PetGetPayload<{ select: typeof petIdentitySelect }>;
export type PetCardIdentity = Prisma.PetGetPayload<{ select: typeof petCardSelect }>;
+12 -14
View File
@@ -5,6 +5,7 @@ import { TRPCError } from "@trpc/server";
import { AdminRoleType, BlacklistEntryType, type PrismaClient, type ReportReason } from "@prisma/client";
import { clerkClient } from "@clerk/nextjs/server";
import { router, protectedProcedure } from "@/trpc/init";
import { petIdentitySelect } from "@/repositories/pet-repository";
import {
createSignedUploadUrl,
ALLOWED_CONTENT_TYPES,
@@ -291,7 +292,7 @@ export const adminRouter = router({
}
: undefined,
include: {
pet: { select: { id: true, name: true, nickname: true, avatarKey: true, ownerId: true } },
pet: { select: { ...petIdentitySelect, ownerId: true } },
images: { orderBy: { position: "asc" }, take: 1 },
videoPost: { select: { muxPlaybackId: true } },
milestone: true,
@@ -557,7 +558,7 @@ export const adminRouter = router({
await assertAdmin(ctx.prisma, ctx.userId);
return ctx.prisma.comment.findMany({
where: { postId: input.postId },
include: { pet: { select: { id: true, name: true, nickname: true, avatarKey: true } } },
include: { pet: { select: petIdentitySelect } },
orderBy: { createdAt: "asc" },
});
}),
@@ -632,7 +633,7 @@ export const adminRouter = router({
...(input.role !== "all" && input.role !== "none" ? { adminRole: { role: input.role } } : {}),
},
include: {
pets: { select: { id: true, name: true, nickname: true, avatarKey: true, isVerified: true }, take: 3 },
pets: { select: { ...petIdentitySelect, isVerified: true }, take: 3 },
ban: true,
shadowban: true,
adminRole: true,
@@ -862,10 +863,7 @@ export const adminRouter = router({
include: {
pet: {
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
...petIdentitySelect,
isVerified: true,
ownerId: true,
bio: true,
@@ -1061,8 +1059,8 @@ export const adminRouter = router({
const convs = await ctx.prisma.conversation.findMany({
where: { OR: [{ petAId: teamPet.id }, { petBId: teamPet.id }] },
include: {
petA: { select: { id: true, name: true, nickname: true, avatarKey: true } },
petB: { select: { id: true, name: true, nickname: true, avatarKey: true } },
petA: { select: petIdentitySelect },
petB: { select: petIdentitySelect },
messages: { orderBy: { createdAt: "desc" }, take: 1 },
_count: {
select: { messages: { where: { senderPetId: { not: teamPet.id }, readAt: null } } },
@@ -1096,7 +1094,7 @@ export const adminRouter = router({
where: { conversationId: input.conversationId },
orderBy: { createdAt: "asc" },
take: 200,
include: { senderPet: { select: { id: true, name: true, nickname: true, avatarKey: true } } },
include: { senderPet: { select: petIdentitySelect } },
});
// Fire-and-forget — any admin opening the thread marks it read for the whole team.
@@ -1199,7 +1197,7 @@ export const adminRouter = router({
const reports = await ctx.prisma.report.findMany({
include: {
reporter: { select: { id: true, name: true, nickname: true, avatarKey: true } },
reporter: { select: petIdentitySelect },
},
orderBy: { createdAt: "desc" },
take: 100,
@@ -1220,7 +1218,7 @@ export const adminRouter = router({
createdAt: true,
reactionCount: true,
commentCount: true,
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
images: {
orderBy: { position: "asc" },
take: 4,
@@ -1263,7 +1261,7 @@ export const adminRouter = router({
type: true,
createdAt: true,
hiddenAt: true,
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
images: {
orderBy: { position: "asc" },
select: { storageKey: true, position: true, blurhash: true },
@@ -1272,7 +1270,7 @@ export const adminRouter = router({
orderBy: { createdAt: "desc" },
take: 200,
select: {
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
},
},
},
+3 -2
View File
@@ -2,6 +2,7 @@ import "server-only";
import { z } from "zod";
import { router, protectedProcedure } from "@/trpc/init";
import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { petIdentitySelect } from "@/repositories/pet-repository";
export const adsRouter = router({
// Returns view/reaction/comment/repost counts + whether the given pet has interacted
@@ -91,7 +92,7 @@ export const adsRouter = router({
return ctx.prisma.adComment.findMany({
where: { adId: input.adId },
include: {
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
},
orderBy: { createdAt: "asc" },
take: 100,
@@ -113,7 +114,7 @@ export const adsRouter = router({
ctx.prisma.adComment.create({
data: { adId: input.adId, petId: input.petId, body: input.body },
include: {
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
},
}),
ctx.prisma.advertisement.update({
+3 -2
View File
@@ -5,6 +5,7 @@ import { router, protectedProcedure, rateLimited } from "@/trpc/init";
import { createMentionNotifications } from "@/lib/mention-helpers";
import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { notify } from "@/lib/notify";
import { petIdentitySelect } from "@/repositories/pet-repository";
export const commentsRouter = router({
create: protectedProcedure
@@ -27,7 +28,7 @@ export const commentsRouter = router({
body: input.body,
},
include: {
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
},
}),
ctx.prisma.post.update({
@@ -77,7 +78,7 @@ export const commentsRouter = router({
orderBy: { createdAt: "asc" },
take: input.limit + 1,
include: {
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
},
});
+2 -10
View File
@@ -3,6 +3,7 @@ import { z } from "zod";
import { router, publicProcedure, rateLimited } from "@/trpc/init";
import { pickLocalizedName } from "@/lib/localized-name";
import { getSpiderExclusionId } from "@/lib/spider-exclusion";
import { petCardSelect } from "@/repositories/pet-repository";
const TRENDING_POSTS_WINDOW_MS = 48 * 60 * 60 * 1000; // 48 h
const TRENDING_HASHTAGS_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
@@ -75,16 +76,7 @@ export const exploreRouter = router({
},
include: {
images: { orderBy: { position: "asc" } },
pet: {
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
isVerified: true,
owner: { select: { sponsorSince: true, sponsorPeriodEnd: true } },
},
},
pet: { select: petCardSelect },
milestone: true,
},
orderBy: [{ pinnedAt: { sort: "desc", nulls: "last" } }, { reactionCount: "desc" }],
+3 -20
View File
@@ -6,6 +6,7 @@ import { redis } from "@/lib/redis";
import { getFeedPage } from "@/lib/feed-helpers";
import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { getSpiderExclusionId } from "@/lib/spider-exclusion";
import { petCardSelect } from "@/repositories/pet-repository";
// Engagement-based ranking score for the algorithmic feed.
// Recency decays logarithmically; reaction/comment/repost weights are tuned
@@ -27,16 +28,7 @@ function algoScore(
}
const postInclude = {
pet: {
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
isVerified: true,
owner: { select: { sponsorSince: true, sponsorPeriodEnd: true } },
},
},
pet: { select: petCardSelect },
images: { orderBy: { position: "asc" } as const },
milestone: true,
videoPost: { select: { status: true, muxPlaybackId: true } },
@@ -44,16 +36,7 @@ const postInclude = {
include: {
originalPost: {
include: {
pet: {
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
isVerified: true,
owner: { select: { sponsorSince: true, sponsorPeriodEnd: true } },
},
},
pet: { select: petCardSelect },
images: { orderBy: { position: "asc" } as const },
milestone: true,
},
+5 -10
View File
@@ -8,6 +8,7 @@ import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { notify } from "@/lib/notify";
import { FOLLOWER_MILESTONE_THRESHOLDS as FOLLOWER_MILESTONES } from "@/lib/milestone-thresholds";
import { pickLocalizedName } from "@/lib/localized-name";
import { petIdentitySelect } from "@/repositories/pet-repository";
/**
* Follows tRPC router — pet-to-pet social graph (SOCL-01, SOCL-02, SOCL-03).
@@ -83,7 +84,7 @@ export const followsRouter = router({
include: {
images: { orderBy: { position: "asc" } },
milestone: true,
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
},
});
@@ -174,10 +175,7 @@ export const followsRouter = router({
include: {
follower: {
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
...petIdentitySelect,
species: { select: { name: true, translations: { where: { locale: ctx.locale } } } },
},
},
@@ -221,7 +219,7 @@ export const followsRouter = router({
},
},
select: {
followee: { select: { id: true, name: true, nickname: true, avatarKey: true } },
followee: { select: petIdentitySelect },
},
orderBy: { createdAt: "desc" },
take: 100,
@@ -252,10 +250,7 @@ export const followsRouter = router({
include: {
followee: {
select: {
id: true,
name: true,
nickname: true,
avatarKey: true,
...petIdentitySelect,
species: { select: { name: true, translations: { where: { locale: ctx.locale } } } },
},
},
+5 -4
View File
@@ -5,6 +5,7 @@ import { router, protectedProcedure, rateLimited } from "@/trpc/init";
import type { PrismaClient } from "@prisma/client";
import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { notify } from "@/lib/notify";
import { petIdentitySelect } from "@/repositories/pet-repository";
/**
* Messages tRPC router — Phase 6 Direct Messages.
@@ -50,8 +51,8 @@ export const conversationsRouter = router({
const convs = await ctx.prisma.conversation.findMany({
where: { OR: [{ petAId: input.petId }, { petBId: input.petId }] },
include: {
petA: { select: { id: true, name: true, nickname: true, avatarKey: true } },
petB: { select: { id: true, name: true, nickname: true, avatarKey: true } },
petA: { select: petIdentitySelect },
petB: { select: petIdentitySelect },
messages: {
orderBy: { createdAt: "desc" },
take: 1,
@@ -188,7 +189,7 @@ export const messagesRouter = router({
orderBy: { createdAt: "asc" },
take: input.limit + 1,
include: {
senderPet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
senderPet: { select: petIdentitySelect },
},
});
@@ -237,7 +238,7 @@ export const messagesRouter = router({
senderPetId: input.senderPetId,
body: input.body,
},
include: { senderPet: { select: { id: true, name: true, nickname: true, avatarKey: true } } },
include: { senderPet: { select: petIdentitySelect } },
}),
// Touch conversation updatedAt so inbox re-orders correctly
ctx.prisma.conversation.update({
+3 -2
View File
@@ -5,6 +5,7 @@ import { redis } from "@/lib/redis";
import { fanOutPost } from "@/lib/feed-helpers";
import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { USER_INITIATED_MILESTONE_TYPES } from "@/lib/milestone-meta";
import { petIdentitySelect } from "@/repositories/pet-repository";
/**
* Milestones tRPC router — structured milestone posts (MILE-01, MILE-02).
@@ -66,7 +67,7 @@ export const milestonesRouter = router({
include: {
images: { orderBy: { position: "asc" } },
milestone: true,
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
},
});
@@ -97,7 +98,7 @@ export const milestonesRouter = router({
include: {
milestone: true,
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
},
orderBy: { createdAt: "desc" },
take: input.limit,
+2 -1
View File
@@ -3,6 +3,7 @@ import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { router, protectedProcedure } from "@/trpc/init";
import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { petIdentitySelect } from "@/repositories/pet-repository";
const PAGE_SIZE = 30;
@@ -26,7 +27,7 @@ export const notificationsRouter = router({
orderBy: { createdAt: "desc" },
take: input.limit + 1,
include: {
actorPet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
actorPet: { select: petIdentitySelect },
post: { select: { id: true, petId: true, images: { take: 1, orderBy: { position: "asc" } } } },
comment: { select: { id: true, body: true } },
},
+2 -1
View File
@@ -4,6 +4,7 @@ import { router, protectedProcedure } from "@/trpc/init";
import { generateInviteCode, INVITE_CODES_PER_USER } from "@/lib/invite-codes";
import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { pickLocalizedName } from "@/lib/localized-name";
import { petCardSelect } from "@/repositories/pet-repository";
export const petsRouter = router({
/**
@@ -124,7 +125,7 @@ export const petsRouter = router({
include: {
species: { include: { translations: { where: { locale: ctx.locale } } } },
breed: { include: { translations: { where: { locale: ctx.locale } } } },
owner: { select: { sponsorSince: true, sponsorPeriodEnd: true } },
owner: petCardSelect.owner,
},
});
if (!pet) throw new TRPCError({ code: "NOT_FOUND" });
+7 -6
View File
@@ -15,6 +15,7 @@ import { extractHashtags, saveHashtags } from "@/lib/hashtag-helpers";
import { checkAgainstBlacklist } from "@/lib/blacklist";
import { createMentionNotifications } from "@/lib/mention-helpers";
import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { petIdentitySelect } from "@/repositories/pet-repository";
/**
* Posts tRPC router — photo post creation, caption edit, deletion, feed hydration.
@@ -129,7 +130,7 @@ export const postsRouter = router({
},
include: {
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
milestone: true,
},
});
@@ -197,7 +198,7 @@ export const postsRouter = router({
data: { caption: input.caption },
include: {
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
milestone: true,
},
});
@@ -249,14 +250,14 @@ export const postsRouter = router({
take: input.limit + 1,
include: {
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
milestone: true,
videoPost: { select: { status: true, muxPlaybackId: true } },
repost: {
include: {
originalPost: {
include: {
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
images: { orderBy: { position: "asc" } },
milestone: true,
},
@@ -291,14 +292,14 @@ export const postsRouter = router({
},
include: {
images: { orderBy: { position: "asc" } },
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
milestone: true,
videoPost: { select: { status: true, muxPlaybackId: true } },
repost: {
include: {
originalPost: {
include: {
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
images: { orderBy: { position: "asc" } },
milestone: true,
},
+2 -1
View File
@@ -4,6 +4,7 @@ import { router, protectedProcedure } from "@/trpc/init";
import { clerkClient } from "@clerk/nextjs/server";
import { pickLocalizedName } from "@/lib/localized-name";
import { getSpiderExclusionId } from "@/lib/spider-exclusion";
import { petIdentitySelect } from "@/repositories/pet-repository";
export const searchRouter = router({
pets: protectedProcedure
@@ -85,7 +86,7 @@ export const searchRouter = router({
include: {
post: {
include: {
pet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
pet: { select: petIdentitySelect },
images: { orderBy: { position: "asc" }, take: 1 },
},
},
+2 -1
View File
@@ -8,6 +8,7 @@ import {
MAX_FILE_SIZE,
} from "@/lib/supabase-storage";
import { randomUUID } from "crypto";
import { petIdentitySelect } from "@/repositories/pet-repository";
import { assertPetOwnership } from "@/lib/assert-pet-ownership";
import { mux } from "@/lib/mux";
import { MAX_STORY_VIDEO_DURATION_SECS } from "@/lib/story-video-limits";
@@ -270,7 +271,7 @@ export const storiesRouter = router({
return ctx.prisma.storyView.findMany({
where: { storyId: input.storyId },
include: {
viewerPet: { select: { id: true, name: true, nickname: true, avatarKey: true } },
viewerPet: { select: petIdentitySelect },
},
orderBy: { viewedAt: "desc" },
take: 200,