bcgov/common-forms-toolkit

View on GitHub
app/src/forms/attestations/dataService.js

Summary

Maintainability
F
2 wks
Test Coverage
const equal = require('fast-deep-equal');
const log = require('npmlog');
const Problem = require('api-problem');
const {transaction} = require('objection');
const {v4: uuidv4} = require('uuid');

const DataService = require('../common/dataService');

class FormDataService extends DataService {

  constructor(models, constants) {
    super(models);
    this._constants = constants;
  }

  _copyAndRemoveStamps(obj){
    let items = obj;
    if (!Array.isArray(items)) {
      items = [obj];
    }
    return items.map(o => {
      const x = {...o};
      ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'].forEach(p => delete x[p]);
      return x;
    });
  }

  _equalTo(a, b) {
    const x = this._copyAndRemoveStamps(a);
    const y = this._copyAndRemoveStamps(b);
    return equal(x, y);
  }

  // eslint-disable-next-line no-unused-vars
  async validateCreate(obj) {
    return true;
  }

  async create(obj, user) {
    if (!obj) {
      throw Error(`${this._constants.TITLE} cannot be created without data`);
    }

    await this.validateCreate(obj);

    let trx;
    try {
      trx = await transaction.start(this._models.Metadata.knex());

      const formId = uuidv4();
      // set the metadata
      const metadata = {
        formId: formId,
        slug: this._constants.SLUG,
        prefix: this._constants.PREFIX,
        ...obj.metadata
      };
      // set the versions
      const versions = obj.versions.map(v => {
        // this is a new version, set the user stamp
        v.statusCodes.forEach(s => s.createdBy = user.username);
        v.createdBy = user.username;
        return v;
      });

      await this._models.Form.query(trx).insertGraph({
        metadata: {
          createdBy: user.username,
          ...metadata
        },
        createdBy: user.username,
        description: obj.description,
        versions: versions
      });

      await trx.commit();

      const result = await this.read();
      return result;
    } catch (err) {
      log.error('create', `Error creating ${this._constants.TITLE} record: ${err.message}. Rolling back...`);
      log.error(err);
      if (trx) await trx.rollback();
      throw err;
    }
  }

  // eslint-disable-next-line no-unused-vars
  async validateUpdate(obj) {
    return true;
  }

  async update(obj, user) {
    if (!obj) {
      throw Error(`${this._constants.TITLE} cannot be updated without data`);
    }

    await this.validateUpdate(obj);

    let trx;
    try {
      trx = await transaction.start(this._models.Metadata.knex());

      // this property could come in from a /current query...
      // it's an additional helper field we don't want here.
      delete obj.formVersionId;

      // if exists, then we update with a new version...
      const current = await this._models.Form.query()
        .first()
        .throwIfNotFound();

      // set the metadata (ensure nothing critical has changed...)
      const metadata = {
        formId: current.formId,
        slug: this._constants.SLUG,
        prefix: this._constants.PREFIX,
        ...obj.metadata
      };

      // set the versions (only add the new one(s))
      const version = obj.versions.find(f => !f.formVersionId);
      version.formId = current.formId;
      version.statusCodes.forEach(s => {
        s.createdBy = user.username;
        s.updatedBy = user.username;
      });
      version.createdBy = user.username;

      // update the form metadata...
      await this._models.Metadata.query(trx).patchAndFetchById(current.formId, metadata);

      // update the form...
      await this._models.Form.query(trx).patchAndFetchById(current.formId, {
        updatedBy: user.username,
        description: obj.description
      });

      // add/update status codes
      await this._models.StatusCode.query(trx).upsertGraph(version.statusCodes, {insertMissing: true});

      // add new version
      const versionRec = await this._models.Version.query(trx).insert(version);

      // add the relationships for version/codes
      await versionRec.$relatedQuery('statusCodes', trx).relate(version.statusCodes);

      await trx.commit();

      const result = await this.read();
      return result;
    } catch (err) {
      log.error('create', `Error updating ${this._constants.TITLE} record: ${err.message}. Rolling back...`);
      log.error(err);
      if (trx) await trx.rollback();
      throw err;
    }
  }

  async exists() {
    return this._models.Form.query().first();
  }

  async read() {
    return this._models.Form.query()
      .first()
      .allowGraph('[versions.statusCodes, metadata]')
      .withGraphFetched('metadata')
      .withGraphFetched('versions(orderDescending).statusCodes')
      .throwIfNotFound();
  }

  async current(tiny) {
    let form;
    let version;

    if (tiny) {
      form = await this._models.Form.query()
        .first()
        .throwIfNotFound();
      version = await this._models.Version.query()
        .first()
        .where('formId', form.formId)
        .modify('orderDescending')
        .throwIfNotFound();
    } else {
      form = await this._models.Form.query()
        .first()
        .withGraphFetched({metadata: true})
        .throwIfNotFound();
      version = await this._models.Version.query()
        .first()
        .where('formId', form.formId)
        .withGraphFetched({statusCodes: true})
        .modify('orderDescending')
        .throwIfNotFound();
    }
    form.versions = [version];
    form.formVersionId = version.formVersionId;
    return form;
  }

  // eslint-disable-next-line no-unused-vars
  async validateCreateSubmission(obj) {
    return;
  }

  async createSubmission(obj, user) {
    if (!obj) {
      throw Error(`${this._constants.TITLE} Submission cannot be created without data`);
    }

    await this.validateCreateSubmission(obj);

    let trx;
    try {
      trx = await transaction.start(this._models.Submission.knex());

      // all submissions use the current version...
      const current = await this.current(true);
      obj.formVersionId = current.formVersionId;

      // set up the non-generated ids...
      const submissionId = uuidv4();
      const confirmationId = submissionId.substring(0, 8).toUpperCase(); // for that nice frontend look!
      obj.submissionId = submissionId;
      obj.confirmationId = confirmationId;
      obj.attestation.attestationId = uuidv4();
      obj.createdBy = user.username;


      // add the initial submitted status to the graph
      obj.statuses = [{
        code: this._constants.INITIAL_STATUS_CODE,
        createdBy: user.username
      }];

      await this._models.Submission.query(trx).insertGraph(obj);
      await trx.commit();
      const result = await this.readSubmission(submissionId);
      return result;
    } catch (err) {
      log.error('create', `Error creating ${this._constants.TITLE} submission record: ${err.message}. Rolling back...`);
      log.error(err);
      if (trx) await trx.rollback();
      throw err;
    }
  }

  async readSubmission(submissionId, tiny) {
    if (tiny) {
      return this._models.Submission.query()
        .findById(submissionId)
        .allowGraph('[attestation, business, contacts, location]')
        .withGraphFetched('[attestation, business, location]')
        .withGraphFetched('contacts(orderContactType)')
        .throwIfNotFound();
    } else {
      return this._models.Submission.query()
        .findById(submissionId)
        .allowGraph('[attestation, business, contacts, location, statuses.[notes, statusCode], notes]')
        .withGraphFetched('[attestation, business, location]')
        .withGraphFetched('contacts(orderContactType)')
        .withGraphFetched('statuses(orderDescending).[notes(orderDescending),statusCode]')
        .withGraphFetched('notes(orderDescending)')
        .throwIfNotFound();
    }
  }

  // eslint-disable-next-line no-unused-vars
  async validateUpdateSubmission(submissionId, obj) {
    return;
  }

  async updateSubmission(submissionId, obj, user) {
    // update: location, contacts, business
    if (!obj) {
      throw Error(`${this._constants.TITLE} Submission cannot be updated without data`);
    }

    await this.validateUpdateSubmission(submissionId, obj);

    let trx;
    try {
      trx = await transaction.start(this._models.Submission.knex());
      let doTheUpdate = false;
      const currentSubmission = await this._models.Submission.query()
        .first()
        .where({submissionId: submissionId})
        .where({submissionId: obj.submissionId})
        .withGraphFetched('[business, contacts, location]')
        .throwIfNotFound();

      // check business... any changes?
      if (!this._equalTo(currentSubmission.business, obj.business)) {
        obj.business.updatedBy = user.username;
        await this._models.Business.query(trx).patchAndFetchById(obj.business.businessId, obj.business);
        doTheUpdate = true;
      }

      // check contacts... any changes?
      const primary = obj.contacts.find(x => x.contactType === this._constants.CONTACT_TYPE_PRIMARY);
      if (!this._equalTo(currentSubmission.contacts.find(x => x.contactType === this._constants.CONTACT_TYPE_PRIMARY), primary)) {
        primary.updatedBy = user.username;
        await this._models.Contact.query(trx).patchAndFetchById(primary.contactId, primary);
        doTheUpdate = true;
      }
      const covid = obj.contacts.find(x => x.contactType === this._constants.CONTACT_TYPE_COVID);
      if (!this._equalTo(currentSubmission.contacts.find(x => x.contactType === this._constants.CONTACT_TYPE_COVID), covid)) {
        covid.updatedBy = user.username;
        await this._models.Contact.query(trx).patchAndFetchById(covid.contactId, covid);
        doTheUpdate = true;
      }

      // check location... any changes?
      if (!this._equalTo(currentSubmission.location, obj.location)) {
        obj.location.updatedBy = user.username;
        await this._models.Location.query(trx).patchAndFetchById(obj.location.locationId, obj.location);
        doTheUpdate = true;
      }

      if (doTheUpdate) {
        // only want to update the who and when...
        await this._models.Submission.query(trx).patchAndFetchById(obj.submissionId, {updatedBy: user.username});
      }
      await trx.commit();
      const result = await this.readSubmission(obj.submissionId);
      return result;
    } catch (err) {
      log.error('create', `Error updating ${this._constants.TITLE} submission: ${err.message}. Rolling back...`);
      log.error(err);
      if (trx) await trx.rollback();
      throw err;
    }
  }

  // eslint-disable-next-line no-unused-vars
  async validateDeleteSubmission(submissionId) {
    return;
  }

  async deleteSubmission(submissionId, user) {
    if (!submissionId) {
      throw Error(`${this._constants.TITLE} Submission cannot be deleted without an id`);
    }

    await this.validateDeleteSubmission(submissionId);

    let trx;
    try {
      trx = await transaction.start(this._models.Submission.knex());
      await this._models.Submission.query(trx).patchAndFetchById(submissionId, {
        deleted: true,
        updatedBy: user.username
      });
      await trx.commit();
      const result = await this.readSubmission(submissionId);
      return result;
    } catch (err) {
      log.error('create', `Error deleting ${this._constants.TITLE} submission: ${err.message}. Rolling back...`);
      log.error(err);
      if (trx) await trx.rollback();
      throw err;
    }
  }

  async searchSubmissions(params) {
    if (params && params.tiny) {
      const submissions = await this._models.SubmissionSearchView.query()
        .modify('filterVersion', params.version)
        .modify('filterConfirmationId', params.confirmationId)
        .modify('filterBusinessName', params.business)
        .modify('filterCity', params.city)
        .modify('filterDeleted', params.deleted)
        .modify('orderDescending');
      return submissions;
    } else {
      const submissions = await this._models.Submission.query()
        .allowGraph('[attestation, business, contacts, location, statuses.[notes, statusCode], notes]')
        .withGraphFetched('[attestation, business, location]')
        .withGraphFetched('contacts(orderContactType)')
        .withGraphFetched('statuses(orderDescending).[notes(orderDescending),statusCode]')
        .withGraphFetched('notes(orderDescending)')
        .joinRelated('business')
        .joinRelated('location')
        .modify('filterVersion', params.version)
        .modify('filterConfirmationId', params.confirmationId)
        .modify('filterBusinessName', params.business)
        .modify('filterCity', params.city)
        .modify('filterDeleted', params.deleted)
        .modify('orderDescending');
      return submissions;
    }
  }
}

class OperationTypesDataService extends FormDataService {

  constructor(models, constants) {
    super(models, constants);
  }

  async readTypes(enabled) {
    return this._models.OperationType.query()
      .modify('filterEnabled', enabled)
      .modify('orderAscending');
  }

  async validateCreateSubmission(obj) {
    const types = await this.readTypes(true);
    const typeCode = types.find(x => equal(x.type, obj.type));
    if (!typeCode) {
      throw new Problem(422, 'Invalid Operation Type Code', {detail: `${obj.type||'<null>'} is not a valid, enabled operation type code.`});
    }
  }

  async readSubmission(submissionId, tiny) {
    if (tiny) {
      return this._models.Submission.query()
        .findById(submissionId)
        .allowGraph('[operationType, attestation, business, contacts, location]')
        .withGraphFetched('[operationType, attestation, business, location]')
        .withGraphFetched('contacts(orderContactType)')
        .throwIfNotFound();
    } else {
      return this._models.Submission.query()
        .findById(submissionId)
        .allowGraph('[operationType, attestation, business, contacts, location, statuses.[notes, statusCode], notes]')
        .withGraphFetched('[operationType, attestation, business, location]')
        .withGraphFetched('contacts(orderContactType)')
        .withGraphFetched('statuses(orderDescending).[notes(orderDescending),statusCode]')
        .withGraphFetched('notes(orderDescending)')
        .throwIfNotFound();
    }
  }

  async searchSubmissions(params) {
    if (params && params.tiny) {
      const submissions = await this._models.SubmissionSearchView.query()
        .modify('filterVersion', params.version)
        .modify('filterConfirmationId', params.confirmationId)
        .modify('filterBusinessName', params.business)
        .modify('filterCity', params.city)
        .modify('filterType', params.type)
        .modify('filterDeleted', params.deleted)
        .modify('orderDescending');
      return submissions;
    } else {

      const submissions = await this._models.Submission.query()
        .allowGraph('[operationType, attestation, business, contacts, location, statuses.[notes, statusCode], notes]')
        .withGraphFetched('[operationType, attestation, business, location]')
        .withGraphFetched('contacts(orderContactType)')
        .withGraphFetched('statuses(orderDescending).[notes(orderDescending),statusCode]')
        .withGraphFetched('notes(orderDescending)')
        .joinRelated('business')
        .joinRelated('location')
        .modify('filterVersion', params.version)
        .modify('filterConfirmationId', params.confirmationId)
        .modify('filterBusinessName', params.business)
        .modify('filterCity', params.city)
        .modify('filterType', params.type)
        .modify('filterDeleted', params.deleted)
        .modify('orderDescending');
      return submissions;
    }
  }

}

module.exports.FormDataService = FormDataService;
module.exports.OperationTypesDataService = OperationTypesDataService;