All files / src/utils projectIdentifier.ts

100% Statements 27/27
100% Branches 12/12
100% Functions 5/5
100% Lines 27/27

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 101132x                                   826x                   132x 819x     814x 235x               579x   579x                   132x 813x 808x             132x 810x             132x 17x 4x     13x 13x 1x       12x 3x         9x 9x   2x       7x 1x     6x    
import { canonicalProjectPath } from './api-path';
 
/**
 * Smart project identifier that can be numeric ID or namespace path
 */
export interface ProjectIdentifier {
  /** The normalized project identifier for API calls */
  identifier: string;
  /** Whether this is a numeric ID (true) or namespace path (false) */
  isNumericId: boolean;
  /** Original input value */
  originalValue: string;
}
 
/**
 * Detects if a string is a numeric ID
 */
function isNumericId(str: string): boolean {
  return /^\d+$/.test(str.trim());
}
 
/**
 * Smart processing of project identifier that handles:
 * - Numeric IDs: "123" -> stays as "123"
 * - URL-encoded paths: decoded once, then encoded as one API segment
 * - Regular paths: "group/project" -> converts to "group%2Fproject"
 * - Dot segments and ambiguous encodings are rejected before any request
 */
export function processProjectIdentifier(input: string): ProjectIdentifier {
  const trimmedInput = canonicalProjectPath(input);
 
  // Check if it's a numeric ID
  if (isNumericId(trimmedInput)) {
    return {
      identifier: trimmedInput,
      isNumericId: true,
      originalValue: input,
    };
  }
 
  // For namespace paths, we need to ensure proper encoding for GitLab API
  const identifier = encodeURIComponent(trimmedInput);
 
  return {
    identifier,
    isNumericId: false,
    originalValue: input,
  };
}
 
/**
 * Canonical API segment for a project identifier supplied raw or URL-encoded.
 */
export function safeEncodeProjectId(projectId: string): string {
  const processed = processProjectIdentifier(projectId);
  return processed.identifier;
}
 
/**
 * Normalize project identifier for API calls
 * Ensures the identifier is properly formatted for GitLab REST API
 */
export function normalizeProjectId(projectId: string): string {
  return safeEncodeProjectId(projectId);
}
 
/**
 * Validate project identifier format
 * Returns error message if invalid, null if valid
 */
export function validateProjectIdentifier(input: string): string | null {
  if (!input || typeof input !== 'string') {
    return 'Project identifier is required and must be a string';
  }
 
  const trimmed = input.trim();
  if (trimmed.length === 0) {
    return 'Project identifier cannot be empty';
  }
 
  // Check if it's numeric ID
  if (isNumericId(trimmed)) {
    return null; // Valid numeric ID
  }
 
  // Check if it's a valid namespace path (with or without encoding)
  let decoded: string;
  try {
    decoded = canonicalProjectPath(trimmed);
  } catch {
    return 'Invalid project identifier format. Use numeric ID or namespace/project path.';
  }
 
  // Basic validation for namespace/project format
  if (!/^[a-zA-Z0-9\-_./]+$/.test(decoded)) {
    return 'Invalid project identifier format. Use numeric ID or namespace/project path.';
  }
 
  return null; // Valid
}