Skip to content

Commit

Permalink
Return deep proposal json for proposal queries
Browse files Browse the repository at this point in the history
This is the big one...

We're changing the way we populate deep proposals, leveraging psql
directly instead of putting that burden on the application. We may find
that there are optimizations to be made in future, but this removes
a large amount of complexity from the application.

As part of this we are also improving the deep object so that it
includes the base field information directly, meaning a client won't
need to load a list of base fields in order to understand the context of
an application form field.

Issue #821 Change our query approach to use DB-level json functions
  • Loading branch information
slifty committed Mar 15, 2024
1 parent ed18a2b commit c037eef
Show file tree
Hide file tree
Showing 26 changed files with 276 additions and 853 deletions.
133 changes: 84 additions & 49 deletions src/__tests__/proposals.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ describe('/proposals', () => {
id: 1,
applicationFormId: 1,
baseFieldId: 1,
baseField: {
createdAt: expectTimestamp,
dataType: 'string',
description: 'A summary of the proposal',
id: 1,
label: 'Summary',
shortCode: 'summary',
},
label: 'Short summary',
position: 1,
createdAt: expectTimestamp,
Expand Down Expand Up @@ -196,6 +204,14 @@ describe('/proposals', () => {
id: 1,
applicationFormId: 1,
baseFieldId: 1,
baseField: {
createdAt: expectTimestamp,
dataType: 'string',
description: 'A summary of the proposal',
id: 1,
label: 'Summary',
shortCode: 'summary',
},
label: 'Short summary',
position: 1,
createdAt: expectTimestamp,
Expand Down Expand Up @@ -285,6 +301,14 @@ describe('/proposals', () => {
id: 1,
applicationFormId: 1,
baseFieldId: 1,
baseField: {
createdAt: expectTimestamp,
dataType: 'string',
description: 'A summary of the proposal',
id: 1,
label: 'Summary',
shortCode: 'summary',
},
label: 'Concise summary',
position: 1,
createdAt: expectTimestamp,
Expand Down Expand Up @@ -368,8 +392,7 @@ describe('/proposals', () => {
.expect(404);
expect(response.body).toEqual({
name: 'NotFoundError',
message:
'Not found. Find existing proposals by calling with no parameters.',
message: expect.any(String) as string,
details: [
{
name: 'NotFoundError',
Expand All @@ -378,6 +401,18 @@ describe('/proposals', () => {
});
});

it('returns 400 when given id a string', async () => {
const response = await agent
.get('/proposals/foobar')
.set(authHeader)
.expect(400);
expect(response.body).toEqual({
name: 'InputValidationError',
message: expect.any(String) as string,
details: [],
});
});

it('returns the one proposal asked for', async () => {
await db.query(`
INSERT INTO opportunities (
Expand All @@ -404,12 +439,13 @@ describe('/proposals', () => {
expect(response.body).toEqual({
id: 2,
externalId: 'proposal-2',
versions: [],
opportunityId: 1,
createdAt: '2525-01-03T00:00:05.000Z',
createdAt: expectTimestamp,
});
});

it('returns one proposal with deep fields when includeFieldsAndValues=true', async () => {
it('returns one proposal with deep fields', async () => {
await createTestBaseFields();
await db.query(`
INSERT INTO opportunities (
Expand Down Expand Up @@ -475,36 +511,44 @@ describe('/proposals', () => {
( 2, 2, 2, 'Abstract for version 2 from 2525-01-04', '2525-01-04T00:00:13Z' );
`);
const response = await agent
.get('/proposals/1/?includeFieldsAndValues=true')
.get('/proposals/1')
.set(authHeader)
.expect(200);
expect(response.body).toEqual({
id: 1,
opportunityId: 1,
externalId: 'proposal-2525-01-04T00Z',
createdAt: '2525-01-04T00:00:07.000Z',
createdAt: expectTimestamp,
versions: [
{
id: 2,
proposalId: 1,
applicationFormId: 1,
version: 2,
createdAt: '2525-01-04T00:00:09.000Z',
createdAt: expectTimestamp,
fieldValues: [
{
id: 3,
proposalVersionId: 2,
applicationFormFieldId: 1,
position: 1,
value: 'Title for version 2 from 2525-01-04',
createdAt: '2525-01-04T00:00:12.000Z',
createdAt: expectTimestamp,
applicationFormField: {
id: 1,
applicationFormId: 1,
baseFieldId: 2,
baseField: {
createdAt: expectTimestamp,
dataType: 'string',
description: 'The title of the proposal',
id: 2,
label: 'Title',
shortCode: 'title',
},
position: 1,
label: 'Short summary or title',
createdAt: '2525-01-04T00:00:05.000Z',
createdAt: expectTimestamp,
},
},
{
Expand All @@ -513,14 +557,22 @@ describe('/proposals', () => {
applicationFormFieldId: 2,
position: 2,
value: 'Abstract for version 2 from 2525-01-04',
createdAt: '2525-01-04T00:00:13.000Z',
createdAt: expectTimestamp,
applicationFormField: {
id: 2,
applicationFormId: 1,
baseFieldId: 1,
baseField: {
createdAt: expectTimestamp,
dataType: 'string',
description: 'A summary of the proposal',
id: 1,
label: 'Summary',
shortCode: 'summary',
},
position: 2,
label: 'Long summary or abstract',
createdAt: '2525-01-04T00:00:06.000Z',
createdAt: expectTimestamp,
},
},
],
Expand All @@ -530,22 +582,30 @@ describe('/proposals', () => {
proposalId: 1,
applicationFormId: 1,
version: 1,
createdAt: '2525-01-04T00:00:08.000Z',
createdAt: expectTimestamp,
fieldValues: [
{
id: 1,
proposalVersionId: 1,
applicationFormFieldId: 1,
position: 1,
value: 'Title for version 1 from 2525-01-04',
createdAt: '2525-01-04T00:00:10.000Z',
createdAt: expectTimestamp,
applicationFormField: {
id: 1,
applicationFormId: 1,
baseFieldId: 2,
baseField: {
createdAt: expectTimestamp,
dataType: 'string',
description: 'The title of the proposal',
id: 2,
label: 'Title',
shortCode: 'title',
},
position: 1,
label: 'Short summary or title',
createdAt: '2525-01-04T00:00:05.000Z',
createdAt: expectTimestamp,
},
},
{
Expand All @@ -554,54 +614,29 @@ describe('/proposals', () => {
applicationFormFieldId: 2,
position: 2,
value: 'Abstract for version 1 from 2525-01-04',
createdAt: '2525-01-04T00:00:11.000Z',
createdAt: expectTimestamp,
applicationFormField: {
id: 2,
applicationFormId: 1,
baseFieldId: 1,
baseField: {
createdAt: expectTimestamp,
dataType: 'string',
description: 'A summary of the proposal',
id: 1,
label: 'Summary',
shortCode: 'summary',
},
position: 2,
label: 'Long summary or abstract',
createdAt: '2525-01-04T00:00:06.000Z',
createdAt: expectTimestamp,
},
},
],
},
],
});
});

it('returns 404 when given id is not present and includeFieldsAndValues=true', async () => {
const response = await agent
.get('/proposals/9002?includeFieldsAndValues=true')
.set(authHeader)
.expect(404);
expect(response.body).toEqual({
name: 'NotFoundError',
message: expect.any(String) as string,
details: [
{
name: 'NotFoundError',
},
],
});
});

it('returns 404 when given id is not present and includeFieldsAndValues=true', async () => {
const response = await agent
.get('/proposals/9002?includeFieldsAndValues=true')
.set(authHeader)
.expect(404);
expect(response.body).toEqual({
name: 'NotFoundError',
message:
'Not found. Find existing proposals by calling with no parameters.',
details: [
{
name: 'NotFoundError',
},
],
});
});
});

describe('POST /', () => {
Expand Down
24 changes: 0 additions & 24 deletions src/database/enrichers/enrichProposalFieldValues.ts

This file was deleted.

27 changes: 0 additions & 27 deletions src/database/enrichers/enrichProposalVersions.ts

This file was deleted.

25 changes: 0 additions & 25 deletions src/database/enrichers/enrichProposals.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/database/enrichers/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/database/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './db';
export * from './enrichers';
export * from './migrate';
export * from './operations';
export * from './parameters';
21 changes: 21 additions & 0 deletions src/database/initialization/application_form_field_to_json.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CREATE OR REPLACE FUNCTION application_form_field_to_json(application_form_field application_form_fields)
RETURNS JSONB AS $$
DECLARE
base_field_json JSONB;
BEGIN
SELECT base_field_to_json(base_fields.*)
INTO base_field_json
FROM base_fields
WHERE base_fields.id = application_form_field.base_field_id;

RETURN jsonb_build_object(
'id', application_form_field.id,
'applicationFormId', application_form_field.application_form_id,
'baseFieldId', application_form_field.base_field_id,
'baseField', base_field_json,
'position', application_form_field.position,
'label', application_form_field.label,
'createdAt', application_form_field.created_at
);
END;
$$ LANGUAGE plpgsql;
23 changes: 23 additions & 0 deletions src/database/initialization/organization_to_json.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
CREATE OR REPLACE FUNCTION organization_to_json(organization organizations)
RETURNS JSONB AS $$
DECLARE
proposals_json JSONB;
BEGIN
SELECT jsonb_agg(
proposal_to_json(proposals.*)
ORDER BY proposals.id DESC
)
INTO proposals_json
FROM proposals
JOIN organizations_proposals ON organizations_proposals.proposal_id = proposal.id
WHERE organizations_proposals.organization_id = organization.id;

RETURN jsonb_build_object(
'id', organization.id,
'employerIdentificationNumber', organization.employer_identification_number,
'name', organization.name,
'proposals', COALESCE(proposals_json, '[]'::JSONB),
'createdAt', organization.created_at
);
END;
$$ LANGUAGE plpgsql;
Loading

0 comments on commit c037eef

Please sign in to comment.