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.

refactor(system): add createGqlParamDecorator utility and refactor param decorators

+132 -61
+32 -37
apps/server/src/modules/authentication/token/refresh-token-cookie.decorator.ts
··· 1 - import { unauthorized } from "@cv/system"; 2 - import { createParamDecorator, ExecutionContext } from "@nestjs/common"; 1 + import { createGqlParamDecorator, unauthorized } from "@cv/system"; 2 + import type { ExecutionContext } from "@nestjs/common"; 3 3 import { GqlExecutionContext } from "@nestjs/graphql"; 4 - import type { Request } from "express"; 4 + import { z } from "zod/v4"; 5 5 6 - type RequestWithCookies<TCookies = Record<string, string>> = Request & { 7 - cookies?: TCookies; 8 - }; 6 + const cookieSchema = z.looseObject({ 7 + cookies: z.looseObject({ 8 + refresh_token: z.string(), 9 + }), 10 + }); 9 11 10 - interface RefreshTokenCookieOptions { 11 - optional?: boolean; 12 - } 12 + const resolveRequest = (context: ExecutionContext) => 13 + GqlExecutionContext.create(context).getContext().req ?? 14 + context.switchToHttp().getRequest(); 13 15 14 - export const RefreshTokenCookie = createParamDecorator( 15 - ( 16 - options: RefreshTokenCookieOptions | unknown, 17 - context: ExecutionContext, 18 - ): string | undefined => { 19 - const opts = 20 - typeof options === "object" && options !== null 21 - ? (options as RefreshTokenCookieOptions) 22 - : { optional: false }; 16 + const required = createGqlParamDecorator({ 17 + schema: cookieSchema, 18 + resolve: resolveRequest, 19 + lens: (data) => data.cookies.refresh_token, 20 + fallback: () => unauthorized("Refresh token not found in cookies"), 21 + }); 23 22 24 - const gqlContext = GqlExecutionContext.create(context); 25 - const httpContext = context.switchToHttp(); 26 - 27 - const request: Request | RequestWithCookies<{ refresh_token?: string }> = 28 - gqlContext.getContext().req 29 - ? gqlContext.getContext().req 30 - : httpContext.getRequest(); 31 - 32 - const token = request.cookies?.["refresh_token"]; 33 - 34 - if (!token) { 35 - if (opts.optional) { 36 - return undefined; 37 - } 38 - return unauthorized("Refresh token not found in cookies"); 39 - } 23 + const optional = createGqlParamDecorator({ 24 + schema: cookieSchema, 25 + resolve: resolveRequest, 26 + lens: (data) => data.cookies.refresh_token, 27 + fallback: undefined, 28 + }); 40 29 41 - return token; 42 - }, 43 - ); 30 + export function RefreshTokenCookie(): ParameterDecorator; 31 + export function RefreshTokenCookie(options: { 32 + optional: true; 33 + }): ParameterDecorator; 34 + export function RefreshTokenCookie(options?: { 35 + optional?: boolean; 36 + }): ParameterDecorator { 37 + return options?.optional ? optional() : required(); 38 + }
+12 -14
apps/server/src/modules/current-user/current-refresh-token-id.decorator.ts
··· 1 - import { createParamDecorator, type ExecutionContext } from "@nestjs/common"; 2 - import { GqlExecutionContext } from "@nestjs/graphql"; 1 + import { createGqlParamDecorator } from "@cv/system"; 2 + import { z } from "zod/v4"; 3 3 4 - interface JwtPayload { 5 - refreshTokenId?: string; 6 - } 4 + const jwtRequestSchema = z.looseObject({ 5 + jwtPayload: z.looseObject({ 6 + refreshTokenId: z.string(), 7 + }), 8 + }); 7 9 8 - export const CurrentRefreshTokenId = createParamDecorator( 9 - (_data: unknown, context: ExecutionContext): string | undefined => { 10 - const ctx = GqlExecutionContext.create(context); 11 - const request = ctx.getContext().req as { 12 - jwtPayload?: JwtPayload; 13 - }; 14 - return request.jwtPayload?.refreshTokenId; 15 - }, 16 - ); 10 + export const CurrentRefreshTokenId = createGqlParamDecorator({ 11 + schema: jwtRequestSchema, 12 + lens: ({ jwtPayload: jwt }) => jwt.refreshTokenId, 13 + fallback: undefined, 14 + });
+15 -10
apps/server/src/modules/current-user/current-user.decorator.ts
··· 1 - import { raise } from "@cv/utils"; 2 - import { createParamDecorator, type ExecutionContext } from "@nestjs/common"; 3 - import { GqlExecutionContext } from "@nestjs/graphql"; 1 + import { createGqlParamDecorator, raise } from "@cv/system"; 2 + import { z } from "zod/v4"; 3 + 4 + const requestSchema = z.looseObject({ 5 + user: z.looseObject({ 6 + id: z.string(), 7 + name: z.string(), 8 + role: z.string(), 9 + }), 10 + }); 4 11 5 - export const CurrentUser = createParamDecorator( 6 - (_data: unknown, context: ExecutionContext) => { 7 - const ctx = GqlExecutionContext.create(context); 8 - const request = ctx.getContext().req; 9 - return request.user ?? raise("User not found in request context"); 10 - }, 11 - ); 12 + export const CurrentUser = createGqlParamDecorator({ 13 + schema: requestSchema, 14 + lens: ({ user }) => user, 15 + fallback: () => raise("User not found in request context"), 16 + });
+72
packages/system/src/base/create-gql-param-decorator.ts
··· 1 + import { createParamDecorator, type ExecutionContext } from "@nestjs/common"; 2 + import { GqlExecutionContext } from "@nestjs/graphql"; 3 + 4 + type SafeParseResult<T> = 5 + | { success: true; data: T } 6 + | { success: false; error: unknown }; 7 + 8 + interface SafeParseable<T> { 9 + safeParse(data: unknown): SafeParseResult<T>; 10 + } 11 + 12 + interface GqlParamDecoratorOptions<TSchema, TSuccess, TFallback> { 13 + schema: SafeParseable<TSchema>; 14 + source?: "request" | "context"; 15 + resolve?: (context: ExecutionContext) => unknown; 16 + lens: (data: TSchema) => TSuccess; 17 + fallback: TFallback | (() => never); 18 + } 19 + 20 + /** 21 + * Creates a NestJS GQL param decorator that validates the execution context 22 + * with a schema (e.g. Zod) and extracts a value using a lens function. 23 + * 24 + * @param options.schema - A schema with a `safeParse` method (e.g. a Zod schema) 25 + * @param options.source - What to parse: "request" (req object) or "context" (full GQL context). Defaults to "request". 26 + * @param options.resolve - Custom function to extract raw data from the ExecutionContext. Overrides `source`. 27 + * @param options.lens - Extracts the desired value from the parsed data 28 + * @param options.fallback - Value to return on parse failure, or a function that throws 29 + * 30 + * @example 31 + * ```typescript 32 + * const CurrentRefreshTokenId = createGqlParamDecorator({ 33 + * schema: z.object({ jwtPayload: z.object({ refreshTokenId: z.string() }) }), 34 + * lens: (data) => data.jwtPayload.refreshTokenId, 35 + * fallback: undefined, 36 + * }); 37 + * 38 + * const CurrentUser = createGqlParamDecorator({ 39 + * schema: z.object({ user: z.object({ id: z.string(), email: z.string() }) }), 40 + * source: "context", 41 + * lens: (data) => data.user, 42 + * fallback: () => raise("User not found in request context"), 43 + * }); 44 + * ``` 45 + */ 46 + export const createGqlParamDecorator = <TSchema, TSuccess, TFallback>({ 47 + schema, 48 + source, 49 + resolve = (ctx) => resolveDefault(ctx, source), 50 + lens, 51 + fallback, 52 + }: GqlParamDecoratorOptions<TSchema, TSuccess, TFallback>) => 53 + createParamDecorator( 54 + (_data: unknown, context: ExecutionContext): TSuccess | TFallback => { 55 + const result = schema.safeParse(resolve(context)); 56 + 57 + return result.success 58 + ? lens(result.data) 59 + : typeof fallback === "function" 60 + ? (fallback as () => never)() 61 + : fallback; 62 + }, 63 + ); 64 + 65 + const resolveDefault = ( 66 + context: ExecutionContext, 67 + source?: "request" | "context", 68 + ) => { 69 + const ctx = GqlExecutionContext.create(context).getContext(); 70 + 71 + return source === "context" ? ctx : ctx.req; 72 + };
+1
packages/system/src/base/index.ts
··· 1 1 export * from "./base.entity"; 2 + export * from "./create-gql-param-decorator"; 2 3 export * from "./base.module"; 3 4 export * from "./clock.service"; 4 5 export * from "./connection.types";