All files / src/configuration namespace-capabilities.ts

100% Statements 70/70
100% Branches 45/45
100% Functions 9/9
100% Lines 54/54

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 11119x             19x 19x 19x       19x 19x                     19x 46x 5x 5x 6x 5x 12x 5x       19x 5x       5x 2x 5x 5x 5x     19x 132x 15x       27x       18x 18x   40x   39x 39x   1x   38x   38x 36x 31x 38x   16x       14x 14x 15x 14x   13x       19x           56x 18x 18x   16x 16x 21x 20x 15x     2x 1x   14x    
import {
  extractGroupsFromArgs,
  extractProjectsFromArgs,
  numericTargets,
} from '../profiles/scope-enforcer';
import type { ScopeConfig } from '../profiles/types';
import type { ToolRequirements } from '../types';
import { getNamespaceTier } from '../services/NamespaceTierDetector';
import { TIER_ORDER } from '../services/InstanceCapabilities';
import {
  hasNamespaceSubscriptions,
  runWithNamespaceTier,
} from '../services/namespace-tier-context';
import { canonicalProjectPath } from '../utils/api-path';
import { mapBounded } from '../utils/bounded-map';
import type { GitLabTier } from '../services/GitLabVersionDetector';
 
export interface NamespaceTarget {
  path: string;
  kind: 'project' | 'group' | 'either';
}
 
/** Stop optional lookups once one target makes a complete tier decision impossible. */
class UnknownSubscription extends Error {}
 
export function namespaceTargetsForScope(scope: ScopeConfig | undefined): NamespaceTarget[] {
  if (!scope) return [];
  const targets: NamespaceTarget[] = [];
  for (const path of [scope.project, ...(scope.projects ?? [])])
    if (path !== undefined) targets.push({ path, kind: 'project' });
  for (const path of [scope.group, scope.namespace, ...(scope.groups ?? [])])
    if (path !== undefined) targets.push({ path, kind: 'group' });
  return targets;
}
 
/** Numeric namespace arguments retain the handler's group-first ambiguity. */
export function namespaceTargetsForArgs(args: Record<string, unknown>): NamespaceTarget[] {
  const targets: NamespaceTarget[] = numericTargets(args).map(({ id, kind }) => ({
    path: id,
    kind,
  }));
  for (const path of extractProjectsFromArgs(args))
    if (!/^\d+$/.test(path)) targets.push({ path, kind: 'project' });
  for (const path of extractGroupsFromArgs(args))
    if (!/^\d+$/.test(path)) targets.push({ path, kind: 'group' });
  return targets;
}
 
export function needsNamespaceTier(requirements: ToolRequirements | undefined): boolean {
  if (!requirements) return false;
  return [
    requirements.default,
    ...Object.values(requirements.actions ?? {}),
    ...Object.values(requirements.parameters ?? {}),
  ].some((req) => req.tier !== undefined && req.tier !== 'free');
}
 
function distinctRoots(targets: readonly NamespaceTarget[]): NamespaceTarget[] | null {
  const unique = new Map<string, NamespaceTarget>();
  for (const target of targets) {
    // An opaque GID is resolved by its owning handler, not guessed to be a namespace.
    if (target.path.startsWith('gid://')) return null;
    let path: string;
    try {
      path = canonicalProjectPath(target.path);
    } catch {
      return null;
    }
    const slash = path.indexOf('/');
    let key: string;
    if (/^\d+$/.test(path)) key = `${target.kind}:${path}`;
    else if (slash < 0) key = path;
    else key = path.slice(0, slash);
    unique.set(key, target);
  }
  return [...unique.values()];
}
 
function selectedTier(tiers: readonly GitLabTier[], union: boolean): GitLabTier | null {
  let tier: GitLabTier | null = null;
  for (const observation of tiers) {
    if (!union && tier !== null && tier !== observation) return null;
    if (tier === null || TIER_ORDER[observation] > TIER_ORDER[tier]) tier = observation;
  }
  return tier;
}
 
/** Catalogs expose the union; a call spanning different subscriptions defers to GitLab. */
export async function withNamespaceCapabilities<T>(
  instanceUrl: string,
  targets: readonly NamespaceTarget[],
  union: boolean,
  fn: () => T,
): Promise<Awaited<T>> {
  if (!hasNamespaceSubscriptions(instanceUrl)) return await fn();
  const unique = distinctRoots(targets);
  if (unique === null) return await runWithNamespaceTier(instanceUrl, null, fn);
  let observations;
  try {
    observations = await mapBounded(unique, 5, async (target) => {
      const info = await getNamespaceTier(target.path, target.kind);
      if (info.tier === null) throw new UnknownSubscription();
      return info.tier;
    });
  } catch (error) {
    if (!(error instanceof UnknownSubscription)) throw error;
    return await runWithNamespaceTier(instanceUrl, null, fn);
  }
  return await runWithNamespaceTier(instanceUrl, selectedTier(observations, union), fn);
}