Skip to content

Commit

Permalink
Include fields in GET /applicationForms/{id} optionally
Browse files Browse the repository at this point in the history
Use query parameter `includeFields=true` to include application form
fields in the response from `GET /applicationForms/{id}`, otherwise a
shallow response with only direct attributes on application form will
be returned.

The implementation is more akin to the one for /proposals in PR #210.

This commit combines four commits from 2022-12-06, 2022-12-12,
2022-12-12, 2022-12-16, 2023-02-09, respectively, and cleanup on
2023-02-13. Some history of changes should be available at
#157

Issue #132 Provide visibility of application form fields
  • Loading branch information
bickelj committed Feb 13, 2023
1 parent c9bc6d9 commit 46828c9
Show file tree
Hide file tree
Showing 5 changed files with 435 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,133 @@ describe('/applicationForms', () => {
);
});

it('returns an application form without its fields', async () => {
await db.query(`
INSERT INTO opportunities (title)
VALUES
( 'Summer opportunity 🩴' ),
( 'Spring opportunity 🌺' );
`);
await db.query(`
INSERT INTO application_forms (
opportunity_id,
version,
created_at
)
VALUES
( 1, 1, '2510-02-02 00:00:01+0000' ),
( 1, 2, '2510-02-02 00:00:02+0000' ),
( 2, 1, '2510-02-02 00:00:03+0000' )
`);
await db.query(`
INSERT INTO canonical_fields (
label,
short_code,
data_type,
created_at
)
VALUES
( 'Organization Name', 'organizationName', '{ type: "string" }', '2510-02-02 00:00:04+0000' ),
( 'Years of work', 'yearsOfWork', '{ type: "integer" }', '2510-02-02 00:00:05+0000' );
`);
await agent
.get('/applicationForms/2')
.set(dummyApiKey)
.expect(
200,
{
id: 2,
opportunityId: 1,
version: 2,
createdAt: '2510-02-02T00:00:02.000Z',
},
);
});

it('returns an application form with its fields', async () => {
await db.query(`
INSERT INTO opportunities (title)
VALUES
( 'Holiday opportunity 🎄' ),
( 'Another holiday opportunity 🕎' );
`);
await db.query(`
INSERT INTO application_forms (
opportunity_id,
version,
created_at
)
VALUES
( 1, 1, '2510-02-01 00:00:01+0000' ),
( 1, 2, '2510-02-01 00:00:02+0000' ),
( 2, 1, '2510-02-01 00:00:03+0000' )
`);
await db.query(`
INSERT INTO canonical_fields (
label,
short_code,
data_type,
created_at
)
VALUES
( 'Organization Name', 'organizationName', '{ type: "string" }', '2510-02-01 00:00:04+0000' ),
( 'Years of work', 'yearsOfWork', '{ type: "integer" }', '2510-02-01 00:00:05+0000' );
`);
await db.query(`
INSERT INTO application_form_fields (
application_form_id,
canonical_field_id,
position,
label,
created_at
)
VALUES
( 3, 2, 1, 'Anni Worki', '2510-02-01 00:00:06+0000' ),
( 3, 1, 2, 'Org Nomen', '2510-02-01 00:00:07+0000' ),
( 2, 1, 2, 'Name of Organization', '2510-02-01 00:00:08+0000' ),
( 2, 2, 1, 'Duration of work in years', '2510-02-01 00:00:09+0000' )
`);
await agent
.get('/applicationForms/2')
.query({ includeFields: 'true' })
.set(dummyApiKey)
.expect(
200,
{
id: 2,
opportunityId: 1,
version: 2,
fields: [
{
id: 4,
applicationFormId: 2,
canonicalFieldId: 2,
position: 1,
label: 'Duration of work in years',
createdAt: '2510-02-01T00:00:09.000Z',
},
{
id: 3,
applicationFormId: 2,
canonicalFieldId: 1,
position: 2,
label: 'Name of Organization',
createdAt: '2510-02-01T00:00:08.000Z',
},
],
createdAt: '2510-02-01T00:00:02.000Z',
},
);
});

it('should error if the database returns an unexpected data structure', async () => {
jest.spyOn(db, 'sql')
.mockImplementationOnce(async () => ({
rows: [{ foo: 'not a valid result' }],
}) as Result<object>);
const result = await agent
.get('/applicationForms')
.get('/applicationForms/2')
.query({ includeFields: 'true' })
.set(dummyApiKey)
.expect(500);
expect(result.body).toMatchObject({
Expand Down Expand Up @@ -135,6 +255,122 @@ describe('/applicationForms', () => {
}],
});
});

it('returns 503 DatabaseError if an insufficient resources database error is thrown when selecting one', async () => {
jest.spyOn(db, 'sql')
.mockImplementationOnce(async () => {
throw new TinyPgError(
'Something went wrong',
undefined,
{
error: {
code: PostgresErrorCode.INSUFFICIENT_RESOURCES,
},
},
);
});
const result = await agent
.get('/applicationForms/3')
.set(dummyApiKey)
.expect(503);
expect(result.body).toMatchObject({
name: 'DatabaseError',
details: [{
code: PostgresErrorCode.INSUFFICIENT_RESOURCES,
}],
});
});

it('should error if the database returns an unexpected data structure', async () => {
jest.spyOn(db, 'sql')
.mockImplementationOnce(async () => ({
rows: [{ foo: 'not a valid applicationForm' }],
}) as Result<object>);
const result = await agent
.get('/applicationForms/5')
.set(dummyApiKey)
.expect(500);
expect(result.body).toMatchObject({
name: 'InternalValidationError',
details: expect.any(Array) as unknown[],
});
});

it('should return 404 when the applicationForm is not found (shallow)', async () => {
const result = await agent
.get('/applicationForms/6')
.set(dummyApiKey)
.expect(404);
expect(result.body).toMatchObject({
name: 'NotFoundError',
details: expect.any(Array) as unknown[],
});
});
it('should return 404 when the applicationForm is not found (with fields)', async () => {
const result = await agent
.get('/applicationForms/7')
.query({ includeFields: 'true' })
.set(dummyApiKey)
.expect(404);
expect(result.body).toMatchObject({
name: 'NotFoundError',
details: expect.any(Array) as unknown[],
});
});

it('should return 500 when the application form fields returned are invalid', async () => {
jest.spyOn(db, 'sql')
.mockImplementationOnce(async () => ({
command: '',
row_count: 1,
rows: [
{
id: 1,
opportunityId: 1,
version: 1,
createdAt: new Date(),
},
],
}))
.mockImplementationOnce(async () => ({
rows: [{ foo: 'not a valid application form fields result' }],
}) as Result<object>);
const result = await agent
.get('/applicationForms/8')
.query({ includeFields: 'true' })
.set(dummyApiKey)
.expect(500);
expect(result.body).toMatchObject({
name: 'InternalValidationError',
details: expect.any(Array) as unknown[],
});
});
});

it('returns 503 DatabaseError if an insufficient resources database error is thrown when selecting one', async () => {
jest.spyOn(db, 'sql')
.mockImplementationOnce(async () => {
throw new TinyPgError(
'Something went wrong',
undefined,
{
error: {
code: PostgresErrorCode.INSUFFICIENT_RESOURCES,
},
},
);
});
const result = await agent
.get('/applicationForms/4')
.set(dummyApiKey)
.query({ includeFields: 'true' })
.expect(503);
expect(result.body).toMatchObject({
name: 'DatabaseError',
details: [{
code: PostgresErrorCode.INSUFFICIENT_RESOURCES,
}],
});
});

describe('POST /', () => {
Expand Down Expand Up @@ -391,7 +627,6 @@ describe('/applicationForms', () => {
id: 1,
opportunityId: 1,
version: 1,
fields: [],
createdAt: new Date(),
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ SELECT
aff.created_at as "createdAt"
FROM application_form_fields aff
WHERE aff.application_form_id = :applicationFormId
ORDER BY aff.position;
106 changes: 106 additions & 0 deletions src/handlers/applicationFormsHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DatabaseError,
InputValidationError,
InternalValidationError,
NotFoundError,
} from '../errors';
import type {
Request,
Expand Down Expand Up @@ -58,6 +59,110 @@ const getApplicationForms = (
});
};

const getShallowApplicationForm = (
req: Request,
res: Response,
next: NextFunction,
): void => {
db.sql('applicationForms.selectById', { id: req.params.id })
.then((applicationFormsQueryResult: Result<ApplicationForm>) => {
if (applicationFormsQueryResult.row_count === 0) {
throw new NotFoundError(
'Not found. Find existing application forms by calling with no parameters.',
);
}
const applicationForm = applicationFormsQueryResult.rows[0];
if (!isApplicationForm(applicationForm)) {
throw new InternalValidationError(
'The database responded with an unexpected format.',
isApplicationForm.errors ?? [],
);
}
res.status(200)
.contentType('application/json')
.send(applicationForm);
})
.catch((error: unknown) => {
if (isTinyPgErrorWithQueryContext(error)) {
next(new DatabaseError(
'Error retrieving application forms.',
error,
));
return;
}
next(error);
});
};

const getApplicationFormWithFields = (
req: Request,
res: Response,
next: NextFunction,
): void => {
db.sql('applicationForms.selectById', { id: req.params.id })
.then((applicationFormsQueryResult: Result<ApplicationForm>) => {
if (applicationFormsQueryResult.row_count === 0) {
throw new NotFoundError(
'Not found. Find existing application forms by calling with no parameters.',
);
}
const baseApplicationForm = applicationFormsQueryResult.rows[0];
if (!isApplicationForm(baseApplicationForm)) {
throw new InternalValidationError(
'The database responded with an unexpected format.',
isApplicationForm.errors ?? [],
);
}
db.sql('applicationFormFields.selectByApplicationFormId', { applicationFormId: req.params.id })
.then((applicationFormFieldsQueryResult) => {
if (!isApplicationFormFieldArray(applicationFormFieldsQueryResult.rows)) {
throw new InternalValidationError(
'The database responded with an unexpected format.',
isApplicationFormFieldArray.errors ?? [],
);
}
const applicationForm = {
...baseApplicationForm,
fields: applicationFormFieldsQueryResult.rows,
};
res.status(200)
.contentType('application/json')
.send(applicationForm);
}).catch((error: unknown) => {
if (isTinyPgErrorWithQueryContext(error)) {
next(new DatabaseError(
'Error retrieving application form.',
error,
));
return;
}
next(error);
});
})
.catch((error: unknown) => {
if (isTinyPgErrorWithQueryContext(error)) {
next(new DatabaseError(
'Error retrieving application form.',
error,
));
return;
}
next(error);
});
};

const getApplicationForm = (
req: Request,
res: Response,
next: NextFunction,
): void => {
if (req.query.includeFields !== undefined && req.query.includeFields === 'true') {
getApplicationFormWithFields(req, res, next);
} else {
getShallowApplicationForm(req, res, next);
}
};

const postApplicationForms = (
req: Request<unknown, unknown, ApplicationFormWrite>,
res: Response,
Expand Down Expand Up @@ -134,6 +239,7 @@ const postApplicationForms = (
};

export const applicationFormsHandlers = {
getApplicationForm,
getApplicationForms,
postApplicationForms,
};

0 comments on commit 46828c9

Please sign in to comment.