Skip to content

Commit

Permalink
feat(form-service): submission delete endpoint
Browse files Browse the repository at this point in the history
Also, registering resource types for forms and submissions.
  • Loading branch information
tzuge committed Jan 28, 2025
1 parent b625d44 commit c9d507b
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 19 deletions.
2 changes: 2 additions & 0 deletions apps/configuration-service/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const initializeApp = async (): Promise<express.Application> => {
ignoreServiceAud: true,
values: [ServiceMetricsValueDefinition],
serviceConfigurations: [
// Register cache service target for configuration.
{
serviceId: adspId`urn:ads:platform:cache-service`,
configuration: {
Expand Down Expand Up @@ -116,6 +117,7 @@ const initializeApp = async (): Promise<express.Application> => {
},
},
},
// Register directory service resource type for configuration.
{
serviceId: adspId`urn:ads:platform:directory-service`,
configuration: {
Expand Down
7 changes: 7 additions & 0 deletions apps/form-service/src/form/events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
FormStatusSubmittedDefinition,
SubmissionDispositionedDefinition,
formSchema,
SubmissionDeletedDefinition,
} from './events';

describe('events', () => {
Expand Down Expand Up @@ -68,6 +69,12 @@ describe('events', () => {
service.validate('test', 'payload', SubmissionDispositionedDefinition.payloadSchema);
});

it('submission-deleted is valid json schema', () => {
const service = new AjvValidationService(logger as unknown as Logger);
service.setSchema('payload', { $ref: 'http://json-schema.org/draft-07/schema#' });
service.validate('test', 'payload', SubmissionDeletedDefinition.payloadSchema);
});

it('form definition schema is valid json schema', () => {
const service = new AjvValidationService(logger as unknown as Logger);
service.setSchema('payload', { $ref: 'http://json-schema.org/draft-07/schema#' });
Expand Down
45 changes: 42 additions & 3 deletions apps/form-service/src/form/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export const FORM_UNLOCKED = 'form-unlocked';
export const FORM_SET_TO_DRAFT = 'form-to-draft';
export const FORM_SUBMITTED = 'form-submitted';
export const FORM_ARCHIVED = 'form-archived';

export const SUBMISSION_DISPOSITIONED = 'submission-dispositioned';
export const SUBMISSION_DELETED = 'submission-deleted';

const userInfoSchema = {
type: 'object',
Expand Down Expand Up @@ -73,6 +75,7 @@ export const FormDeletedDefinition: DomainEventDefinition = {
payloadSchema: {
type: 'object',
properties: {
urn: { type: 'string' },
id: { type: 'string' },
deletedBy: userInfoSchema,
},
Expand Down Expand Up @@ -153,7 +156,7 @@ export const FormStatusArchivedDefinition: DomainEventDefinition = {

export const SubmissionDispositionedDefinition: DomainEventDefinition = {
name: SUBMISSION_DISPOSITIONED,
description: 'Signalled when a form submission is dispositioned',
description: 'Signalled when a form submission is dispositioned.',
payloadSchema: {
type: 'object',
properties: {
Expand Down Expand Up @@ -182,8 +185,21 @@ export const SubmissionDispositionedDefinition: DomainEventDefinition = {
},
};

export const SubmissionDeletedDefinition: DomainEventDefinition = {
name: SUBMISSION_DELETED,
description: 'Signalled when a form submission is deleted.',
payloadSchema: {
type: 'object',
properties: {
id: { type: 'string' },
urn: { type: 'string' },
deletedBy: userInfoSchema,
},
},
};

function getCorrelationId(form: ReturnType<typeof mapForm>) {
return form.urn;
return form?.urn;
}

export function formCreated(apiId: AdspId, user: User, form: FormEntity): DomainEvent {
Expand Down Expand Up @@ -217,6 +233,7 @@ export function formDeleted(apiId: AdspId, user: User, form: FormEntity): Domain
definitionId: form.definition.id,
},
payload: {
urn: formResponse.urn,
id: form.id,
deletedBy: {
id: user.id,
Expand Down Expand Up @@ -356,7 +373,7 @@ export function submissionDispositioned(apiId: AdspId, user: User, submission: F
payload: {
form: formResponse,
submission: {
urn: `${formResponse.urn}/submissions/${submission.id}`,
urn: `${apiId}:/submissions/${submission.id}`,
id: submission.id,
disposition: {
createdBy: {
Expand All @@ -371,6 +388,28 @@ export function submissionDispositioned(apiId: AdspId, user: User, submission: F
};
}

export function submissionDeleted(apiId: AdspId, user: User, submission: FormSubmissionEntity): DomainEvent {
const form = submission.form;
const formResponse = form ? mapForm(apiId, form) : null;
return {
name: SUBMISSION_DELETED,
timestamp: new Date(),
tenantId: submission.tenantId,
correlationId: getCorrelationId(formResponse),
context: {
definitionId: submission.definition?.id,
},
payload: {
urn: `${apiId}:/submissions/${submission.id}`,
id: submission.id,
deletedBy: {
id: user.id,
name: user.name,
},
},
};
}

export const SubmittedFormPdfUpdatesStream: Stream = {
id: 'submitted-form-pdf-updates',
name: 'Submitted form PDF generation updates',
Expand Down
6 changes: 3 additions & 3 deletions apps/form-service/src/form/mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function mapFormWithFormSubmission(

export function mapFormSubmission(apiId: AdspId, entity: FormSubmissionEntity) {
return {
urn: `${apiId}:/forms/${entity.formId}/submissions/${entity.id}`,
urn: `${apiId}:/submissions/${entity.id}`,
id: entity.id,
formId: entity.formId,
formDefinitionId: entity.formDefinitionId,
Expand All @@ -98,8 +98,8 @@ export function mapFormSubmission(apiId: AdspId, entity: FormSubmissionEntity) {
updatedBy: { id: entity.updatedBy.id, name: entity.updatedBy.name },
hash: entity.hash,
_links: {
self: { href: `${apiId}:/forms/${entity.formId}/submissions/${entity.id}` },
alternate: { href: `${apiId}:/submissions/${entity.id}` },
self: { href: `${apiId}:/submissions/${entity.id}` },
alternate: { href: `${apiId}:/forms/${entity.formId}/submissions/${entity.id}` },
form: { href: `${apiId}:/forms/${entity.formId}` },
collection: { href: `${apiId}:/submissions` },
},
Expand Down
9 changes: 7 additions & 2 deletions apps/form-service/src/form/model/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { NotificationService, Subscriber } from '../../notification';
import { QueueTaskService } from '../../task';
import { FormDefinitionEntity } from '../model';
import { FormRepository, FormSubmissionRepository } from '../repository';
import { FormServiceRoles } from '../roles';
import { DirectoryServiceRoles, FormServiceRoles } from '../roles';
import { Form, FormStatus, SecurityClassificationType } from '../types';
import { FormSubmissionEntity } from './formSubmission';
import { PdfService } from '../pdf';
Expand Down Expand Up @@ -102,7 +102,12 @@ export class FormEntity implements Form {
// Admins, intake apps, clerks and assessors are allowed read of the form.
// Applicants are allowed to read forms they created.
return (
isAllowedUser(user, this.tenantId, [FormServiceRoles.Admin, FormServiceRoles.IntakeApp], true) ||
isAllowedUser(
user,
this.tenantId,
[FormServiceRoles.Admin, FormServiceRoles.IntakeApp, DirectoryServiceRoles.ResourceResolver],
true
) ||
isAllowedUser(user, this.tenantId, [
...(this.definition?.clerkRoles || []),
...(this.definition?.assessorRoles || []),
Expand Down
4 changes: 2 additions & 2 deletions apps/form-service/src/form/model/formSubmission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AdspId, isAllowedUser, UnauthorizedUserError, User } from '@abgov/adsp-
import { InvalidValueError } from '@core-services/core-common';
import { v4 as uuidv4 } from 'uuid';
import { FormSubmissionRepository } from '../repository';
import { FormServiceRoles } from '../roles';
import { DirectoryServiceRoles, FormServiceRoles } from '../roles';
import { FormDisposition, FormSubmission, SecurityClassificationType } from '../types';
import { FormEntity } from './form';
import { FormDefinitionEntity } from './definition';
Expand Down Expand Up @@ -84,7 +84,7 @@ export class FormSubmissionEntity implements FormSubmission {

canRead(user: User): boolean {
return (
isAllowedUser(user, this.tenantId, FormServiceRoles.Admin, true) ||
isAllowedUser(user, this.tenantId, [FormServiceRoles.Admin, DirectoryServiceRoles.ResourceResolver], true) ||
isAllowedUser(user, this.tenantId, this.definition?.assessorRoles || [])
);
}
Expand Down
4 changes: 4 additions & 0 deletions apps/form-service/src/form/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export enum FormServiceRoles {
Tester = 'form-tester',
}

export enum DirectoryServiceRoles {
ResourceResolver = 'urn:ads:platform:directory-service:resource-resolver',
}

export enum ExportServiceRoles {
ExportJob = 'urn:ads:platform:export-service:export-job',
}
77 changes: 77 additions & 0 deletions apps/form-service/src/form/router/form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FormDefinitionEntity, FormEntity, FormSubmissionEntity } from '../model
import {
createForm,
createFormRouter,
deleteFormSubmission,
findFormSubmissions,
findSubmissions,
getFormDefinitions,
Expand Down Expand Up @@ -1677,6 +1678,82 @@ describe('form router', () => {
});
});

describe('deleteFormSubmission', () => {
const user = {
tenantId,
id: 'tester',
roles: [FormServiceRoles.Admin],
};

it('can create handler getFormSubmission', () => {
const handler = deleteFormSubmission(apiId, eventServiceMock, formSubmissionMock);
expect(handler).toBeTruthy();
});

it('can delete form submission by id', async () => {
const req = {
user,
tenant: {
id: tenantId,
},
body: { dispositionStatus: 'invalid status', dispositionReason: 'invalid data' },
params: { formId: 'test-form', submissionId: 'submissionId' },
};
const res = { send: jest.fn() };
const next = jest.fn();
formSubmissionMock.get.mockResolvedValueOnce(formSubmissionEntity);
formSubmissionMock.delete.mockResolvedValueOnce(true);

const handler = deleteFormSubmission(apiId, eventServiceMock, formSubmissionMock);
await handler(req as unknown as Request, res as unknown as Response, next);

expect(res.send).toHaveBeenCalledWith(expect.objectContaining({ deleted: true }));
});

it('can delete form submission by id and return false', async () => {
const req = {
user,
tenant: {
id: tenantId,
},
body: { dispositionStatus: 'invalid status', dispositionReason: 'invalid data' },
params: { formId: 'test-form', submissionId: 'submissionId' },
};
const res = { send: jest.fn() };
const next = jest.fn();
formSubmissionMock.get.mockResolvedValueOnce(null);

const handler = deleteFormSubmission(apiId, eventServiceMock, formSubmissionMock);
await handler(req as unknown as Request, res as unknown as Response, next);

expect(res.send).toHaveBeenCalledWith(expect.objectContaining({ deleted: false }));
});

it('can call next with unauthorized', async () => {
const user = {
tenantId,
id: 'tester',
roles: [],
};
const req = {
user,
tenant: {
id: tenantId,
},
body: { dispositionStatus: 'invalid status', dispositionReason: 'invalid data' },
params: { formId: 'test-form', submissionId: 'submissionId' },
};
const res = { send: jest.fn() };
const next = jest.fn();

const handler = deleteFormSubmission(apiId, eventServiceMock, formSubmissionMock);
await handler(req as unknown as Request, res as unknown as Response, next);

expect(res.send).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(expect.any(UnauthorizedUserError));
});
});

describe('findSubmissions', () => {
const user = {
tenantId,
Expand Down
14 changes: 14 additions & 0 deletions apps/form-service/src/form/router/form.swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -726,3 +726,17 @@ components:
application/json:
schema:
$ref: "#/components/schemas/FormSubmission"
delete:
tags:
- Form Submission
description: Deletes a form submission.
responses:
200:
description: Successfully deleted the form submission.
content:
application/json:
schema:
type: object
properties:
deleted:
type: boolean
51 changes: 43 additions & 8 deletions apps/form-service/src/form/router/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,14 @@ import {
formSetToDraft,
formSubmitted,
formUnlocked,
submissionDeleted,
submissionDispositioned,
} from '../events';
import { mapForm, mapFormDefinition, mapFormSubmission, mapFormWithFormSubmission } from '../mapper';
import { FormDefinitionEntity, FormEntity, FormSubmissionEntity } from '../model';
import { FormRepository, FormSubmissionRepository } from '../repository';
import { ExportServiceRoles, FormServiceRoles } from '../roles';
import {
Form,
FormCriteria,
FormDefinition,
FormStatus,
FormSubmissionCriteria,
Intake,
} from '../types';
import { Form, FormCriteria, FormDefinition, FormStatus, FormSubmissionCriteria, Intake } from '../types';
import {
ARCHIVE_FORM_OPERATION,
FormOperations,
Expand Down Expand Up @@ -445,6 +439,40 @@ export function getFormSubmission(apiId: AdspId, submissionRepository: FormSubmi
};
}

export function deleteFormSubmission(
apiId: AdspId,
eventService: EventService,
submissionRepository: FormSubmissionRepository
): RequestHandler {
return async (req, res, next) => {
try {
const end = startBenchmark(req, 'get-entity-time');
const { formId, submissionId } = req.params;
const user = req.user;
const tenant = req.tenant;

if (!isAllowedUser(user, tenant.id, FormServiceRoles.Admin, true)) {
throw new UnauthorizedUserError('delete form submission', user);
}

let deleted = false;
const formSubmission = await submissionRepository.get(tenant.id, submissionId, formId);
if (formSubmission) {
deleted = await submissionRepository.delete(formSubmission);
}

if (deleted) {
eventService.send(submissionDeleted(apiId, user, formSubmission));
}

end();
res.send({ deleted });
} catch (err) {
next(err);
}
};
}

export function updateFormSubmissionDisposition(
apiId: AdspId,
logger: Logger,
Expand Down Expand Up @@ -877,6 +905,13 @@ export function createFormRouter({
getFormSubmission(apiId, submissionRepository)
);

router.delete(
'/submissions/:submissionId',
assertAuthenticatedHandler,
createValidationHandler(param('submissionId').isUUID()),
deleteFormSubmission(apiId, eventService, submissionRepository)
);

router.get(
'/forms/:formId/submissions',
assertAuthenticatedHandler,
Expand Down
1 change: 0 additions & 1 deletion apps/form-service/src/form/types/fileTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { FileType, SecurityClassifications } from '@abgov/adsp-service-sdk';
import { FormServiceRoles } from '../roles';

export const FORM_SUPPORTING_DOCS = 'form-supporting-documents';
export const FORM_SUPPORTING_ANONYMOUS_DOCS = 'form-anonymous-supporting-documents';

export const FormSupportingDocFileType: FileType = {
id: FORM_SUPPORTING_DOCS,
Expand Down
Loading

0 comments on commit c9d507b

Please sign in to comment.