Skip to content

Commit f65b032

Browse files
authored
feat: FORMS-1042 new document templates backend (#1317)
1 parent 871c7b3 commit f65b032

15 files changed

Lines changed: 969 additions & 17 deletions

File tree

app/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
clearMocks: true,
33
collectCoverage: true,
4-
collectCoverageFrom: ['src/**/*.js', '!src/db/migrations/*.js', '!src/db/seeds/*.js', '!frontend/**/*.*'],
4+
collectCoverageFrom: ['src/**/*.js', '!src/db/migrations/*.js', '!src/db/seeds/*.js', '!src/forms/common/models/(tables|views)/*.js', '!frontend/**/*.*'],
55
moduleFileExtensions: ['js', 'json'],
66
moduleNameMapper: {
77
'^~/(.*)$': '<rootDir>/src/$1',
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
const uuid = require('uuid');
2+
3+
const stamps = require('../stamps');
4+
const { Permissions, Roles } = require('../../forms/common/constants');
5+
6+
const CREATED_BY = 'migration-044';
7+
8+
/**
9+
* @param { import("knex").Knex } knex
10+
* @returns { Promise<void> }
11+
*/
12+
exports.up = function (knex) {
13+
return Promise.resolve().then(() =>
14+
knex.schema
15+
.createTable('document_template', (table) => {
16+
table.uuid('id').primary();
17+
table.uuid('formId').references('id').inTable('form').notNullable().index();
18+
table.boolean('active').defaultTo(true);
19+
table.string('filename', 1024).notNullable();
20+
table.binary('template').notNullable();
21+
stamps(knex, table);
22+
})
23+
24+
.then(() =>
25+
knex.schema.alterTable('form', (table) => {
26+
table.boolean('enableDocumentTemplates').defaultTo(false).comment('Allow document templates to be stored');
27+
})
28+
)
29+
30+
.then(() => {
31+
const permission = {
32+
createdBy: CREATED_BY,
33+
code: Permissions.DOCUMENT_TEMPLATE_CREATE,
34+
display: 'Document Template Create',
35+
description: 'Can create document templates for a form',
36+
active: true,
37+
};
38+
return knex('permission').insert(permission);
39+
})
40+
.then(() => {
41+
const permission = {
42+
createdBy: CREATED_BY,
43+
code: Permissions.DOCUMENT_TEMPLATE_DELETE,
44+
display: 'Document Template Delete',
45+
description: 'Can delete document templates for a form',
46+
active: true,
47+
};
48+
return knex('permission').insert(permission);
49+
})
50+
.then(() => {
51+
const permission = {
52+
createdBy: CREATED_BY,
53+
code: Permissions.DOCUMENT_TEMPLATE_READ,
54+
display: 'Document Template Read',
55+
description: 'Can view document templates for a form',
56+
active: true,
57+
};
58+
return knex('permission').insert(permission);
59+
})
60+
61+
.then(() => {
62+
const rolePermission = {
63+
id: uuid.v4(),
64+
createdBy: CREATED_BY,
65+
role: Roles.OWNER,
66+
permission: Permissions.DOCUMENT_TEMPLATE_CREATE,
67+
};
68+
return knex('role_permission').insert(rolePermission);
69+
})
70+
.then(() => {
71+
const rolePermission = {
72+
id: uuid.v4(),
73+
createdBy: CREATED_BY,
74+
role: Roles.OWNER,
75+
permission: Permissions.DOCUMENT_TEMPLATE_DELETE,
76+
};
77+
return knex('role_permission').insert(rolePermission);
78+
})
79+
.then(() => {
80+
const rolePermission = {
81+
id: uuid.v4(),
82+
createdBy: CREATED_BY,
83+
role: Roles.OWNER,
84+
permission: Permissions.DOCUMENT_TEMPLATE_READ,
85+
};
86+
return knex('role_permission').insert(rolePermission);
87+
})
88+
);
89+
};
90+
91+
/**
92+
* @param { import("knex").Knex } knex
93+
* @returns { Promise<void> }
94+
*/
95+
exports.down = function (knex) {
96+
return Promise.resolve()
97+
.then(() =>
98+
knex('role_permission')
99+
.where({
100+
createdBy: CREATED_BY,
101+
})
102+
.del()
103+
)
104+
105+
.then(() =>
106+
knex('permission')
107+
.where({
108+
createdBy: CREATED_BY,
109+
})
110+
.del()
111+
)
112+
113+
.then(() =>
114+
knex.schema.alterTable('form', (table) => {
115+
table.dropColumn('enableDocumentTemplates');
116+
})
117+
)
118+
119+
.then(() => knex.schema.dropTableIfExists('document_template'));
120+
};

app/src/forms/common/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ module.exports = Object.freeze({
1414
REMINDER_FORM_NOT_FILL: 'formNotFill',
1515
},
1616
Permissions: {
17+
DOCUMENT_TEMPLATE_CREATE: 'document_template_create',
18+
DOCUMENT_TEMPLATE_DELETE: 'document_template_delete',
19+
DOCUMENT_TEMPLATE_READ: 'document_template_read',
1720
EMAIL_TEMPLATE_READ: 'email_template_read',
1821
EMAIL_TEMPLATE_UPDATE: 'email_template_update',
1922
FORM_API_CREATE: 'form_api_create',

app/src/forms/common/middleware/validateParameter.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,32 @@ const _validateUuid = (parameter, parameterName) => {
1818
}
1919
};
2020

21+
/**
22+
* Validates that the :documentTemplateId route parameter exists and is a UUID.
23+
* This validator requires that the :formId route parameter also exists.
24+
*
25+
* @param {*} req the Express object representing the HTTP request
26+
* @param {*} _res the Express object representing the HTTP response - unused
27+
* @param {*} next the Express chaining function
28+
* @param {*} documentTemplateId the :documentTemplateId value from the route
29+
*/
30+
const validateDocumentTemplateId = async (req, _res, next, documentTemplateId) => {
31+
try {
32+
_validateUuid(documentTemplateId, 'documentTemplateId');
33+
34+
const documentTemplate = await formService.documentTemplateRead(documentTemplateId);
35+
if (!documentTemplate || documentTemplate.formId !== req.params.formId) {
36+
throw new Problem(404, {
37+
detail: 'documentTemplateId does not exist on this form',
38+
});
39+
}
40+
41+
next();
42+
} catch (error) {
43+
next(error);
44+
}
45+
};
46+
2147
/**
2248
* Validates that the :formId route parameter exists and is a UUID.
2349
*
@@ -89,6 +115,7 @@ const validateFormVersionId = async (req, _res, next, formVersionId) => {
89115
};
90116

91117
module.exports = {
118+
validateDocumentTemplateId,
92119
validateFormId,
93120
validateFormVersionId,
94121
validateFormVersionDraftId,

app/src/forms/common/models/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module.exports = {
22
// Tables
3+
DocumentTemplate: require('./tables/documentTemplate'),
34
FileStorage: require('./tables/fileStorage'),
45
Form: require('./tables/form'),
56
FormApiKey: require('./tables/formApiKey'),
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const { Model } = require('objection');
2+
const { Timestamps } = require('../mixins');
3+
const { Regex } = require('../../constants');
4+
const stamps = require('../jsonSchema').stamps;
5+
6+
class DocumentTemplate extends Timestamps(Model) {
7+
static get tableName() {
8+
return 'document_template';
9+
}
10+
11+
static get modifiers() {
12+
return {
13+
filterActive(query, value) {
14+
if (value) {
15+
query.where('active', value);
16+
}
17+
},
18+
filterFormId(query, value) {
19+
if (value) {
20+
query.where('formId', value);
21+
}
22+
},
23+
filterId(query, value) {
24+
if (value) {
25+
query.where('id', value);
26+
}
27+
},
28+
};
29+
}
30+
31+
static get jsonSchema() {
32+
return {
33+
type: 'object',
34+
required: ['filename', 'formId', 'template'],
35+
properties: {
36+
id: { type: 'string', pattern: Regex.UUID },
37+
formId: { type: 'string', pattern: Regex.UUID },
38+
active: { type: 'boolean' },
39+
filename: { type: 'string' },
40+
template: { type: 'string' },
41+
...stamps,
42+
},
43+
additionalProperties: false,
44+
};
45+
}
46+
}
47+
48+
module.exports = DocumentTemplate;

app/src/forms/common/models/tables/form.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class Form extends Timestamps(Model) {
119119
'active',
120120
'allowSubmitterToUploadFile',
121121
'showSubmissionConfirmation',
122+
'enableDocumentTemplates',
122123
'enableStatusUpdates',
123124
'schedule',
124125
'subscribe',
@@ -149,6 +150,7 @@ class Form extends Timestamps(Model) {
149150
sendSubmissionReceivedEmail: { type: 'boolean' },
150151
showSubmissionConfirmation: { type: 'boolean' },
151152
submissionReceivedEmails: { type: ['array', 'null'], items: { type: 'string', pattern: Regex.EMAIL } },
153+
enableDocumentTemplates: { type: 'boolean' },
152154
enableStatusUpdates: { type: 'boolean' },
153155
enableSubmitterDraft: { type: 'boolean' },
154156
schedule: { type: 'object' },

app/src/forms/form/controller.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,71 @@ const service = require('./service');
66
const fileService = require('../file/service');
77

88
module.exports = {
9+
/**
10+
* Creates a document template that can be used to generate a document from
11+
* a form's submission data.
12+
*
13+
* @param {Object} req the Express object representing the HTTP request
14+
* @param {Object} res the Express object representing the HTTP response
15+
* @param {Object} next the Express chaining function
16+
*/
17+
documentTemplateCreate: async (req, res, next) => {
18+
try {
19+
const response = await service.documentTemplateCreate(req.params.formId, req.body, req.currentUser.usernameIdp);
20+
res.status(201).json(response);
21+
} catch (error) {
22+
next(error);
23+
}
24+
},
25+
26+
/**
27+
* Deletes an active document template given its ID.
28+
*
29+
* @param {Object} req the Express object representing the HTTP request
30+
* @param {Object} res the Express object representing the HTTP response
31+
* @param {Object} next the Express chaining function
32+
*/
33+
documentTemplateDelete: async (req, res, next) => {
34+
try {
35+
await service.documentTemplateDelete(req.params.documentTemplateId, req.currentUser.usernameIdp);
36+
res.status(204).send();
37+
} catch (error) {
38+
next(error);
39+
}
40+
},
41+
42+
/**
43+
* Gets the active document templates for a form.
44+
*
45+
* @param {Object} req the Express object representing the HTTP request
46+
* @param {Object} res the Express object representing the HTTP response
47+
* @param {Object} next the Express chaining function
48+
*/
49+
documentTemplateList: async (req, res, next) => {
50+
try {
51+
const response = await service.documentTemplateList(req.params.formId);
52+
res.status(200).json(response);
53+
} catch (error) {
54+
next(error);
55+
}
56+
},
57+
58+
/**
59+
* Reads an active document template given its ID.
60+
*
61+
* @param {Object} req the Express object representing the HTTP request
62+
* @param {Object} res the Express object representing the HTTP response
63+
* @param {Object} next the Express chaining function
64+
*/
65+
documentTemplateRead: async (req, res, next) => {
66+
try {
67+
const response = await service.documentTemplateRead(req.params.documentTemplateId);
68+
res.status(200).json(response);
69+
} catch (error) {
70+
next(error);
71+
}
72+
},
73+
974
export: async (req, res, next) => {
1075
try {
1176
const result = await exportService.export(req.params.formId, req.query, req.currentUser, req.headers.referer);

app/src/forms/form/routes.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const controller = require('./controller');
1111

1212
routes.use(currentUser);
1313

14+
routes.param('documentTemplateId', validateParameter.validateDocumentTemplateId);
1415
routes.param('formId', validateParameter.validateFormId);
1516
routes.param('formVersionDraftId', validateParameter.validateFormVersionDraftId);
1617
routes.param('formVersionId', validateParameter.validateFormVersionId);
@@ -27,6 +28,22 @@ routes.get('/:formId', rateLimiter, apiAccess, hasFormPermissions([P.FORM_READ])
2728
await controller.readForm(req, res, next);
2829
});
2930

31+
routes.get('/:formId/documentTemplates', rateLimiter, apiAccess, hasFormPermissions([P.DOCUMENT_TEMPLATE_READ]), async (req, res, next) => {
32+
await controller.documentTemplateList(req, res, next);
33+
});
34+
35+
routes.post('/:formId/documentTemplates', rateLimiter, apiAccess, hasFormPermissions([P.DOCUMENT_TEMPLATE_CREATE]), async (req, res, next) => {
36+
await controller.documentTemplateCreate(req, res, next);
37+
});
38+
39+
routes.delete('/:formId/documentTemplates/:documentTemplateId', rateLimiter, apiAccess, hasFormPermissions([P.DOCUMENT_TEMPLATE_DELETE]), async (req, res, next) => {
40+
await controller.documentTemplateDelete(req, res, next);
41+
});
42+
43+
routes.get('/:formId/documentTemplates/:documentTemplateId', rateLimiter, apiAccess, hasFormPermissions([P.DOCUMENT_TEMPLATE_READ]), async (req, res, next) => {
44+
await controller.documentTemplateRead(req, res, next);
45+
});
46+
3047
routes.get('/:formId/export', rateLimiter, apiAccess, hasFormPermissions([P.FORM_READ, P.SUBMISSION_READ]), async (req, res, next) => {
3148
await controller.export(req, res, next);
3249
});

0 commit comments

Comments
 (0)