bcgov/common-forms-toolkit

View on GitHub
docs/sample/forms/myform/dataService.js

Summary

Maintainability
F
1 wk
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();
      obj.submissionId = submissionId;
      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('[survey]')
        .withGraphFetched('[survey]')
        .throwIfNotFound();
    } else {
      return this._models.Submission.query()
        .findById(submissionId)
        .allowGraph('[survey, statuses.[notes, statusCode], notes]')
        .withGraphFetched('[survey]')
        .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('[survey]')
        .throwIfNotFound();

      // check survey... any changes?
      if (!this._equalTo(currentSubmission.survey, obj.survey)) {
        obj.survey.updatedBy = user.username;
        await this._models.Survey.query(trx).patchAndFetchById(obj.survey.surveyId, obj.survey);
        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) {
    const tiny = data => {
      if (!data || !Array.isArray(data) || !data.length) {
        return [];
      }
      // asked for the tiny result set, so shrink it down!
      return data.map(d => {
        return {
          submissionId: d.submissionId,
          formVersionId: d.formVersionId,
          createdAt: d.createdAt,
          surveyId: d.survey.surveyId,
          submitter: d.survey.submitter,
          city: d.location.city,
          status: d.statuses[0].statusCode.display,
          assignedTo: d.statuses[0].assignedTo,
          deleted: d.deleted
        };
      });
    };

    const submissions = await this._models.Submission.query()
      .allowGraph('[survey, statuses.[notes, statusCode], notes]')
      .withGraphFetched('[survey]')
      .withGraphFetched('statuses(orderDescending).[notes(orderDescending),statusCode]')
      .withGraphFetched('notes(orderDescending)')
      .modify('filterVersion', params.version)
      .modify('filterDeleted', params.deleted)
      .modify('orderDescending');

    return params.tiny ? tiny(submissions) : submissions;
  }

}

module.exports.FormDataService = FormDataService;