Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,6 @@ typings/
.vscode

.config

# ignore any cached templates
/questionnaire/template-versions
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ RUN groupadd -g 1014 dc_user \
&& useradd -rm -d /usr/src/app -u 1015 -g dc_user dc_user
USER dc_user

# Install git (needed for npm to install from GitHub)
RUN apt-get update && \
apt-get install -y git && \
rm -rf /var/lib/apt/lists/*

# Essentially running mkdir <name> inside the current working
# directory, and then cd <name>
WORKDIR /usr/src/app
RUN mkdir -p /usr/src/app/questionnaire/template-versions \
&& chown -R dc_user:dc_user /usr/src/app/questionnaire

#no chnage made
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
Expand Down Expand Up @@ -55,6 +63,8 @@ USER dc_user
# Essentially running mkdir <name> inside the current working
# directory, and then cd <name>
WORKDIR /usr/src/app
RUN mkdir -p /usr/src/app/questionnaire/template-versions \
&& chown -R dc_user:dc_user /usr/src/app/questionnaire
#no chnage made
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
Expand Down
5 changes: 3 additions & 2 deletions nodemon.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
".sass-cache",
"bower_components",
"coverage",
"./node_modules/!(q-templates-application)/dist/template.json"
"./node_modules/!(q-templates-application)/dist/template.json",
"./questionnaire/template-versions"
],
"watch": [
"node_modules/q-templates-application/dist/template.json",
Expand All @@ -14,7 +15,7 @@
"middleware",
"openapi",
"public",
"questionnaire",
"questionnaire/!(template-versions)",
"services",
"app.js"
]
Expand Down
8 changes: 7 additions & 1 deletion openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}$"
}
}
}
Expand All @@ -153,7 +158,8 @@
},
"external": {
"id": "urn:uuid:f81d4fae-7dec-11d0-a765-123456781234"
}
},
"templateVersion": "1.0.0"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
"$ref": "../../../../models/definitions/external-id.json"
}
}
},
"templateVersion": {
"$ref": "../../../../models/definitions/semver.json"
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion openapi/src/openapi-src.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
},
"external": {
"id": "urn:uuid:f81d4fae-7dec-11d0-a765-123456781234"
}
},
"templateVersion": "1.0.0"
}
}
}
Expand Down
22 changes: 10 additions & 12 deletions questionnaire/questionnaire-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -62,18 +62,16 @@ 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 initiateQuestionnaire = getTemplate(templateName, templateVersion);
const questionnaire = initiateQuestionnaire(uuidV4);

if (!ownerData) {
throw new VError(
Expand Down
22 changes: 14 additions & 8 deletions questionnaire/questionnaire-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,20 @@ jest.doMock('./utils/isQuestionnaireVersionCompatible', () => questionnaireVersi
return questionnaireVersion !== incompatibleQuestionnaireFixture.version;
});

jest.doMock('./templates', () => {
const getTemplateMock = jest.fn(templateName => {
return id => ({
id,
templateName,
routes: {
type: 'mockTemplate'
}
});
});

return getTemplateMock;
});

const mockDalService = require('./questionnaire-dal')();

const createQuestionnaireService = require('./questionnaire-service');
Expand Down Expand Up @@ -245,14 +259,6 @@ describe('Questionnaire Service', () => {
});
});

it('Should error if templateName not found', async () => {
const templatename = 'not-a-template';

await expect(
questionnaireService.createQuestionnaire(templatename, ownerData)
).rejects.toThrow('Template "not-a-template" does not exist');
});

it('Should set owner data in the answers', async () => {
await questionnaireService.createQuestionnaire(templatename, ownerData);

Expand Down
5 changes: 3 additions & 2 deletions questionnaire/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down
75 changes: 65 additions & 10 deletions questionnaire/templates.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,71 @@
/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */

'use strict';

const applicationTemplate = require('q-templates-application');
const path = require('path');
const fs = require('fs');
const {execSync} = require('child_process');
const {createRequire} = require('module');
const VError = require('verror');

const templatesDir = path.resolve(__dirname, 'template-versions');
const registry = require('../supported-templates');

const moduleCache = {};

function getTemplate(templateName, version) {
const templateConfig = registry[templateName];

if (!templateConfig) {
throw new VError({name: 'ResourceNotFound'}, `Template "${templateName}" does not exist`);
}

const {moduleName, moduleUrl, supportedVersions} = templateConfig;
const cacheKey = `${templateName}@${version || 'latest'}`;

if (!moduleCache[cacheKey]) {
if (!version) {
moduleCache[cacheKey] = require(moduleName);
} else {
if (supportedVersions && !supportedVersions.includes(version)) {
throw new VError(
{name: 'ResourceNotFound'},
`Version "${version}" not supported for template "${templateName}"`
);
}

const installDir = path.join(templatesDir, templateName, `v${version}`);
const modulePath = path.join(installDir, 'node_modules', moduleName);

if (!fs.existsSync(modulePath)) {
console.log(`Installing ${moduleName}#${version} for ${templateName}...`);
fs.mkdirSync(installDir, {recursive: true});

const applicationTemplateAsJson = JSON.stringify(applicationTemplate);
if (!fs.existsSync(path.join(installDir, 'package.json'))) {
execSync('npm init -y', {
cwd: installDir,
stdio: 'inherit'
});
}
execSync(`npm install ${moduleUrl}#v${version} --no-save`, {
cwd: installDir,
stdio: 'inherit'
});
}
const requireFrom = createRequire(installDir);
moduleCache[cacheKey] = requireFrom(moduleName);
}
}

function getApplicationTemplateCopy() {
return JSON.parse(applicationTemplateAsJson);
const loadedModule = moduleCache[cacheKey];
return id => {
const templateInstance = JSON.parse(JSON.stringify(loadedModule));
return {
id,
...templateInstance
};
};
}

module.exports = {
'sexual-assault': id => ({
id,
...getApplicationTemplateCopy()
})
};
module.exports = getTemplate;
72 changes: 72 additions & 0 deletions questionnaire/templates.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

const fs = require('fs');
const {execSync} = require('child_process');

jest.mock('fs');
jest.mock('child_process');
jest.mock('module', () => {
return {
createRequire: jest.fn(() => {
return () => ({
type: 'loaded-requested'
});
})
};
});
jest.mock('q-templates-application', () => ({
type: 'loaded-latest'
}));

fs.existsSync.mockImplementation(() => true);
fs.mkdirSync.mockImplementation(() => {});
execSync.mockImplementation(() => {});

jest.doMock('../supported-templates.js', () => ({
'sexual-assault': {
moduleName: 'q-templates-application',
moduleUrl: 'github:CriminalInjuriesCompensationAuthority/q-templates-application',
supportedVersions: ['12.3.9', '12.0.0', '11.0.0']
}
}));

const getTemplate = require('./templates');

const testId = '123ImAnId';

beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});

describe('Templates.js', () => {
it('should use installed template when no version is specified', () => {
const questionnaire = getTemplate('sexual-assault')(testId);

expect(questionnaire).toMatchObject({
id: '123ImAnId',
type: 'loaded-latest'
});
});

it('should dynamically load a specific version', () => {
const questionnaire = getTemplate('sexual-assault', '12.0.0')(testId);

expect(questionnaire).toMatchObject({
id: '123ImAnId',
type: 'loaded-requested'
});
});

it('should throw if template name is unknown', () => {
expect(() => {
getTemplate('non-existent-template')(testId);
}).toThrow('Template "non-existent-template" does not exist');
});

it('should throw if version is unsupported', () => {
expect(() => {
getTemplate('sexual-assault', '99.9.9')(testId);
}).toThrow('Version "99.9.9" not supported for template "sexual-assault"');
});
});
9 changes: 9 additions & 0 deletions supported-templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

module.exports = {
'sexual-assault': {
moduleName: 'q-templates-application',
moduleUrl: 'github:CriminalInjuriesCompensationAuthority/q-templates-application',
supportedVersions: ['12.3.9', '12.0.0', '11.0.0']
}
};