Source: store/recordData.js

import { isEmpty, isEqual } from "lodash";
// import Vue from "vue";
import RESTClient from "@/lib/Client/RESTClient.js";

import Client from "../lib/GraphClient/GraphClient.js";
import recordDataAccessQuery from "../lib/GraphClient/queries/editor/getRecordDataAccess.json";
import recordRelationsQuery from "../lib/GraphClient/queries/editor/getRecordRelations.json";
import recordQuery from "../lib/GraphClient/queries/getRecord.json";
import recordHistory from "../lib/GraphClient/queries/getRecordHistory.json";
import recordOrganisationsQuery from "../lib/GraphClient/queries/getRecordOrganisations.json";
import { initEditorSections } from "./utils.js";

let client = new Client();
let restClient = new RESTClient();

/**
 * The record store handles the requests related to record (fairsharingRecord).
 * @type {Object}
 */
let recordStore = {
  namespaced: true,
  state: {
    currentRecord: {
      fairsharingRecord: {
        metadata: {
          citations: [],
        },
      },
    },
    currentRecordHistory: {},
    recordUpdate: {
      error: false,
      message: null,
      id: null,
    },
    sections: {
      generalInformation: initEditorSections(false, ["generalInformation"])
        .generalInformation,
    },
    editOrganisationLink: {
      showOverlay: false,
      data: {},
      id: null,
    },
    newRecord: false,
    currentID: null,
  },
  mutations: {
    setCurrentRecord(state, data) {
      state.currentRecord = data;
      let tags = ["subjects", "domains", "taxonomies", "userDefinedTags"];
      tags.forEach((tag) => {
        if (
          state.currentRecord["fairsharingRecord"][tag].length &&
          state.currentRecord["fairsharingRecord"][tag]
        ) {
          state.currentRecord["fairsharingRecord"][tag].forEach((item) => {
            item.type = tag;
          });
        }
      });
    },
    setRecordHistory(state, data) {
      state.currentRecordHistory = data;
    },
    resetCurrentRecordHistory(state) {
      state.currentRecordHistory = {};
    },
    setSections(state, data) {
      state.currentID = data["fairsharingRecord"].id;
      let sectionsNames = [
        "generalInformation",
        "support",
        "dataAccess",
        "publications",
        "organisations",
        "additionalInformation",
        "relations",
      ];
      state.sections = initEditorSections(
        data["fairsharingRecord"],
        sectionsNames,
      );
    },
    setGeneralInformation(state, data) {
      state.sections.generalInformation = initEditorSections(
        data["fairsharingRecord"],
        ["generalInformation"],
      ).generalInformation;
      if (data["fairsharingRecord"])
        state.sections.generalInformation.message =
          "Record successfully updated!";
    },
    resetMessage(state, sectionName) {
      state.sections[sectionName].message = null;
      state.sections[sectionName].message = false;
    },
    setSectionError(state, error) {
      state.sections[error.section].message = error.value;
      state.sections[error.section].error = true;
    },
    setChanges(state, diff) {
      state.sections[diff.section].changes = diff.value;
    },
    setContacts(state, contacts) {
      state.sections.generalInformation.data.metadata.contacts = contacts;
    },
    setTags(state, field) {
      state.sections.generalInformation.data[field.target] = field.value;
    },
    resetRegistry(state) {
      state.sections.generalInformation.data.type = "";
    },
    setPublications(state, publications) {
      state.sections.publications.data = publications;
    },
    setAdditionalInformation(state, additionalInformation) {
      if (!additionalInformation.subfieldName) {
        state.sections.additionalInformation.data[
          additionalInformation.fieldName
        ] = additionalInformation.fieldValue;
      } else {
        state.sections.additionalInformation.data[
          additionalInformation.fieldName
        ][additionalInformation.subfieldName] =
          additionalInformation.fieldValue;
      }
    },
    setAdditionalInformationSubField(state, additionalInformation) {
      if (additionalInformation.id !== null) {
        state.sections.additionalInformation.data[
          additionalInformation.fieldName
        ][additionalInformation.id] = additionalInformation.fieldValue;
      } else {
        if (
          !state.sections.additionalInformation.data[
            additionalInformation.fieldName
          ]
        ) {
          state.sections.additionalInformation.data[
            additionalInformation.fieldName
          ] = [];
        }
        try {
          state.sections.additionalInformation.data[
            additionalInformation.fieldName
          ][
            state.sections.additionalInformation.data[
              additionalInformation.fieldName
            ].length
          ] = additionalInformation.fieldValue;
          // eslint-disable-next-line no-empty
        } catch (e) {
          // TODO: Investigate comments below.
          // Github has been failing tests (which are fine locally) here for reasons which
          // have not so far been determined.
          // TypeError: Cannot read property 'shallow' of undefined
          // ...on the Vue.set, above.
          // eslint-enable-next-line no-empty
        }
      }
    },
    removeAdditionalInformationSubField(state, additionalInformation) {
      state.sections.additionalInformation.data[
        additionalInformation.fieldName
      ].splice(additionalInformation.id, 1);
    },
    updateOrganisationsLinks(state, links) {
      state.sections.organisations.data = links;
      state.sections.organisations.initialData = JSON.parse(
        JSON.stringify(links),
      );
      state.sections.organisations.changes = 0;
      state.sections.organisations.message = "Record successfully updated!";
    },
    setEditOrganisationLink(state, newLink) {
      state.editOrganisationLink = newLink;
    },
    setEditOrganisationLinkOrganisation(state, organisation) {
      state.editOrganisationLink.data.organisation = organisation;
    },
    setEditOrganisationLinkGrant(state, grant) {
      state.editOrganisationLink.data.grant = grant;
    },
    setDataAccess(state, dataAccess) {
      let record = {
        exhaustiveLicences: dataAccess.exhaustiveLicences,
        licences: dataAccess["licenceLinks"],
        support_links: dataAccess.metadata.support_links,
      };
      state.sections.generalInformation.data.metadata.support_links =
        JSON.parse(JSON.stringify(record.support_links));
      state.sections.dataAccess.data.exhaustiveLicences =
        record.exhaustiveLicences;
      state.sections.generalInformation.initialData.metadata.support_links =
        JSON.parse(JSON.stringify(record.support_links));
      record.support_links.forEach((supportLink) => {
        if (supportLink.name)
          supportLink.url = { title: supportLink.name, url: supportLink.url };
      });
      state.sections.dataAccess.data = record;
      state.sections.dataAccess.initialData = JSON.parse(
        JSON.stringify(record),
      );
      state.sections.dataAccess.changes = 0;
      state.sections.dataAccess.message = "Record successfully updated!";
    },
    updateAdditionalInformation(state, additionalInformation) {
      let record = {};
      Object.keys(additionalInformation.record).forEach((field) => {
        if (additionalInformation.fields.includes(field)) {
          record[field] = additionalInformation.record[field];
          state.sections.generalInformation.data.metadata[field] = JSON.parse(
            JSON.stringify(record[field]),
          );
          state.sections.generalInformation.initialData.metadata[field] =
            JSON.parse(JSON.stringify(record[field]));
        }
      });
      state.sections.additionalInformation.data = record;
      state.sections.additionalInformation.initialData = JSON.parse(
        JSON.stringify(record),
      );
      state.sections.additionalInformation.changes = 0;
      state.sections.additionalInformation.message =
        "Record successfully updated!";
    },
    setCreatingNewRecord(state) {
      state.newRecord = true;
    },
    setEditingRecord(state) {
      state.newRecord = false;
    },
    setRelations(state, relations) {
      state.sections.relations.data.recordAssociations = relations;
      state.sections.relations.initialData.recordAssociations = JSON.parse(
        JSON.stringify(relations),
      );
      state.sections.relations.changes = 0;
      state.sections.relations.message = "Record successfully updated!";
      state.sections.relations.error = false;
    },
    setMessage(state, message) {
      state.sections[message.target].message = message.value;
    },
    setNewRecord(state, id) {
      state.recordUpdate = {
        error: false,
        message: "success",
        id: id,
      };
    },
    setError(state, error) {
      state.recordUpdate = {
        error: true,
        message: error,
        id: null,
      };
    },
    cleanRecordStore(state) {
      state.sections = null;
      state.sections = initEditorSections(false, [
        "generalInformation",
        "support",
        "dataAccess",
        "publications",
        "organisations",
        "additionalInformation",
      ]);
    },
  },
  actions: {
    async fetchRecord(state, options) {
      state.commit("resetCurrentRecordHistory");
      recordQuery.queryParam = {
        id: options.id,
      };
      if (options.token) {
        client.setHeader(options.token);
      }
      let data = await client.executeQuery(recordQuery);
      client.initalizeHeader();
      if (!data["fairsharingRecord"]["metadata"]["contacts"]) {
        data["fairsharingRecord"]["metadata"]["contacts"] = [];
      }
      // Citations should be created if empty.
      if (!data["fairsharingRecord"]["metadata"]["citations"]) {
        data["fairsharingRecord"]["metadata"]["citations"] = [];
      }
      state.commit("setCurrentRecord", JSON.parse(JSON.stringify(data)));
      state.commit("setSections", JSON.parse(JSON.stringify(data)));
    },
    async fetchPreviewRecord(state, id) {
      state.commit("resetCurrentRecordHistory");
      recordQuery.queryParam = {
        id: id,
      };
      let data = await client.executeQuery(recordQuery);
      state.commit("setCurrentRecord", data);
    },
    async fetchRecordHistory(state, options) {
      recordHistory.queryParam = { id: options.id };
      client.setHeader(options.token);
      let data = await client.executeQuery(recordHistory);
      state.commit("setRecordHistory", data["fairsharingRecord"]);
    },
    async updateGeneralInformation({ state, commit }, options) {
      commit("resetMessage", "generalInformation");
      let {
          type,
          countries,
          userDefinedTags,
          objectTypes,
          domains,
          subjects,
          taxonomies,
          status,
          curator_notes,
          isHidden,
          logo,
          maintainers,
          watchers,
          ...record
        } = JSON.parse(JSON.stringify(state.sections.generalInformation.data)),
        newTags = [],
        oldTags = [],
        tags = [];
      userDefinedTags.forEach((tag) => {
        if (Object.keys(tag).indexOf("id") === -1) {
          newTags.push(tag.label);
        } else {
          oldTags.push(tag.id);
        }
      });
      newTags = await Promise.all(
        newTags.map((tag) =>
          restClient.createNewUserDefinedTag(tag, options.token),
        ),
      );
      newTags.forEach((tag) => {
        if (!tag.error) {
          tags.push(tag.id);
        } else {
          commit("setSectionError", {
            section: "generalInformation",
            value: tag.error,
          });
          return tag.error;
        }
      });

      isEmpty(logo) ? delete record["logo"] : (record.logo = logo);
      record.country_ids = countries.map((obj) => obj.id);
      if (type.id) record.record_type_id = type.id;
      record.metadata.status = status;
      record.curator_notes = curator_notes;
      record.hidden = isHidden;
      record.object_type_ids = objectTypes.map((obj) => obj.id);
      record.domain_ids = domains.map((obj) => obj.id);
      record.subject_ids = subjects.map((obj) => obj.id);
      record.taxonomy_ids = taxonomies.map((obj) => obj.id);
      record.maintainer_ids = maintainers.map((obj) => obj.id);
      record.watcher_ids = watchers.map((obj) => obj.id);
      record.user_defined_tag_ids = tags.concat(
        oldTags.filter(function (el) {
          return el != null;
        }),
      );
      if (options.change) {
        record.remove_additional_properties = true;
      }
      let response = await restClient.updateRecord({
        record: record,
        token: options.token,
        id: options.id,
      });
      if (response.error) {
        commit("setSectionError", {
          section: "generalInformation",
          value: response.error,
        });
        return response.error;
      } else {
        let newRecord = JSON.parse(
          JSON.stringify(state.sections.generalInformation.data),
        );
        let userDefinedTags = [];
        newRecord.userDefinedTags.forEach((obj) => {
          if (Object.keys(obj).indexOf("id") === -1) {
            obj.id = newTags.filter((tag) => {
              tag.label = obj.label;
            })[0];
            userDefinedTags.push(obj);
          } else userDefinedTags.push(obj);
        });
        newRecord.userDefinedTags = userDefinedTags;
        commit("setGeneralInformation", { fairsharingRecord: newRecord });
      }
    },
    async updatePublications({ state, commit }, options) {
      commit("resetMessage", "publications");
      let publications = JSON.parse(
        JSON.stringify(state.sections.publications.data),
      );
      let record_data = {
        publication_ids: [],
        citation_ids: [],
      };
      publications.forEach(function (publication) {
        record_data.publication_ids.push(publication.id);
        if (publication.isCitation) {
          record_data.citation_ids.push(publication.id);
        }
        delete publication.isCitation;
      });
      const record = {
        record: record_data,
        token: options.token,
        id: options.id,
      };
      let response = await restClient.updateRecord(record);
      if (response.error) {
        commit("setSectionError", {
          section: "publications",
          value: response.error,
        });
        return response.error;
      } else {
        commit("setMessage", {
          target: "publications",
          value: "Record successfully updated!",
        });
      }
    },
    async updateOrganisations({ state, commit }, userToken) {
      commit("resetMessage", "organisations");
      let deleteItems = [],
        updateItems = [],
        createItems = [];
      state.sections.organisations.initialData.forEach((obj) => {
        let found = state.sections.organisations.data.filter(
          (org) => org.id === obj.id,
        )[0];
        if (!found) {
          deleteItems.push(obj);
        }
      });
      state.sections.organisations.data.forEach(function (obj) {
        let query = {
          fairsharing_record_id: state.currentRecord["fairsharingRecord"].id,
          organisation_id: obj.organisation.id,
          relation: obj.relation,
          grant_id: obj.grant ? obj.grant.id : null,
          is_lead: obj.isLead,
        };
        if (Object.prototype.hasOwnProperty.call(obj, "id"))
          updateItems.push({ query: query, id: obj.id });
        else createItems.push(query);
      });
      let queries = await Promise.all([
        ...deleteItems.map((organisation) =>
          restClient.deleteOrganisationLink(organisation.id, userToken),
        ),
        ...createItems.map((organisation) =>
          restClient.createOrganisationLink(organisation, userToken),
        ),
        ...updateItems.map((organisation) =>
          restClient.updateOrganisationLink(
            organisation.query,
            organisation.id,
            userToken,
          ),
        ),
      ]);
      queries.forEach((org) => {
        if (org.error) {
          commit("setSectionError", {
            section: "organisations",
            value: org.error,
          });
        }
      });
      recordOrganisationsQuery.queryParam = {
        id: state.currentRecord.fairsharingRecord.id,
      };
      client.setHeader(userToken);
      let organisations = await client.executeQuery(recordOrganisationsQuery);
      commit(
        "updateOrganisationsLinks",
        organisations.fairsharingRecord.organisationLinks,
      );
    },
    async updateAdditionalInformation({ state, commit }, options) {
      commit("resetMessage", "additionalInformation");
      let newRecord = {
        metadata: state.sections.generalInformation.initialData.metadata,
      };
      options.fields.forEach((field) => {
        if (state.sections.additionalInformation.data[field]) {
          Object.keys(state.sections.additionalInformation.data[field]).forEach(
            (key) => {
              if (
                state.sections.additionalInformation.data[field][key] === ""
              ) {
                delete state.sections.additionalInformation.data[field][key];
              }
            },
          );
          newRecord.metadata[field] =
            state.sections.additionalInformation.data[field];
        } else if (state.sections.additionalInformation.data[field] === null) {
          // if its the case that there is a single string textInput only
          state.sections.additionalInformation.data[field] = "";
          newRecord.metadata[field] =
            state.sections.additionalInformation.data[field];
        }
      });
      let response = await restClient.updateRecord({
        record: newRecord,
        token: options.token,
        id: options.id,
      });
      if (response.error) {
        commit("setSectionError", {
          section: "additionalInformation",
          value: response.error,
        });
        return response.error;
      } else {
        commit("setMessage", {
          target: "additionalInformation",
          value: "Record successfully updated!",
        });
        commit("updateAdditionalInformation", {
          record: newRecord.metadata,
          fields: options.fields,
        });
      }
    },
    async updateDataAccess({ state, commit }, options) {
      commit("resetMessage", "dataAccess");
      let newRecord = {
        metadata: state.sections.generalInformation.initialData.metadata,
      };
      newRecord.metadata.support_links =
        state.sections.dataAccess.data.support_links;
      newRecord.metadata.support_links.forEach((supportLink) => {
        if (typeof supportLink.url !== "string") {
          supportLink.url = supportLink.url.url;
        }
      });

      let initialLicences = state.sections.dataAccess.initialData.licences,
        currentLicences = state.sections.dataAccess.data.licences,
        toDelete = [],
        toUpdate = [],
        toCreate = [];
      initialLicences.forEach((licence) => {
        let found = currentLicences.filter((obj) => obj.id === licence.id)[0];
        if (!found) toDelete.push(licence.id);
      });
      currentLicences.forEach((licence) => {
        let found = initialLicences.filter((obj) => obj.id === licence.id)[0],
          newLicence = prepareLicence(licence);
        if (!found) {
          toCreate.push(newLicence);
        } else if (found && !isEqual(licence, found)) {
          toUpdate.push(newLicence);
        }
      });
      newRecord.exhaustive_licences =
        state.sections.dataAccess.data.exhaustiveLicences;
      let responses = await Promise.all([
        restClient.updateRecord({
          record: newRecord,
          token: options.token,
          id: options.id,
        }),
        ...toCreate.map((licence) =>
          restClient.createLicenceLink(licence, options.token),
        ),
        ...toUpdate.map((licence) =>
          restClient.updateLicenceLink(licence, options.token),
        ),
        ...toDelete.map((licence) =>
          restClient.deleteLicenceLink(licence, options.token),
        ),
      ]);
      responses.forEach((response) => {
        if (response.error) {
          commit("setSectionError", {
            section: "dataAccess",
            value: response.error,
          });
          return response.error;
        }
      });

      recordDataAccessQuery.queryParam = {
        id: state.currentRecord.fairsharingRecord.id,
      };
      client.setHeader(options.token);
      let dataAccess = await client.executeQuery(recordDataAccessQuery);
      commit("setDataAccess", dataAccess.fairsharingRecord);
    },
    async updateRelations({ state, commit }, options) {
      commit("resetMessage", "relations");
      let newAssociations = [],
        deleteAssociations = [],
        oldAssociations = [];
      state.sections.relations.data.recordAssociations.forEach(
        (association) => {
          if (association.new) {
            const newAssociation = {
              fairsharing_record_id: options.source,
              linked_record_id: association.linkedRecord.id,
              record_assoc_label_id: association.recordAssocLabel.id,
            };
            newAssociations.push(newAssociation);
          } else {
            // Using a combination of record_id and label_id as this should be unique.
            // Using only record_id produced:
            // https://github.com/FAIRsharing/fairsharing.github.io/issues/1620
            let id =
              association.linkedRecord.id +
              "_" +
              association.recordAssocLabelId;
            oldAssociations.push(id);
          }
        },
      );
      state.sections.relations.initialData.recordAssociations.forEach(
        (oldAssociation) => {
          // Same unique ID as above.
          let id =
            oldAssociation.linkedRecord.id +
            "_" +
            oldAssociation.recordAssocLabelId;
          if (id && !oldAssociations.includes(id)) {
            deleteAssociations.push({
              id: oldAssociation.id,
              _destroy: 1,
            });
          }
        },
      );
      let responses = await Promise.all([
        restClient.saveRelations({
          token: options.token,
          relations: newAssociations,
          target: options.source,
        }),
        restClient.deleteRelations({
          token: options.token,
          relations: deleteAssociations,
          target: options.source,
        }),
      ]);
      let error = false;
      for (let response of responses) {
        if (response.error) {
          commit("setSectionError", {
            section: "relations",
            value: response.error,
          });
          error = true;
        }
      }
      if (!error) {
        recordRelationsQuery.queryParam = { id: options.source };
        client.setHeader(options.token);
        let relations = await client.executeQuery(recordRelationsQuery);
        commit(
          "setRelations",
          relations["fairsharingRecord"].recordAssociations,
        );
      }
    },
    resetRecord(state) {
      state.commit("setGeneralInformation", { fairsharingRecord: false });
    },
    async updateRecord(state, newRecord) {
      let response = await restClient.updateRecord(newRecord);
      if (response.error) {
        state.commit("setError", response.error.response);
      } else {
        state.commit("setNewRecord", response);
      }
    },
  },
  getters: {
    getField: (state) => (fieldName) => {
      return state.currentRecord["fairsharingRecord"][fieldName];
    },
    getSection: (state) => (sectionName) => {
      return state.sections[sectionName];
    },
    getChanges: (state) => {
      let changes = {};
      Object.keys(state.sections).forEach((section) => {
        changes[section] = state.sections[section].changes;
      });
      return changes;
    },
    getAllChanges: (state) => {
      let changes = 0;
      Object.keys(state.sections).forEach((section) => {
        changes += state.sections[section].changes;
      });
      return changes;
    },
    getCreatingNewRecord: (state) => {
      return state.newRecord;
    },
    getRecordType: (state) => {
      return state.sections["generalInformation"].initialData.type;
    },
  },
};

function prepareLicence(rawLicence) {
  let preparedLicence = { relation: rawLicence.relation };
  preparedLicence.fairsharing_record_id = rawLicence.fairsharingRecord
    ? rawLicence.fairsharingRecord.id
    : rawLicence.fairsharing_record_id;
  if (rawLicence.id) preparedLicence.id = rawLicence.id;

  if (rawLicence.licence.id) {
    preparedLicence.licence_id = rawLicence.licence.id;
  } else {
    preparedLicence.licence_attributes = rawLicence.licence;
  }
  return preparedLicence;
}

export default recordStore;