Skip to content

Commit fa970af

Browse files
committed
feat: cache invalidation
1 parent 7d8820e commit fa970af

File tree

2 files changed

+176
-14
lines changed

2 files changed

+176
-14
lines changed

packages/rpc/src/routers/mini-charts.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ const getAuthorizedWebsiteIds = (
6868
};
6969

7070
const calculateTrend = (data: { date: string; value: number }[]) => {
71-
if (!data || data.length === 0) return null;
71+
if (!data || data.length === 0) {
72+
return null;
73+
}
7274

7375
const mid = Math.floor(data.length / 2);
7476
const [first, second] = [data.slice(0, mid), data.slice(mid)];
@@ -77,15 +79,19 @@ const calculateTrend = (data: { date: string; value: number }[]) => {
7779
arr.length > 0 ? arr.reduce((sum, p) => sum + p.value, 0) / arr.length : 0;
7880
const [prevAvg, currAvg] = [avg(first), avg(second)];
7981

80-
if (prevAvg === 0)
82+
if (prevAvg === 0) {
8183
return currAvg > 0
8284
? { type: 'up' as const, value: 100 }
8385
: { type: 'neutral' as const, value: 0 };
86+
}
8487

8588
const change = ((currAvg - prevAvg) / prevAvg) * 100;
8689
let type: 'up' | 'down' | 'neutral' = 'neutral';
87-
if (change > 5) type = 'up';
88-
else if (change < -5) type = 'down';
90+
if (change > 5) {
91+
type = 'up';
92+
} else if (change < -5) {
93+
type = 'down';
94+
}
8995
return { type, value: Math.abs(change) };
9096
};
9197

packages/rpc/src/routers/websites.ts

Lines changed: 166 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export const websitesRouter = createTRPCRouter({
306306
update: protectedProcedure
307307
.input(updateWebsiteSchema)
308308
.mutation(async ({ ctx, input }) => {
309-
// Authorize access before transaction
309+
// Authorize access and get billing info before transaction
310310
const websiteToUpdate = await authorizeWebsiteAccess(
311311
ctx,
312312
input.id,
@@ -349,6 +349,78 @@ export const websitesRouter = createTRPCRouter({
349349
websiteCache.invalidateByKey(`getById:${input.id}`),
350350
]);
351351

352+
// If isPublic status changed, invalidate all related caches
353+
if (
354+
input.isPublic !== undefined &&
355+
input.isPublic !== websiteToUpdate.isPublic
356+
) {
357+
// Call the invalidateCaches procedure logic directly
358+
await Promise.all([
359+
// Website caches
360+
websiteCache.invalidateByTables(['websites']),
361+
websiteCache.invalidateByKey(`getById:${input.id}`),
362+
363+
createDrizzleCache({
364+
redis,
365+
namespace: 'website_by_id',
366+
}).invalidateByKey(`website_by_id:${input.id}`),
367+
createDrizzleCache({ redis, namespace: 'auth' }).invalidateByKey(
368+
`auth:${ctx.user.id}:${input.id}`
369+
),
370+
371+
// Funnel caches
372+
createDrizzleCache({
373+
redis,
374+
namespace: 'funnels',
375+
}).invalidateByTables(['funnelDefinitions']),
376+
createDrizzleCache({ redis, namespace: 'funnels' }).invalidateByKey(
377+
`funnels:list:${input.id}`
378+
),
379+
createDrizzleCache({ redis, namespace: 'funnels' }).invalidateByKey(
380+
`funnels:listPublic:${input.id}`
381+
),
382+
383+
// Goals caches
384+
createDrizzleCache({ redis, namespace: 'goals' }).invalidateByTables([
385+
'goals',
386+
]),
387+
createDrizzleCache({ redis, namespace: 'goals' }).invalidateByKey(
388+
`goals:list:${input.id}`
389+
),
390+
391+
// Autocomplete caches
392+
createDrizzleCache({
393+
redis,
394+
namespace: 'autocomplete',
395+
}).invalidateByTables(['websites']),
396+
397+
// Mini-charts caches
398+
createDrizzleCache({
399+
redis,
400+
namespace: 'mini-charts',
401+
}).invalidateByTables(['websites']),
402+
createDrizzleCache({
403+
redis,
404+
namespace: 'mini-charts',
405+
}).invalidateByKey(`mini-charts:${ctx.user.id}:${input.id}`),
406+
createDrizzleCache({
407+
redis,
408+
namespace: 'mini-charts',
409+
}).invalidateByKey(`mini-charts:public:${input.id}`),
410+
]);
411+
412+
logger.info(
413+
'Public status changed - caches invalidated',
414+
`Website ${input.id} public status changed to ${input.isPublic}`,
415+
{
416+
websiteId: input.id,
417+
oldIsPublic: websiteToUpdate.isPublic,
418+
newIsPublic: input.isPublic,
419+
userId: ctx.user.id,
420+
}
421+
);
422+
}
423+
352424
return updatedWebsite;
353425
}),
354426

@@ -420,22 +492,18 @@ export const websitesRouter = createTRPCRouter({
420492
const transferredWebsite = await ctx.db.transaction(async (tx) => {
421493
const [website] = await tx
422494
.update(websites)
423-
.set({ organizationId: input.organizationId ?? null })
495+
.set({
496+
organizationId: input.organizationId ?? null,
497+
updatedAt: new Date().toISOString(),
498+
})
424499
.where(eq(websites.id, input.websiteId))
425500
.returning();
426501

427-
if (!website) {
428-
throw new TRPCError({
429-
code: 'NOT_FOUND',
430-
message: 'Website not found',
431-
});
432-
}
433-
434502
return website;
435503
});
436504

437505
// Log success after transaction
438-
logger.success(
506+
logger.info(
439507
'Website Transferred',
440508
`Website "${transferredWebsite.name}" was transferred to organization "${input.organizationId}"`,
441509
{
@@ -454,6 +522,94 @@ export const websitesRouter = createTRPCRouter({
454522
return transferredWebsite;
455523
}),
456524

525+
invalidateCaches: protectedProcedure
526+
.input(z.object({ websiteId: z.string() }))
527+
.mutation(async ({ ctx, input }) => {
528+
// Authorize access
529+
await authorizeWebsiteAccess(ctx, input.websiteId, 'update');
530+
531+
try {
532+
// Invalidate all caches related to this website
533+
await Promise.all([
534+
// Website caches
535+
websiteCache.invalidateByTables(['websites']),
536+
websiteCache.invalidateByKey(`getById:${input.websiteId}`),
537+
538+
createDrizzleCache({
539+
redis,
540+
namespace: 'website_by_id',
541+
}).invalidateByKey(`website_by_id:${input.websiteId}`),
542+
createDrizzleCache({ redis, namespace: 'auth' }).invalidateByKey(
543+
`auth:${ctx.user.id}:${input.websiteId}`
544+
),
545+
546+
// Funnel caches
547+
createDrizzleCache({
548+
redis,
549+
namespace: 'funnels',
550+
}).invalidateByTables(['funnelDefinitions']),
551+
createDrizzleCache({ redis, namespace: 'funnels' }).invalidateByKey(
552+
`funnels:list:${input.websiteId}`
553+
),
554+
createDrizzleCache({ redis, namespace: 'funnels' }).invalidateByKey(
555+
`funnels:listPublic:${input.websiteId}`
556+
),
557+
558+
// Goals caches
559+
createDrizzleCache({ redis, namespace: 'goals' }).invalidateByTables([
560+
'goals',
561+
]),
562+
createDrizzleCache({ redis, namespace: 'goals' }).invalidateByKey(
563+
`goals:list:${input.websiteId}`
564+
),
565+
566+
// Autocomplete caches
567+
createDrizzleCache({
568+
redis,
569+
namespace: 'autocomplete',
570+
}).invalidateByTables(['websites']),
571+
572+
// Mini-charts caches
573+
createDrizzleCache({
574+
redis,
575+
namespace: 'mini-charts',
576+
}).invalidateByTables(['websites']),
577+
createDrizzleCache({
578+
redis,
579+
namespace: 'mini-charts',
580+
}).invalidateByKey(`mini-charts:${ctx.user.id}:${input.websiteId}`),
581+
createDrizzleCache({
582+
redis,
583+
namespace: 'mini-charts',
584+
}).invalidateByKey(`mini-charts:public:${input.websiteId}`),
585+
]);
586+
587+
logger.info(
588+
'Caches invalidated',
589+
`All caches invalidated for website ${input.websiteId}`,
590+
{
591+
websiteId: input.websiteId,
592+
userId: ctx.user.id,
593+
}
594+
);
595+
596+
return { success: true };
597+
} catch (error) {
598+
logger.error(
599+
'Failed to invalidate caches',
600+
error instanceof Error ? error.message : String(error),
601+
{
602+
websiteId: input.websiteId,
603+
userId: ctx.user.id,
604+
}
605+
);
606+
throw new TRPCError({
607+
code: 'INTERNAL_SERVER_ERROR',
608+
message: 'Failed to invalidate caches',
609+
});
610+
}
611+
}),
612+
457613
isTrackingSetup: publicProcedure
458614
.input(z.object({ websiteId: z.string() }))
459615
.query(async ({ ctx, input }) => {

0 commit comments

Comments
 (0)