From 8e4bb63c6813ec6302f081fcd585ec28f34eecb2 Mon Sep 17 00:00:00 2001 From: Barry Piccinni Date: Wed, 16 Apr 2025 16:14:47 +0100 Subject: [PATCH 1/2] Update OpenAPI contract for POST questionaires endpoint --- openapi/openapi.json | 8 +++++++- .../json-schemas/api/_questionnaires/post/req/201.json | 3 +++ openapi/src/openapi-src.json | 3 ++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openapi/openapi.json b/openapi/openapi.json index 2e0c360d..9794a11c 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -132,6 +132,11 @@ "pattern": "^urn:uuid:" } } + }, + "templateVersion": { + "title": "semver", + "type": "string", + "pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$" } } } @@ -153,7 +158,8 @@ }, "external": { "id": "urn:uuid:f81d4fae-7dec-11d0-a765-123456781234" - } + }, + "templateVersion": "1.0.0" } } } diff --git a/openapi/src/json-schemas/api/_questionnaires/post/req/201.json b/openapi/src/json-schemas/api/_questionnaires/post/req/201.json index 37d19a14..09b43312 100644 --- a/openapi/src/json-schemas/api/_questionnaires/post/req/201.json +++ b/openapi/src/json-schemas/api/_questionnaires/post/req/201.json @@ -53,6 +53,9 @@ "$ref": "../../../../models/definitions/external-id.json" } } + }, + "templateVersion": { + "$ref": "../../../../models/definitions/semver.json" } } } diff --git a/openapi/src/openapi-src.json b/openapi/src/openapi-src.json index 057863e8..cdbab3a6 100644 --- a/openapi/src/openapi-src.json +++ b/openapi/src/openapi-src.json @@ -76,7 +76,8 @@ }, "external": { "id": "urn:uuid:f81d4fae-7dec-11d0-a765-123456781234" - } + }, + "templateVersion": "1.0.0" } } } From 2255c6383bdc6aed0b6eade4c1102493a4092d4b Mon Sep 17 00:00:00 2001 From: Barry Piccinni Date: Wed, 16 Apr 2025 17:14:51 +0100 Subject: [PATCH 2/2] Update templates.js to select a version --- package-lock.json | 24 ++++++++- package.json | 4 +- questionnaire/questionnaire-service.js | 21 ++++---- questionnaire/routes.js | 5 +- questionnaire/templates.js | 48 +++++++++++++---- questionnaire/templates.test.js | 73 ++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 26 deletions(-) create mode 100644 questionnaire/templates.test.js diff --git a/package-lock.json b/package-lock.json index 87f282ec..a5c373eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,9 @@ "pino-std-serializers": "^6.2.2", "q-expressions": "github:CriminalInjuriesCompensationAuthority/q-expressions", "q-router": "github:CriminalInjuriesCompensationAuthority/q-router#v3.1.1", - "q-templates-application": "github:CriminalInjuriesCompensationAuthority/q-templates-application#v12.3.6", + "q-templates-application": "github:CriminalInjuriesCompensationAuthority/q-templates-application", + "q-templates-application-v11": "github:CriminalInjuriesCompensationAuthority/q-templates-application#v11.0.0", + "q-templates-application-v12": "github:CriminalInjuriesCompensationAuthority/q-templates-application#v12.0.0", "semver": "^7.5.4", "swagger-ui-express": "^4.6.3", "uuid": "^3.3.2", @@ -10338,6 +10340,26 @@ "npm": ">=8.5.2" } }, + "node_modules/q-templates-application-v11": { + "name": "q-templates-application", + "version": "11.0.0", + "resolved": "git+ssh://git@github.com/CriminalInjuriesCompensationAuthority/q-templates-application.git#32e4d7b8833e0dc060a60cfa7ad9e59fe554c460", + "license": "MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=8.5.2" + } + }, + "node_modules/q-templates-application-v12": { + "name": "q-templates-application", + "version": "12.0.0", + "resolved": "git+ssh://git@github.com/CriminalInjuriesCompensationAuthority/q-templates-application.git#bf0bf168388194d717b880828fdddc3ebb2abb2c", + "license": "MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=8.5.2" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", diff --git a/package.json b/package.json index 5e9b6a45..2ceb7666 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,9 @@ "pino-std-serializers": "^6.2.2", "q-expressions": "github:CriminalInjuriesCompensationAuthority/q-expressions", "q-router": "github:CriminalInjuriesCompensationAuthority/q-router#v3.1.1", - "q-templates-application": "github:CriminalInjuriesCompensationAuthority/q-templates-application#v12.3.6", + "q-templates-application": "github:CriminalInjuriesCompensationAuthority/q-templates-application", + "q-templates-application-v12": "github:CriminalInjuriesCompensationAuthority/q-templates-application#v12.0.0", + "q-templates-application-v11": "github:CriminalInjuriesCompensationAuthority/q-templates-application#v11.0.0", "semver": "^7.5.4", "swagger-ui-express": "^4.6.3", "uuid": "^3.3.2", diff --git a/questionnaire/questionnaire-service.js b/questionnaire/questionnaire-service.js index b668d538..d45d805e 100644 --- a/questionnaire/questionnaire-service.js +++ b/questionnaire/questionnaire-service.js @@ -8,7 +8,7 @@ const VError = require('verror'); const router = require('q-router'); const uuidv4 = require('uuid/v4'); const ajvFormatsMobileUk = require('ajv-formats-mobile-uk'); -const templates = require('./templates'); +const getTemplate = require('./templates'); const questionnaireResource = require('./resources/questionnaire-resource'); const createQuestionnaireHelper = require('./questionnaire/questionnaire'); const isQuestionnaireCompatible = require('./utils/isQuestionnaireVersionCompatible'); @@ -62,18 +62,15 @@ function createQuestionnaireService({ return false; } - async function createQuestionnaire(templateName, ownerData, originData, externalData) { - if (!(templateName in templates)) { - throw new VError( - { - name: 'ResourceNotFound' - }, - `Template "${templateName}" does not exist` - ); - } - + async function createQuestionnaire( + templateName, + ownerData, + originData, + externalData, + templateVersion + ) { const uuidV4 = uuidv4(); - const questionnaire = templates[templateName](uuidV4); + const questionnaire = getTemplate(templateName, templateVersion)(uuidV4); if (!ownerData) { throw new VError( diff --git a/questionnaire/routes.js b/questionnaire/routes.js index 5a29fc0f..cdd2744e 100644 --- a/questionnaire/routes.js +++ b/questionnaire/routes.js @@ -28,7 +28,7 @@ router.route('/').post(permissions('create:questionnaires'), async (req, res, ne throw err; } - const {templateName, owner, origin, external} = req.body.data.attributes; + const {templateName, owner, origin, external, templateVersion} = req.body.data.attributes; const questionnaireService = createQuestionnaireService({ logger: req.log, @@ -39,7 +39,8 @@ router.route('/').post(permissions('create:questionnaires'), async (req, res, ne templateName, owner, origin, - external + external, + templateVersion ); res.status(201).json(response); diff --git a/questionnaire/templates.js b/questionnaire/templates.js index 2ae48f86..1365b8aa 100644 --- a/questionnaire/templates.js +++ b/questionnaire/templates.js @@ -1,16 +1,44 @@ +/* eslint-disable global-require */ + 'use strict'; -const applicationTemplate = require('q-templates-application'); +const VError = require('verror'); + +const registry = { + 'sexual-assault': { + latest: require('q-templates-application'), + '12.0.0': require('q-templates-application-v12'), + '11.0.0': require('q-templates-application-v11') + } +}; -const applicationTemplateAsJson = JSON.stringify(applicationTemplate); +function getTemplate(templateName, version = 'latest') { + const versions = registry[templateName]; + if (!versions) { + throw new VError( + { + name: 'ResourceNotFound' + }, + `Template "${templateName}" does not exist` + ); + } -function getApplicationTemplateCopy() { - return JSON.parse(applicationTemplateAsJson); -} + const template = versions[version]; + if (!template) { + throw new VError( + { + name: 'ResourceNotFound' + }, + `Version "${version}" not found for template "${templateName}"` + ); + } -module.exports = { - 'sexual-assault': id => ({ + const applicationTemplateAsJson = JSON.stringify(template); + + return id => ({ id, - ...getApplicationTemplateCopy() - }) -}; + ...JSON.parse(applicationTemplateAsJson) // deep copy + }); +} + +module.exports = getTemplate; diff --git a/questionnaire/templates.test.js b/questionnaire/templates.test.js new file mode 100644 index 00000000..7e046d20 --- /dev/null +++ b/questionnaire/templates.test.js @@ -0,0 +1,73 @@ +/* eslint-disable global-require */ + +'use strict'; + +const fakeUuid = 'some-id'; +let getTemplates; + +beforeEach(() => { + jest.resetModules(); + + jest.doMock('q-templates-application', () => ({ + version: '12.3.3' + })); + + jest.doMock('q-templates-application-v12', () => ({ + version: '12.0.0' + })); + + getTemplates = require('./templates'); +}); + +describe('Templates', () => { + it('Should select a template', () => { + const templateName = 'sexual-assault'; + const templateVersion = 'latest'; + const questionnaire = getTemplates(templateName, templateVersion)(fakeUuid); + + expect(questionnaire).toMatchObject({ + id: 'some-id', + version: '12.3.3' + }); + }); + + it('Should select a template with the desired version', () => { + const templateName = 'sexual-assault'; + const templateVersion = '12.0.0'; + const questionnaire = getTemplates(templateName, templateVersion)(fakeUuid); + + expect(questionnaire).toMatchObject({ + id: 'some-id', + version: '12.0.0' + }); + }); + + it('Should select the latest template if no version is specified', () => { + const templateName = 'sexual-assault'; + const templateVersion = undefined; + const questionnaire = getTemplates(templateName, templateVersion)(fakeUuid); + + expect(questionnaire).toMatchObject({ + id: 'some-id', + version: '12.3.3' + }); + }); + + it('Should error if the templateName is not available', () => { + const templateName = 'not-a-template'; + const templateVersion = undefined; + + expect(() => { + getTemplates(templateName, templateVersion)(fakeUuid); + }).toThrow('Template "not-a-template" does not exist'); + }); + + it('Should error if the templateVersion is not supported', () => { + const templateName = 'sexual-assault'; + const templateVersion = '20.0.0'; + + expect(() => { + getTemplates(templateName, templateVersion)(fakeUuid); + }).toThrow('Version "20.0.0" not found for template "sexual-assault"'); + }); +});