Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add API to retrieve grant notes for specific user via Finder API #3471

66 changes: 25 additions & 41 deletions packages/server/__tests__/lib/grants-collaboration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { expect, use } = require('chai');
const chaiAsPromised = require('chai-as-promised');
const knex = require('../../src/db/connection');
const fixtures = require('../db/seeds/fixtures');
const { saveNoteRevision, getOrganizationNotesForGrant } = require('../../src/lib/grantsCollaboration/notes');
const { saveNoteRevision, getOrganizationNotesForGrantByUser } = require('../../src/lib/grantsCollaboration/notes');
const {
followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant,
} = require('../../src/lib/grantsCollaboration/followers');
Expand All @@ -22,9 +22,10 @@ describe('Grants Collaboration', () => {
expect(result2.id).not.to.equal(result1.id);
});
});
context('getOrganizationNotesForGrant', () => {
context('getOrganizationNotesForGrantByUser', () => {
let revision1;
let revision2;

beforeEach(async () => {
const [grantNote] = await knex('grant_notes')
.insert({ grant_id: fixtures.grants.earFellowship.grant_id, user_id: fixtures.roles.adminRole.id }, 'id');
Expand All @@ -36,8 +37,15 @@ describe('Grants Collaboration', () => {
.insert({ grant_note_id: grantNote.id, text: 'This is a test revision #2' }, 'id');
});

it('get existing organization notes for grant', async () => {
const result = await getOrganizationNotesForGrant(knex, fixtures.grants.earFellowship.grant_id, fixtures.agencies.accountancy.tenant_id);
it('retrieves notes for a specific user and grant', async () => {
const result = await getOrganizationNotesForGrantByUser(
knex,
fixtures.tenants.SBA.id, // organization ID
fixtures.roles.adminRole.id, // user ID
fixtures.grants.earFellowship.grant_id, // grant ID
{},
);

const expectedNoteStructure = {
notes: [{
id: revision2.id,
Expand All @@ -62,29 +70,22 @@ describe('Grants Collaboration', () => {
},
};

// validate createdAt is valid time
expect(result.notes[0].createdAt).to.satisfy((date) => {
const timestamp = new Date(date).getTime();
return !Number.isNaN(timestamp);
});

// remove createdAt to validate the rest of the structure
delete expectedNoteStructure.notes[0].createdAt;
delete result.notes[0].createdAt;

expect(result).to.deep.equal(expectedNoteStructure);
});
it('get existing organization notes for grant after a revision', async () => {
const result = await getOrganizationNotesForGrant(

it('retrieves notes after a specific revision for user and grant', async () => {
const result = await getOrganizationNotesForGrantByUser(
knex,
fixtures.tenants.SBA.id,
fixtures.roles.adminRole.id,
fixtures.grants.earFellowship.grant_id,
fixtures.agencies.accountancy.tenant_id,
{ afterRevision: revision1.id },
);

const expectedNoteStructure = {
notes: [{
id: revision2.id,
createdAt: result.notes[0].createdAt, // store to pass structure check
createdAt: result.notes[0].createdAt,
text: 'This is a test revision #2',
grant: { id: fixtures.grants.earFellowship.grant_id },
user: {
Expand All @@ -104,40 +105,23 @@ describe('Grants Collaboration', () => {
from: revision2.id,
},
};
// validate createdAt is valid time
expect(result.notes[0].createdAt).to.satisfy((date) => {
const timestamp = new Date(date).getTime();
return !Number.isNaN(timestamp);
});

// remove createdAt to validate the rest of the structure
delete expectedNoteStructure.notes[0].createdAt;
delete result.notes[0].createdAt;

expect(result).to.deep.equal(expectedNoteStructure);
});
it('get no organization notes for grant after a revision', async () => {
const result = await getOrganizationNotesForGrant(

it('returns no notes when no notes exist after the given revision', async () => {
const result = await getOrganizationNotesForGrantByUser(
knex,
fixtures.tenants.SBA.id,
fixtures.roles.adminRole.id,
fixtures.grants.earFellowship.grant_id,
fixtures.agencies.accountancy.tenant_id,
{ afterRevision: revision2.id },
);
const expectedNoteStructure = {
notes: [],
pagination: {
from: revision2.id,
},
};

expect(result).to.deep.equal(expectedNoteStructure);
});
it('get no organization notes for grant', async () => {
const result = await getOrganizationNotesForGrant(knex, fixtures.grants.earFellowship.grant_id, fixtures.agencies.usdr.tenant_id);
const expectedNoteStructure = {
notes: [],
pagination: {
from: undefined,
from: revision2.id,
},
};

Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/lib/grantsCollaboration/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const { saveNoteRevision, getOrganizationNotesForGrant } = require('./notes');
const { saveNoteRevision, getOrganizationNotesForGrant, getOrganizationNotesForGrantByUser } = require('./notes');
const {
followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant,
} = require('./followers');

module.exports = {
saveNoteRevision, getOrganizationNotesForGrant, followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant,
saveNoteRevision, getOrganizationNotesForGrant, getOrganizationNotesForGrantByUser, followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant,
};
51 changes: 46 additions & 5 deletions packages/server/src/lib/grantsCollaboration/notes.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ async function saveNoteRevision(knex, grantId, userId, text) {
return grantNotesRevisionId;
}

async function getOrganizationNotesForGrant(knex, grantId, organizationId, { afterRevision, limit = 50 } = {}) {
async function getCurrentNoteRevisions(
knex,
{ grantId, organizationId, userId } = {},
{ afterRevision, limit = 50 } = {},
) {
const subquery = knex.select([
'r.id',
'r.grant_note_id',
Expand Down Expand Up @@ -64,9 +68,22 @@ async function getOrganizationNotesForGrant(knex, grantId, organizationId, { aft
.joinRaw(`LEFT JOIN LATERAL (${subquery.toQuery()}) AS rev ON rev.grant_note_id = grant_notes.id`)
.join('users', 'users.id', 'grant_notes.user_id')
.join('agencies', 'agencies.id', 'users.agency_id')
.join('tenants', 'tenants.id', 'users.tenant_id')
.where('grant_notes.grant_id', grantId)
.andWhere('tenants.id', organizationId);
.join('tenants', 'tenants.id', 'users.tenant_id');

// Conditionally applying filters based on grantID if it is null or undefined or not
if (grantId !== null && grantId !== undefined) {
query = query.where('grant_notes.grant_id', grantId);
}

// Conditionally applying filters based on organizationID if it is null or undefined or not
if (organizationId !== null && organizationId !== undefined) {
query = query.andWhere('tenants.id', organizationId);
}

// Conditionally applying filters based on userID if it is null or undefined or not
if (userId !== null && userId !== undefined) {
query = query.andWhere('grant_notes.user_id', userId);
}

if (afterRevision) {
query = query.andWhere('rev.id', '>', afterRevision);
Expand Down Expand Up @@ -100,4 +117,28 @@ async function getOrganizationNotesForGrant(knex, grantId, organizationId, { aft
};
}

module.exports = { saveNoteRevision, getOrganizationNotesForGrant };
async function getOrganizationNotesForGrantByUser(
knex,
organizationId,
userId,
grantId,
{ afterRevision, limit = 50 } = {},
) {
return getCurrentNoteRevisions(knex, { grantId, organizationId, userId }, { afterRevision, limit });
}

async function getOrganizationNotesForGrant(
knex,
grantId,
organizationId,
{ afterRevision, limit = 50 } = {},
) {
return getCurrentNoteRevisions(knex, { grantId, organizationId }, { afterRevision, limit });
}

module.exports = {
saveNoteRevision,
getCurrentNoteRevisions,
getOrganizationNotesForGrant,
getOrganizationNotesForGrantByUser,
};
33 changes: 33 additions & 0 deletions packages/server/src/routes/grants.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { requireUser, isUserAuthorized } = require('../lib/access-helpers');
const knex = require('../db/connection');
const {
saveNoteRevision, followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant, getOrganizationNotesForGrant,
getOrganizationNotesForGrantByUser,
} = require('../lib/grantsCollaboration');

const router = express.Router({ mergeParams: true });
Expand Down Expand Up @@ -45,6 +46,38 @@ router.get('/', requireUser, async (req, res) => {
res.json(grants);
});

// getting notes for a specific user and grant
router.get('/:grantId/notes/user/:userId', requireUser, async (req, res) => {
const { grantId, userId } = req.params;
const { tenant_id: organizationId } = req.session.user;
const { paginateFrom, limit } = req.query;

// Converting limit to an integer
const limitInt = limit ? parseInt(limit, 10) : undefined;

// Validating the limit query parameter
if (limit && (!Number.isInteger(limitInt) || limitInt < 1 || limitInt > 100)) {
res.status(400).send('Invalid limit parameter');
return;
}

try {
// Fetching the notes using getOrganizationNotesForGrantByUser function
const notes = await getOrganizationNotesForGrantByUser(
knex,
organizationId,
userId,
grantId,
{ afterRevision: paginateFrom, limit: limitInt },
);

// sending the notes as JSON response
res.json(notes);
} catch (error) {
res.status(500).json({ error: 'Failed to retrieve notes' });
}
});

function criteriaToFiltersObj(criteria, agencyId) {
const filters = criteria || {};
const postedWithinOptions = {
Expand Down
Loading