<commit_before>// @flow

const R = require('ramda');
const validator = require('validator');
const sshpk = require('sshpk');
const { keycloakAdminClient } = require('../../clients/keycloakClient');
const searchguardClient = require('../../clients/searchguardClient');
const logger = require('../../logger');
const {
  inClause,
  prepare,
  query,
  whereAnd,
  isPatchEmpty,
} = require('../../util/db');

const Helpers = require('./helpers');
const KeycloakOperations = require('./keycloak');
const { SearchguardOperations } = require('../group/searchguard');
const Sql = require('./sql');
const { generatePrivateKey, getSshKeyFingerprint } = require('../sshKey');
const sshKeySql = require('../sshKey/sql');

/* ::

import type {ResolversObj} from '../';

*/

const removePrivateKey = R.assoc('privateKey', null);

const getAllProjects = async (
  root,
  args,
  {
    sqlClient,
    hasPermission,
    dataSources,
    keycloakGrant,
  },
) => {
  let where;
  try {
    await hasPermission('project', 'viewAll');

    where = whereAnd([
      args.createdAfter ? 'created >= :created_after' : '',
      args.gitUrl ? 'git_url = :git_url' : '',
    ]);
  } catch (err) {
    if (!keycloakGrant) {
      logger.warn('No grant available for getAllProjects');
      return [];
    }

    const userProjectIds = await dataSources.UserModel.getAllProjectsIdsForUser({
      id: keycloakGrant.access_token.content.sub,
    });

    where = whereAnd([
      args.createdAfter ? 'created >= :created_after' : '',
      args.gitUrl ? 'git_url = :git_url' : '',
      inClause('id', userProjectIds),
    ]);
  }

  const order = args.order ? ` ORDER BY ${R.toLower(args.order)} ASC` : '';

  const prep = prepare(sqlClient, `SELECT * FROM project ${where}${order}`);
  const rows = await query(sqlClient, prep(args));

  return rows;
};

const getProjectByEnvironmentId = async (
  { id: eid },
  args,
  {
    sqlClient,
    hasPermission,
  },
) => {
  const prep = prepare(
    sqlClient,
    `SELECT
        p.*
      FROM environment e
      JOIN project p ON e.project = p.id
      WHERE e.id = :eid
      LIMIT 1
    `,
  );

  const rows = await query(sqlClient, prep({ eid }));

  const project = rows[0];

  await hasPermission('project', 'view', {
    project: project.id,
  });

  try {
    await hasPermission('project', 'viewPrivateKey', {
      project: project.id,
    });

    return project;
  } catch (err) {
    return removePrivateKey(project);
  }
};

const getProjectByGitUrl = async (
  root,
  args,
  {
    sqlClient,
    hasPermission,
  },
) => {
  const str = `
      SELECT
        *
      FROM project
      WHERE git_url = :git_url
      LIMIT 1
    `;

  const prep = prepare(sqlClient, str);
  const rows = await query(sqlClient, prep(args));

  const project = rows[0];

  await hasPermission('project', 'view', {
    project: project.id,
  });

  try {
    await hasPermission('project', 'viewPrivateKey', {
      project: project.id,
    });

    return project;
  } catch (err) {
    return removePrivateKey(project);
  }
};

const getProjectByName = async (
  root,
  args,
  {
    sqlClient,
    hasPermission,
  },
) => {
  const str = `
      SELECT
        *
      FROM project
      WHERE name = :name
    `;

  const prep = prepare(sqlClient, str);

  const rows = await query(sqlClient, prep(args));
  const project = rows[0];

  await hasPermission('project', 'view', {
    project: project.id,
  });

  try {
    await hasPermission('project', 'viewPrivateKey', {
      project: project.id,
    });

    return project;
  } catch (err) {
    return removePrivateKey(project);
  }
};

const addProject = async (
  root,
  { input },
  {
    hasPermission,
    sqlClient,
    dataSources,
  },
) => {
  await hasPermission('project', 'add');

  if (validator.matches(input.name, /[^0-9a-z-]/)) {
    throw new Error(
      'Only lowercase characters, numbers and dashes allowed for name!',
    );
  }

  let keyPair = {};
  try {
    const privateKey = R.cond([
      [R.isNil, generatePrivateKey],
      [R.isEmpty, generatePrivateKey],
      [R.T, sshpk.parsePrivateKey],
    ])(R.prop('privateKey', input));

    const publicKey = privateKey.toPublic();

    keyPair = {
      ...keyPair,
      private: R.replace(/\n/g, '\n', privateKey.toString('openssh')),
      public: publicKey.toString(),
    };
  } catch (err) {
    throw new Error(`There was an error with the privateKey: ${err.message}`);
  }

  const prep = prepare(
    sqlClient,
    `CALL CreateProject(
        :id,
        :name,
        :git_url,
        :private_key,
        ${input.subfolder ? ':subfolder' : 'NULL'},
        :openshift,
        ${
  input.openshiftProjectPattern ? ':openshift_project_pattern' : 'NULL'
},
        ${
  input.activeSystemsDeploy
    ? ':active_systems_deploy'
    : '"lagoon_openshiftBuildDeploy"'
},
        ${
  input.activeSystemsPromote
    ? ':active_systems_promote'
    : '"lagoon_openshiftBuildDeploy"'
},
        ${
  input.activeSystemsRemove
    ? ':active_systems_remove'
    : '"lagoon_openshiftRemove"'
},
        ${
  input.activeSystemsTask
    ? ':active_systems_task'
    : '"lagoon_openshiftJob"'
},
        ${input.branches ? ':branches' : '"true"'},
        ${input.pullrequests ? ':pullrequests' : '"true"'},
        ${input.productionEnvironment ? ':production_environment' : 'NULL'},
        ${input.autoIdle ? ':auto_idle' : '1'},
        ${input.storageCalc ? ':storage_calc' : '1'},
        ${
  input.developmentEnvironmentsLimit
    ? ':development_environments_limit'
    : '5'
}
      );
    `,
  );

  const rows = await query(sqlClient, prep({
    ...input,
    privateKey: keyPair.private,
  }));
  const project = R.path([0, 0], rows);

  // Create a default group for this project
  let group;
  try {
    group = await dataSources.GroupModel.addGroup({
      name: `project-${project.name}`,
      attributes: {
        type: ['project-default-group'],
        'lagoon-projects': [project.id],
      },
    });
  } catch (err) {
    logger.error(`Could not create default project group for ${project.name}: ${err.message}`);
  }

  await SearchguardOperations(sqlClient, dataSources.GroupModel).syncGroup(`project-${project.name}`, project.id);

  // Find or create a user that has the public key linked to them
  const userRows = await query(
    sqlClient,
    sshKeySql.selectUserIdsBySshKeyFingerprint(getSshKeyFingerprint(keyPair.public)),
  );
  const userId = R.path([0, 'usid'], userRows);

  let user;
  if (!userId) {
    try {
      user = await dataSources.UserModel.addUser({
        email: `default-user@${project.name}`,
        username: `default-user@${project.name}`,
        comment: `autogenerated user for project ${project.name}`,
      });

      const keyParts = keyPair.public.split(' ');

      const {
        info: { insertId },
      } = await query(
        sqlClient,
        sshKeySql.insertSshKey({
          id: null,
          name: 'auto-add via api',
          keyValue: keyParts[1],
          keyType: keyParts[0],
          keyFingerprint: getSshKeyFingerprint(keyPair.public),
        }),
      );
      await query(sqlClient, sshKeySql.addSshKeyToUser({ sshKeyId: insertId, userId: user.id }));
    } catch (err) {
      logger.error(`Could not create default project user for ${project.name}: ${err.message}`);
    }
  } else {
    user = await dataSources.UserModel.loadUserById(userId);
  }

  // Add the user (with linked public key) to the default group as guest
  try {
    await dataSources.GroupModel.addUserToGroup(user, group, 'guest');
  } catch (err) {
    logger.error(`Could not link user to default projet group for ${project.name}: ${err.message}`);
  }

  // TODO
  // await SearchguardOperations(sqlClient).addProject(project);

  return project;
};

const deleteProject = async (
  root,
  { input: { project: projectName } },
  {
    sqlClient,
    hasPermission,
    dataSources,
  },
) => {
  // Will throw on invalid conditions
  const pid = await Helpers(sqlClient).getProjectIdByName(projectName);
  const project = await Helpers(sqlClient).getProjectById(pid);

  await hasPermission('project', 'delete', {
    project: pid,
  });

  const prep = prepare(sqlClient, 'CALL DeleteProject(:name)');
  await query(sqlClient, prep(project));

  // Remove the default group and user
  try {
    const group = await dataSources.GroupModel.loadGroupByName(`project-${project.name}`);
    await dataSources.GroupModel.deleteGroup(group.id);
    await SearchguardOperations(sqlClient, dataSources.GroupModel).deleteGroup(group.name);
  } catch (err) {
    logger.error(`Could not delete default group for project ${project.name}: ${err.message}`);
  }

  try {
    const user = await dataSources.UserModel.loadUserByUsername(`default-user@${project.name}`);
    await dataSources.UserModel.deleteUser(user.id);
  } catch (err) {
    logger.error(`Could not delete default user for project ${project.name}: ${err.message}`);
  }

  // TODO searchguard
  // await KeycloakOperations.deleteGroup(project);

  // try {
  //   // Delete SearchGuard Role for this project with the same name as the Project
  //   await searchguardClient.delete(`roles/${project}`);
  // } catch (err) {
  //   logger.error(`SearchGuard delete role error: ${err}`);
  //   throw new Error(`SearchGuard delete role error: ${err}`);
  // }
  // TODO: maybe check rows for changed result
  return 'success';
};

const updateProject = async (
  root,
  {
    input: {
      id,
      patch,
      patch: {
        name,
        gitUrl,
        privateKey,
        subfolder,
        activeSystemsDeploy,
        activeSystemsRemove,
        activeSystemsTask,
        branches,
        productionEnvironment,
        autoIdle,
        storageCalc,
        pullrequests,
        openshift,
        openshiftProjectPattern,
        developmentEnvironmentsLimit,
      },
    },
  },
  {
    sqlClient,
    hasPermission,
    dataSources,
  },
) => {
  await hasPermission('project', 'update', {
    project: id,
  });

  if (isPatchEmpty({ patch })) {
    throw new Error('input.patch requires at least 1 attribute');
  }

  if (typeof name === 'string') {
    if (validator.matches(name, /[^0-9a-z-]/)) {
      throw new Error(
        'Only lowercase characters, numbers and dashes allowed for name!',
      );
    }
  }

  const oldProject = await Helpers(sqlClient).getProjectById(id);

  // TODO If the privateKey changes, automatically remove the old one from the
  // default user and link the new one.

  // const originalProject = await Helpers(sqlClient).getProjectById(id);
  // const originalName = R.prop('name', originalProject);
  // const originalCustomer = parseInt(R.prop('customer', originalProject));

  // // If the project will be updating the `name` or `customer` fields, update Keycloak groups and users accordingly
  // if (typeof customer === 'number' && customer !== originalCustomer) {
  //   // Delete Keycloak users from original projects where given user ids do not have other access via `project_user` (projects where the user loses access if they lose customer access).
  //   await Helpers(sqlClient).mapIfNoDirectProjectAccess(
  //     id,
  //     originalCustomer,
  //     async ({
  //       keycloakUserId,
  //       keycloakUsername,
  //       keycloakGroupId,
  //       keycloakGroupName,
  //     }) => {
  //       await keycloakAdminClient.users.delFromGroup({
  //         id: keycloakUserId,
  //         groupId: keycloakGroupId,
  //       });
  //       logger.debug(
  //         `Removed Keycloak user ${keycloakUsername} from group "${keycloakGroupName}"`,
  //       );
  //     },
  //   );
  // }

  await query(
    sqlClient,
    Sql.updateProject({
      id,
      patch: {
        name,
        gitUrl,
        privateKey,
        subfolder,
        activeSystemsDeploy,
        activeSystemsRemove,
        activeSystemsTask,
        branches,
        productionEnvironment,
        autoIdle,
        storageCalc,
        pullrequests,
        openshift,
        openshiftProjectPattern,
        developmentEnvironmentsLimit,
      },
    }),
  );

  // Rename the default group and user
  if (patch.name && oldProject.name !== patch.name) {
    try {
      const group = await dataSources.GroupModel.loadGroupByName(`project-${oldProject.name}`);
      await dataSources.GroupModel.updateGroup({
        id: group.id,
        name: `project-${patch.name}`,
      });
    } catch (err) {
      logger.error(`Could not rename default group for project ${patch.name}: ${err.message}`);
    }

    try {
      const user = await dataSources.UserModel.loadUserByUsername(`default-user@${oldProject.name}`);
      await dataSources.UserModel.updateUser({
        id: user.id,
        email: `default-user@${patch.name}`,
        username: `default-user@${patch.name}`,
        comment: `autogenerated user for project ${patch.name}`,
      });
    } catch (err) {
      logger.error(`Could not rename default user for project ${patch.name}: ${err.message}`);
    }
  }

  // if (typeof name === 'string' && name !== originalName) {
  //   const groupId = await KeycloakOperations.findGroupIdByName(originalName);

  //   await keycloakAdminClient.groups.update({ id: groupId }, { name });
  //   logger.debug(
  //     `Renamed Keycloak group ${groupId} from "${originalName}" to "${name}"`,
  //   );
  // }

  // if (typeof customer === 'number' && customer !== originalCustomer) {
  //   // Add Keycloak users to new projects where given user ids do not have other access via `project_user` (projects where the user loses access if they lose customer access).
  //   await Helpers(sqlClient).mapIfNoDirectProjectAccess(
  //     id,
  //     customer,
  //     async ({
  //       keycloakUserId,
  //       keycloakUsername,
  //       keycloakGroupId,
  //       keycloakGroupName,
  //     }) => {
  //       await keycloakAdminClient.users.addToGroup({
  //         id: keycloakUserId,
  //         groupId: keycloakGroupId,
  //       });
  //       logger.debug(
  //         `Added Keycloak user ${keycloakUsername} to group "${keycloakGroupName}"`,
  //       );
  //     },
  //   );
  // }

  return Helpers(sqlClient).getProjectById(id);
};

const deleteAllProjects = async (
  root,
  args,
  { sqlClient, hasPermission },
) => {
  await hasPermission('project', 'deleteAll');

  const projectNames = await Helpers(sqlClient).getAllProjectNames();

  await query(sqlClient, Sql.truncateProject());

  for (const name of projectNames) {
    await KeycloakOperations.deleteGroup(name);
  }

  // TODO: Check rows for success
  return 'success';
};

const Resolvers /* : ResolversObj */ = {
  deleteProject,
  addProject,
  getProjectByName,
  getProjectByGitUrl,
  getProjectByEnvironmentId,
  getAllProjects,
  updateProject,
  deleteAllProjects,
};

module.exports = Resolvers;
<commit_msg>Give default project user maintainer role by default.<commit_after>// @flow

const R = require('ramda');
const validator = require('validator');
const sshpk = require('sshpk');
const { keycloakAdminClient } = require('../../clients/keycloakClient');
const searchguardClient = require('../../clients/searchguardClient');
const logger = require('../../logger');
const {
  inClause,
  prepare,
  query,
  whereAnd,
  isPatchEmpty,
} = require('../../util/db');

const Helpers = require('./helpers');
const KeycloakOperations = require('./keycloak');
const { SearchguardOperations } = require('../group/searchguard');
const Sql = require('./sql');
const { generatePrivateKey, getSshKeyFingerprint } = require('../sshKey');
const sshKeySql = require('../sshKey/sql');

/* ::

import type {ResolversObj} from '../';

*/

const removePrivateKey = R.assoc('privateKey', null);

const getAllProjects = async (
  root,
  args,
  {
    sqlClient,
    hasPermission,
    dataSources,
    keycloakGrant,
  },
) => {
  let where;
  try {
    await hasPermission('project', 'viewAll');

    where = whereAnd([
      args.createdAfter ? 'created >= :created_after' : '',
      args.gitUrl ? 'git_url = :git_url' : '',
    ]);
  } catch (err) {
    if (!keycloakGrant) {
      logger.warn('No grant available for getAllProjects');
      return [];
    }

    const userProjectIds = await dataSources.UserModel.getAllProjectsIdsForUser({
      id: keycloakGrant.access_token.content.sub,
    });

    where = whereAnd([
      args.createdAfter ? 'created >= :created_after' : '',
      args.gitUrl ? 'git_url = :git_url' : '',
      inClause('id', userProjectIds),
    ]);
  }

  const order = args.order ? ` ORDER BY ${R.toLower(args.order)} ASC` : '';

  const prep = prepare(sqlClient, `SELECT * FROM project ${where}${order}`);
  const rows = await query(sqlClient, prep(args));

  return rows;
};

const getProjectByEnvironmentId = async (
  { id: eid },
  args,
  {
    sqlClient,
    hasPermission,
  },
) => {
  const prep = prepare(
    sqlClient,
    `SELECT
        p.*
      FROM environment e
      JOIN project p ON e.project = p.id
      WHERE e.id = :eid
      LIMIT 1
    `,
  );

  const rows = await query(sqlClient, prep({ eid }));

  const project = rows[0];

  await hasPermission('project', 'view', {
    project: project.id,
  });

  try {
    await hasPermission('project', 'viewPrivateKey', {
      project: project.id,
    });

    return project;
  } catch (err) {
    return removePrivateKey(project);
  }
};

const getProjectByGitUrl = async (
  root,
  args,
  {
    sqlClient,
    hasPermission,
  },
) => {
  const str = `
      SELECT
        *
      FROM project
      WHERE git_url = :git_url
      LIMIT 1
    `;

  const prep = prepare(sqlClient, str);
  const rows = await query(sqlClient, prep(args));

  const project = rows[0];

  await hasPermission('project', 'view', {
    project: project.id,
  });

  try {
    await hasPermission('project', 'viewPrivateKey', {
      project: project.id,
    });

    return project;
  } catch (err) {
    return removePrivateKey(project);
  }
};

const getProjectByName = async (
  root,
  args,
  {
    sqlClient,
    hasPermission,
  },
) => {
  const str = `
      SELECT
        *
      FROM project
      WHERE name = :name
    `;

  const prep = prepare(sqlClient, str);

  const rows = await query(sqlClient, prep(args));
  const project = rows[0];

  await hasPermission('project', 'view', {
    project: project.id,
  });

  try {
    await hasPermission('project', 'viewPrivateKey', {
      project: project.id,
    });

    return project;
  } catch (err) {
    return removePrivateKey(project);
  }
};

const addProject = async (
  root,
  { input },
  {
    hasPermission,
    sqlClient,
    dataSources,
  },
) => {
  await hasPermission('project', 'add');

  if (validator.matches(input.name, /[^0-9a-z-]/)) {
    throw new Error(
      'Only lowercase characters, numbers and dashes allowed for name!',
    );
  }

  let keyPair = {};
  try {
    const privateKey = R.cond([
      [R.isNil, generatePrivateKey],
      [R.isEmpty, generatePrivateKey],
      [R.T, sshpk.parsePrivateKey],
    ])(R.prop('privateKey', input));

    const publicKey = privateKey.toPublic();

    keyPair = {
      ...keyPair,
      private: R.replace(/\n/g, '\n', privateKey.toString('openssh')),
      public: publicKey.toString(),
    };
  } catch (err) {
    throw new Error(`There was an error with the privateKey: ${err.message}`);
  }

  const prep = prepare(
    sqlClient,
    `CALL CreateProject(
        :id,
        :name,
        :git_url,
        :private_key,
        ${input.subfolder ? ':subfolder' : 'NULL'},
        :openshift,
        ${
  input.openshiftProjectPattern ? ':openshift_project_pattern' : 'NULL'
},
        ${
  input.activeSystemsDeploy
    ? ':active_systems_deploy'
    : '"lagoon_openshiftBuildDeploy"'
},
        ${
  input.activeSystemsPromote
    ? ':active_systems_promote'
    : '"lagoon_openshiftBuildDeploy"'
},
        ${
  input.activeSystemsRemove
    ? ':active_systems_remove'
    : '"lagoon_openshiftRemove"'
},
        ${
  input.activeSystemsTask
    ? ':active_systems_task'
    : '"lagoon_openshiftJob"'
},
        ${input.branches ? ':branches' : '"true"'},
        ${input.pullrequests ? ':pullrequests' : '"true"'},
        ${input.productionEnvironment ? ':production_environment' : 'NULL'},
        ${input.autoIdle ? ':auto_idle' : '1'},
        ${input.storageCalc ? ':storage_calc' : '1'},
        ${
  input.developmentEnvironmentsLimit
    ? ':development_environments_limit'
    : '5'
}
      );
    `,
  );

  const rows = await query(sqlClient, prep({
    ...input,
    privateKey: keyPair.private,
  }));
  const project = R.path([0, 0], rows);

  // Create a default group for this project
  let group;
  try {
    group = await dataSources.GroupModel.addGroup({
      name: `project-${project.name}`,
      attributes: {
        type: ['project-default-group'],
        'lagoon-projects': [project.id],
      },
    });
  } catch (err) {
    logger.error(`Could not create default project group for ${project.name}: ${err.message}`);
  }

  await SearchguardOperations(sqlClient, dataSources.GroupModel).syncGroup(`project-${project.name}`, project.id);

  // Find or create a user that has the public key linked to them
  const userRows = await query(
    sqlClient,
    sshKeySql.selectUserIdsBySshKeyFingerprint(getSshKeyFingerprint(keyPair.public)),
  );
  const userId = R.path([0, 'usid'], userRows);

  let user;
  if (!userId) {
    try {
      user = await dataSources.UserModel.addUser({
        email: `default-user@${project.name}`,
        username: `default-user@${project.name}`,
        comment: `autogenerated user for project ${project.name}`,
      });

      const keyParts = keyPair.public.split(' ');

      const {
        info: { insertId },
      } = await query(
        sqlClient,
        sshKeySql.insertSshKey({
          id: null,
          name: 'auto-add via api',
          keyValue: keyParts[1],
          keyType: keyParts[0],
          keyFingerprint: getSshKeyFingerprint(keyPair.public),
        }),
      );
      await query(sqlClient, sshKeySql.addSshKeyToUser({ sshKeyId: insertId, userId: user.id }));
    } catch (err) {
      logger.error(`Could not create default project user for ${project.name}: ${err.message}`);
    }
  } else {
    user = await dataSources.UserModel.loadUserById(userId);
  }

  // Add the user (with linked public key) to the default group with maintainer role
  try {
    await dataSources.GroupModel.addUserToGroup(user, group, 'maintainer');
  } catch (err) {
    logger.error(`Could not link user to default projet group for ${project.name}: ${err.message}`);
  }

  // TODO
  // await SearchguardOperations(sqlClient).addProject(project);

  return project;
};

const deleteProject = async (
  root,
  { input: { project: projectName } },
  {
    sqlClient,
    hasPermission,
    dataSources,
  },
) => {
  // Will throw on invalid conditions
  const pid = await Helpers(sqlClient).getProjectIdByName(projectName);
  const project = await Helpers(sqlClient).getProjectById(pid);

  await hasPermission('project', 'delete', {
    project: pid,
  });

  const prep = prepare(sqlClient, 'CALL DeleteProject(:name)');
  await query(sqlClient, prep(project));

  // Remove the default group and user
  try {
    const group = await dataSources.GroupModel.loadGroupByName(`project-${project.name}`);
    await dataSources.GroupModel.deleteGroup(group.id);
    await SearchguardOperations(sqlClient, dataSources.GroupModel).deleteGroup(group.name);
  } catch (err) {
    logger.error(`Could not delete default group for project ${project.name}: ${err.message}`);
  }

  try {
    const user = await dataSources.UserModel.loadUserByUsername(`default-user@${project.name}`);
    await dataSources.UserModel.deleteUser(user.id);
  } catch (err) {
    logger.error(`Could not delete default user for project ${project.name}: ${err.message}`);
  }

  // TODO searchguard
  // await KeycloakOperations.deleteGroup(project);

  // try {
  //   // Delete SearchGuard Role for this project with the same name as the Project
  //   await searchguardClient.delete(`roles/${project}`);
  // } catch (err) {
  //   logger.error(`SearchGuard delete role error: ${err}`);
  //   throw new Error(`SearchGuard delete role error: ${err}`);
  // }
  // TODO: maybe check rows for changed result
  return 'success';
};

const updateProject = async (
  root,
  {
    input: {
      id,
      patch,
      patch: {
        name,
        gitUrl,
        privateKey,
        subfolder,
        activeSystemsDeploy,
        activeSystemsRemove,
        activeSystemsTask,
        branches,
        productionEnvironment,
        autoIdle,
        storageCalc,
        pullrequests,
        openshift,
        openshiftProjectPattern,
        developmentEnvironmentsLimit,
      },
    },
  },
  {
    sqlClient,
    hasPermission,
    dataSources,
  },
) => {
  await hasPermission('project', 'update', {
    project: id,
  });

  if (isPatchEmpty({ patch })) {
    throw new Error('input.patch requires at least 1 attribute');
  }

  if (typeof name === 'string') {
    if (validator.matches(name, /[^0-9a-z-]/)) {
      throw new Error(
        'Only lowercase characters, numbers and dashes allowed for name!',
      );
    }
  }

  const oldProject = await Helpers(sqlClient).getProjectById(id);

  // TODO If the privateKey changes, automatically remove the old one from the
  // default user and link the new one.

  // const originalProject = await Helpers(sqlClient).getProjectById(id);
  // const originalName = R.prop('name', originalProject);
  // const originalCustomer = parseInt(R.prop('customer', originalProject));

  // // If the project will be updating the `name` or `customer` fields, update Keycloak groups and users accordingly
  // if (typeof customer === 'number' && customer !== originalCustomer) {
  //   // Delete Keycloak users from original projects where given user ids do not have other access via `project_user` (projects where the user loses access if they lose customer access).
  //   await Helpers(sqlClient).mapIfNoDirectProjectAccess(
  //     id,
  //     originalCustomer,
  //     async ({
  //       keycloakUserId,
  //       keycloakUsername,
  //       keycloakGroupId,
  //       keycloakGroupName,
  //     }) => {
  //       await keycloakAdminClient.users.delFromGroup({
  //         id: keycloakUserId,
  //         groupId: keycloakGroupId,
  //       });
  //       logger.debug(
  //         `Removed Keycloak user ${keycloakUsername} from group "${keycloakGroupName}"`,
  //       );
  //     },
  //   );
  // }

  await query(
    sqlClient,
    Sql.updateProject({
      id,
      patch: {
        name,
        gitUrl,
        privateKey,
        subfolder,
        activeSystemsDeploy,
        activeSystemsRemove,
        activeSystemsTask,
        branches,
        productionEnvironment,
        autoIdle,
        storageCalc,
        pullrequests,
        openshift,
        openshiftProjectPattern,
        developmentEnvironmentsLimit,
      },
    }),
  );

  // Rename the default group and user
  if (patch.name && oldProject.name !== patch.name) {
    try {
      const group = await dataSources.GroupModel.loadGroupByName(`project-${oldProject.name}`);
      await dataSources.GroupModel.updateGroup({
        id: group.id,
        name: `project-${patch.name}`,
      });
    } catch (err) {
      logger.error(`Could not rename default group for project ${patch.name}: ${err.message}`);
    }

    try {
      const user = await dataSources.UserModel.loadUserByUsername(`default-user@${oldProject.name}`);
      await dataSources.UserModel.updateUser({
        id: user.id,
        email: `default-user@${patch.name}`,
        username: `default-user@${patch.name}`,
        comment: `autogenerated user for project ${patch.name}`,
      });
    } catch (err) {
      logger.error(`Could not rename default user for project ${patch.name}: ${err.message}`);
    }
  }

  // if (typeof name === 'string' && name !== originalName) {
  //   const groupId = await KeycloakOperations.findGroupIdByName(originalName);

  //   await keycloakAdminClient.groups.update({ id: groupId }, { name });
  //   logger.debug(
  //     `Renamed Keycloak group ${groupId} from "${originalName}" to "${name}"`,
  //   );
  // }

  // if (typeof customer === 'number' && customer !== originalCustomer) {
  //   // Add Keycloak users to new projects where given user ids do not have other access via `project_user` (projects where the user loses access if they lose customer access).
  //   await Helpers(sqlClient).mapIfNoDirectProjectAccess(
  //     id,
  //     customer,
  //     async ({
  //       keycloakUserId,
  //       keycloakUsername,
  //       keycloakGroupId,
  //       keycloakGroupName,
  //     }) => {
  //       await keycloakAdminClient.users.addToGroup({
  //         id: keycloakUserId,
  //         groupId: keycloakGroupId,
  //       });
  //       logger.debug(
  //         `Added Keycloak user ${keycloakUsername} to group "${keycloakGroupName}"`,
  //       );
  //     },
  //   );
  // }

  return Helpers(sqlClient).getProjectById(id);
};

const deleteAllProjects = async (
  root,
  args,
  { sqlClient, hasPermission },
) => {
  await hasPermission('project', 'deleteAll');

  const projectNames = await Helpers(sqlClient).getAllProjectNames();

  await query(sqlClient, Sql.truncateProject());

  for (const name of projectNames) {
    await KeycloakOperations.deleteGroup(name);
  }

  // TODO: Check rows for success
  return 'success';
};

const Resolvers /* : ResolversObj */ = {
  deleteProject,
  addProject,
  getProjectByName,
  getProjectByGitUrl,
  getProjectByEnvironmentId,
  getAllProjects,
  updateProject,
  deleteAllProjects,
};

module.exports = Resolvers;
<|endoftext|>


<commit_before>import PIXI from "pixi.js";

import {Vec2}   from "@ignavia/ella";
import {Layout} from "@ignavia/earl";

import {graphVisualizer} from "./graph/graph.js";
import {nodeVisualizer}  from "./node/node.js";
import {edgeVisualizer}  from "./edge/edge.js";

import PolarFisheye     from "./filters/PolarFisheye.js";
import CartesianFisheye from "./filters/CartesianFisheye.js";

export default class GraphView {

    /**
     * @param {Graph} graphObj
     * The graph object to display.
     *
     * @param {Object} options
     * The options object.
     *
     * @param {Object} options.graphConf
     * The configuration of the graph visualizer.
     *
     * @param {Map<String, Object>} options.nodeConfs
     * Maps from node IDs to the configuration of the visualizer.
     *
     * @param {Map<String, Object>} options.edgeConfs
     * Maps from edge IDs to the configuration of the visualizer.
     *
     * @param {Layout} layout
     * The layout of the graph.
     */
    constructor(graphObj, {
            graphConf = {},
            nodeConfs = new Map(),
            edgeConfs = new Map(),
            layout    = new Layout(),
        } = {}) {

        /**
         * The graph getting displayed.
         *
         * @type {Graph}
         * @private
         */
        this.graph = graphObj;

        /**
         * The configuration of the edge visualizers.
         *
         * @type {Map<String, Object>}
         * @private
         */
        this.edgeConfs = edgeConfs;

        const {
            renderer,
            stage,
            selectedNodeContainer,
            nodeContainer,
            selectedEdgeContainer,
            edgeContainer
        } = graphVisualizer(graphConf);

        /**
         * The renderer used to draw the stage.
         *
         * @type {Renderer}
         * @private
         */
        this.renderer = renderer;

        /**
         * The display object to draw with the renderer.
         *
         * @type {DisplayObject}
         * @private
         */
        this.stage = stage;

        /**
         * The container for selected node display objects.
         *
         * @type {DisplayObject}
         * @private
         */
        this.selectedNodeContainer = selectedNodeContainer;

        /**
         * The container for the node display objects.
         *
         * @type {DisplayObject}
         * @private
         */
        this.nodeContainer = nodeContainer;

        /**
         * The container for selected edge display objects.
         *
         * @type {DisplayObject}
         * @private
         */
        this.selectedEdgeContainer = selectedEdgeContainer;

        /**
         * The container for the edge display object.
         *
         * @type {DisplayObject}
         * @private
         */
        this.edgeContainer = edgeContainer;

        /**
         * Maps from node IDs to their display objects.
         *
         * @type {Map<String, DisplayObject>}
         * @private
         */
        this.nodes = new Map();

        /**
         * The IDs of the selected nodes.
         *
         * @type {Set<String>}
         */
        this.selectedNodes = new Set();

        /**
         * Maps from edge IDs to their display objects.
         *
         * @type {Map<String, DisplayObject>}
         * @private
         */
        this.edges = new Map();

        /**
         * The IDs of the selected edges.
         *
         * @type {Set<String>}
         */
        this.selectedEdges = new Set();

        /**
         * A flag whether the render loop is active.
         *
         * @type {Boolean}
         * @private
         */
        this.renderLoopIsActive = false;

        this.init(nodeConfs, edgeConfs, layout);
    }

    /**
     * Draws the graph and starts the render loop.
     *
     * @param {Map<String, Object>} nodeConfs
     * Maps from node IDs to the configuration of the visualizer.
     *
     * @param {Map<String, Object>} edgeConfs
     * Maps from edge IDs to the configuration of the visualizer.
     */
    init(nodeConfs, edgeConfs, layout) {
        this.setupFilters();
        this.visualizeNodes(nodeConfs, layout);
        this.visualizeEdges();
        this.center();
        this.startRenderLoop();
    }

    /**
     * Sets the filter area of the stage.
     */
    setupFilters() {
        this.stage.filterArea = new PIXI.Rectangle(
            0,
            0,
            this.renderer.width,
            this.renderer.height
        );
    }

    /**
     * Draws the nodes of the graph.
     *
     * @param {Map<String, Object>} nodeConfs
     * Maps from node IDs to the configuration of the visualizer.
     *
     * @param {Layout} layout
     * The layout of the graph.
     */
    visualizeNodes(nodeConfs, layout) {
        for (let nodeObj of this.graph.iterNodes()) {
            const conf     = nodeConfs.get(nodeObj.id);
            const position = layout.getPosition(nodeObj);
            this.addNode(nodeObj, conf, position);
        }
    }

    /**
     * Adds the given edge object to the scene.
     *
     * @param {Edge} edgeObj
     * The edge object to add.
     *
     * @param {Object} conf
     * The configuration of the vsualizer.
     *
     * @param {Vec2} position
     * Where to move the created graphic.
     */
    addNode(nodeObj, conf, position) {
        const displayObject = nodeVisualizer(conf);

        if (position) {
            displayObject.x = position.x * this.renderer.width;
            displayObject.y = position.y * this.renderer.height;
        } else {
            displayObject.x = Math.random() * this.renderer.width;
            displayObject.y = Math.random() * this.renderer.height;
        }

        this.nodes.set(nodeObj.id, displayObject);

        if (this.selectedNodes.has(nodeObj.id)) {
            this.selectedNodeContainer.addChild(displayObject);
        } else {
            this.nodeContainer.addChild(displayObject);
        }
    }

    /**
     * Removes the node graphic with the given ID from the scene.
     *
     * @param {String} id
     * The ID of the graphic to remove.
     */
    removeNode(id) {
        const nodeG = this.getNodeDisplayObjectById(id);

        this.selectedNodes.delete(id);
        this.nodeContainer.removeChild(nodeG);
        this.selectedNodeContainer.removeChild(nodeG);
        this.nodes.delete(id);
    }

    /**
     * Returns the display object for the given node ID.
     *
     * @param {String} nodeId
     * The ID of the node to get the display object for.
     *
     * @return {DisplayObject}
     * The display object for the node.
     */
    getNodeDisplayObjectById(nodeId) {
        return this.nodes.get(nodeId);
    }

    selectNodes(nodesToSelect) { // deselect selected nodes and select the new ones
        for (let [id, node] of this.nodes) {
            if (nodesToSelect.has(id)) {
                this.nodeContainer.removeChild(node);
                this.selectedNodeContainer.addChild(node);
            } else {
                this.selectedNodeContainer.removeChild(node);
                this.nodeContainer.addChild(node);
            }
        }

        this.selectedNodes = nodesToSelect;
    }

    /**
     * Draws the edges of the graph.
     */
    visualizeEdges() {
        for (let edgeObj of this.graph.iterEdges()) {
            this.addEdge(edgeObj);
        }
    }

    /**
     * Adds the given edge object to the scene.
     *
     * @param {Edge} edgeObj
     * The edge object to add.
     */
    addEdge(edgeObj) {
        const conf    = this.edgeConfs.get(edgeObj.id);
        const sourceG = this.nodes.get(edgeObj.sourceId);
        const targetG = this.nodes.get(edgeObj.targetId);

        const displayObject = edgeVisualizer(
            new Vec2(sourceG.x, sourceG.y),
            new Vec2(targetG.x, targetG.y),
            conf
        );

        this.edgeConfs.set(edgeObj.id, conf);
        this.edges.set(edgeObj.id, displayObject);

        if (this.selectedEdges.has(edgeObj.id)) {
            this.selectedEdgeContainer.addChild(displayObject);
        } else {
            this.edgeContainer.addChild(displayObject);
        }
    }

    /**
     * Removes the edge graphic with the given ID from the scene.
     *
     * @param {String} id
     * The ID of the graphic to remove.
     */
    removeEdge(id) {
        const edgeG = this.getEdgeDisplayObjectById(id);

        this.edgeConfs.delete(id);
        this.selectedEdges.delete(id);
        this.edgeContainer.removeChild(edgeG);
        this.selectedEdgeContainer.removeChild(edgeG);
        this.edges.delete(id);
    }

    /**
     * Returns the display object for the given edge ID.
     *
     * @param {String} edgeId
     * The ID of the edge to get the display object for.
     *
     * @return {DisplayObject}
     * The display object for the edge.
     */
    getEdgeDisplayObjectById(edgeId) {
        return this.edges.get(edgeId);
    }

    selectEdges(edgesToSelect) { // deselect selected nodes and select the new ones
        for (let [id, edge] of this.edges) {
            if (edgesToSelect.has(id)) {
                this.edgeContainer.removeChild(edge);
                this.selectedEdgeContainer.addChild(edge);
            } else {
                this.selectedEdgeContainer.removeChild(edge);
                this.edgeContainer.addChild(edge);
            }
        }
        this.selectedEdges = edgesToSelect;
    }

    center() {
        const boundingRectangle = this.getBoundingRectangle();
        const nodeCenterX       = (boundingRectangle.minX + boundingRectangle.maxX) / 2;
        const nodeCenterY       = (boundingRectangle.minY + boundingRectangle.maxY) / 2;

        const renderCenterX = this.renderer.width  / 2;
        const renderCenterY = this.renderer.height / 2;

        // TODO move stage to rendercenter
    }

    getBoundingRectangle() {
        const result = {
            minX: Number.POSITIVE_INFINITY,
            maxX: Number.NEGATIVE_INFINITY,
            minY: Number.POSITIVE_INFINITY,
            maxY: Number.NEGATIVE_INFINITY,
        };

        for (let [, node] of this.nodes) {
            if (node.visible) {
                result.minX = Math.min(result.minX, node.x);
                result.maxX = Math.max(result.maxX, node.x);
                result.minY = Math.min(result.minY, node.y);
                result.maxY = Math.max(result.maxY, node.y);
            }
        }

        return result;
    }

    moveNodeToTop(nodeId) {
        const nodeG = this.getNodeDisplayObjectById(nodeId);
        if (this.selectedNodes.has(nodeId)) {
            this.selectedNodeContainer.removeChild(nodeG);
            this.selectedNodeContainer.addChild(nodeG);
        } else {
            this.nodeContainer.removeChild(nodeG);
            this.nodeContainer.addChild(nodeG);
        }
    }

    setLayout(layout) {
        this.stopRenderLoop();

        for (let [id, position] of layout) {
            const nodeG = this.getNodeDisplayObjectById(id);
            nodeG.x = position.x;
            nodeG.y = position.y;
        }

        this.edges.clear();
        this.edgeContainer.removeChildren();
        this.selectedEdgeContainer.removeChildren();
        this.visualizeEdges();

        this.startRenderLoop();
    }

    moveNode(nodeId, position) {
        const nodeG = this.getNodeDisplayObjectById(nodeId);
        nodeG.x = position.x;
        nodeG.y = position.y;

        const nodeObj = this.graph.getNodeById(nodeId);
        for (let edgeId of nodeObj.iterIncidentEdges()) {
            const edgeG   = this.getEdgeDisplayObjectById(edgeId);
            const edgeObj = this.graph.getEdgeById(edgeId);

            this.edges.delete(edgeId);
            this.edgeContainer.removeChild(edgeG);
            this.selectedEdgeContainer.removeChild(edgeG);
            this.addEdge(edgeObj);
        }
    }

    setStyle() {
        // TODO, redraw everything in the confs
    }

    /**
     * Sets the visibility of the node and edge graphics. Only the ones in the
     * given sets are shown afterwards.
     *
     * @param {Set<String>} nodesToKeep
     * The nodes to keep.
     *
     * @param {Set<String>} edgesToKeep
     * The edges to keep.
     */
    filterGraph(nodesToKeep, edgesToKeep) {
        for (let [id, node] of this.nodes) {
            node.visible = nodesToKeep.has(id);
        }

        this.filterEdges(edgesToKeep);
    }

    /**
     * Sets the visibility of the edge graphics. Only the ones in the given set
     * are shown afterwards.
     *
     * @param {Set<String>} edgesToKeep
     * The edges to keep.
     */
    filterEdges(edgesToKeep) {
        for (let [id, edge] of this.edges) {
            edge.visible = edgesToKeep.has(id);
        }
    }

    /**
     * Resets the filters. All node and edge graphics are visible again
     * afterwards.
     */
    resetFilters() {
        this.resetNodeFilter();

        for (let [id, edge] of this.edges) {
            edge.visible = true;
        }
    }

    /**
     * Resets the node filter. All node graphics are visible again afterwards.
     */
    resetNodeFilter() {
        for (let [id, node] of this.nodes) {
            node.visible = true;
        }
    }

    /**
     * Resizes the renderer.
     *
     * @param {Number} width
     * The new width.
     *
     * @param {Number} height
     * The new height.
     */
    resize(width, height) {
        this.renderer.resize(width, height);
    }

    /**
     * Starts the render loop.
     */
    startRenderLoop() {
        this.renderLoopIsActive = true;
        this.animate();
    }

    /**
     * Stops the render loop.
     */
    stopRenderLoop() {
        this.renderLoopIsActive = false;
    }

    /**
     * Repeatedly draws the stage.
     *
     * @private
     */
    animate() {
        this.renderer.render(this.stage);
        if (this.renderLoopIsActive) {
            requestAnimationFrame(() => this.animate());
        }
    }
}<commit_msg>Finished doc<commit_after>import PIXI from "pixi.js";

import {Vec2}   from "@ignavia/ella";
import {Layout} from "@ignavia/earl";

import {graphVisualizer} from "./graph/graph.js";
import {nodeVisualizer}  from "./node/node.js";
import {edgeVisualizer}  from "./edge/edge.js";

import PolarFisheye     from "./filters/PolarFisheye.js";
import CartesianFisheye from "./filters/CartesianFisheye.js";

export default class GraphView {

    /**
     * @param {Graph} graphObj
     * The graph object to display.
     *
     * @param {Object} options
     * The options object.
     *
     * @param {Object} options.graphConf
     * The configuration of the graph visualizer.
     *
     * @param {Map<String, Object>} options.nodeConfs
     * Maps from node IDs to the configuration of the visualizer.
     *
     * @param {Map<String, Object>} options.edgeConfs
     * Maps from edge IDs to the configuration of the visualizer.
     *
     * @param {Layout} layout
     * The layout of the graph.
     */
    constructor(graphObj, {
            graphConf = {},
            nodeConfs = new Map(),
            edgeConfs = new Map(),
            layout    = new Layout(),
        } = {}) {

        /**
         * The graph getting displayed.
         *
         * @type {Graph}
         * @private
         */
        this.graph = graphObj;

        /**
         * The configuration of the edge visualizers.
         *
         * @type {Map<String, Object>}
         * @private
         */
        this.edgeConfs = edgeConfs;

        const {
            renderer,
            stage,
            selectedNodeContainer,
            nodeContainer,
            selectedEdgeContainer,
            edgeContainer
        } = graphVisualizer(graphConf);

        /**
         * The renderer used to draw the stage.
         *
         * @type {Renderer}
         * @private
         */
        this.renderer = renderer;

        /**
         * The display object to draw with the renderer.
         *
         * @type {DisplayObject}
         * @private
         */
        this.stage = stage;

        /**
         * The container for selected node display objects.
         *
         * @type {DisplayObject}
         * @private
         */
        this.selectedNodeContainer = selectedNodeContainer;

        /**
         * The container for the node display objects.
         *
         * @type {DisplayObject}
         * @private
         */
        this.nodeContainer = nodeContainer;

        /**
         * The container for selected edge display objects.
         *
         * @type {DisplayObject}
         * @private
         */
        this.selectedEdgeContainer = selectedEdgeContainer;

        /**
         * The container for the edge display object.
         *
         * @type {DisplayObject}
         * @private
         */
        this.edgeContainer = edgeContainer;

        /**
         * Maps from node IDs to their display objects.
         *
         * @type {Map<String, DisplayObject>}
         * @private
         */
        this.nodes = new Map();

        /**
         * The IDs of the selected nodes.
         *
         * @type {Set<String>}
         */
        this.selectedNodes = new Set();

        /**
         * Maps from edge IDs to their display objects.
         *
         * @type {Map<String, DisplayObject>}
         * @private
         */
        this.edges = new Map();

        /**
         * The IDs of the selected edges.
         *
         * @type {Set<String>}
         */
        this.selectedEdges = new Set();

        /**
         * A flag whether the render loop is active.
         *
         * @type {Boolean}
         * @private
         */
        this.renderLoopIsActive = false;

        this.init(nodeConfs, edgeConfs, layout);
    }

    /**
     * Draws the graph and starts the render loop.
     *
     * @param {Map<String, Object>} nodeConfs
     * Maps from node IDs to the configuration of the visualizer.
     *
     * @param {Map<String, Object>} edgeConfs
     * Maps from edge IDs to the configuration of the visualizer.
     */
    init(nodeConfs, edgeConfs, layout) {
        this.setupFilters();
        this.visualizeNodes(nodeConfs, layout);
        this.visualizeEdges();
        this.center();
        this.startRenderLoop();
    }

    /**
     * Sets the filter area of the stage.
     */
    setupFilters() {
        this.stage.filterArea = new PIXI.Rectangle(
            0,
            0,
            this.renderer.width,
            this.renderer.height
        );
    }

    /**
     * Draws the nodes of the graph.
     *
     * @param {Map<String, Object>} nodeConfs
     * Maps from node IDs to the configuration of the visualizer.
     *
     * @param {Layout} layout
     * The layout of the graph.
     */
    visualizeNodes(nodeConfs, layout) {
        for (let nodeObj of this.graph.iterNodes()) {
            const conf     = nodeConfs.get(nodeObj.id);
            const position = layout.getPosition(nodeObj);
            this.addNode(nodeObj, conf, position);
        }
    }

    /**
     * Adds the given edge object to the scene.
     *
     * @param {Edge} edgeObj
     * The edge object to add.
     *
     * @param {Object} conf
     * The configuration of the vsualizer.
     *
     * @param {Vec2} position
     * Where to move the created graphic.
     */
    addNode(nodeObj, conf, position) {
        const displayObject = nodeVisualizer(conf);

        if (position) {
            displayObject.x = position.x * this.renderer.width;
            displayObject.y = position.y * this.renderer.height;
        } else {
            displayObject.x = Math.random() * this.renderer.width;
            displayObject.y = Math.random() * this.renderer.height;
        }

        this.nodes.set(nodeObj.id, displayObject);

        if (this.selectedNodes.has(nodeObj.id)) {
            this.selectedNodeContainer.addChild(displayObject);
        } else {
            this.nodeContainer.addChild(displayObject);
        }
    }

    /**
     * Removes the node graphic with the given ID from the scene.
     *
     * @param {String} nodeId
     * The ID of the graphic to remove.
     */
    removeNode(nodeId) {
        const nodeG = this.getNodeDisplayObjectById(nodeId);

        this.selectedNodes.delete(nodeId);
        this.nodeContainer.removeChild(nodeG);
        this.selectedNodeContainer.removeChild(nodeG);
        this.nodes.delete(nodeId);
    }

    /**
     * Returns the display object for the given node ID.
     *
     * @param {String} nodeId
     * The ID of the node to get the display object for.
     *
     * @return {DisplayObject}
     * The display object for the node.
     */
    getNodeDisplayObjectById(nodeId) {
        return this.nodes.get(nodeId);
    }

    /**
     * Selects the given nodes. Those are highlighted afterwards.
     *
     * @param {Set<String>} nodesToSelect
     * The IDs of the nodes to select.
     */
    selectNodes(nodesToSelect) { // deselect selected nodes and select the new ones
        for (let [id, node] of this.nodes) {
            if (nodesToSelect.has(id)) {
                this.nodeContainer.removeChild(node);
                this.selectedNodeContainer.addChild(node);
            } else {
                this.selectedNodeContainer.removeChild(node);
                this.nodeContainer.addChild(node);
            }
        }

        this.selectedNodes = nodesToSelect;
    }

    /**
     * Draws the edges of the graph.
     */
    visualizeEdges() {
        for (let edgeObj of this.graph.iterEdges()) {
            this.addEdge(edgeObj);
        }
    }

    /**
     * Adds the given edge object to the scene.
     *
     * @param {Edge} edgeObj
     * The edge object to add.
     */
    addEdge(edgeObj) {
        const conf    = this.edgeConfs.get(edgeObj.id);
        const sourceG = this.nodes.get(edgeObj.sourceId);
        const targetG = this.nodes.get(edgeObj.targetId);

        const displayObject = edgeVisualizer(
            new Vec2(sourceG.x, sourceG.y),
            new Vec2(targetG.x, targetG.y),
            conf
        );

        this.edgeConfs.set(edgeObj.id, conf);
        this.edges.set(edgeObj.id, displayObject);

        if (this.selectedEdges.has(edgeObj.id)) {
            this.selectedEdgeContainer.addChild(displayObject);
        } else {
            this.edgeContainer.addChild(displayObject);
        }
    }

    /**
     * Removes the edge graphic with the given ID from the scene.
     *
     * @param {String} edgeId
     * The ID of the graphic to remove.
     */
    removeEdge(edgeId) {
        const edgeG = this.getEdgeDisplayObjectById(edgeId);

        this.edgeConfs.delete(edgeId);
        this.selectedEdges.delete(edgeId);
        this.edgeContainer.removeChild(edgeG);
        this.selectedEdgeContainer.removeChild(edgeG);
        this.edges.delete(edgeId);
    }

    /**
     * Returns the display object for the given edge ID.
     *
     * @param {String} edgeId
     * The ID of the edge to get the display object for.
     *
     * @return {DisplayObject}
     * The display object for the edge.
     */
    getEdgeDisplayObjectById(edgeId) {
        return this.edges.get(edgeId);
    }

    /**
     * Selects the given edges. Those are highlighted afterwards.
     *
     * @param {Set<String>} edgesToSelect
     * The IDs of the edges to select.
     */
    selectEdges(edgesToSelect) { // deselect selected edges and select the new ones
        for (let [id, edge] of this.edges) {
            if (edgesToSelect.has(id)) {
                this.edgeContainer.removeChild(edge);
                this.selectedEdgeContainer.addChild(edge);
            } else {
                this.selectedEdgeContainer.removeChild(edge);
                this.edgeContainer.addChild(edge);
            }
        }
        this.selectedEdges = edgesToSelect;
    }

    /**
     * Centers the stage.
     */
    center() {
        const br = this.getBoundingRectangle();
        this.stage.x = (this.renderer.width  - br.minX - br.maxX) / 2 * this.stage.scale.x;
        this.stage.y = (this.renderer.height - br.minY - br.maxY) / 2 * this.stage.scale.y;
    }

    /**
     * Returns the rectangle around the nodes.
     *
     * @return {Object}
     * The bounding rectangle. The top-left corner is (minX, minY) and the
     * bottom-right corner is (maxX, maxY).
     *
     * @private
     */
    getBoundingRectangle() {
        const result = {
            minX: Number.POSITIVE_INFINITY,
            maxX: Number.NEGATIVE_INFINITY,
            minY: Number.POSITIVE_INFINITY,
            maxY: Number.NEGATIVE_INFINITY,
        };

        for (let [, node] of this.nodes) {
            if (node.visible) {
                result.minX = Math.min(result.minX, node.x);
                result.maxX = Math.max(result.maxX, node.x);
                result.minY = Math.min(result.minY, node.y);
                result.maxY = Math.max(result.maxY, node.y);
            }
        }

        return result;
    }

    /**
     * Moves the node graphic with the given ID to the top.
     *
     * @param {String} nodeId
     * The ID of the graphic.
     */
    moveNodeToTop(nodeId) {
        const nodeG = this.getNodeDisplayObjectById(nodeId);
        if (this.selectedNodes.has(nodeId)) {
            this.selectedNodeContainer.removeChild(nodeG);
            this.selectedNodeContainer.addChild(nodeG);
        } else {
            this.nodeContainer.removeChild(nodeG);
            this.nodeContainer.addChild(nodeG);
        }
    }

    /**
     * Sets the layout of the graph and moves the nodes accordingly.
     *
     * @param {Layout} layout
     * The new layout.
     */
    setLayout(layout) {
        this.stopRenderLoop();

        for (let [id, position] of layout) {
            const nodeG = this.getNodeDisplayObjectById(id);
            nodeG.x = position.x;
            nodeG.y = position.y;
        }

        this.edges.clear();
        this.edgeContainer.removeChildren();
        this.selectedEdgeContainer.removeChildren();
        this.visualizeEdges();

        this.startRenderLoop();
    }

    /**
     * Moves the node graphic with the given ID to the position.
     *
     * @param {String} nodeId
     * The ID of the graphic.
     *
     * @param {Vec2} position
     * Where to move the node.
     */
    moveNode(nodeId, position) {
        const nodeG = this.getNodeDisplayObjectById(nodeId);
        nodeG.x = position.x;
        nodeG.y = position.y;

        const nodeObj = this.graph.getNodeById(nodeId);
        for (let edgeId of nodeObj.iterIncidentEdges()) {
            const edgeG   = this.getEdgeDisplayObjectById(edgeId);
            const edgeObj = this.graph.getEdgeById(edgeId);

            this.edges.delete(edgeId);
            this.edgeContainer.removeChild(edgeG);
            this.selectedEdgeContainer.removeChild(edgeG);
            this.addEdge(edgeObj);
        }
    }

    setStyle() {
        // TODO
    }

    /**
     * Sets the visibility of the node and edge graphics. Only the ones in the
     * given sets are shown afterwards.
     *
     * @param {Set<String>} nodesToKeep
     * The nodes to keep.
     *
     * @param {Set<String>} edgesToKeep
     * The edges to keep.
     */
    filterGraph(nodesToKeep, edgesToKeep) {
        for (let [id, node] of this.nodes) {
            node.visible = nodesToKeep.has(id);
        }

        this.filterEdges(edgesToKeep);
    }

    /**
     * Sets the visibility of the edge graphics. Only the ones in the given set
     * are shown afterwards.
     *
     * @param {Set<String>} edgesToKeep
     * The edges to keep.
     */
    filterEdges(edgesToKeep) {
        for (let [id, edge] of this.edges) {
            edge.visible = edgesToKeep.has(id);
        }
    }

    /**
     * Resets the filters. All node and edge graphics are visible again
     * afterwards.
     */
    resetFilters() {
        this.resetNodeFilter();

        for (let [id, edge] of this.edges) {
            edge.visible = true;
        }
    }

    /**
     * Resets the node filter. All node graphics are visible again afterwards.
     */
    resetNodeFilter() {
        for (let [id, node] of this.nodes) {
            node.visible = true;
        }
    }

    /**
     * Resizes the renderer.
     *
     * @param {Number} width
     * The new width.
     *
     * @param {Number} height
     * The new height.
     */
    resize(width, height) {
        this.renderer.resize(width, height);
    }

    /**
     * Starts the render loop.
     */
    startRenderLoop() {
        this.renderLoopIsActive = true;
        this.animate();
    }

    /**
     * Stops the render loop.
     */
    stopRenderLoop() {
        this.renderLoopIsActive = false;
    }

    /**
     * Repeatedly draws the stage.
     *
     * @private
     */
    animate() {
        this.renderer.render(this.stage);
        if (this.renderLoopIsActive) {
            requestAnimationFrame(() => this.animate());
        }
    }
}<|endoftext|>


<commit_before>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 Joachim Segala
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 *     The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 *     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
d3.circlePackingTimeline = function (json, width, height, containerSelector, color) {
    if (typeof(color) === 'undefined') {
        color = d3.scale.linear()
            .domain([-1, 5])
            .range(["hsl(35, 100%, 62%)", "hsl(35, 64%, 34%)"])
            .interpolate(d3.interpolateHcl);
    }

    var margin = 20;

    d3.json("data.json", function (error, root) {
        if (error) {
            throw error;
        }

        var focus = undefined;
        var steps = root.children;
        var count = steps.length;

        var container = d3.select(containerSelector)
            .on("click", reset)
            .append("svg")
            .attr("width", width - margin)
            .attr("height", height - margin)
            .append("g")
            .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(1)");

        /**
         * Title
         */
        container.append("text").attr("class", "title").attr("y", -height / 3).text(root.name);

        /**
         * Steps
         */
        steps.forEach(function (step, cmp) {
            var pack = d3.layout.pack()
                .size([width / count, height / count])
                .value(function (d) {
                    return d.size || 1;
                });
            var dec = count % 2 == 0 ? cmp - count / 2 + 0.5 : cmp - Math.floor(count / 2);
            var g = container.append("g");
            var nodes = pack.nodes(step);

            /**
             * Arrow
             */
            if (cmp != 0) {
                container.append("path")
                    .attr("d", arrow(width / count * (dec - 1) + step.r + margin, 0,
                            width / count * dec - step.r - margin, 0))
                    .style("stroke", color(1))
                    .style("fill", "none");
            }

            /**
             * Circles
             */
            var circle = g.selectAll("circle")
                .data(nodes)
                .enter().append("circle")
                .attr("class", function (d) {
                    return d.children ? "node" : "node node--leaf";
                })
                .attr("cx", function (d) {
                    return d.x - step.x + width / count * dec;
                })
                .attr("cy", function (d) {
                    return d.y - step.y;
                })
                .attr("r", function (d) {
                    return d.r;
                })
                .style("fill", function (d) {
                    return color(d.depth);
                })
                .on("click", zoomTo);

            /**
             * Texts
             */
            var texts = g.selectAll(".label")
                .data(nodes)
                .enter().append("g")
                .attr("class", "label")
                .style("display", function (d) {
                    return d.parent === undefined ? "inline" : "none";
                })
                .attr("transform", function (d) {
                    return "translate("
                        + (d.x - step.x + width / count * dec) + ","
                        + (d.y - step.y) + ")";
                });

            // Title
            texts.append("text")
                .attr("class", "subtitle")
                .attr("y", function (d) {
                    if (d.desc) {
                        if (d.start && d.end) {
                            return -20;
                        } else {
                            return -5;
                        }
                    }
                    return 0;
                })
                .text(function (d) {
                    return d.name;
                });

            // Description
            texts.filter(function (d) {
                return d.desc
            })
                .append("text")
                .attr("y", function (d) {
                    if (d.start && d.end) {
                        return 0;
                    }
                    return 5;
                })
                .text(function (d) {
                    return d.desc;
                });

            // Date
            texts.filter(function (d) {
                return d.start && d.end
            })
                .append("text")
                .attr("y", "20")
                .text(function (d) {
                    return d.start + " - " + d.end;
                });
        });

        /**
         * Footer
         */
        container.append("a").attr("xlink:href", root.url).attr("target", "_blank").attr("class", "footer")
            .append("text").attr("y", 2 * height / 5).text("By me!");

        /**
         * Call on event in order to zoom on called element.
         */
        function zoomTo() {
            var element = d3.select(this);
            focus = element.datum();
            var dx = parseInt(element.attr("cx"));
            var dy = parseInt(element.attr("cy"));
            var r = parseInt(element.attr("r"));

            transform(dx, dy, 0.4 * height / r);
            d3.event.stopPropagation();
        }

        /**
         * Call on event in order to reset the container view.
         */
        function reset() {
            focus = undefined;
            transform(0, 0, 1);
            d3.event.stopPropagation();
        }

        /**
         * Transform the container view center on (x,y) with a scale.
         * @param x coordinate
         * @param y coordinate
         * @param scale
         */
        function transform(x, y, scale) {
            var translate = [width / 2 - scale * x, height / 2 - scale * y];

            container.transition()
                .duration(750)
                .attr("transform", "translate(" + translate + ")scale(" + scale + ")")
                .selectAll(".label")
                .each("start", function (d) {
                    if (d.parent !== focus || d.parent === undefined) this.style.display = "none";
                })
                .each("end", function (d) {
                    if (d.parent === focus) this.style.display = "inline";
                })
                .selectAll("text")
                .style("font-size", 15 / scale);
        }

        /**
         * Compute a SVG path drawing an arrow.
         * @param x1 x start coordinate
         * @param y1 y start coordinate
         * @param x2 x end coordinate
         * @param y2 y end coordinate
         * @returns {string} SVG path
         */
        function arrow(x1, y1, x2, y2) {
            return "M" + x1 + " " + (y1 + 2) + " " +
                "L" + (x2 - 12) + " " + (y2 + 2) + " " +
                "L" + (x2 - 16) + " " + (y2 + 16) + " " +
                "L" + x2 + " " + y2 + " " +
                "L" + (x2 - 16) + " " + (y2 - 16) + " " +
                "L" + (x2 - 12) + " " + (y2 - 2) + " " +
                "L" + x1 + " " + (y1 - 2) + " Z";
        }
    });
};<commit_msg>Refactor variable name & quote<commit_after>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 Joachim Segala
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 *     The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 *     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
d3.circlePackingTimeline = function (json, width, height, containerSelector, color) {
    if (typeof(color) === 'undefined') {
        color = d3.scale.linear()
            .domain([-1, 5])
            .range(['hsl(35, 100%, 62%)', 'hsl(35, 64%, 34%)'])
            .interpolate(d3.interpolateHcl);
    }

    var margin = 20;

    d3.json(json, function (error, root) {
        if (error) {
            throw error;
        }

        var focus = undefined;
        var steps = root.children;
        var count = steps.length;

        var container = d3.select(containerSelector).on('click', reset)
            .append('svg').attr('width', width - margin).attr('height', height - margin)
            .append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')scale(1)');

        /**
         * Title
         */
        container.append('text').attr('class', 'title').attr('y', -height / 3).text(root.name);

        /**
         * Steps
         */
        steps.forEach(function (step, cmp) {
            var pack = d3.layout.pack()
                .size([width / count + margin, height / count + margin])
                .value(function (item) {
                    return item.size || 1;
                });
            var dec = count % 2 == 0 ? cmp - count / 2 + 0.5 : cmp - Math.floor(count / 2);
            var stepItems = pack.nodes(step);

            /**
             * Arrow
             */
            if (cmp != 0) {
                container.append('path')
                    .attr('d', arrow(width / count * (dec - 1) + step.r + margin, 0,
                            width / count * dec - step.r - margin, 0))
                    .style('stroke', color(1))
                    .style('fill', 'none');
            }

            /**
             * Circles
             */
            var stepContainer = container.append('g');
            var circle = stepContainer.selectAll('circle')
                .data(stepItems)
                .enter().append('circle')
                .attr('class', function (item) {
                    return item.children ? 'item' : 'item item--leaf';
                })
                .attr('cx', function (item) {
                    return item.x - step.x + width / count * dec;
                })
                .attr('cy', function (item) {
                    return item.y - step.y;
                })
                .attr('r', function (item) {
                    return item.r;
                })
                .style('fill', function (item) {
                    return color(item.depth);
                })
                .on('click', zoomTo);

            /**
             * Texts
             */
            var texts = stepContainer.selectAll('.label')
                .data(stepItems)
                .enter().append('g')
                .attr('class', 'label')
                .style('display', function (item) {
                    return item.parent === undefined ? 'inline' : 'none';
                })
                .attr('transform', function (item) {
                    return 'translate('
                        + (item.x - step.x + width / count * dec) + ','
                        + (item.y - step.y) + ')';
                });

            // Title
            texts.append('text')
                .attr('class', 'subtitle')
                .attr('y', function (item) {
                    if (item.desc) {
                        if (item.start && item.end) {
                            return -20;
                        } else {
                            return -5;
                        }
                    }
                    return 0;
                })
                .text(function (item) {
                    return item.name;
                });

            // Description
            texts.filter(function (item) {
                return item.desc
            })
                .append('text')
                .attr('y', function (item) {
                    if (item.start && item.end) {
                        return 0;
                    }
                    return 5;
                })
                .text(function (item) {
                    return item.desc;
                });

            // Date
            texts.filter(function (item) {
                return item.start && item.end
            })
                .append('text')
                .attr('y', '20')
                .text(function (item) {
                    return item.start + ' - ' + item.end;
                });
        });

        /**
         * Footer
         */
        container.append('a').attr('xlink:href', root.url).attr('target', '_blank').attr('class', 'footer')
            .append('text').attr('y', 2 * height / 5).text('By me!');

        /**
         * Call on event in order to zoom on called element.
         */
        function zoomTo() {
            var item = d3.select(this);
            focus = item.datum();
            var dx = parseInt(item.attr('cx'));
            var dy = parseInt(item.attr('cy'));
            var r = parseInt(item.attr('r'));

            transform(dx, dy, 0.4 * height / r);
            d3.event.stopPropagation();
        }

        /**
         * Call on event in order to reset the container view.
         */
        function reset() {
            if (focus !== undefined) {
                focus = undefined;
                transform(0, 0, 1);
            }
            d3.event.stopPropagation();
        }

        /**
         * Transform the container view center on (x,y) with a scale.
         * @param x coordinate
         * @param y coordinate
         * @param scale
         */
        function transform(x, y, scale) {
            var translate = [width / 2 - scale * x, height / 2 - scale * y];

            container.transition().duration(750)
                .attr('transform', 'translate(' + translate + ')scale(' + scale + ')')
                .selectAll('.label')
                .each('start', function (item) {
                    if (item.parent !== focus || item.parent === undefined) {
                        this.style.display = 'none';
                    }
                })
                .each('end', function (item) {
                    if (item.parent === focus) {
                        this.style.display = 'inline';
                    }
                })
                .selectAll('text')
                .style('font-size', 15 / scale);
        }

        /**
         * Compute a SVG path drawing an arrow.
         * @param x1 x start coordinate
         * @param y1 y start coordinate
         * @param x2 x end coordinate
         * @param y2 y end coordinate
         * @returns {string} SVG path
         */
        function arrow(x1, y1, x2, y2) {
            return 'M' + x1 + ' ' + (y1 + 2) + ' ' +
                'L' + (x2 - 12) + ' ' + (y2 + 2) + ' ' +
                'L' + (x2 - 16) + ' ' + (y2 + 16) + ' ' +
                'L' + x2 + ' ' + y2 + ' ' +
                'L' + (x2 - 16) + ' ' + (y2 - 16) + ' ' +
                'L' + (x2 - 12) + ' ' + (y2 - 2) + ' ' +
                'L' + x1 + ' ' + (y1 - 2) + ' Z';
        }
    });
};<|endoftext|>


<commit_before>const http = require('http')
const https = require('https')
http.globalAgent.maxSockets = 300
https.globalAgent.maxSockets = 300

const settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
const metrics = require('metrics-sharelatex')

const meter = require('stream-meter')
const fs = require('fs')
const S3 = require('aws-sdk/clients/s3')
const { URL } = require('url')
const { callbackify } = require('util')
const {
  WriteError,
  ReadError,
  NotFoundError,
  SettingsError
} = require('./Errors')

module.exports = {
  sendFile: callbackify(sendFile),
  sendStream: callbackify(sendStream),
  getFileStream: callbackify(getFileStream),
  deleteDirectory: callbackify(deleteDirectory),
  getFileSize: callbackify(getFileSize),
  deleteFile: callbackify(deleteFile),
  copyFile: callbackify(copyFile),
  checkIfFileExists: callbackify(checkIfFileExists),
  directorySize: callbackify(directorySize),
  promises: {
    sendFile,
    sendStream,
    getFileStream,
    deleteDirectory,
    getFileSize,
    deleteFile,
    copyFile,
    checkIfFileExists,
    directorySize
  }
}

async function sendFile(bucketName, key, fsPath) {
  let readStream
  try {
    readStream = fs.createReadStream(fsPath)
  } catch (err) {
    throw _wrapError(
      err,
      'error reading file from disk',
      { bucketName, key, fsPath },
      ReadError
    )
  }
  return sendStream(bucketName, key, readStream)
}

async function sendStream(bucketName, key, readStream) {
  try {
    const meteredStream = meter()
    meteredStream.on('finish', () => {
      metrics.count('s3.egress', meteredStream.bytes)
    })

    const response = await _client(bucketName)
      .upload({
        Bucket: bucketName,
        Key: key,
        Body: readStream.pipe(meteredStream)
      })
      .promise()

    logger.log({ response, bucketName, key }, 'data uploaded to s3')
  } catch (err) {
    throw _wrapError(
      err,
      'upload to S3 failed',
      { bucketName, key },
      WriteError
    )
  }
}

async function getFileStream(bucketName, key, opts) {
  opts = opts || {}

  const params = {
    Bucket: bucketName,
    Key: key
  }
  if (opts.start != null && opts.end != null) {
    params.Range = `bytes=${opts.start}-${opts.end}`
  }

  return new Promise((resolve, reject) => {
    const stream = _client(bucketName)
      .getObject(params)
      .createReadStream()

    const meteredStream = meter()
    meteredStream.on('finish', () => {
      metrics.count('s3.ingress', meteredStream.bytes)
    })

    const onStreamReady = function() {
      stream.removeListener('readable', onStreamReady)
      resolve(stream.pipe(meteredStream))
    }
    stream.on('readable', onStreamReady)
    stream.on('error', err => {
      reject(_wrapError(err, 'error reading from S3', params, ReadError))
    })
  })
}

async function deleteDirectory(bucketName, key) {
  logger.log({ key, bucketName }, 'deleting directory')
  let response

  try {
    response = await _client(bucketName)
      .listObjects({ Bucket: bucketName, Prefix: key })
      .promise()
  } catch (err) {
    throw _wrapError(
      err,
      'failed to list objects in S3',
      { bucketName, key },
      ReadError
    )
  }

  const objects = response.Contents.map(item => ({ Key: item.Key }))
  if (objects.length) {
    try {
      await _client(bucketName)
        .deleteObjects({
          Bucket: bucketName,
          Delete: {
            Objects: objects,
            Quiet: true
          }
        })
        .promise()
    } catch (err) {
      throw _wrapError(
        err,
        'failed to delete objects in S3',
        { bucketName, key },
        WriteError
      )
    }
  }
}

async function getFileSize(bucketName, key) {
  try {
    const response = await _client(bucketName)
      .headObject({ Bucket: bucketName, Key: key })
      .promise()
    return response.ContentLength
  } catch (err) {
    throw _wrapError(
      err,
      'error getting size of s3 object',
      { bucketName, key },
      ReadError
    )
  }
}

async function deleteFile(bucketName, key) {
  try {
    await _client(bucketName)
      .deleteObject({ Bucket: bucketName, Key: key })
      .promise()
  } catch (err) {
    throw _wrapError(
      err,
      'failed to delete file in S3',
      { bucketName, key },
      WriteError
    )
  }
}

async function copyFile(bucketName, sourceKey, destKey) {
  const params = {
    Bucket: bucketName,
    Key: destKey,
    CopySource: `${bucketName}/${sourceKey}`
  }
  try {
    await _client(bucketName)
      .copyObject(params)
      .promise()
  } catch (err) {
    throw _wrapError(err, 'failed to copy file in S3', params, WriteError)
  }
}

async function checkIfFileExists(bucketName, key) {
  try {
    await getFileSize(bucketName, key)
    return true
  } catch (err) {
    if (err instanceof NotFoundError) {
      return false
    }
    throw _wrapError(
      err,
      'error checking whether S3 object exists',
      { bucketName, key },
      ReadError
    )
  }
}

async function directorySize(bucketName, key) {
  try {
    const response = await _client(bucketName)
      .listObjects({ Bucket: bucketName, Prefix: key })
      .promise()

    return response.Contents.reduce((acc, item) => item.Size + acc, 0)
  } catch (err) {
    throw _wrapError(
      err,
      'error getting directory size in S3',
      { bucketName, key },
      ReadError
    )
  }
}

function _wrapError(error, message, params, ErrorType) {
  if (['NoSuchKey', 'NotFound', 'ENOENT'].includes(error.code)) {
    return new NotFoundError({
      message: 'no such file',
      info: params
    }).withCause(error)
  } else {
    return new ErrorType({
      message: message,
      info: params
    }).withCause(error)
  }
}

const _clients = {}

function _client(bucket) {
  if (_clients[bucket]) {
    return _clients[bucket]
  }

  if (
    settings.filestore.s3.s3BucketCreds &&
    settings.filestore.s3.s3BucketCreds[bucket]
  ) {
    _clients[bucket] = new S3(
      _clientOptions(settings.filestore.s3.s3BucketCreds[bucket])
    )
    return _clients[bucket]
  }

  // no specific credentials for the bucket
  if (_clients.default) {
    return _clients.default
  }

  if (settings.filestore.s3.key) {
    _clients.default = new S3(_clientOptions())
    return _clients.default
  }

  throw new SettingsError({
    message: 'no bucket-specific or default credentials provided',
    info: { bucket }
  })
}

function _clientOptions(bucketCredentials) {
  const options = {}

  if (bucketCredentials) {
    options.credentials = {
      accessKeyId: bucketCredentials.auth_key,
      secretAccessKey: bucketCredentials.auth_secret
    }
  } else {
    options.credentials = {
      accessKeyId: settings.filestore.s3.key,
      secretAccessKey: settings.filestore.s3.secret
    }
  }

  if (settings.filestore.s3.endpoint) {
    const endpoint = new URL(settings.filestore.s3.endpoint)
    options.endpoint = settings.filestore.s3.endpoint
    options.sslEnabled = endpoint.protocol === 'https'
  }

  // path-style access is only used for acceptance tests
  if (settings.filestore.s3.pathStyle) {
    options.s3ForcePathStyle = true
  }

  return options
}
<commit_msg>Improve naming on internal '_client' method and use Map over object<commit_after>const http = require('http')
const https = require('https')
http.globalAgent.maxSockets = 300
https.globalAgent.maxSockets = 300

const settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
const metrics = require('metrics-sharelatex')

const meter = require('stream-meter')
const fs = require('fs')
const S3 = require('aws-sdk/clients/s3')
const { URL } = require('url')
const { callbackify } = require('util')
const {
  WriteError,
  ReadError,
  NotFoundError,
  SettingsError
} = require('./Errors')

module.exports = {
  sendFile: callbackify(sendFile),
  sendStream: callbackify(sendStream),
  getFileStream: callbackify(getFileStream),
  deleteDirectory: callbackify(deleteDirectory),
  getFileSize: callbackify(getFileSize),
  deleteFile: callbackify(deleteFile),
  copyFile: callbackify(copyFile),
  checkIfFileExists: callbackify(checkIfFileExists),
  directorySize: callbackify(directorySize),
  promises: {
    sendFile,
    sendStream,
    getFileStream,
    deleteDirectory,
    getFileSize,
    deleteFile,
    copyFile,
    checkIfFileExists,
    directorySize
  }
}

async function sendFile(bucketName, key, fsPath) {
  let readStream
  try {
    readStream = fs.createReadStream(fsPath)
  } catch (err) {
    throw _wrapError(
      err,
      'error reading file from disk',
      { bucketName, key, fsPath },
      ReadError
    )
  }
  return sendStream(bucketName, key, readStream)
}

async function sendStream(bucketName, key, readStream) {
  try {
    const meteredStream = meter()
    meteredStream.on('finish', () => {
      metrics.count('s3.egress', meteredStream.bytes)
    })

    const response = await _getClientForBucket(bucketName)
      .upload({
        Bucket: bucketName,
        Key: key,
        Body: readStream.pipe(meteredStream)
      })
      .promise()

    logger.log({ response, bucketName, key }, 'data uploaded to s3')
  } catch (err) {
    throw _wrapError(
      err,
      'upload to S3 failed',
      { bucketName, key },
      WriteError
    )
  }
}

async function getFileStream(bucketName, key, opts) {
  opts = opts || {}

  const params = {
    Bucket: bucketName,
    Key: key
  }
  if (opts.start != null && opts.end != null) {
    params.Range = `bytes=${opts.start}-${opts.end}`
  }

  return new Promise((resolve, reject) => {
    const stream = _getClientForBucket(bucketName)
      .getObject(params)
      .createReadStream()

    const meteredStream = meter()
    meteredStream.on('finish', () => {
      metrics.count('s3.ingress', meteredStream.bytes)
    })

    const onStreamReady = function() {
      stream.removeListener('readable', onStreamReady)
      resolve(stream.pipe(meteredStream))
    }
    stream.on('readable', onStreamReady)
    stream.on('error', err => {
      reject(_wrapError(err, 'error reading from S3', params, ReadError))
    })
  })
}

async function deleteDirectory(bucketName, key) {
  logger.log({ key, bucketName }, 'deleting directory')
  let response

  try {
    response = await _getClientForBucket(bucketName)
      .listObjects({ Bucket: bucketName, Prefix: key })
      .promise()
  } catch (err) {
    throw _wrapError(
      err,
      'failed to list objects in S3',
      { bucketName, key },
      ReadError
    )
  }

  const objects = response.Contents.map(item => ({ Key: item.Key }))
  if (objects.length) {
    try {
      await _getClientForBucket(bucketName)
        .deleteObjects({
          Bucket: bucketName,
          Delete: {
            Objects: objects,
            Quiet: true
          }
        })
        .promise()
    } catch (err) {
      throw _wrapError(
        err,
        'failed to delete objects in S3',
        { bucketName, key },
        WriteError
      )
    }
  }
}

async function getFileSize(bucketName, key) {
  try {
    const response = await _getClientForBucket(bucketName)
      .headObject({ Bucket: bucketName, Key: key })
      .promise()
    return response.ContentLength
  } catch (err) {
    throw _wrapError(
      err,
      'error getting size of s3 object',
      { bucketName, key },
      ReadError
    )
  }
}

async function deleteFile(bucketName, key) {
  try {
    await _getClientForBucket(bucketName)
      .deleteObject({ Bucket: bucketName, Key: key })
      .promise()
  } catch (err) {
    throw _wrapError(
      err,
      'failed to delete file in S3',
      { bucketName, key },
      WriteError
    )
  }
}

async function copyFile(bucketName, sourceKey, destKey) {
  const params = {
    Bucket: bucketName,
    Key: destKey,
    CopySource: `${bucketName}/${sourceKey}`
  }
  try {
    await _getClientForBucket(bucketName)
      .copyObject(params)
      .promise()
  } catch (err) {
    throw _wrapError(err, 'failed to copy file in S3', params, WriteError)
  }
}

async function checkIfFileExists(bucketName, key) {
  try {
    await getFileSize(bucketName, key)
    return true
  } catch (err) {
    if (err instanceof NotFoundError) {
      return false
    }
    throw _wrapError(
      err,
      'error checking whether S3 object exists',
      { bucketName, key },
      ReadError
    )
  }
}

async function directorySize(bucketName, key) {
  try {
    const response = await _getClientForBucket(bucketName)
      .listObjects({ Bucket: bucketName, Prefix: key })
      .promise()

    return response.Contents.reduce((acc, item) => item.Size + acc, 0)
  } catch (err) {
    throw _wrapError(
      err,
      'error getting directory size in S3',
      { bucketName, key },
      ReadError
    )
  }
}

function _wrapError(error, message, params, ErrorType) {
  if (['NoSuchKey', 'NotFound', 'ENOENT'].includes(error.code)) {
    return new NotFoundError({
      message: 'no such file',
      info: params
    }).withCause(error)
  } else {
    return new ErrorType({
      message: message,
      info: params
    }).withCause(error)
  }
}

const _clients = new Map()
let _defaultClient

function _getClientForBucket(bucket) {
  if (_clients[bucket]) {
    return _clients[bucket]
  }

  if (
    settings.filestore.s3.s3BucketCreds &&
    settings.filestore.s3.s3BucketCreds[bucket]
  ) {
    _clients[bucket] = new S3(
      _buildClientOptions(settings.filestore.s3.s3BucketCreds[bucket])
    )
    return _clients[bucket]
  }

  // no specific credentials for the bucket
  if (_defaultClient) {
    return _defaultClient
  }

  if (settings.filestore.s3.key) {
    _defaultClient = new S3(_buildClientOptions())
    return _defaultClient
  }

  throw new SettingsError({
    message: 'no bucket-specific or default credentials provided',
    info: { bucket }
  })
}

function _buildClientOptions(bucketCredentials) {
  const options = {}

  if (bucketCredentials) {
    options.credentials = {
      accessKeyId: bucketCredentials.auth_key,
      secretAccessKey: bucketCredentials.auth_secret
    }
  } else {
    options.credentials = {
      accessKeyId: settings.filestore.s3.key,
      secretAccessKey: settings.filestore.s3.secret
    }
  }

  if (settings.filestore.s3.endpoint) {
    const endpoint = new URL(settings.filestore.s3.endpoint)
    options.endpoint = settings.filestore.s3.endpoint
    options.sslEnabled = endpoint.protocol === 'https'
  }

  // path-style access is only used for acceptance tests
  if (settings.filestore.s3.pathStyle) {
    options.s3ForcePathStyle = true
  }

  return options
}
<|endoftext|>