ssube/cautious-journey

View on GitHub
src/sync.ts

Summary

Maintainability
A
3 hrs
Test Coverage
A
96%
import { doesExist, InvalidArgumentError, mustExist } from '@apextoaster/js-utils';
import { Logger } from 'noicejs';

import { ProjectConfig } from './config/index.js';
import { getLabelColor, getLabelNames, getValueName } from './labels.js';
import { LabelUpdate, Remote } from './remote/index.js';
import { resolveProject } from './resolve.js';
import { compareItems, defaultTo, defaultUntil, RandomGenerator } from './utils.js';

/**
 * TODO
 *
 * @public
 */
export interface SyncOptions {
  logger: Logger;
  project: ProjectConfig;
  random: RandomGenerator;
  remote: Remote;
}

/**
 * goes through and resolves each issue in the project.
 * if there are changes and no errors, then updates the issue.
 *
 * @public
 */
export async function syncIssueLabels(options: SyncOptions): Promise<unknown> {
  const { logger, project, remote } = options;

  logger.debug({ project }, 'syncing issue labels for project');

  try {
    const issues = await remote.listIssues({
      project: project.name,
    });

    for (const issue of issues) {
      logger.info({ issue }, 'project issue');

      const { changes, errors, labels } = resolveProject({
        flags: project.flags,
        initial: project.initial,
        labels: issue.labels,
        states: project.states,
      });

      logger.debug({ changes, errors, issue, labels }, 'resolved labels');

      // TODO: prompt user if they want to update this particular issue
      const sameLabels = compareItems(issue.labels, labels) || changes.length === 0;
      if (sameLabels === false && errors.length === 0) {
        logger.info({ changes, errors, issue, labels }, 'updating issue');
        await remote.updateIssue({
          ...issue,
          labels,
        });

        if (project.comment) {
          await remote.createComment({
            ...issue,
            changes,
            errors,
          });
        }
      }
    }
  } catch (err) {
    if (err instanceof Error) {
      logger.error(err, 'error syncing issue labels');
    } else {
      logger.error('unknown error syncing issue labels');
    }
  }

  return undefined;
}

/**
 * TODO
 *
 * @public
 */
/* eslint-disable-next-line sonarjs/cognitive-complexity */
export async function syncProjectLabels(options: SyncOptions): Promise<unknown> {
  const { logger, project, remote } = options;

  logger.debug({ project }, 'syncing project labels');

  try {
    const labels = await remote.listLabels({
      project: project.name,
    });

    const present = new Set(labels.map((l) => l.name));
    const desired = getLabelNames(project.flags, project.states);
    const combined = new Set([...desired, ...present]);

    for (const label of combined) {
      const exists = present.has(label);
      const expected = desired.has(label);

      logger.info({
        exists,
        expected,
        label,
      }, 'label');

      if (exists) {
        if (expected) {
          const data = mustExist(labels.find((l) => l.name === label));
          logger.info({ data, label }, 'update label');
          await updateLabel(options, data);
        } else {
          logger.warn({ label }, 'remove label');
          await deleteLabel(options, label);
        }
      } else {
        if (expected) {
          logger.info({ label }, 'create label');
          await createLabel(options, label);
        } else {
          // skip
          logger.debug({ label }, 'label exists');
        }
      }
    }
  } catch (err) {
    if (err instanceof Error) {
      logger.error(err, 'error syncing project labels');
    } else {
      logger.error('unknown error syncing project labels');
    }
  }

  return undefined;
}

export async function createLabel(options: SyncOptions, name: string) {
  const { project, remote } = options;

  const flag = project.flags.find((it) => name === it.name);
  if (doesExist(flag)) {
    await remote.createLabel({
      color: getLabelColor(project.colors, options.random, flag),
      desc: mustExist(flag.desc),
      name,
      project: project.name,
    });

    return;
  }

  const state = project.states.find((it) => name.startsWith(it.name));
  if (doesExist(state)) {
    const value = state.values.find((it) => getValueName(state, it) === name);
    if (doesExist(value)) {
      await remote.createLabel({
        color: getLabelColor(project.colors, options.random, state, value),
        desc: defaultUntil(value.desc, state.desc, ''),
        name: getValueName(state, value),
        project: project.name,
      });

      return;
    }
  }
}

export async function deleteLabel(options: SyncOptions, name: string) {
  const { project, remote } = options;

  // TODO: check if label is in use, prompt user if they want to remove it
  await remote.deleteLabel({
    name,
    project: project.name,
  });
}

export async function diffUpdateLabel(options: SyncOptions, prevLabel: LabelUpdate, newLabel: LabelUpdate) {
  const { logger, project } = options;

  const dirty =
    prevLabel.color !== mustExist(newLabel.color) ||
    prevLabel.desc !== mustExist(newLabel.desc);

  if (dirty) {
    const body = {
      color: defaultTo(newLabel.color, prevLabel.color),
      desc: defaultTo(newLabel.desc, prevLabel.desc),
      name: prevLabel.name,
      project: project.name,
    };

    logger.debug({ body, newLabel, oldLabel: prevLabel }, 'updating label');
    const resp = await options.remote.updateLabel(body);
    logger.debug({ resp }, 'update response');
  }
}

export async function updateLabel(options: SyncOptions, label: LabelUpdate): Promise<void> {
  const { project } = options;

  const flag = project.flags.find((it) => label.name === it.name);
  if (doesExist(flag)) {
    const color = getLabelColor(project.colors, options.random, flag);
    return diffUpdateLabel(options, label, {
      color,
      desc: defaultTo(flag.desc, label.desc),
      name: flag.name,
      project: project.name,
    });
  }

  const state = project.states.find((it) => label.name.startsWith(it.name));
  if (doesExist(state)) {
    const value = state.values.find((it) => getValueName(state, it) === label.name);
    if (doesExist(value)) {
      const color = mustExist(getLabelColor(project.colors, options.random, state, value));
      return diffUpdateLabel(options, label, {
        color,
        desc: defaultTo(value.desc, label.desc),
        name: getValueName(state, value),
        project: project.name,
      });
    }
  }

  throw new InvalidArgumentError('label is not present in options');
}