because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(server): add base pagination and connection utilities

+331 -4
+5 -4
apps/server/src/modules/base/base.module.ts
··· 1 - import { Global, Module } from "@nestjs/common"; 1 + import { Module } from "@nestjs/common"; 2 + import { CursorService } from "./cursor.service"; 3 + import { PaginationService } from "./pagination.service"; 2 4 3 - @Global() 4 5 @Module({ 5 - providers: [], 6 - exports: [], 6 + providers: [PaginationService, CursorService], 7 + exports: [PaginationService, CursorService], 7 8 }) 8 9 export class BaseModule {}
+25
apps/server/src/modules/base/connection.types.ts
··· 1 + export abstract class BaseEdge<T> { 2 + cursor: string; 3 + node: T; 4 + 5 + constructor(cursor: string, node: T) { 6 + this.cursor = cursor; 7 + this.node = node; 8 + } 9 + 10 + /** 11 + * Static factory method to create an edge from pagination result 12 + */ 13 + static fromPaginationEdge<T, TEdge extends BaseEdge<T>>( 14 + this: new ( 15 + cursor: string, 16 + node: T, 17 + ) => TEdge, 18 + edge: { node: T; cursor: string }, 19 + ): TEdge { 20 + // biome-ignore lint/complexity/noThisInStatic: this is intentional for the factory pattern 21 + return new this(edge.cursor, edge.node); 22 + } 23 + } 24 + 25 + // Base classes for inheritance - concrete classes define their own GraphQL types
+18
apps/server/src/modules/base/cursor.service.ts
··· 1 + import { Injectable } from "@nestjs/common"; 2 + 3 + @Injectable() 4 + export class CursorService { 5 + /** 6 + * Encode an ID to a cursor 7 + */ 8 + encode(id: string): string { 9 + return Buffer.from(id).toString("base64"); 10 + } 11 + 12 + /** 13 + * Decode a cursor to get the ID 14 + */ 15 + decode(cursor: string): string { 16 + return Buffer.from(cursor, "base64").toString("utf-8"); 17 + } 18 + }
+18
apps/server/src/modules/base/not-found.util.ts
··· 1 + import { NotFoundException } from "@nestjs/common"; 2 + 3 + /** 4 + * Utility function to throw a NotFoundException with a consistent message 5 + * @param entityName - The name of the entity that was not found 6 + * @param property - The property that was searched (e.g., "id", "email", "name") 7 + * @param value - The value that was searched for 8 + * @throws {NotFoundException} 9 + */ 10 + export const notFound = ( 11 + entityName: string, 12 + property: string, 13 + value: string, 14 + ): never => { 15 + throw new NotFoundException( 16 + `${entityName} with ${property} ${value} not found`, 17 + ); 18 + };
+129
apps/server/src/modules/base/pagination.service.ts
··· 1 + import { Injectable } from "@nestjs/common"; 2 + import { BaseEntity } from "./base.entity"; 3 + import { CursorService } from "./cursor.service"; 4 + import { 5 + PageInfo, 6 + PaginationOptions, 7 + PaginationResult, 8 + } from "./pagination.types"; 9 + 10 + @Injectable() 11 + export class PaginationService { 12 + constructor(private cursorService: CursorService) {} 13 + 14 + /** 15 + * Parse pagination arguments from GraphQL 16 + */ 17 + parsePaginationArgs(args: { 18 + first?: number | null; 19 + after?: string | null; 20 + last?: number | null; 21 + before?: string | null; 22 + searchTerm?: string | null; 23 + }): PaginationOptions { 24 + const options: PaginationOptions = {}; 25 + 26 + if (args.first) { 27 + options.first = args.first; 28 + } 29 + if (args.after) { 30 + options.after = args.after; 31 + } 32 + if (args.last) { 33 + options.last = args.last; 34 + } 35 + if (args.before) { 36 + options.before = args.before; 37 + } 38 + 39 + return options; 40 + } 41 + 42 + /** 43 + * Build Prisma query options with cursor-based pagination 44 + * This generic method handles all cursor logic for any Prisma model 45 + */ 46 + buildQueryOptions<T extends Record<string, unknown>>( 47 + where: T, 48 + orderBy: Record<string, unknown>, 49 + options: PaginationOptions, 50 + ): { 51 + where: T & { id?: { gt?: string; lt?: string } }; 52 + orderBy: Record<string, unknown>; 53 + take?: number; 54 + } { 55 + const queryOptions: { 56 + where: T & { id?: { gt?: string; lt?: string } }; 57 + orderBy: Record<string, unknown>; 58 + take?: number; 59 + } = { 60 + where: { ...where }, 61 + orderBy, 62 + }; 63 + 64 + // Apply cursor-based pagination 65 + if (options.after) { 66 + const afterId = this.cursorService.decode(options.after); 67 + queryOptions.where = { 68 + ...where, 69 + id: { gt: afterId }, 70 + } as T & { id: { gt: string } }; 71 + } 72 + 73 + if (options.before) { 74 + const beforeId = this.cursorService.decode(options.before); 75 + queryOptions.where = { 76 + ...where, 77 + id: { lt: beforeId }, 78 + } as T & { id: { lt: string } }; 79 + } 80 + 81 + // Apply limit 82 + if (options.first) { 83 + queryOptions.take = options.first; 84 + } 85 + 86 + if (options.last) { 87 + queryOptions.take = options.last; 88 + } 89 + 90 + return queryOptions; 91 + } 92 + 93 + /** 94 + * Build pagination result 95 + */ 96 + buildPaginationResult<T extends BaseEntity>( 97 + items: T[], 98 + totalCount: number, 99 + options: PaginationOptions, 100 + ): PaginationResult<T> { 101 + const edges = items.map((item) => ({ 102 + node: item, 103 + cursor: this.cursorService.encode(item.id), 104 + })); 105 + 106 + // Use early returns for boolean logic 107 + const hasNextPage = options.first ? items.length === options.first : false; 108 + const hasPreviousPage = options.last 109 + ? items.length === options.last 110 + : false; 111 + 112 + // Use early returns for cursor logic 113 + if (edges.length === 0) { 114 + const pageInfo = new PageInfo(hasNextPage, hasPreviousPage, null, null); 115 + return { edges, pageInfo, totalCount }; 116 + } 117 + 118 + const startCursor = edges[0]?.cursor ?? null; 119 + const endCursor = edges[edges.length - 1]?.cursor ?? null; 120 + const pageInfo = new PageInfo( 121 + hasNextPage, 122 + hasPreviousPage, 123 + startCursor, 124 + endCursor, 125 + ); 126 + 127 + return { edges, pageInfo, totalCount }; 128 + } 129 + }
+63
apps/server/src/modules/base/pagination.types.ts
··· 1 + import { ArgsType, Field, InputType, Int, ObjectType } from "@nestjs/graphql"; 2 + import { GraphQLString } from "graphql"; 3 + 4 + @ObjectType() 5 + export class PageInfo { 6 + @Field(() => Boolean) 7 + hasNextPage: boolean; 8 + 9 + @Field(() => Boolean) 10 + hasPreviousPage: boolean; 11 + 12 + @Field(() => GraphQLString, { nullable: true }) 13 + startCursor: string | null; 14 + 15 + @Field(() => GraphQLString, { nullable: true }) 16 + endCursor: string | null; 17 + 18 + constructor( 19 + hasNextPage: boolean, 20 + hasPreviousPage: boolean, 21 + startCursor: string | null, 22 + endCursor: string | null, 23 + ) { 24 + this.hasNextPage = hasNextPage; 25 + this.hasPreviousPage = hasPreviousPage; 26 + this.startCursor = startCursor; 27 + this.endCursor = endCursor; 28 + } 29 + } 30 + 31 + @InputType() 32 + export abstract class BasePaginationArgs { 33 + @Field(() => Int, { nullable: true }) 34 + first?: number | null; 35 + 36 + @Field(() => GraphQLString, { nullable: true }) 37 + after?: string | null; 38 + 39 + @Field(() => Int, { nullable: true }) 40 + last?: number | null; 41 + 42 + @Field(() => GraphQLString, { nullable: true }) 43 + before?: string | null; 44 + } 45 + 46 + @ArgsType() 47 + export class PaginationArgs extends BasePaginationArgs {} 48 + 49 + export interface PaginationResult<T> { 50 + edges: Array<{ 51 + node: T; 52 + cursor: string; 53 + }>; 54 + pageInfo: PageInfo; 55 + totalCount: number; 56 + } 57 + 58 + export interface PaginationOptions { 59 + first?: number; 60 + after?: string; 61 + last?: number; 62 + before?: string; 63 + }
+11
apps/server/src/modules/database/seed/seed.module.ts
··· 1 + import { Module } from "@nestjs/common"; 2 + import { DiscoveryModule } from "@nestjs/core"; 3 + import { DatabaseModule } from "@/modules/database/database.module"; 4 + import { SeedService } from "./seed.service"; 5 + 6 + @Module({ 7 + imports: [DatabaseModule, DiscoveryModule], 8 + providers: [SeedService], 9 + exports: [SeedService], 10 + }) 11 + export class SeedModule {}
+56
apps/server/src/modules/database/seed/seed.service.ts
··· 1 + import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; 2 + import { DiscoveryService } from "@nestjs/core"; 3 + import { PrismaService } from "@/modules/database/prisma.service"; 4 + import { SEEDER_METADATA_KEY } from "./seeder.decorator"; 5 + 6 + export interface Seeder { 7 + seed(prisma: PrismaService): Promise<void>; 8 + } 9 + 10 + @Injectable() 11 + export class SeedService implements OnModuleInit { 12 + private readonly logger = new Logger(SeedService.name); 13 + private readonly seeders: Array<{ name: string; seeder: Seeder }> = []; 14 + 15 + constructor( 16 + private readonly prisma: PrismaService, 17 + private readonly discoveryService: DiscoveryService, 18 + ) {} 19 + 20 + async onModuleInit(): Promise<void> { 21 + // Discover all providers that have the @Seeder decorator 22 + const providers = this.discoveryService.getProviders(); 23 + 24 + for (const wrapper of providers) { 25 + if (wrapper.isDependencyTreeStatic() && wrapper.instance) { 26 + const instance = wrapper.instance; 27 + 28 + // Check if the provider has the @Seeder decorator 29 + const seederMetadata = Reflect.getMetadata( 30 + SEEDER_METADATA_KEY, 31 + instance.constructor, 32 + ); 33 + 34 + if (seederMetadata && instance && typeof instance.seed === "function") { 35 + const name = 36 + typeof seederMetadata === "string" 37 + ? seederMetadata 38 + : instance.constructor.name; 39 + this.seeders.push({ name, seeder: instance }); 40 + this.logger.log(`Registered seeder: ${name}`); 41 + } 42 + } 43 + } 44 + } 45 + 46 + async seed(): Promise<void> { 47 + this.logger.log("🌱 Starting database seeding..."); 48 + 49 + for (const { name, seeder } of this.seeders) { 50 + this.logger.log(`Seeding: ${name}...`); 51 + await seeder.seed(this.prisma); 52 + } 53 + 54 + this.logger.log("✅ Database seeding completed!"); 55 + } 56 + }
+6
apps/server/src/modules/database/seed/seeder.decorator.ts
··· 1 + import { SetMetadata } from "@nestjs/common"; 2 + 3 + export const SEEDER_METADATA_KEY = "seeder"; 4 + 5 + export const Seeder = (name?: string) => 6 + SetMetadata(SEEDER_METADATA_KEY, name ?? true);