Skip to content

Commit

Permalink
feat: FORMS-1042 new document templates backend (bcgov#1317)
Browse files Browse the repository at this point in the history
  • Loading branch information
WalterMoar authored Apr 8, 2024
1 parent 871c7b3 commit f65b032
Show file tree
Hide file tree
Showing 15 changed files with 969 additions and 17 deletions.
2 changes: 1 addition & 1 deletion app/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: ['src/**/*.js', '!src/db/migrations/*.js', '!src/db/seeds/*.js', '!frontend/**/*.*'],
collectCoverageFrom: ['src/**/*.js', '!src/db/migrations/*.js', '!src/db/seeds/*.js', '!src/forms/common/models/(tables|views)/*.js', '!frontend/**/*.*'],
moduleFileExtensions: ['js', 'json'],
moduleNameMapper: {
'^~/(.*)$': '<rootDir>/src/$1',
Expand Down
120 changes: 120 additions & 0 deletions app/src/db/migrations/20240403192833_044-document-templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const uuid = require('uuid');

const stamps = require('../stamps');
const { Permissions, Roles } = require('../../forms/common/constants');

const CREATED_BY = 'migration-044';

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return Promise.resolve().then(() =>
knex.schema
.createTable('document_template', (table) => {
table.uuid('id').primary();
table.uuid('formId').references('id').inTable('form').notNullable().index();
table.boolean('active').defaultTo(true);
table.string('filename', 1024).notNullable();
table.binary('template').notNullable();
stamps(knex, table);
})

.then(() =>
knex.schema.alterTable('form', (table) => {
table.boolean('enableDocumentTemplates').defaultTo(false).comment('Allow document templates to be stored');
})
)

.then(() => {
const permission = {
createdBy: CREATED_BY,
code: Permissions.DOCUMENT_TEMPLATE_CREATE,
display: 'Document Template Create',
description: 'Can create document templates for a form',
active: true,
};
return knex('permission').insert(permission);
})
.then(() => {
const permission = {
createdBy: CREATED_BY,
code: Permissions.DOCUMENT_TEMPLATE_DELETE,
display: 'Document Template Delete',
description: 'Can delete document templates for a form',
active: true,
};
return knex('permission').insert(permission);
})
.then(() => {
const permission = {
createdBy: CREATED_BY,
code: Permissions.DOCUMENT_TEMPLATE_READ,
display: 'Document Template Read',
description: 'Can view document templates for a form',
active: true,
};
return knex('permission').insert(permission);
})

.then(() => {
const rolePermission = {
id: uuid.v4(),
createdBy: CREATED_BY,
role: Roles.OWNER,
permission: Permissions.DOCUMENT_TEMPLATE_CREATE,
};
return knex('role_permission').insert(rolePermission);
})
.then(() => {
const rolePermission = {
id: uuid.v4(),
createdBy: CREATED_BY,
role: Roles.OWNER,
permission: Permissions.DOCUMENT_TEMPLATE_DELETE,
};
return knex('role_permission').insert(rolePermission);
})
.then(() => {
const rolePermission = {
id: uuid.v4(),
createdBy: CREATED_BY,
role: Roles.OWNER,
permission: Permissions.DOCUMENT_TEMPLATE_READ,
};
return knex('role_permission').insert(rolePermission);
})
);
};

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return Promise.resolve()
.then(() =>
knex('role_permission')
.where({
createdBy: CREATED_BY,
})
.del()
)

.then(() =>
knex('permission')
.where({
createdBy: CREATED_BY,
})
.del()
)

.then(() =>
knex.schema.alterTable('form', (table) => {
table.dropColumn('enableDocumentTemplates');
})
)

.then(() => knex.schema.dropTableIfExists('document_template'));
};
3 changes: 3 additions & 0 deletions app/src/forms/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ module.exports = Object.freeze({
REMINDER_FORM_NOT_FILL: 'formNotFill',
},
Permissions: {
DOCUMENT_TEMPLATE_CREATE: 'document_template_create',
DOCUMENT_TEMPLATE_DELETE: 'document_template_delete',
DOCUMENT_TEMPLATE_READ: 'document_template_read',
EMAIL_TEMPLATE_READ: 'email_template_read',
EMAIL_TEMPLATE_UPDATE: 'email_template_update',
FORM_API_CREATE: 'form_api_create',
Expand Down
27 changes: 27 additions & 0 deletions app/src/forms/common/middleware/validateParameter.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ const _validateUuid = (parameter, parameterName) => {
}
};

/**
* Validates that the :documentTemplateId route parameter exists and is a UUID.
* This validator requires that the :formId route parameter also exists.
*
* @param {*} req the Express object representing the HTTP request
* @param {*} _res the Express object representing the HTTP response - unused
* @param {*} next the Express chaining function
* @param {*} documentTemplateId the :documentTemplateId value from the route
*/
const validateDocumentTemplateId = async (req, _res, next, documentTemplateId) => {
try {
_validateUuid(documentTemplateId, 'documentTemplateId');

const documentTemplate = await formService.documentTemplateRead(documentTemplateId);
if (!documentTemplate || documentTemplate.formId !== req.params.formId) {
throw new Problem(404, {
detail: 'documentTemplateId does not exist on this form',
});
}

next();
} catch (error) {
next(error);
}
};

/**
* Validates that the :formId route parameter exists and is a UUID.
*
Expand Down Expand Up @@ -89,6 +115,7 @@ const validateFormVersionId = async (req, _res, next, formVersionId) => {
};

module.exports = {
validateDocumentTemplateId,
validateFormId,
validateFormVersionId,
validateFormVersionDraftId,
Expand Down
1 change: 1 addition & 0 deletions app/src/forms/common/models/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
// Tables
DocumentTemplate: require('./tables/documentTemplate'),
FileStorage: require('./tables/fileStorage'),
Form: require('./tables/form'),
FormApiKey: require('./tables/formApiKey'),
Expand Down
48 changes: 48 additions & 0 deletions app/src/forms/common/models/tables/documentTemplate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { Model } = require('objection');
const { Timestamps } = require('../mixins');
const { Regex } = require('../../constants');
const stamps = require('../jsonSchema').stamps;

class DocumentTemplate extends Timestamps(Model) {
static get tableName() {
return 'document_template';
}

static get modifiers() {
return {
filterActive(query, value) {
if (value) {
query.where('active', value);
}
},
filterFormId(query, value) {
if (value) {
query.where('formId', value);
}
},
filterId(query, value) {
if (value) {
query.where('id', value);
}
},
};
}

static get jsonSchema() {
return {
type: 'object',
required: ['filename', 'formId', 'template'],
properties: {
id: { type: 'string', pattern: Regex.UUID },
formId: { type: 'string', pattern: Regex.UUID },
active: { type: 'boolean' },
filename: { type: 'string' },
template: { type: 'string' },
...stamps,
},
additionalProperties: false,
};
}
}

module.exports = DocumentTemplate;
2 changes: 2 additions & 0 deletions app/src/forms/common/models/tables/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Form extends Timestamps(Model) {
'active',
'allowSubmitterToUploadFile',
'showSubmissionConfirmation',
'enableDocumentTemplates',
'enableStatusUpdates',
'schedule',
'subscribe',
Expand Down Expand Up @@ -149,6 +150,7 @@ class Form extends Timestamps(Model) {
sendSubmissionReceivedEmail: { type: 'boolean' },
showSubmissionConfirmation: { type: 'boolean' },
submissionReceivedEmails: { type: ['array', 'null'], items: { type: 'string', pattern: Regex.EMAIL } },
enableDocumentTemplates: { type: 'boolean' },
enableStatusUpdates: { type: 'boolean' },
enableSubmitterDraft: { type: 'boolean' },
schedule: { type: 'object' },
Expand Down
65 changes: 65 additions & 0 deletions app/src/forms/form/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,71 @@ const service = require('./service');
const fileService = require('../file/service');

module.exports = {
/**
* Creates a document template that can be used to generate a document from
* a form's submission data.
*
* @param {Object} req the Express object representing the HTTP request
* @param {Object} res the Express object representing the HTTP response
* @param {Object} next the Express chaining function
*/
documentTemplateCreate: async (req, res, next) => {
try {
const response = await service.documentTemplateCreate(req.params.formId, req.body, req.currentUser.usernameIdp);
res.status(201).json(response);
} catch (error) {
next(error);
}
},

/**
* Deletes an active document template given its ID.
*
* @param {Object} req the Express object representing the HTTP request
* @param {Object} res the Express object representing the HTTP response
* @param {Object} next the Express chaining function
*/
documentTemplateDelete: async (req, res, next) => {
try {
await service.documentTemplateDelete(req.params.documentTemplateId, req.currentUser.usernameIdp);
res.status(204).send();
} catch (error) {
next(error);
}
},

/**
* Gets the active document templates for a form.
*
* @param {Object} req the Express object representing the HTTP request
* @param {Object} res the Express object representing the HTTP response
* @param {Object} next the Express chaining function
*/
documentTemplateList: async (req, res, next) => {
try {
const response = await service.documentTemplateList(req.params.formId);
res.status(200).json(response);
} catch (error) {
next(error);
}
},

/**
* Reads an active document template given its ID.
*
* @param {Object} req the Express object representing the HTTP request
* @param {Object} res the Express object representing the HTTP response
* @param {Object} next the Express chaining function
*/
documentTemplateRead: async (req, res, next) => {
try {
const response = await service.documentTemplateRead(req.params.documentTemplateId);
res.status(200).json(response);
} catch (error) {
next(error);
}
},

export: async (req, res, next) => {
try {
const result = await exportService.export(req.params.formId, req.query, req.currentUser, req.headers.referer);
Expand Down
17 changes: 17 additions & 0 deletions app/src/forms/form/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const controller = require('./controller');

routes.use(currentUser);

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

routes.get('/:formId/documentTemplates', rateLimiter, apiAccess, hasFormPermissions([P.DOCUMENT_TEMPLATE_READ]), async (req, res, next) => {
await controller.documentTemplateList(req, res, next);
});

routes.post('/:formId/documentTemplates', rateLimiter, apiAccess, hasFormPermissions([P.DOCUMENT_TEMPLATE_CREATE]), async (req, res, next) => {
await controller.documentTemplateCreate(req, res, next);
});

routes.delete('/:formId/documentTemplates/:documentTemplateId', rateLimiter, apiAccess, hasFormPermissions([P.DOCUMENT_TEMPLATE_DELETE]), async (req, res, next) => {
await controller.documentTemplateDelete(req, res, next);
});

routes.get('/:formId/documentTemplates/:documentTemplateId', rateLimiter, apiAccess, hasFormPermissions([P.DOCUMENT_TEMPLATE_READ]), async (req, res, next) => {
await controller.documentTemplateRead(req, res, next);
});

routes.get('/:formId/export', rateLimiter, apiAccess, hasFormPermissions([P.FORM_READ, P.SUBMISSION_READ]), async (req, res, next) => {
await controller.export(req, res, next);
});
Expand Down
Loading

0 comments on commit f65b032

Please sign in to comment.