● Zion Boggan
repos/Stepping App/packages/api/src/root.ts
zionboggan.com ↗
716 lines · typescript
History for this file →
1
import { randomBytes } from "node:crypto";
2
import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
3
import { z } from "zod";
4
import {
5
  cities,
6
  classes,
7
  comments,
8
  djs,
9
  eventPhotos,
10
  events,
11
  flags,
12
  instructors,
13
  orders,
14
  proAccounts,
15
  promoters,
16
  sponsors,
17
  steppingGroups,
18
  ticketTypes,
19
  tickets,
20
  travelPlaces,
21
  vendors,
22
  videos,
23
} from "@stepping/db";
24
import {
25
  EVENT_TYPE,
26
  SUBJECT_TYPE,
27
  TRAVEL_CATEGORY,
28
  VIDEO_PLATFORM,
29
} from "@stepping/types";
30
import {
31
  adminProcedure,
32
  protectedProcedure,
33
  publicProcedure,
34
  router,
35
} from "./trpc";
36
 
37
const uuid = z.string().uuid();
38
const links = z
39
  .array(z.object({ label: z.string().min(1), url: z.string().url() }))
40
  .optional();
41
const uploadUrl = z.string().regex(/^\/api\/uploads\/[a-f0-9]{32}\.[a-z0-9]{3,4}$/);
42
 
43
export const appRouter = router({
44
  city: router({
45
    listLaunched: publicProcedure.query(({ ctx }) =>
46
      ctx.db.select().from(cities).where(eq(cities.isLaunched, true)),
47
    ),
48
    bySlug: publicProcedure.input(z.object({ slug: z.string() })).query(
49
      async ({ ctx, input }) => {
50
        const [row] = await ctx.db
51
          .select()
52
          .from(cities)
53
          .where(eq(cities.slug, input.slug));
54
        return row ?? null;
55
      },
56
    ),
57
  }),
58
 
59
  event: router({
60
    list: publicProcedure
61
      .input(z.object({ cityId: uuid, type: z.enum(EVENT_TYPE).optional() }))
62
      .query(({ ctx, input }) =>
63
        ctx.db
64
          .select()
65
          .from(events)
66
          .where(
67
            and(
68
              eq(events.cityId, input.cityId),
69
              eq(events.status, "active"),
70
              input.type ? eq(events.type, input.type) : undefined,
71
            ),
72
          )
73
          .orderBy(desc(events.startsAt)),
74
      ),
75
    byId: publicProcedure.input(z.object({ id: uuid })).query(
76
      async ({ ctx, input }) => {
77
        const [row] = await ctx.db
78
          .select()
79
          .from(events)
80
          .where(eq(events.id, input.id));
81
        return row ?? null;
82
      },
83
    ),
84
    create: protectedProcedure
85
      .input(
86
        z.object({
87
          cityId: uuid,
88
          title: z.string().min(1),
89
          type: z.enum(EVENT_TYPE),
90
          description: z.string().optional(),
91
          startsAt: z.coerce.date().optional(),
92
          endsAt: z.coerce.date().optional(),
93
          venueId: uuid.optional(),
94
          externalUrl: z.string().url().optional(),
95
          ticketUrl: z.string().url().optional(),
96
          isFree: z.boolean().optional(),
97
          isPromoted: z.boolean().optional(),
98
        }),
99
      )
100
      .mutation(async ({ ctx, input }) => {
101
        const [row] = await ctx.db
102
          .insert(events)
103
          .values({ ...input, ownerId: ctx.user.id })
104
          .returning();
105
        return row;
106
      }),
107
    setLive: protectedProcedure
108
      .input(
109
        z.object({
110
          id: uuid,
111
          isLive: z.boolean(),
112
          liveUrl: z.string().url().optional(),
113
        }),
114
      )
115
      .mutation(async ({ ctx, input }) => {
116
        const [row] = await ctx.db
117
          .update(events)
118
          .set({ isLive: input.isLive, liveUrl: input.liveUrl ?? null })
119
          .where(eq(events.id, input.id))
120
          .returning();
121
        return row;
122
      }),
123
    boost: protectedProcedure
124
      .input(z.object({ id: uuid, days: z.number().int().min(1).max(60).default(14) }))
125
      .mutation(async ({ ctx, input }) => {
126
        const until = new Date(Date.now() + input.days * 86_400_000);
127
        const [row] = await ctx.db
128
          .update(events)
129
          .set({ isPromoted: true, boostedUntil: until })
130
          .where(eq(events.id, input.id))
131
          .returning();
132
        await ctx.db.insert(orders).values({
133
          purpose: "boost",
134
          buyerId: ctx.user.id,
135
          eventId: input.id,
136
          amountCents: 0,
137
          status: "reserved",
138
          meta: { days: input.days },
139
        });
140
        return row;
141
      }),
142
    setFlyer: adminProcedure
143
      .input(z.object({ id: uuid, flyerUrl: uploadUrl.nullable() }))
144
      .mutation(async ({ ctx, input }) => {
145
        const [row] = await ctx.db
146
          .update(events)
147
          .set({ flyerUrl: input.flyerUrl })
148
          .where(eq(events.id, input.id))
149
          .returning();
150
        return row;
151
      }),
152
    setProgram: adminProcedure
153
      .input(z.object({ id: uuid, programUrl: uploadUrl.nullable() }))
154
      .mutation(async ({ ctx, input }) => {
155
        const [row] = await ctx.db
156
          .update(events)
157
          .set({ programUrl: input.programUrl })
158
          .where(eq(events.id, input.id))
159
          .returning();
160
        return row;
161
      }),
162
  }),
163
 
164
  eventPhoto: router({
165
    list: publicProcedure.input(z.object({ eventId: uuid })).query(({ ctx, input }) =>
166
      ctx.db
167
        .select()
168
        .from(eventPhotos)
169
        .where(and(eq(eventPhotos.eventId, input.eventId), eq(eventPhotos.status, "active")))
170
        .orderBy(desc(eventPhotos.createdAt)),
171
    ),
172
    submit: protectedProcedure
173
      .input(
174
        z.object({
175
          eventId: uuid,
176
          url: uploadUrl,
177
          caption: z.string().max(200).optional(),
178
        }),
179
      )
180
      .mutation(async ({ ctx, input }) => {
181
        const [row] = await ctx.db
182
          .insert(eventPhotos)
183
          .values({ ...input, ownerId: ctx.user.id, status: "pending" })
184
          .returning();
185
        return row;
186
      }),
187
    adminPending: adminProcedure.query(({ ctx }) =>
188
      ctx.db
189
        .select({
190
          id: eventPhotos.id,
191
          url: eventPhotos.url,
192
          caption: eventPhotos.caption,
193
          createdAt: eventPhotos.createdAt,
194
          eventId: eventPhotos.eventId,
195
          eventTitle: events.title,
196
        })
197
        .from(eventPhotos)
198
        .innerJoin(events, eq(events.id, eventPhotos.eventId))
199
        .where(eq(eventPhotos.status, "pending"))
200
        .orderBy(desc(eventPhotos.createdAt))
201
        .limit(100),
202
    ),
203
    adminReview: adminProcedure
204
      .input(z.object({ id: uuid, approve: z.boolean() }))
205
      .mutation(async ({ ctx, input }) => {
206
        const [row] = await ctx.db
207
          .update(eventPhotos)
208
          .set(
209
            input.approve
210
              ? { status: "active", verifiedAt: new Date() }
211
              : { status: "removed", hiddenAt: new Date() },
212
          )
213
          .where(eq(eventPhotos.id, input.id))
214
          .returning();
215
        return row;
216
      }),
217
  }),
218
 
219
  vendor: router({
220
    listForEvent: publicProcedure
221
      .input(z.object({ cityId: uuid, eventId: uuid }))
222
      .query(({ ctx, input }) =>
223
        ctx.db
224
          .select()
225
          .from(vendors)
226
          .where(
227
            and(
228
              eq(vendors.status, "active"),
229
              or(
230
                eq(vendors.eventId, input.eventId),
231
                and(eq(vendors.cityId, input.cityId), isNull(vendors.eventId)),
232
              ),
233
            ),
234
          )
235
          .orderBy(vendors.name),
236
      ),
237
    listByCity: publicProcedure.input(z.object({ cityId: uuid })).query(({ ctx, input }) =>
238
      ctx.db
239
        .select()
240
        .from(vendors)
241
        .where(and(eq(vendors.cityId, input.cityId), eq(vendors.status, "active")))
242
        .orderBy(vendors.name),
243
    ),
244
    adminList: adminProcedure.query(({ ctx }) =>
245
      ctx.db.select().from(vendors).orderBy(desc(vendors.createdAt)).limit(100),
246
    ),
247
    adminCreate: adminProcedure
248
      .input(
249
        z.object({
250
          cityId: uuid,
251
          eventId: uuid.optional(),
252
          name: z.string().min(1).max(120),
253
          description: z.string().max(500).optional(),
254
          category: z.string().max(60).optional(),
255
          linkUrl: z.string().url().optional(),
256
        }),
257
      )
258
      .mutation(async ({ ctx, input }) => {
259
        const [row] = await ctx.db
260
          .insert(vendors)
261
          .values({ ...input, ownerId: ctx.user.id })
262
          .returning();
263
        return row;
264
      }),
265
    adminSetStatus: adminProcedure
266
      .input(z.object({ id: uuid, status: z.enum(["active", "hidden"]) }))
267
      .mutation(async ({ ctx, input }) => {
268
        const [row] = await ctx.db
269
          .update(vendors)
270
          .set(
271
            input.status === "hidden"
272
              ? { status: "hidden", hiddenAt: new Date() }
273
              : { status: "active", hiddenAt: null },
274
          )
275
          .where(eq(vendors.id, input.id))
276
          .returning();
277
        return row;
278
      }),
279
  }),
280
 
281
  ticketType: router({
282
    list: publicProcedure.input(z.object({ eventId: uuid })).query(({ ctx, input }) =>
283
      ctx.db
284
        .select()
285
        .from(ticketTypes)
286
        .where(and(eq(ticketTypes.eventId, input.eventId), eq(ticketTypes.isActive, true)))
287
        .orderBy(ticketTypes.priceCents),
288
    ),
289
    create: protectedProcedure
290
      .input(
291
        z.object({
292
          eventId: uuid,
293
          name: z.string().min(1),
294
          priceCents: z.number().int().min(0).max(1_000_000),
295
          capacity: z.number().int().min(1).max(100_000).optional(),
296
        }),
297
      )
298
      .mutation(async ({ ctx, input }) => {
299
        const [row] = await ctx.db
300
          .insert(ticketTypes)
301
          .values({ ...input, ownerId: ctx.user.id })
302
          .returning();
303
        return row;
304
      }),
305
  }),
306
 
307
  ticket: router({
308
    reserve: protectedProcedure
309
      .input(z.object({ ticketTypeId: uuid, quantity: z.number().int().min(1).max(10).default(1) }))
310
      .mutation(async ({ ctx, input }) => {
311
        const [tt] = await ctx.db
312
          .select()
313
          .from(ticketTypes)
314
          .where(and(eq(ticketTypes.id, input.ticketTypeId), eq(ticketTypes.isActive, true)));
315
        if (!tt) throw new Error("Ticket type not available.");
316
        if (tt.capacity != null && tt.sold + input.quantity > tt.capacity) {
317
          throw new Error("Not enough tickets left.");
318
        }
319
        const [order] = await ctx.db
320
          .insert(orders)
321
          .values({
322
            purpose: "ticket",
323
            buyerId: ctx.user.id,
324
            eventId: tt.eventId,
325
            ticketTypeId: tt.id,
326
            quantity: input.quantity,
327
            amountCents: tt.priceCents * input.quantity,
328
            status: "reserved",
329
          })
330
          .returning();
331
        if (!order) throw new Error("Could not create order.");
332
        const rows = Array.from({ length: input.quantity }, () => ({
333
          orderId: order.id,
334
          eventId: tt.eventId,
335
          ticketTypeId: tt.id,
336
          holderId: ctx.user.id,
337
          code: randomBytes(5).toString("hex").toUpperCase(),
338
        }));
339
        const issued = await ctx.db.insert(tickets).values(rows).returning();
340
        await ctx.db
341
          .update(ticketTypes)
342
          .set({ sold: sql`${ticketTypes.sold} + ${input.quantity}` })
343
          .where(eq(ticketTypes.id, tt.id));
344
        return { orderId: order.id, tickets: issued };
345
      }),
346
    mine: protectedProcedure.query(({ ctx }) =>
347
      ctx.db
348
        .select({
349
          id: tickets.id,
350
          code: tickets.code,
351
          status: tickets.status,
352
          createdAt: tickets.createdAt,
353
          eventId: tickets.eventId,
354
          eventTitle: events.title,
355
          startsAt: events.startsAt,
356
          typeName: ticketTypes.name,
357
        })
358
        .from(tickets)
359
        .innerJoin(events, eq(events.id, tickets.eventId))
360
        .leftJoin(ticketTypes, eq(ticketTypes.id, tickets.ticketTypeId))
361
        .where(eq(tickets.holderId, ctx.user.id))
362
        .orderBy(desc(tickets.createdAt)),
363
    ),
364
  }),
365
 
366
  sponsor: router({
367
    listActive: publicProcedure.input(z.object({ cityId: uuid })).query(({ ctx, input }) =>
368
      ctx.db
369
        .select()
370
        .from(sponsors)
371
        .where(and(eq(sponsors.cityId, input.cityId), eq(sponsors.status, "active")))
372
        .orderBy(desc(sponsors.createdAt)),
373
    ),
374
    adminList: adminProcedure.query(({ ctx }) =>
375
      ctx.db.select().from(sponsors).orderBy(desc(sponsors.createdAt)).limit(100),
376
    ),
377
    adminCreate: adminProcedure
378
      .input(
379
        z.object({
380
          cityId: uuid,
381
          name: z.string().min(1),
382
          blurb: z.string().optional(),
383
          linkUrl: z.string().url().optional(),
384
          status: z.enum(["pending", "active", "ended"]).default("active"),
385
        }),
386
      )
387
      .mutation(async ({ ctx, input }) => {
388
        const [row] = await ctx.db
389
          .insert(sponsors)
390
          .values({ ...input, ownerId: ctx.user.id })
391
          .returning();
392
        return row;
393
      }),
394
    adminSetStatus: adminProcedure
395
      .input(z.object({ id: uuid, status: z.enum(["pending", "active", "ended"]) }))
396
      .mutation(async ({ ctx, input }) => {
397
        const [row] = await ctx.db
398
          .update(sponsors)
399
          .set({ status: input.status })
400
          .where(eq(sponsors.id, input.id))
401
          .returning();
402
        return row;
403
      }),
404
  }),
405
 
406
  proAccount: router({
407
    adminList: adminProcedure.query(({ ctx }) =>
408
      ctx.db.select().from(proAccounts).orderBy(desc(proAccounts.createdAt)).limit(100),
409
    ),
410
    adminSetStatus: adminProcedure
411
      .input(
412
        z.object({
413
          id: uuid,
414
          status: z.enum(["pending", "active", "expired", "cancelled"]),
415
        }),
416
      )
417
      .mutation(async ({ ctx, input }) => {
418
        const [row] = await ctx.db
419
          .update(proAccounts)
420
          .set({
421
            status: input.status,
422
            startedAt: input.status === "active" ? new Date() : undefined,
423
          })
424
          .where(eq(proAccounts.id, input.id))
425
          .returning();
426
        return row;
427
      }),
428
  }),
429
 
430
  order: router({
431
    adminRecent: adminProcedure.query(({ ctx }) =>
432
      ctx.db.select().from(orders).orderBy(desc(orders.createdAt)).limit(50),
433
    ),
434
  }),
435
 
436
  video: router({
437
    list: publicProcedure.input(z.object({ cityId: uuid })).query(({ ctx, input }) =>
438
      ctx.db
439
        .select()
440
        .from(videos)
441
        .where(and(eq(videos.cityId, input.cityId), eq(videos.status, "active")))
442
        .orderBy(desc(videos.createdAt)),
443
    ),
444
    create: protectedProcedure
445
      .input(
446
        z.object({
447
          cityId: uuid,
448
          title: z.string().min(1),
449
          embedUrl: z.string().url(),
450
          platform: z.enum(VIDEO_PLATFORM).default("other"),
451
        }),
452
      )
453
      .mutation(async ({ ctx, input }) => {
454
        const [row] = await ctx.db
455
          .insert(videos)
456
          .values({ ...input, ownerId: ctx.user.id })
457
          .returning();
458
        return row;
459
      }),
460
  }),
461
 
462
  comment: router({
463
    list: publicProcedure
464
      .input(z.object({ subjectType: z.enum(SUBJECT_TYPE), subjectId: uuid }))
465
      .query(({ ctx, input }) =>
466
        ctx.db
467
          .select()
468
          .from(comments)
469
          .where(
470
            and(
471
              eq(comments.subjectType, input.subjectType),
472
              eq(comments.subjectId, input.subjectId),
473
              eq(comments.status, "active"),
474
            ),
475
          )
476
          .orderBy(desc(comments.createdAt)),
477
      ),
478
    create: protectedProcedure
479
      .input(
480
        z.object({
481
          subjectType: z.enum(SUBJECT_TYPE),
482
          subjectId: uuid,
483
          body: z.string().min(1).max(2000),
484
        }),
485
      )
486
      .mutation(async ({ ctx, input }) => {
487
        const [row] = await ctx.db
488
          .insert(comments)
489
          .values({ ...input, ownerId: ctx.user.id })
490
          .returning();
491
        return row;
492
      }),
493
  }),
494
 
495
  flag: router({
496
    create: protectedProcedure
497
      .input(
498
        z.object({
499
          subjectType: z.enum(SUBJECT_TYPE),
500
          subjectId: uuid,
501
          reason: z.string().max(500).optional(),
502
        }),
503
      )
504
      .mutation(async ({ ctx, input }) => {
505
        const [row] = await ctx.db
506
          .insert(flags)
507
          .values({ ...input, reporterId: ctx.user.id })
508
          .returning();
509
        return row;
510
      }),
511
    adminOpen: adminProcedure.query(({ ctx }) =>
512
      ctx.db
513
        .select()
514
        .from(flags)
515
        .where(eq(flags.status, "open"))
516
        .orderBy(desc(flags.createdAt))
517
        .limit(100),
518
    ),
519
    adminResolve: adminProcedure
520
      .input(z.object({ id: uuid, status: z.enum(["reviewed", "actioned", "dismissed"]) }))
521
      .mutation(async ({ ctx, input }) => {
522
        const [row] = await ctx.db
523
          .update(flags)
524
          .set({ status: input.status })
525
          .where(eq(flags.id, input.id))
526
          .returning();
527
        return row;
528
      }),
529
  }),
530
 
531
  promoter: router({
532
    list: publicProcedure.input(z.object({ cityId: uuid })).query(({ ctx, input }) =>
533
      ctx.db
534
        .select()
535
        .from(promoters)
536
        .where(and(eq(promoters.cityId, input.cityId), eq(promoters.status, "active"))),
537
    ),
538
    byId: publicProcedure.input(z.object({ id: uuid })).query(async ({ ctx, input }) => {
539
      const [row] = await ctx.db.select().from(promoters).where(eq(promoters.id, input.id));
540
      return row ?? null;
541
    }),
542
    create: protectedProcedure
543
      .input(
544
        z.object({ cityId: uuid, name: z.string().min(1), bio: z.string().optional(), links }),
545
      )
546
      .mutation(async ({ ctx, input }) => {
547
        const [row] = await ctx.db
548
          .insert(promoters)
549
          .values({ ...input, ownerId: ctx.user.id })
550
          .returning();
551
        return row;
552
      }),
553
  }),
554
 
555
  dj: router({
556
    list: publicProcedure.input(z.object({ cityId: uuid })).query(({ ctx, input }) =>
557
      ctx.db
558
        .select()
559
        .from(djs)
560
        .where(and(eq(djs.cityId, input.cityId), eq(djs.status, "active"))),
561
    ),
562
    byId: publicProcedure.input(z.object({ id: uuid })).query(async ({ ctx, input }) => {
563
      const [row] = await ctx.db.select().from(djs).where(eq(djs.id, input.id));
564
      return row ?? null;
565
    }),
566
    create: protectedProcedure
567
      .input(
568
        z.object({ cityId: uuid, name: z.string().min(1), bio: z.string().optional(), links }),
569
      )
570
      .mutation(async ({ ctx, input }) => {
571
        const [row] = await ctx.db
572
          .insert(djs)
573
          .values({ ...input, ownerId: ctx.user.id })
574
          .returning();
575
        return row;
576
      }),
577
  }),
578
 
579
  instructor: router({
580
    list: publicProcedure.input(z.object({ cityId: uuid })).query(({ ctx, input }) =>
581
      ctx.db
582
        .select()
583
        .from(instructors)
584
        .where(and(eq(instructors.cityId, input.cityId), eq(instructors.status, "active"))),
585
    ),
586
    byId: publicProcedure.input(z.object({ id: uuid })).query(async ({ ctx, input }) => {
587
      const [row] = await ctx.db.select().from(instructors).where(eq(instructors.id, input.id));
588
      return row ?? null;
589
    }),
590
    create: protectedProcedure
591
      .input(
592
        z.object({ cityId: uuid, name: z.string().min(1), bio: z.string().optional(), links }),
593
      )
594
      .mutation(async ({ ctx, input }) => {
595
        const [row] = await ctx.db
596
          .insert(instructors)
597
          .values({ ...input, ownerId: ctx.user.id })
598
          .returning();
599
        return row;
600
      }),
601
  }),
602
 
603
  steppingGroup: router({
604
    list: publicProcedure.input(z.object({ cityId: uuid })).query(({ ctx, input }) =>
605
      ctx.db
606
        .select()
607
        .from(steppingGroups)
608
        .where(
609
          and(eq(steppingGroups.cityId, input.cityId), eq(steppingGroups.status, "active")),
610
        ),
611
    ),
612
    byId: publicProcedure.input(z.object({ id: uuid })).query(async ({ ctx, input }) => {
613
      const [row] = await ctx.db
614
        .select()
615
        .from(steppingGroups)
616
        .where(eq(steppingGroups.id, input.id));
617
      return row ?? null;
618
    }),
619
    create: protectedProcedure
620
      .input(
621
        z.object({
622
          cityId: uuid,
623
          name: z.string().min(1),
624
          bio: z.string().optional(),
625
          links,
626
        }),
627
      )
628
      .mutation(async ({ ctx, input }) => {
629
        const [row] = await ctx.db
630
          .insert(steppingGroups)
631
          .values({ ...input, ownerId: ctx.user.id })
632
          .returning();
633
        return row;
634
      }),
635
  }),
636
 
637
  class: router({
638
    list: publicProcedure.input(z.object({ cityId: uuid })).query(({ ctx, input }) =>
639
      ctx.db
640
        .select()
641
        .from(classes)
642
        .where(and(eq(classes.cityId, input.cityId), eq(classes.status, "active"))),
643
    ),
644
    byId: publicProcedure.input(z.object({ id: uuid })).query(async ({ ctx, input }) => {
645
      const [row] = await ctx.db.select().from(classes).where(eq(classes.id, input.id));
646
      return row ?? null;
647
    }),
648
    create: protectedProcedure
649
      .input(
650
        z.object({
651
          cityId: uuid,
652
          title: z.string().min(1),
653
          instructor: z.string().optional(),
654
          schedule: z.string().optional(),
655
          venueId: uuid.optional(),
656
          priceText: z.string().optional(),
657
          isFree: z.boolean().optional(),
658
        }),
659
      )
660
      .mutation(async ({ ctx, input }) => {
661
        const [row] = await ctx.db
662
          .insert(classes)
663
          .values({ ...input, ownerId: ctx.user.id })
664
          .returning();
665
        return row;
666
      }),
667
  }),
668
 
669
  travelPlace: router({
670
    list: publicProcedure
671
      .input(z.object({ cityId: uuid, category: z.enum(TRAVEL_CATEGORY).optional() }))
672
      .query(({ ctx, input }) =>
673
        ctx.db
674
          .select()
675
          .from(travelPlaces)
676
          .where(
677
            and(
678
              eq(travelPlaces.cityId, input.cityId),
679
              eq(travelPlaces.status, "active"),
680
              input.category ? eq(travelPlaces.category, input.category) : undefined,
681
            ),
682
          ),
683
      ),
684
    create: protectedProcedure
685
      .input(
686
        z.object({
687
          cityId: uuid,
688
          category: z.enum(TRAVEL_CATEGORY),
689
          name: z.string().min(1),
690
          address: z.string().optional(),
691
        }),
692
      )
693
      .mutation(async ({ ctx, input }) => {
694
        const [row] = await ctx.db.insert(travelPlaces).values(input).returning();
695
        return row;
696
      }),
697
  }),
698
 
699
  moderation: router({
700
    recentEvents: adminProcedure.query(({ ctx }) =>
701
      ctx.db.select().from(events).orderBy(desc(events.createdAt)).limit(50),
702
    ),
703
    hideEvent: adminProcedure
704
      .input(z.object({ id: uuid }))
705
      .mutation(async ({ ctx, input }) => {
706
        const [row] = await ctx.db
707
          .update(events)
708
          .set({ status: "hidden", hiddenAt: new Date() })
709
          .where(eq(events.id, input.id))
710
          .returning();
711
        return row;
712
      }),
713
  }),
714
});
715
 
716
export type AppRouter = typeof appRouter;