diff --git a/packages/element-templates-json-schema-shared/test/helpers/index.js b/packages/element-templates-json-schema-shared/test/helpers/index.js index cf5ea815..910a80c4 100644 --- a/packages/element-templates-json-schema-shared/test/helpers/index.js +++ b/packages/element-templates-json-schema-shared/test/helpers/index.js @@ -1,20 +1,15 @@ -const { - forEach, - set -} = require('min-dash'); - -const chai = require('chai'); - const { default: Ajv } = require('ajv'); const AjvErrors = require('ajv-errors'); +const { withErrorMessages, withDeprecationWarnings, getDeprecationWarnings } = require('./utils'); + module.exports = { - createValidator, - withErrorMessages + createValidator }; -function createValidator(schema, errors) { +function createValidator(schema, errors, deprecations) { + let deprecationWarnings = []; const ajv = new Ajv({ allErrors: true, strict: false, @@ -23,52 +18,51 @@ function createValidator(schema, errors) { AjvErrors(ajv); - return ajv.compile(withErrorMessages(schema, errors)); -} - -function withErrorMessages(schema, errors) { - - if (!errors || !errors.length) { - return schema; - } - - // clone a new copy - let newSchema = JSON.parse(JSON.stringify(schema)); - - // set keyword for given path - forEach(errors, function(error) { - newSchema = setErrorMessage(newSchema, error); + ajv.addKeyword({ + keyword: 'isDeprecated', + errors: true, + compile(schema, parentSchema) { + return function(data, dataCtx) { + if (schema) { + deprecationWarnings = []; + + // AJV doesn't support real warnings, so this adds a non-strict validation with keyword 'isDeprecated' + const deprecationWarning = { + keyword: 'isDeprecated', + + // TODO: schemaPath + dataPath: dataCtx.dataPath, + message: parentSchema.deprecatedWarning || 'This property is deprecated', + }; + + deprecationWarnings.push({ + id: dataCtx.rootData.id, + warningDescription: deprecationWarning, + }); + + // just return true to not fail validation + return true; + } + return true; + }; + } }); - return newSchema; -} - -function setErrorMessage(schema, error) { - const { - path, - errorMessage - } = error; - - const errorMessagePath = [ - ...path, - 'errorMessage' - ]; + const validator = ajv.compile(withErrorMessages(withDeprecationWarnings(schema, deprecations), errors)); - return set(schema, errorMessagePath, errorMessage); -} + // wrapper function checks for warnings before each validation + const wrappedValidator = function(data, ...args) { -function eqlErrors(chai, utils) { + // Empty deprecation before each validation + deprecationWarnings = []; - const Assertion = chai.Assertion; + const result = validator.call(this, data, ...args); - Assertion.addMethod('eqlErrors', function(expectedErrors, filter) { + wrappedValidator.errors = validator.errors; + wrappedValidator.warnings = getDeprecationWarnings(deprecationWarnings, data); - const actualErrors = this._obj; - - // formats the validation errors, so that they can be used directly in the fixture files. - this.eql(expectedErrors, - `Errors from validation do not match expected.\n\tValidation returned this error (you can use it in the fixture):\n\t${JSON.stringify(actualErrors, null, 2).replace(/"([^"]+)":/g, '$1:')}\n`); - }); -} + return result; + }; -chai.use(eqlErrors); \ No newline at end of file + return wrappedValidator; +} \ No newline at end of file diff --git a/packages/element-templates-json-schema-shared/test/helpers/utils.js b/packages/element-templates-json-schema-shared/test/helpers/utils.js new file mode 100644 index 00000000..83704bc2 --- /dev/null +++ b/packages/element-templates-json-schema-shared/test/helpers/utils.js @@ -0,0 +1,97 @@ +const chai = require('chai'); + +const { + forEach, + set +} = require('min-dash'); + +function withErrorMessages(schema, errors) { + + if (!errors || !errors.length) { + return schema; + } + + // clone a new copy + let newSchema = JSON.parse(JSON.stringify(schema)); + + // set keyword for given path + forEach(errors, function(error) { + newSchema = setErrorMessage(newSchema, error); + }); + + return newSchema; +} + +function setErrorMessage(schema, error) { + const { + path, + errorMessage + } = error; + + const errorMessagePath = [ + ...path, + 'errorMessage' + ]; + + return set(schema, errorMessagePath, errorMessage); +} + + +function withDeprecationWarnings(schema, deprecations) { + if (!deprecations || !deprecations.length) { + return schema; + } + + // clone a new copy + let newSchema = JSON.parse(JSON.stringify(schema)); + + // set deprecation warnings for given paths + forEach(deprecations, function(deprecation) { + newSchema = setDeprecationWarning(newSchema, deprecation); + }); + + return newSchema; +} + +function setDeprecationWarning(schema, deprecation) { + const { + path, + warningMessage + } = deprecation; + + const deprecationPath = [ + ...path, + 'deprecatedWarning' + ]; + + return set(schema, deprecationPath, warningMessage); +} + +function getDeprecationWarnings(deprecationWarnings, data) { + if (deprecationWarnings.length > 0 && data.id === deprecationWarnings[0].id) { + return deprecationWarnings.map(warning => warning.warningDescription); + } +}; + +function eqlErrors(chai, utils) { + + const Assertion = chai.Assertion; + + Assertion.addMethod('eqlErrors', function(expectedErrors, filter) { + + const actualErrors = this._obj; + + // formats the validation errors, so that they can be used directly in the fixture files. + this.eql(expectedErrors, + `Errors from validation do not match expected.\n\tValidation returned this error (you can use it in the fixture):\n\t${JSON.stringify(actualErrors, null, 2).replace(/"([^"]+)":/g, '$1:')}\n`); + }); +} + +chai.use(eqlErrors); + +module.exports = { + withErrorMessages, + withDeprecationWarnings, + getDeprecationWarnings +}; + diff --git a/packages/zeebe-element-templates-json-schema/package.json b/packages/zeebe-element-templates-json-schema/package.json index 1c9fce54..fbc12cb7 100644 --- a/packages/zeebe-element-templates-json-schema/package.json +++ b/packages/zeebe-element-templates-json-schema/package.json @@ -10,8 +10,9 @@ "test:integration": "mocha --reporter=spec --recursive test/integration", "dev": "npm run test -- --watch", "all": "run-s build test", - "build": "run-s build:error-messages build:schema", + "build": "run-s build:error-messages build:deprecated-warnings build:schema", "build:error-messages": "node ../../tasks/generate-error-messages.js --input=./src/error-messages.json --output=./resources/error-messages.json", + "build:deprecated-warnings": "node ../../tasks/generate-deprecated-warnings.js --input=./src/deprecated-warnings.json --output=./resources/deprecated-warnings.json", "build:schema": "node ../../tasks/generate-schema.js --input=./src/schema.json --output=./resources/schema.json", "prepare": "run-s build" }, diff --git a/packages/zeebe-element-templates-json-schema/src/defs/properties.json b/packages/zeebe-element-templates-json-schema/src/defs/properties.json index 896f0e87..0721503a 100644 --- a/packages/zeebe-element-templates-json-schema/src/defs/properties.json +++ b/packages/zeebe-element-templates-json-schema/src/defs/properties.json @@ -627,6 +627,45 @@ }, { "$ref": "./properties/taskSchedule.json" + }, + { + "if": { + "properties": { + "type": { + "const": "Hidden" + }, + "binding": { + "properties": { + "type": { + "not": { + "const": "zeebe:userTask" + } + } + }, + "required": [ + "type" + ] + } + }, + "required": [ + "type" + ] + }, + "then": { + "anyOf": [ + { "required": [ "value" ] }, + { "required": [ "generatedValue" ] }, + { + "not": { + "anyOf": [ + { "required": [ "value" ] }, + { "required": [ "generatedValue" ] } + ] + }, + "isDeprecated": true + } + ] + } } ], "properties": { diff --git a/packages/zeebe-element-templates-json-schema/src/deprecated-warnings.json b/packages/zeebe-element-templates-json-schema/src/deprecated-warnings.json new file mode 100644 index 00000000..4743ec07 --- /dev/null +++ b/packages/zeebe-element-templates-json-schema/src/deprecated-warnings.json @@ -0,0 +1,17 @@ +[ + { + "path": [ + "definitions", + "properties", + "allOf", + 1, + "items", + "allOf", + 25, + "then", + "anyOf", + 2 + ], + "warningMessage": "Hidden property must specify either \"value\" or \"generatedValue\"" + } +] \ No newline at end of file diff --git a/packages/zeebe-element-templates-json-schema/test/fixtures/hidden-property.js b/packages/zeebe-element-templates-json-schema/test/fixtures/hidden-property.js new file mode 100644 index 00000000..fecbbc68 --- /dev/null +++ b/packages/zeebe-element-templates-json-schema/test/fixtures/hidden-property.js @@ -0,0 +1,37 @@ +export const template = { + 'name': 'HiddenProperty', + 'id': 'com.camunda.example.HiddenProperty', + 'appliesTo': [ + 'bpmn:Task' + ], + 'elementType': { + 'value': 'bpmn:BusinessRuleTask' + }, + 'properties': [ + { + 'type': 'Hidden', + 'value': 'decision', + 'binding': { + 'type': 'zeebe:calledDecision', + 'property': 'decisionId' + } + }, + { + 'type': 'Hidden', + 'binding': { + 'type': 'zeebe:calledDecision', + 'property': 'resultVariable' + } + } + ] +}; + +export const errors = null; + +export const warnings = [ + { + keyword: 'isDeprecated', + dataPath: '/properties/1', + message: 'Hidden property must specify either "value" or "generatedValue"' + } +]; \ No newline at end of file diff --git a/packages/zeebe-element-templates-json-schema/test/fixtures/hidden-zeebe-user-task.js b/packages/zeebe-element-templates-json-schema/test/fixtures/hidden-zeebe-user-task.js new file mode 100644 index 00000000..f0758e21 --- /dev/null +++ b/packages/zeebe-element-templates-json-schema/test/fixtures/hidden-zeebe-user-task.js @@ -0,0 +1,22 @@ +export const template = { + 'name': 'Zeebe User Task Hidden Property', + 'id': 'com.camunda.example.ZeebeUserTaskHiddenProperty', + 'description': 'A template to define a value less hidden property for a Zeebe user task.', + 'version': 1, + 'appliesTo': [ + 'bpmn:Task' + ], + 'elementType': { + 'value': 'bpmn:UserTask' + }, + 'properties': [ + { + 'type': 'Hidden', + 'binding': { + 'type': 'zeebe:userTask', + } + } + ] +}; + +export const errors = null; \ No newline at end of file diff --git a/packages/zeebe-element-templates-json-schema/test/spec/validationSpec.js b/packages/zeebe-element-templates-json-schema/test/spec/validationSpec.js index cf01da4a..56254c29 100644 --- a/packages/zeebe-element-templates-json-schema/test/spec/validationSpec.js +++ b/packages/zeebe-element-templates-json-schema/test/spec/validationSpec.js @@ -5,12 +5,13 @@ const util = require('util'); const schema = require('../../resources/schema.json'); const errorMessages = require('../../resources/error-messages.json'); +const deprecatedWarnings = require('../../resources/deprecated-warnings.json'); const { createValidator } = require('../../../element-templates-json-schema-shared/test/helpers'); -const validator = createValidator(schema, errorMessages); +const validator = createValidator(schema, errorMessages, deprecatedWarnings); // we save this for some other shinanigans const iit = it; @@ -20,10 +21,12 @@ function validateTemplate(template) { const valid = validator(template); const errors = validator.errors; + const warnings = validator.warnings; return { valid, - errors + errors, + warnings }; } @@ -40,16 +43,27 @@ function createTest(name, file, it) { const { errors: expectedErrors, - template + template, + warnings: expectedWarnings } = testDefinition; // when const { - errors + errors, + warnings } = validateTemplate(template); // then expect(errors).to.eqlErrors(expectedErrors); + + // less strict check for warnings + if (expectedWarnings) { + expect(warnings).to.eql(expectedWarnings); + } else if (warnings && warnings.length > 0) { + + // log warnings without failing the test + warnings.forEach(x => console.warn('Deprecation warning:', x.message)); + } }); } @@ -199,6 +213,12 @@ describe('validation', function() { it('element-type-invalid'); + it('hidden-property'); + + + it('hidden-zeebe-user-task'); + + describe('element type - event definition', function() { it('element-type-event-definition'); diff --git a/tasks/generate-deprecated-warnings.js b/tasks/generate-deprecated-warnings.js new file mode 100644 index 00000000..32eacf76 --- /dev/null +++ b/tasks/generate-deprecated-warnings.js @@ -0,0 +1,49 @@ +const readFile = require('fs').readFileSync, + writeFile = require('fs').writeFileSync, + mkdir = require('fs').mkdirSync; + +const pathJoin = require('path').join, + dirname = require('path').dirname; + +const mri = require('mri'); + +const argv = process.argv.slice(2); + +async function bundleWarnings(warningMessages, path) { + return writeWarnings(warningMessages, path); +} + + +function writeWarnings(warningMessages, path) { + const filePath = pathJoin(path); + + try { + mkdir(dirname(filePath)); + } catch { + + // directory may already exist + } + + writeFile(filePath, JSON.stringify(warningMessages, 0, 2)); + + return filePath; +} + + +const { + input, + output +} = mri(argv, { + alias: { + i: 'input', + o: 'output' + } +}); + +if (!input || !output) { + console.error('Arguments missing.'); + console.error('Example: node tasks/generate-error-messages.js --input=./src/error-messages.json --output=./resources/error-messages.json'); + process.exit(1); +} + +bundleWarnings(JSON.parse(readFile(input)), output);