From 4025cb130e2bb61d5d5acc618cc8cc06b75ec0b6 Mon Sep 17 00:00:00 2001 From: andrewbrazzatti Date: Tue, 17 Dec 2024 09:25:35 +1030 Subject: [PATCH] Feature: Trigger to validate fields using a lodash template (#2692) * Added template based validator trigger for records * reinstated missing function * Added tests for the validateFieldsUsingTemplate trigger * Removed verbose error message * Added forceRun to the test configuration to allow it to pass the triggerCondition * Moved util functions to the template execution context as functions cannot be passed as imports * Fixed expectations for failed test --- test/unit/services/TriggerService.test.js | 52 ++++++++++++++ typescript/api/services/TriggerService.ts | 86 ++++++++++++++++++++--- 2 files changed, 130 insertions(+), 8 deletions(-) diff --git a/test/unit/services/TriggerService.test.js b/test/unit/services/TriggerService.test.js index 8b8db10d39..266411c5da 100644 --- a/test/unit/services/TriggerService.test.js +++ b/test/unit/services/TriggerService.test.js @@ -13,6 +13,7 @@ describe('The TriggerService', function () { fieldLanguageCode: "title-required", arrayObjFieldDBName: 'row-item', trimLeadingAndTrailingSpacesBeforeValidation: true, + forceRun: true // caseSensitive: true, - default // allowNulls: true, - default }; @@ -31,6 +32,7 @@ describe('The TriggerService', function () { trimLeadingAndTrailingSpacesBeforeValidation: false, caseSensitive: true, allowNulls: false, + forceRun: true }; try { await TriggerService.validateFieldUsingRegex(oid, record, options); @@ -55,6 +57,7 @@ describe('The TriggerService', function () { trimLeadingAndTrailingSpacesBeforeValidation: false, caseSensitive: false, allowNulls: true, + forceRun: true }; try { @@ -77,6 +80,7 @@ describe('The TriggerService', function () { trimLeadingAndTrailingSpacesBeforeValidation: false, caseSensitive: false, allowNulls: true, + forceRun: true }; const result = await TriggerService.validateFieldUsingRegex(oid, record, options); expect(result).to.eql(record); @@ -92,6 +96,7 @@ describe('The TriggerService', function () { trimLeadingAndTrailingSpacesBeforeValidation: false, caseSensitive: false, allowNulls: false, + forceRun: true }; try { await TriggerService.validateFieldUsingRegex(oid, record, options); @@ -103,4 +108,51 @@ describe('The TriggerService', function () { } }); }); + + + describe('should validate fields using lodash template', function () { + it('valid value passes', async function () { + const oid = "triggerservice-template-validpasses"; + const record = {'testing-field': 'valid-value'}; + const options = { + template: `<% let errorList = [] + if (_.get(record,'testing-field') !== 'valid-value') { + addError(errorList, 'testing-field', 'title-required', 'invalid-format' ); + } + return errorList; %>`, + forceRun: true + }; + + try { + const result = await TriggerService.validateFieldsUsingTemplate(oid, record, options); + expect(result).to.eql({'testing-field': 'valid-value'}); + } catch (err) { + expect.fail("Should not have thrown error"); + } + + }); + + it('invalid value fails', async function () { + const oid = "triggerservice-template-validpasses"; + const record = {'testing-field': 'invalid-value'}; + const options = { + template: `<% let errorList = [] + if (_.get(record,'testing-field') !== 'valid-value') { + addError(errorList, 'testing-field', 'title-required', 'invalid-format' ); + } + return errorList; %>`, + forceRun: true + }; + try { + const result = await TriggerService.validateFieldsUsingTemplate(oid, record, options); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.name).to.eq("RBValidationError"); + const errorMap = JSON.parse(err.message) + expect(errorMap.errorFieldList[0].label).to.eq("Title is required"); + } + + }); + + }); }); \ No newline at end of file diff --git a/typescript/api/services/TriggerService.ts b/typescript/api/services/TriggerService.ts index 102d1a4326..ad6d2db9af 100644 --- a/typescript/api/services/TriggerService.ts +++ b/typescript/api/services/TriggerService.ts @@ -22,6 +22,7 @@ import { RBValidationError, BrandingModel, Services as services, + PopulateExportedMethods, } from '@researchdatabox/redbox-core-types'; import { Sails, Model } from "sails"; import { default as moment } from 'moment'; @@ -43,16 +44,9 @@ export module Services { * Author: Shilo Banihit * */ + @PopulateExportedMethods export class Trigger extends services.Core.Service { - protected _exportedMethods: any = [ - 'transitionWorkflow', - 'runHooksSync', - 'validateFieldUsingRegex', - 'applyFieldLevelPermissions', - 'validateFieldMapUsingRegex', - 'runTemplatesOnRelatedRecord' - ]; /** * Used in changing the workflow stages automatically based on configuration. @@ -285,6 +279,82 @@ export module Services { return record; } + /** + * Trigger function that will run a lodash template that can be used to validate a record. + * The trigger expects the template to return an array of error objects of the format: + * { + * name: 'the field name that is in validation error', + * label: 'the human readable label for the field', + * error: 'optional error message' + * } + * + * An empty array should be returned if no errors are found. + * + * @param oid + * @param record + * @param options + * @returns + */ + public async validateFieldsUsingTemplate(oid, record, options) { + sails.log.verbose('validateFieldsUsingTemplate - enter'); + if (this.metTriggerCondition(oid, record, options) === "true") { + + sails.log.verbose('validateFieldsUsingTemplate - metTriggerCondition'); + + + const getErrorMessage = function (errorLanguageCode: string) { + let baseErrorMessage = TranslationService.t(errorLanguageCode); + return baseErrorMessage; + } + + const addError = function (errorFieldList, name, label, errorLabel) { + let errorField:any = {}; + _.set(errorField,'name', name); + _.set(errorField,'label',getErrorMessage(label)); + let error = getErrorMessage(errorLabel); + if(error != '') { + _.set(errorField,'error',error); + } + errorFieldList.push(errorField); + } + + let template = _.get(options,'template',"<% return []; %>"); + + const imports = { + moment: moment, + numeral: numeral, + _ : _, + TranslationService: TranslationService + } + + let altErrorMessage = _.get(options,'altErrorMessage',[]); + + if(_.isString(template)) { + const compiledTemplate = _.template(template, imports); + options.template = compiledTemplate; + template = compiledTemplate; + } + + const errorFieldList = template({oid:oid, record: record, options: options, addError: addError, + getErrorMessage: getErrorMessage}); + + + const errorMap = { + altErrorMessage: altErrorMessage, + errorFieldList: errorFieldList + }; + + if(!_.isEmpty(errorMap.errorFieldList)) { + let customError: RBValidationError; + customError = new RBValidationError(JSON.stringify(errorMap)); + throw customError; + } + + sails.log.debug('validateFieldsUsingTemplate data value passed check'); + } + return record; + } + public async validateFieldMapUsingRegex(oid, record, options) { sails.log.verbose('validateFieldMapUsingRegex - enter'); if (this.metTriggerCondition(oid, record, options) === "true") {