@@ -18,19 +18,27 @@ import {
1818 trackWebsiteUsage ,
1919} from '../utils/billing' ;
2020
21+ // Cache configuration
2122const websiteCache = createDrizzleCache ( { redis, namespace : 'websites' } ) ;
22- const CACHE_DURATION = 60 ;
23- const TREND_THRESHOLD = 5 ;
24-
25- const buildFullDomain = ( domain : string , subdomain ?: string ) =>
26- subdomain ? `${ subdomain } .${ domain } ` : domain ;
23+ const CACHE_DURATION = 60 ; // seconds
24+ const TREND_THRESHOLD = 5 ; // percentage
2725
26+ // Types
2827interface ChartDataPoint {
2928 websiteId : string ;
3029 date : string ;
3130 value : number ;
3231}
3332
33+ // Helper functions
34+ const buildFullDomain = ( domain : string , subdomain ?: string ) =>
35+ subdomain ? `${ subdomain } .${ domain } ` : domain ;
36+
37+ const buildWebsiteFilter = ( userId : string , organizationId ?: string ) =>
38+ organizationId
39+ ? eq ( websites . organizationId , organizationId )
40+ : and ( eq ( websites . userId , userId ) , isNull ( websites . organizationId ) ) ;
41+
3442const calculateAverage = ( values : { value : number } [ ] ) =>
3543 values . length > 0
3644 ? values . reduce ( ( sum , item ) => sum + item . value , 0 ) / values . length
@@ -140,11 +148,7 @@ const fetchChartData = async (
140148 return processedData ;
141149} ;
142150
143- const buildWebsiteFilter = ( userId : string , organizationId ?: string ) =>
144- organizationId
145- ? eq ( websites . organizationId , organizationId )
146- : and ( eq ( websites . userId , userId ) , isNull ( websites . organizationId ) ) ;
147-
151+ // Router definition
148152export const websitesRouter = createTRPCRouter ( {
149153 list : protectedProcedure
150154 . input ( z . object ( { organizationId : z . string ( ) . optional ( ) } ) . default ( { } ) )
@@ -213,6 +217,7 @@ export const websitesRouter = createTRPCRouter({
213217 create : protectedProcedure
214218 . input ( createWebsiteSchema )
215219 . mutation ( async ( { ctx, input } ) => {
220+ // Validate organization permissions upfront
216221 if ( input . organizationId ) {
217222 const { success } = await websitesApi . hasPermission ( {
218223 headers : ctx . headers ,
@@ -230,6 +235,8 @@ export const websitesRouter = createTRPCRouter({
230235 ctx . user . id ,
231236 input . organizationId
232237 ) ;
238+
239+ // Check billing limits before starting transaction
233240 const creationLimitCheck =
234241 await checkAndTrackWebsiteCreation ( billingCustomerId ) ;
235242 if ( ! creationLimitCheck . allowed ) {
@@ -245,32 +252,40 @@ export const websitesRouter = createTRPCRouter({
245252 buildWebsiteFilter ( ctx . user . id , input . organizationId )
246253 ) ;
247254
248- const duplicateWebsite = await ctx . db . query . websites . findFirst ( {
249- where : websiteFilter ,
250- } ) ;
251-
252- if ( duplicateWebsite ) {
253- const scopeDescription = input . organizationId
254- ? 'in this organization'
255- : 'for your account' ;
256- throw new TRPCError ( {
257- code : 'CONFLICT' ,
258- message : `A website with the domain "${ domainToCreate } " already exists ${ scopeDescription } .` ,
255+ // Execute database operations in transaction
256+ const createdWebsite = await ctx . db . transaction ( async ( tx ) => {
257+ // Check for duplicate websites within transaction
258+ const duplicateWebsite = await tx . query . websites . findFirst ( {
259+ where : websiteFilter ,
259260 } ) ;
260- }
261261
262- const [ createdWebsite ] = await ctx . db
263- . insert ( websites )
264- . values ( {
265- id : nanoid ( ) ,
266- name : input . name ,
267- domain : domainToCreate ,
268- userId : ctx . user . id ,
269- organizationId : input . organizationId ,
270- status : 'ACTIVE' ,
271- } )
272- . returning ( ) ;
262+ if ( duplicateWebsite ) {
263+ const scopeDescription = input . organizationId
264+ ? 'in this organization'
265+ : 'for your account' ;
266+ throw new TRPCError ( {
267+ code : 'CONFLICT' ,
268+ message : `A website with the domain "${ domainToCreate } " already exists ${ scopeDescription } .` ,
269+ } ) ;
270+ }
273271
272+ // Create website
273+ const [ website ] = await tx
274+ . insert ( websites )
275+ . values ( {
276+ id : nanoid ( ) ,
277+ name : input . name ,
278+ domain : domainToCreate ,
279+ userId : ctx . user . id ,
280+ organizationId : input . organizationId ,
281+ status : 'ACTIVE' ,
282+ } )
283+ . returning ( ) ;
284+
285+ return website ;
286+ } ) ;
287+
288+ // Log success after transaction completes
274289 logger . success (
275290 'Website Created' ,
276291 `New website "${ createdWebsite . name } " was created with domain "${ createdWebsite . domain } "` ,
@@ -282,6 +297,7 @@ export const websitesRouter = createTRPCRouter({
282297 }
283298 ) ;
284299
300+ // Invalidate cache after successful creation
285301 await websiteCache . invalidateByTables ( [ 'websites' ] ) ;
286302
287303 return createdWebsite ;
@@ -290,18 +306,32 @@ export const websitesRouter = createTRPCRouter({
290306 update : protectedProcedure
291307 . input ( updateWebsiteSchema )
292308 . mutation ( async ( { ctx, input } ) => {
309+ // Authorize access before transaction
293310 const websiteToUpdate = await authorizeWebsiteAccess (
294311 ctx ,
295312 input . id ,
296313 'update'
297314 ) ;
298315
299- const [ updatedWebsite ] = await ctx . db
300- . update ( websites )
301- . set ( { name : input . name } )
302- . where ( eq ( websites . id , input . id ) )
303- . returning ( ) ;
316+ // Execute update in transaction
317+ const updatedWebsite = await ctx . db . transaction ( async ( tx ) => {
318+ const [ website ] = await tx
319+ . update ( websites )
320+ . set ( { name : input . name } )
321+ . where ( eq ( websites . id , input . id ) )
322+ . returning ( ) ;
323+
324+ if ( ! website ) {
325+ throw new TRPCError ( {
326+ code : 'NOT_FOUND' ,
327+ message : 'Website not found' ,
328+ } ) ;
329+ }
304330
331+ return website ;
332+ } ) ;
333+
334+ // Log success after transaction
305335 logger . info (
306336 'Website Updated' ,
307337 `Website "${ websiteToUpdate . name } " was renamed to "${ updatedWebsite . name } "` ,
@@ -313,6 +343,7 @@ export const websitesRouter = createTRPCRouter({
313343 }
314344 ) ;
315345
346+ // Invalidate cache after successful update
316347 await Promise . all ( [
317348 websiteCache . invalidateByTables ( [ 'websites' ] ) ,
318349 websiteCache . invalidateByKey ( `getById:${ input . id } ` ) ,
@@ -324,6 +355,7 @@ export const websitesRouter = createTRPCRouter({
324355 delete : protectedProcedure
325356 . input ( z . object ( { id : z . string ( ) } ) )
326357 . mutation ( async ( { ctx, input } ) => {
358+ // Authorize access and get billing info before transaction
327359 const websiteToDelete = await authorizeWebsiteAccess (
328360 ctx ,
329361 input . id ,
@@ -334,11 +366,16 @@ export const websitesRouter = createTRPCRouter({
334366 websiteToDelete . organizationId
335367 ) ;
336368
337- await Promise . all ( [
338- ctx . db . delete ( websites ) . where ( eq ( websites . id , input . id ) ) ,
339- trackWebsiteUsage ( billingCustomerId , - 1 ) ,
340- ] ) ;
369+ // Execute deletion and billing update in transaction
370+ await ctx . db . transaction ( async ( tx ) => {
371+ // Delete website
372+ await tx . delete ( websites ) . where ( eq ( websites . id , input . id ) ) ;
373+
374+ // Track billing usage (decrement)
375+ await trackWebsiteUsage ( billingCustomerId , - 1 ) ;
376+ } ) ;
341377
378+ // Log after successful deletion
342379 logger . warning (
343380 'Website Deleted' ,
344381 `Website "${ websiteToDelete . name } " with domain "${ websiteToDelete . domain } " was deleted` ,
@@ -350,6 +387,7 @@ export const websitesRouter = createTRPCRouter({
350387 }
351388 ) ;
352389
390+ // Invalidate cache after successful deletion
353391 await Promise . all ( [
354392 websiteCache . invalidateByTables ( [ 'websites' ] ) ,
355393 websiteCache . invalidateByKey ( `getById:${ input . id } ` ) ,
@@ -361,8 +399,10 @@ export const websitesRouter = createTRPCRouter({
361399 transfer : protectedProcedure
362400 . input ( transferWebsiteSchema )
363401 . mutation ( async ( { ctx, input } ) => {
402+ // Authorize access before transaction
364403 await authorizeWebsiteAccess ( ctx , input . websiteId , 'update' ) ;
365404
405+ // Validate organization permissions upfront
366406 if ( input . organizationId ) {
367407 const { success } = await websitesApi . hasPermission ( {
368408 headers : ctx . headers ,
@@ -376,19 +416,25 @@ export const websitesRouter = createTRPCRouter({
376416 }
377417 }
378418
379- const [ transferredWebsite ] = await ctx . db
380- . update ( websites )
381- . set ( { organizationId : input . organizationId ?? null } )
382- . where ( eq ( websites . id , input . websiteId ) )
383- . returning ( ) ;
419+ // Execute transfer in transaction
420+ const transferredWebsite = await ctx . db . transaction ( async ( tx ) => {
421+ const [ website ] = await tx
422+ . update ( websites )
423+ . set ( { organizationId : input . organizationId ?? null } )
424+ . where ( eq ( websites . id , input . websiteId ) )
425+ . returning ( ) ;
384426
385- if ( ! transferredWebsite ) {
386- throw new TRPCError ( {
387- code : 'NOT_FOUND' ,
388- message : 'Website not found' ,
389- } ) ;
390- }
427+ if ( ! website ) {
428+ throw new TRPCError ( {
429+ code : 'NOT_FOUND' ,
430+ message : 'Website not found' ,
431+ } ) ;
432+ }
433+
434+ return website ;
435+ } ) ;
391436
437+ // Log success after transaction
392438 logger . success (
393439 'Website Transferred' ,
394440 `Website "${ transferredWebsite . name } " was transferred to organization "${ input . organizationId } "` ,
@@ -399,6 +445,7 @@ export const websitesRouter = createTRPCRouter({
399445 }
400446 ) ;
401447
448+ // Invalidate cache after successful transfer
402449 await Promise . all ( [
403450 websiteCache . invalidateByTables ( [ 'websites' ] ) ,
404451 websiteCache . invalidateByKey ( `getById:${ input . websiteId } ` ) ,
0 commit comments