All files / src/entities utils.ts

96.77% Statements 30/31
94.44% Branches 17/18
100% Functions 6/6
96.66% Lines 29/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 13683x 83x   83x           83x         83x                                               83x       749x 1x       748x                         83x 1265x 122x   1143x 1143x 1141x   2x       83x                 1000x                               83x 14x 2x                     83x         12x 6x   6x 6x                     83x 704x 29x      
import { z } from 'zod';
import { isActionDenied } from '../config';
 
const DEFAULT_NULL = process.env.DEFAULT_NULL === 'true';
 
/**
 * GitLab REST API default pagination value.
 * @see https://docs.gitlab.com/ee/api/rest/index.html#pagination
 */
export const GITLAB_DEFAULT_PER_PAGE = 20;
 
/**
 * Maximum items per page allowed by GitLab API.
 */
export const GITLAB_MAX_PER_PAGE = 100;
 
/**
 * Creates pagination fields for Zod schemas with dynamic descriptions.
 * The description automatically includes the default value.
 *
 * @param defaultPerPage - Default items per page (default: 20)
 * @param maxPerPage - Maximum items per page (default: 100)
 * @returns Object with page and per_page Zod fields
 *
 * @example
 * // In schema definition:
 * const ListSchema = z.object({
 *   action: z.literal("list"),
 *   ...paginationFields(),  // Uses GitLab defaults (20, max 100)
 * });
 *
 * @example
 * // With custom default:
 * const ListSchema = z.object({
 *   action: z.literal("list"),
 *   ...paginationFields(50),  // Default 50, max 100
 * });
 */
export function paginationFields(
  defaultPerPage: number = GITLAB_DEFAULT_PER_PAGE,
  maxPerPage: number = GITLAB_MAX_PER_PAGE,
) {
  if (defaultPerPage > maxPerPage) {
    throw new Error(
      `Invalid pagination config: defaultPerPage (${defaultPerPage}) cannot exceed maxPerPage (${maxPerPage})`,
    );
  }
  return {
    per_page: z
      .number()
      .int()
      .min(1)
      .max(maxPerPage)
      .optional()
      .default(defaultPerPage)
      .describe(`Number of items per page (default: ${defaultPerPage}, max: ${maxPerPage})`),
    page: z.number().int().min(1).optional().describe('Page number'),
  };
}
 
export const flexibleBoolean = z.preprocess((val) => {
  if (typeof val === 'boolean') {
    return val;
  }
  try {
    const result = String(val).toLowerCase();
    return ['true', 't', '1'].includes(result);
  } catch {
    return false;
  }
}, z.boolean());
 
export const flexibleBooleanNullable = DEFAULT_NULL
  ? flexibleBoolean.nullable().default(null)
  : flexibleBoolean.nullable();
 
/**
 * Required ID field that accepts string or number input.
 * Unlike z.coerce.string(), this properly rejects undefined/null values
 * instead of coercing them to the literal string "undefined"/"null".
 */
export const requiredId = z.preprocess((val) => val ?? '', z.coerce.string().min(1));
 
/**
 * Asserts that a value is defined (not undefined).
 * Used for fields validated by Zod .refine() where TypeScript cannot
 * automatically narrow the type after runtime validation.
 *
 * Note: This intentionally only checks for undefined, not empty strings.
 * Empty string validation is handled by Zod schema .refine() checks which
 * run during Schema.parse(args) BEFORE handler code executes. This function
 * exists solely for TypeScript type narrowing after validation passes.
 *
 * @param value - The value to assert
 * @param fieldName - Name of the field for error messages
 * @throws Error if value is undefined
 */
export function assertDefined<T>(value: T | undefined, fieldName: string): asserts value is T {
  if (value === undefined) {
    throw new Error(`${fieldName} is required but was not provided`);
  }
}
 
/**
 * Validates that the appropriate ID field is provided based on scope.
 * Used by webhook schemas to ensure projectId or groupId is present.
 *
 * @param data - Object containing scope, projectId, and groupId fields
 * @returns true if validation passes
 */
export function validateScopeId(data: {
  scope: 'project' | 'group';
  projectId?: string;
  groupId?: string;
}): boolean {
  if (data.scope === 'project') {
    return !!data.projectId;
  }
  Eif (data.scope === 'group') {
    return !!data.groupId;
  }
  return true;
}
 
/**
 * Reject a tool action disabled via denied-actions config, even if it bypassed
 * schema-level filtering. Shared by entity handlers so the guard is defined once.
 *
 * @throws Error when the action is denied for the given tool.
 */
export function assertActionAllowed(toolName: string, action: string): void {
  if (isActionDenied(toolName, action)) {
    throw new Error(`Action '${action}' is not allowed for ${toolName} tool`);
  }
}