feat(02-01): shared libs + 7 tRPC router stubs registered in _app.ts
- src/lib/redis.ts: @upstash/redis singleton (server-only)
- src/lib/feed-helpers.ts: feedKey, fanOutPost, backfillOnFollow, getFeedPage, trimFeed, FEED_RETENTION_MS
- src/lib/media-url.ts: getMediaUrl (mirrors getAvatarUrl signature, client-safe)
- src/lib/milestone-thresholds.ts: FOLLOWER_MILESTONE_THRESHOLDS constant (D-09, A3)
- Router stubs: posts, stories, milestones, follows, blocks, reports, feed (all NOT_IMPLEMENTED)
- All 9 routers registered in _app.ts (pets, media + 7 new)
- feed.getFeed input includes cursor: z.number().nullish() per Pitfall 6
- Rule 1 fix: @upstash/redis zrange requires {offset, count} not just count
- npx tsc --noEmit exits 0
This commit is contained in:
1 parent
b2db6310fe
commit
c435404b2d
12 files changed
+567
No files matched your search
@@ -0,0 +1,129 @@
|
||||
import "server-only";
|
||||
import type { Redis } from "@upstash/redis";
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* Feed helpers for the Redis sorted-set fan-out feed pattern.
|
||||
*
|
||||
* Key convention: feed:{petId}
|
||||
* - sorted set, score = Unix timestamp (ms), member = postId
|
||||
*
|
||||
* Security (T-02-02): server-only — UPSTASH tokens never reach client bundle.
|
||||
*/
|
||||
|
||||
export const PAGE_SIZE = 20;
|
||||
export const FEED_RETENTION_MS = 90 * 24 * 60 * 60 * 1000; // 90 days
|
||||
|
||||
/**
|
||||
* Returns the Redis sorted-set key for a pet's personal feed inbox.
|
||||
*/
|
||||
export function feedKey(petId: string): string {
|
||||
return `feed:${petId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fan-out a new post to all followers' feed inboxes and the poster's own feed.
|
||||
* Score = Unix timestamp (ms); member = postId.
|
||||
* Called synchronously in posts.create (acceptable at v1 scale — Open Question 3).
|
||||
*/
|
||||
export async function fanOutPost(
|
||||
redis: Redis,
|
||||
postId: string,
|
||||
petId: string,
|
||||
createdAt: Date,
|
||||
prisma: PrismaClient
|
||||
): Promise<void> {
|
||||
const score = createdAt.getTime();
|
||||
|
||||
const followers = await prisma.follow.findMany({
|
||||
where: { followeePetId: petId },
|
||||
select: { followerPetId: true },
|
||||
});
|
||||
|
||||
const pipeline = redis.pipeline();
|
||||
|
||||
// Fan-out to every follower's inbox
|
||||
for (const { followerPetId } of followers) {
|
||||
pipeline.zadd(feedKey(followerPetId), { score, member: postId });
|
||||
}
|
||||
|
||||
// Also add to own feed so own posts appear in the poster's timeline (Open Question 3)
|
||||
pipeline.zadd(feedKey(petId), { score, member: postId });
|
||||
|
||||
await pipeline.exec();
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfill the follower's feed when they follow a new pet.
|
||||
* Adds the followee's recent posts (up to `limit`) to the follower's feed.
|
||||
*/
|
||||
export async function backfillOnFollow(
|
||||
redis: Redis,
|
||||
followerPetId: string,
|
||||
followeePetId: string,
|
||||
prisma: PrismaClient,
|
||||
limit = 20
|
||||
): Promise<void> {
|
||||
const recentPosts = await prisma.post.findMany({
|
||||
where: { petId: followeePetId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
select: { id: true, createdAt: true },
|
||||
});
|
||||
|
||||
if (recentPosts.length === 0) return;
|
||||
|
||||
const pipeline = redis.pipeline();
|
||||
for (const post of recentPosts) {
|
||||
pipeline.zadd(feedKey(followerPetId), {
|
||||
score: post.createdAt.getTime(),
|
||||
member: post.id,
|
||||
});
|
||||
}
|
||||
await pipeline.exec();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a page of postIds from a pet's feed inbox.
|
||||
* Uses ZRANGE with byScore + rev for O(log N + page) retrieval.
|
||||
*
|
||||
* Pitfall 2: rev:true requires maxScore as first positional arg.
|
||||
* Pitfall 1: @upstash/redis 1.38.0 returns Array<{member, score}>, not interleaved.
|
||||
*/
|
||||
export async function getFeedPage(
|
||||
redis: Redis,
|
||||
petId: string,
|
||||
cursor: number | null
|
||||
): Promise<{ postIds: string[]; nextCursor: number | null }> {
|
||||
const maxScore = cursor ?? Date.now();
|
||||
const minScore = 0;
|
||||
|
||||
// Fetch PAGE_SIZE + 1 to detect hasNextPage
|
||||
// @upstash/redis requires {offset, count} together when limiting (not count alone)
|
||||
const results = (await redis.zrange(feedKey(petId), maxScore, minScore, {
|
||||
byScore: true,
|
||||
rev: true,
|
||||
withScores: true,
|
||||
offset: 0,
|
||||
count: PAGE_SIZE + 1,
|
||||
})) as Array<{ member: string; score: number }>;
|
||||
|
||||
const hasNextPage = results.length > PAGE_SIZE;
|
||||
const items = hasNextPage ? results.slice(0, PAGE_SIZE) : results;
|
||||
const nextCursor =
|
||||
hasNextPage ? items[items.length - 1].score - 1 : null;
|
||||
|
||||
return {
|
||||
postIds: items.map((r) => r.member),
|
||||
nextCursor,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim a pet's feed to the 90-day retention window.
|
||||
* Called by the pg_cron cleanup job — not on the request path.
|
||||
*/
|
||||
export async function trimFeed(redis: Redis, petId: string): Promise<void> {
|
||||
const cutoff = Date.now() - FEED_RETENTION_MS;
|
||||
await redis.zremrangebyscore(feedKey(petId), 0, cutoff);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Constructs the public CDN URL for media stored in Supabase Storage.
|
||||
* Safe to use in both server and client components (uses NEXT_PUBLIC_ env var).
|
||||
*
|
||||
* Mirrors getAvatarUrl signature. Used for post images and story media
|
||||
* stored under the posts/ and stories/ path prefixes in the pet-avatars bucket.
|
||||
*/
|
||||
export function getMediaUrl(key: string | null | undefined): string | null {
|
||||
if (!key) return null;
|
||||
return `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/pet-avatars/${key}`;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MilestoneType } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* Hard-coded follower milestone thresholds and their corresponding MilestoneType enum values.
|
||||
*
|
||||
* Per D-09 and A3: these values are a constant array, not a DB table.
|
||||
* Used by the follows router to detect when a follower count threshold is crossed
|
||||
* and emit an auto-triggered milestone (D-10).
|
||||
*/
|
||||
export const FOLLOWER_MILESTONE_THRESHOLDS: Array<{
|
||||
count: number;
|
||||
type: MilestoneType;
|
||||
}> = [
|
||||
{ count: 500, type: MilestoneType.FOLLOWERS_500 },
|
||||
{ count: 1_000, type: MilestoneType.FOLLOWERS_1K },
|
||||
{ count: 2_500, type: MilestoneType.FOLLOWERS_2_5K },
|
||||
{ count: 5_000, type: MilestoneType.FOLLOWERS_5K },
|
||||
{ count: 10_000, type: MilestoneType.FOLLOWERS_10K },
|
||||
{ count: 25_000, type: MilestoneType.FOLLOWERS_25K },
|
||||
{ count: 50_000, type: MilestoneType.FOLLOWERS_50K },
|
||||
{ count: 100_000, type: MilestoneType.FOLLOWERS_100K },
|
||||
{ count: 250_000, type: MilestoneType.FOLLOWERS_250K },
|
||||
{ count: 500_000, type: MilestoneType.FOLLOWERS_500K },
|
||||
{ count: 1_000_000, type: MilestoneType.FOLLOWERS_1M },
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
import "server-only";
|
||||
import { Redis } from "@upstash/redis";
|
||||
|
||||
/**
|
||||
* @upstash/redis singleton for Phase 2 feed fan-out sorted sets.
|
||||
*
|
||||
* Security (T-02-02): server-only import ensures UPSTASH_REDIS_REST_TOKEN
|
||||
* never reaches the client bundle.
|
||||
*
|
||||
* Environment: UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN must be
|
||||
* set in .env.local (Upstash Console → Redis database → REST API).
|
||||
*/
|
||||
export const redis = new Redis({
|
||||
url: process.env.UPSTASH_REDIS_REST_URL!,
|
||||
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
||||
});
|
||||
@@ -1,10 +1,24 @@
|
||||
import { router } from "@/trpc/init";
|
||||
import { petsRouter } from "@/trpc/routers/pets";
|
||||
import { mediaRouter } from "@/trpc/routers/media";
|
||||
import { postsRouter } from "@/trpc/routers/posts";
|
||||
import { storiesRouter } from "@/trpc/routers/stories";
|
||||
import { milestonesRouter } from "@/trpc/routers/milestones";
|
||||
import { followsRouter } from "@/trpc/routers/follows";
|
||||
import { blocksRouter } from "@/trpc/routers/blocks";
|
||||
import { reportsRouter } from "@/trpc/routers/reports";
|
||||
import { feedRouter } from "@/trpc/routers/feed";
|
||||
|
||||
export const appRouter = router({
|
||||
pets: petsRouter,
|
||||
media: mediaRouter,
|
||||
posts: postsRouter,
|
||||
stories: storiesRouter,
|
||||
milestones: milestonesRouter,
|
||||
follows: followsRouter,
|
||||
blocks: blocksRouter,
|
||||
reports: reportsRouter,
|
||||
feed: feedRouter,
|
||||
});
|
||||
|
||||
// Export the type for use in the client
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import "server-only";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { router, protectedProcedure } from "@/trpc/init";
|
||||
|
||||
/**
|
||||
* Blocks tRPC router — safety baseline (SAFE-01).
|
||||
*
|
||||
* Blocks are stored in PostgreSQL (authoritative) — never in Redis.
|
||||
* Block filter is applied at Prisma hydration time in feed.getFeed.
|
||||
*
|
||||
* Procedure keys:
|
||||
* create — block a pet (insert Block row)
|
||||
* list — list pets blocked by the active pet
|
||||
*
|
||||
* Implementation: downstream Wave 1 plans.
|
||||
*/
|
||||
export const blocksRouter = router({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
blockerPetId: z.string().min(1),
|
||||
blockedPetId: z.string().min(1),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
cursor: z.string().nullish(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
})
|
||||
)
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import "server-only";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { router, protectedProcedure } from "@/trpc/init";
|
||||
|
||||
/**
|
||||
* Feed tRPC router — Redis sorted-set fan-out home feed (FEED-01).
|
||||
*
|
||||
* getFeed is an infinite query: tRPC v11 requires the input schema to include
|
||||
* a field named exactly `cursor` for useInfiniteQuery to work (Pitfall 6).
|
||||
*
|
||||
* Feed hydration order:
|
||||
* 1. ZRANGE feed:{petId} (by score desc, cursor-based)
|
||||
* 2. Block filter applied at Prisma level (pet: { id: { notIn: blockedPetIds } })
|
||||
* 3. Post array sorted to match Redis score order (Pitfall 7)
|
||||
*
|
||||
* Implementation: downstream Wave 1 plans.
|
||||
*/
|
||||
export const feedRouter = router({
|
||||
getFeed: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
cursor: z.number().nullish(), // Unix ms timestamp — must be named 'cursor' (Pitfall 6)
|
||||
limit: z.number().min(1).max(50).default(20),
|
||||
})
|
||||
)
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import "server-only";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { router, protectedProcedure } from "@/trpc/init";
|
||||
|
||||
/**
|
||||
* Follows tRPC router — pet-to-pet social graph (SOCL-01, SOCL-02, SOCL-03).
|
||||
*
|
||||
* Follow is pet-to-pet exclusively — never owner-to-owner.
|
||||
* On follow: Prisma INSERT Follow + Redis backfill of followee's recent posts.
|
||||
* On unfollow: Prisma DELETE Follow (no Redis cleanup — feed TTL handles it).
|
||||
*
|
||||
* Procedure keys:
|
||||
* create — follow a pet
|
||||
* delete — unfollow a pet
|
||||
* isFollowing — check follow status (used for optimistic updates)
|
||||
* listFollowers — list pets following a given pet (SOCL-03)
|
||||
* listFollowing — list pets a given pet is following (SOCL-03)
|
||||
*
|
||||
* Implementation: downstream Wave 1 plans.
|
||||
*/
|
||||
export const followsRouter = router({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
followerPetId: z.string().min(1),
|
||||
followeePetId: z.string().min(1),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
followerPetId: z.string().min(1),
|
||||
followeePetId: z.string().min(1),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
isFollowing: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
followerPetId: z.string().min(1),
|
||||
followeePetId: z.string().min(1),
|
||||
})
|
||||
)
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
listFollowers: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
cursor: z.string().nullish(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
})
|
||||
)
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
listFollowing: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
cursor: z.string().nullish(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
})
|
||||
)
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import "server-only";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { MilestoneType } from "@prisma/client";
|
||||
import { router, protectedProcedure } from "@/trpc/init";
|
||||
|
||||
/**
|
||||
* Milestones tRPC router — structured milestone posts (D-09).
|
||||
*
|
||||
* Procedure keys:
|
||||
* create — create a milestone post with a validated MilestoneType enum value
|
||||
* listByPet — list milestone posts for a pet (MILE-02)
|
||||
*
|
||||
* Implementation: downstream Wave 1 plans.
|
||||
*/
|
||||
export const milestonesRouter = router({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
milestoneType: z.nativeEnum(MilestoneType),
|
||||
notes: z.string().max(500).optional(),
|
||||
caption: z.string().max(500).optional(),
|
||||
imageKey: z.string().startsWith("posts/").optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
listByPet: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
cursor: z.string().nullish(),
|
||||
limit: z.number().min(1).max(50).default(20),
|
||||
})
|
||||
)
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import "server-only";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { router, protectedProcedure } from "@/trpc/init";
|
||||
|
||||
/**
|
||||
* Posts tRPC router — photo post creation, caption edit, deletion, feed hydration.
|
||||
*
|
||||
* Security: All mutations assert pet ownership via ctx.userId.
|
||||
* Procedure keys:
|
||||
* create — create a photo or milestone post (1–10 images)
|
||||
* updateCaption — edit caption on an existing post
|
||||
* delete — delete post + cascade images + remove from Redis feed
|
||||
* byPetId — list posts for a given pet (paginated)
|
||||
* getPresignedUrl — generate presigned upload URL for post images
|
||||
* confirmUpload — validate storage key and save post to DB
|
||||
*
|
||||
* Implementation: downstream Wave 1 plans (02-02 onwards).
|
||||
*/
|
||||
export const postsRouter = router({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
caption: z.string().max(500).optional(),
|
||||
imageKeys: z.array(z.string().startsWith("posts/")).min(1).max(10),
|
||||
type: z.enum(["PHOTO"]).default("PHOTO"),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
updateCaption: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
postId: z.string().min(1),
|
||||
caption: z.string().max(500),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.input(z.object({ postId: z.string().min(1) }))
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
byPetId: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
cursor: z.string().nullish(),
|
||||
limit: z.number().min(1).max(50).default(20),
|
||||
})
|
||||
)
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
getPresignedUrl: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
contentType: z.enum(["image/jpeg", "image/png", "image/webp"]),
|
||||
fileSize: z.number().max(10 * 1024 * 1024),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
confirmUpload: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
// Path traversal prevention (T-02, mirrors media.confirmUpload for avatars/)
|
||||
key: z.string().startsWith("posts/"),
|
||||
position: z.number().int().min(0).max(9),
|
||||
width: z.number().int().positive().optional(),
|
||||
height: z.number().int().positive().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import "server-only";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { ReportReason } from "@prisma/client";
|
||||
import { router, protectedProcedure } from "@/trpc/init";
|
||||
|
||||
/**
|
||||
* Reports tRPC router — content reporting for review (SAFE-02).
|
||||
*
|
||||
* Reports reference either a post or a pet profile (targetPostId XOR targetPetId).
|
||||
* ReportReason is a validated enum — prevents arbitrary reason strings.
|
||||
*
|
||||
* Procedure keys:
|
||||
* create — submit a report
|
||||
*
|
||||
* Implementation: downstream Wave 1 plans.
|
||||
*/
|
||||
export const reportsRouter = router({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
reporterPetId: z.string().min(1),
|
||||
targetPostId: z.string().min(1).optional(),
|
||||
targetPetId: z.string().min(1).optional(),
|
||||
reason: z.nativeEnum(ReportReason),
|
||||
notes: z.string().max(500).optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import "server-only";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { router, protectedProcedure } from "@/trpc/init";
|
||||
|
||||
/**
|
||||
* Stories tRPC router — 24-hour photo stories with lazy-delete expiry (D-07).
|
||||
*
|
||||
* Procedure keys:
|
||||
* create — create a story with expiresAt = now + 24h
|
||||
* listActive — list non-expired stories for a pet (expiresAt > now)
|
||||
* recordView — upsert a story view (dedup by storyId + viewerPetId per D-08)
|
||||
* getViewers — list pets that viewed a story (STORY-03)
|
||||
* hasActiveStory — returns boolean; used for story ring display (STORY-02)
|
||||
*
|
||||
* Implementation: downstream Wave 1 plans.
|
||||
*/
|
||||
export const storiesRouter = router({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
petId: z.string().min(1),
|
||||
storageKey: z.string().startsWith("stories/"),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
listActive: protectedProcedure
|
||||
.input(z.object({ petId: z.string().min(1) }))
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
recordView: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
storyId: z.string().min(1),
|
||||
viewerPetId: z.string().min(1),
|
||||
})
|
||||
)
|
||||
.mutation(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
getViewers: protectedProcedure
|
||||
.input(z.object({ storyId: z.string().min(1) }))
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
|
||||
hasActiveStory: protectedProcedure
|
||||
.input(z.object({ petId: z.string().min(1) }))
|
||||
.query(async () => {
|
||||
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user