All files / src/entities/context scope-search.ts

100% Statements 17/17
100% Branches 6/6
100% Functions 3/3
100% Lines 17/17

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            23x 23x 23x   23x     23x                   23x                           23x                                                               23x 6x 6x 3x   3x 1x   2x                             1x   1x 1x                
/**
 * Project and group search for choosing a working scope in the settings panel. It reads
 * only what the account can already see in GitLab and returns paths and names, nothing
 * else of the projects.
 */
 
import { gitlab } from '../../utils/gitlab-api';
import { GITLAB_ALLOWED_PROJECT_IDS } from '../../config';
import { findOperatorProjectTargets } from '../../configuration/operator-projects';
 
export const SCOPE_SEARCH_TOOL = 'find_scope_targets';
 
/** Results of one kind; projects and groups are listed together. */
const PER_KIND = 10;
 
export interface ScopeTarget {
  type: 'project' | 'group';
  /** Full path, as the scope setting takes it. */
  path: string;
  /** Display name with its namespace. */
  name: string;
}
 
export const SCOPE_SEARCH_INPUT_SCHEMA = {
  type: 'object' as const,
  properties: {
    query: {
      type: 'string',
      minLength: 2,
      maxLength: 100,
      description: 'Part of a project or group name or path.',
    },
  },
  required: ['query'],
  additionalProperties: false,
};
 
export const SCOPE_SEARCH_OUTPUT_SCHEMA = {
  type: 'object' as const,
  properties: {
    targets: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          type: { type: 'string', enum: ['project', 'group'] },
          path: { type: 'string' },
          name: { type: 'string' },
        },
        required: ['type', 'path', 'name'],
        additionalProperties: false,
      },
    },
  },
  required: ['targets'],
  additionalProperties: false,
};
 
interface GitLabProject {
  path_with_namespace: string;
  name_with_namespace: string;
}
 
interface GitLabGroup {
  full_path: string;
  full_name: string;
}
 
/** Projects the account is a member of and groups it can see, matching the query. */
export async function findScopeTargets(query: string): Promise<{ targets: ScopeTarget[] }> {
  const search = query.trim();
  if (search.length < 2 || search.length > 100) {
    throw new Error('Search for 2 to 100 characters');
  }
  if (GITLAB_ALLOWED_PROJECT_IDS.length > 0) {
    return { targets: await findOperatorProjectTargets(search) };
  }
  const [projects, groups] = await Promise.all([
    gitlab.get<GitLabProject[]>('projects', {
      query: {
        search,
        membership: 'true',
        simple: 'true',
        per_page: String(PER_KIND),
        order_by: 'last_activity_at',
      },
    }),
    // A non-administrator is listed only its member groups unless all_available is set.
    gitlab.get<GitLabGroup[]>('groups', {
      query: { search, all_available: 'true', per_page: String(PER_KIND) },
    }),
  ]);
  return {
    targets: [
      ...groups.map((g) => ({ type: 'group' as const, path: g.full_path, name: g.full_name })),
      ...projects.map((p) => ({
        type: 'project' as const,
        path: p.path_with_namespace,
        name: p.name_with_namespace,
      })),
    ],
  };
}