diff --git a/integration-tests/index.test.js b/integration-tests/index.test.js index b792b47..68634e1 100644 --- a/integration-tests/index.test.js +++ b/integration-tests/index.test.js @@ -8,6 +8,8 @@ import * as orgsWithGrants from './test-queries/orgs-with-grants'; import * as someNews from './test-queries/some-news'; import * as boardTerms from './test-queries/board-terms'; import * as moreMeta from './test-queries/more-meta'; +import * as orgNameLike from './test-queries/org-name-like'; +import * as sortOrgGrantsByDate from './test-queries/sort-org-grants-by-date'; const createServerInstance = async () => { const db = dbFactory(); @@ -67,18 +69,18 @@ test('organization_meta updates automatically', async () => { const { uri, instance, db } = await createServerInstance(); const query = ` query foo { - giver: organizationMetas(id: 3) { - countGrantsTo - countGrantsFrom - countDistinctFunders - countDistinctRecipients - } - receiver: organizationMetas(id: 91) { - countGrantsTo - countGrantsFrom - countDistinctFunders - countDistinctRecipients - } + giver: organization(id: 3) { + countGrantsTo + countGrantsFrom + countDistinctFunders + countDistinctRecipients + } + receiver: organization(id: 91) { + countGrantsTo + countGrantsFrom + countDistinctFunders + countDistinctRecipients + } }`; const resBefore = await request(uri, query); @@ -98,40 +100,89 @@ query foo { // Assert expect(resBefore).toEqual({ - giver: [ - { - countGrantsTo: 0, - countGrantsFrom: 0, - countDistinctFunders: 0, - countDistinctRecipients: 0, - }, - ], - receiver: [ - { - countGrantsTo: 17, - countGrantsFrom: 17, - countDistinctFunders: 17, - countDistinctRecipients: 17, - }, - ], + giver: { + countGrantsTo: 0, + countGrantsFrom: 0, + countDistinctFunders: 0, + countDistinctRecipients: 0, + }, + receiver: { + countGrantsTo: 17, + countGrantsFrom: 17, + countDistinctFunders: 17, + countDistinctRecipients: 17, + }, }); expect(resAfter).toEqual({ - giver: [ - { - countGrantsTo: 0, - countGrantsFrom: 1, - countDistinctFunders: 0, - countDistinctRecipients: 1, - }, - ], - receiver: [ - { - countGrantsTo: 18, - countGrantsFrom: 17, - countDistinctFunders: 18, - countDistinctRecipients: 17, - }, - ], + giver: { + countGrantsTo: 0, + countGrantsFrom: 1, + countDistinctFunders: 0, + countDistinctRecipients: 1, + }, + receiver: { + countGrantsTo: 18, + countGrantsFrom: 17, + countDistinctFunders: 18, + countDistinctRecipients: 17, + }, + }); + + instance.close(); +}); + +test('filters organizations by name', async () => { + const { uri, instance } = await createServerInstance(); + + const res = await request(uri, orgNameLike.query); + + expect(res).toEqual(orgNameLike.expected.data); + + instance.close(); +}); + +test('sorts organization grants by date', async () => { + const { uri, instance } = await createServerInstance(); + + const res = await request(uri, sortOrgGrantsByDate.query); + + expect(res).toEqual(sortOrgGrantsByDate.expected.data); + + instance.close(); +}); + +test('filter organization grants funded/received by name', async () => { + const { uri, instance } = await createServerInstance(); + + const res = await request( + uri, + ` +query filterOrganizationGrants { + organization(id: 91) { + grantsFunded( + limit: 10, + orderByDirection: ASC, + orderBy: dateFrom, + textLike: { description: "grant 2 description" } + ) { + description + } + } +} +` + ); + + expect(res).toEqual({ + organization: { + grantsFunded: [ + { + description: 'grant 2 description', + }, + { + description: 'grant 2 description', + }, + ], + }, }); instance.close(); diff --git a/integration-tests/test-queries/more-meta.js b/integration-tests/test-queries/more-meta.js index cef2073..478243a 100644 --- a/integration-tests/test-queries/more-meta.js +++ b/integration-tests/test-queries/more-meta.js @@ -1,41 +1,36 @@ export const query = ` query foo { - organizationMetas( + organizations( limit: 2 offset: 1 - orderByMulti: [["countGrantsTo", "DESC"], ["id", "ASC"]] + orderBy: countGrantsTo + orderByDirection: DESC ) { countGrantsTo countGrantsFrom countDistinctFunders countDistinctRecipients - organization { - name - } + name } } `; export const expected = { data: { - organizationMetas: [ + organizations: [ { countGrantsTo: 16, countGrantsFrom: 16, countDistinctFunders: 16, countDistinctRecipients: 16, - organization: { - name: 'test organization 89', - }, + name: 'test organization 89', }, { countGrantsTo: 16, countGrantsFrom: 16, countDistinctFunders: 16, countDistinctRecipients: 16, - organization: { - name: 'test organization 91', - }, + name: 'test organization 91', }, ], }, diff --git a/integration-tests/test-queries/org-name-like.js b/integration-tests/test-queries/org-name-like.js new file mode 100644 index 0000000..ad9b4e4 --- /dev/null +++ b/integration-tests/test-queries/org-name-like.js @@ -0,0 +1,25 @@ +export const query = ` +query orgNameLike { + organizations(nameLike: "%organization 9%", limit: 11) { + name + } +} +`; + +export const expected = { + data: { + organizations: [ + { name: 'test organization 9' }, + { name: 'test organization 90' }, + { name: 'test organization 91' }, + { name: 'test organization 92' }, + { name: 'test organization 93' }, + { name: 'test organization 94' }, + { name: 'test organization 95' }, + { name: 'test organization 96' }, + { name: 'test organization 97' }, + { name: 'test organization 98' }, + { name: 'test organization 99' }, + ], + }, +}; diff --git a/integration-tests/test-queries/orgs-with-grants.js b/integration-tests/test-queries/orgs-with-grants.js index b765adf..1d125ac 100644 --- a/integration-tests/test-queries/orgs-with-grants.js +++ b/integration-tests/test-queries/orgs-with-grants.js @@ -1,41 +1,40 @@ export const query = ` -query foo { - organizationMetas( +query orgsWithGrants { + organizations( limit: 2 offset: 1 - orderByMulti: [["totalReceived", "ASC"]] + orderBy: totalReceived + orderByDirection: ASC ) { totalFunded totalReceived grantdatesStart grantdatesEnd - organization { + name + ein + duns + stateCorpId + description + address + links + founded + dissolved + legacyData + publicFunder + grantsFunded { + ...grantFields + } + grantsReceived { + ...grantFields + } + nteeOrganizationTypes { + name + code + description + } + organizationTags { name - ein - duns - stateCorpId description - address - links - founded - dissolved - legacyData - publicFunder - grantsFunded { - ...grantFields - } - grantsReceived { - ...grantFields - } - nteeOrganizationTypes { - name - code - description - } - organizationTags { - name - description - } } } } @@ -68,526 +67,522 @@ fragment grantFields on Grant { export const expected = { data: { - organizationMetas: [ + organizations: [ { - totalFunded: '40', - totalReceived: '40', + totalFunded: 40, + totalReceived: 40, grantdatesStart: '2001-02-02', grantdatesEnd: '2001-02-02', - organization: { - name: 'test organization 19', - ein: '19', - duns: '19', - stateCorpId: '19', - description: 'test organization 19 description!', - address: { - postalCode: '19', + name: 'test organization 19', + ein: '19', + duns: '19', + stateCorpId: '19', + description: 'test organization 19 description!', + address: { + postalCode: '19', + }, + links: [ + { + url: 'ftp://19', + description: 'a link', }, - links: [ - { - url: 'ftp://19', - description: 'a link', - }, - { - url: 'gopher://19', - description: 'another link', - }, - ], - founded: '2000-09-20', - dissolved: '2001-09-20', - legacyData: { - drupalId: 19, + { + url: 'gopher://19', + description: 'another link', }, - publicFunder: true, - grantsFunded: [ - { - from: { - name: 'test organization 19', - }, - to: { - name: 'test organization 18', - }, - dateFrom: '2001-02-02', - dateTo: '2010-02-02', - amount: 20, - source: 'grant 1 source', - description: 'grant 1 description', - internalNotes: 'grant 1 internal notes', - legacyData: { - drupalId: 1, - }, - federalAwardId: 'grant 1 federal award id', - nteeGrantTypes: [ - { - name: 'test ntee grant type 0', - description: 'test ntee grant type 0 description', - }, - { - name: 'test ntee grant type 1', - description: 'test ntee grant type 1 description', - }, - { - name: 'test ntee grant type 2', - description: 'test ntee grant type 2 description', - }, - ], - grantTags: [ - { - name: 'test grant tag 0', - description: 'test grant tag 0 description', - }, - { - name: 'test grant tag 1', - description: 'test grant tag 1 description', - }, - { - name: 'test grant tag 2', - description: 'test grant tag 2 description', - }, - ], + ], + founded: '2000-09-20', + dissolved: '2001-09-20', + legacyData: { + drupalId: 19, + }, + publicFunder: true, + grantsFunded: [ + { + from: { + name: 'test organization 19', }, - { - from: { - name: 'test organization 19', - }, - to: { - name: 'test organization 20', - }, - dateFrom: '2001-02-02', - dateTo: '2010-02-02', - amount: 20, - source: 'grant 1 source', - description: 'grant 1 description', - internalNotes: 'grant 1 internal notes', - legacyData: { - drupalId: 1, - }, - federalAwardId: 'grant 1 federal award id', - nteeGrantTypes: [ - { - name: 'test ntee grant type 0', - description: 'test ntee grant type 0 description', - }, - { - name: 'test ntee grant type 1', - description: 'test ntee grant type 1 description', - }, - { - name: 'test ntee grant type 2', - description: 'test ntee grant type 2 description', - }, - ], - grantTags: [ - { - name: 'test grant tag 0', - description: 'test grant tag 0 description', - }, - { - name: 'test grant tag 1', - description: 'test grant tag 1 description', - }, - { - name: 'test grant tag 2', - description: 'test grant tag 2 description', - }, - ], + to: { + name: 'test organization 18', }, - ], - grantsReceived: [ - { - from: { - name: 'test organization 18', - }, - to: { - name: 'test organization 19', - }, - dateFrom: '2001-02-02', - dateTo: '2010-02-02', - amount: 20, - source: 'grant 1 source', - description: 'grant 1 description', - internalNotes: 'grant 1 internal notes', - legacyData: { - drupalId: 1, - }, - federalAwardId: 'grant 1 federal award id', - nteeGrantTypes: [ - { - name: 'test ntee grant type 0', - description: 'test ntee grant type 0 description', - }, - { - name: 'test ntee grant type 1', - description: 'test ntee grant type 1 description', - }, - { - name: 'test ntee grant type 2', - description: 'test ntee grant type 2 description', - }, - ], - grantTags: [ - { - name: 'test grant tag 0', - description: 'test grant tag 0 description', - }, - { - name: 'test grant tag 1', - description: 'test grant tag 1 description', - }, - { - name: 'test grant tag 2', - description: 'test grant tag 2 description', - }, - ], + dateFrom: '2001-02-02', + dateTo: '2010-02-02', + amount: 20, + source: 'grant 1 source', + description: 'grant 1 description', + internalNotes: 'grant 1 internal notes', + legacyData: { + drupalId: 1, }, - { - from: { - name: 'test organization 20', - }, - to: { - name: 'test organization 19', - }, - dateFrom: '2001-02-02', - dateTo: '2010-02-02', - amount: 20, - source: 'grant 1 source', - description: 'grant 1 description', - internalNotes: 'grant 1 internal notes', - legacyData: { - drupalId: 1, - }, - federalAwardId: 'grant 1 federal award id', - nteeGrantTypes: [ - { - name: 'test ntee grant type 0', - description: 'test ntee grant type 0 description', - }, - { - name: 'test ntee grant type 1', - description: 'test ntee grant type 1 description', - }, - { - name: 'test ntee grant type 2', - description: 'test ntee grant type 2 description', - }, - ], - grantTags: [ - { - name: 'test grant tag 0', - description: 'test grant tag 0 description', - }, - { - name: 'test grant tag 1', - description: 'test grant tag 1 description', - }, - { - name: 'test grant tag 2', - description: 'test grant tag 2 description', - }, - ], + federalAwardId: 'grant 1 federal award id', + nteeGrantTypes: [ + { + name: 'test ntee grant type 0', + description: 'test ntee grant type 0 description', + }, + { + name: 'test ntee grant type 1', + description: 'test ntee grant type 1 description', + }, + { + name: 'test ntee grant type 2', + description: 'test ntee grant type 2 description', + }, + ], + grantTags: [ + { + name: 'test grant tag 0', + description: 'test grant tag 0 description', + }, + { + name: 'test grant tag 1', + description: 'test grant tag 1 description', + }, + { + name: 'test grant tag 2', + description: 'test grant tag 2 description', + }, + ], + }, + { + from: { + name: 'test organization 19', }, - ], - nteeOrganizationTypes: [ - { - name: 'test ntee organization type 1', - code: 'test ntee organization type code 1', - description: 'test ntee organization type 1 description', + to: { + name: 'test organization 20', }, - { - name: 'test ntee organization type 2', - code: 'test ntee organization type code 2', - description: 'test ntee organization type 2 description', + dateFrom: '2001-02-02', + dateTo: '2010-02-02', + amount: 20, + source: 'grant 1 source', + description: 'grant 1 description', + internalNotes: 'grant 1 internal notes', + legacyData: { + drupalId: 1, }, - { - name: 'test ntee organization type 3', - code: 'test ntee organization type code 3', - description: 'test ntee organization type 3 description', + federalAwardId: 'grant 1 federal award id', + nteeGrantTypes: [ + { + name: 'test ntee grant type 0', + description: 'test ntee grant type 0 description', + }, + { + name: 'test ntee grant type 1', + description: 'test ntee grant type 1 description', + }, + { + name: 'test ntee grant type 2', + description: 'test ntee grant type 2 description', + }, + ], + grantTags: [ + { + name: 'test grant tag 0', + description: 'test grant tag 0 description', + }, + { + name: 'test grant tag 1', + description: 'test grant tag 1 description', + }, + { + name: 'test grant tag 2', + description: 'test grant tag 2 description', + }, + ], + }, + ], + grantsReceived: [ + { + from: { + name: 'test organization 18', }, - { - name: 'test ntee organization type 4', - code: 'test ntee organization type code 4', - description: 'test ntee organization type 4 description', + to: { + name: 'test organization 19', }, - ], - organizationTags: [ - { - name: 'test organization tag 1', - description: 'test organization tag 1 description', + dateFrom: '2001-02-02', + dateTo: '2010-02-02', + amount: 20, + source: 'grant 1 source', + description: 'grant 1 description', + internalNotes: 'grant 1 internal notes', + legacyData: { + drupalId: 1, }, - { - name: 'test organization tag 2', - description: 'test organization tag 2 description', + federalAwardId: 'grant 1 federal award id', + nteeGrantTypes: [ + { + name: 'test ntee grant type 0', + description: 'test ntee grant type 0 description', + }, + { + name: 'test ntee grant type 1', + description: 'test ntee grant type 1 description', + }, + { + name: 'test ntee grant type 2', + description: 'test ntee grant type 2 description', + }, + ], + grantTags: [ + { + name: 'test grant tag 0', + description: 'test grant tag 0 description', + }, + { + name: 'test grant tag 1', + description: 'test grant tag 1 description', + }, + { + name: 'test grant tag 2', + description: 'test grant tag 2 description', + }, + ], + }, + { + from: { + name: 'test organization 20', }, - { - name: 'test organization tag 3', - description: 'test organization tag 3 description', + to: { + name: 'test organization 19', }, - { - name: 'test organization tag 4', - description: 'test organization tag 4 description', + dateFrom: '2001-02-02', + dateTo: '2010-02-02', + amount: 20, + source: 'grant 1 source', + description: 'grant 1 description', + internalNotes: 'grant 1 internal notes', + legacyData: { + drupalId: 1, }, - ], - }, + federalAwardId: 'grant 1 federal award id', + nteeGrantTypes: [ + { + name: 'test ntee grant type 0', + description: 'test ntee grant type 0 description', + }, + { + name: 'test ntee grant type 1', + description: 'test ntee grant type 1 description', + }, + { + name: 'test ntee grant type 2', + description: 'test ntee grant type 2 description', + }, + ], + grantTags: [ + { + name: 'test grant tag 0', + description: 'test grant tag 0 description', + }, + { + name: 'test grant tag 1', + description: 'test grant tag 1 description', + }, + { + name: 'test grant tag 2', + description: 'test grant tag 2 description', + }, + ], + }, + ], + nteeOrganizationTypes: [ + { + name: 'test ntee organization type 1', + code: 'test ntee organization type code 1', + description: 'test ntee organization type 1 description', + }, + { + name: 'test ntee organization type 2', + code: 'test ntee organization type code 2', + description: 'test ntee organization type 2 description', + }, + { + name: 'test ntee organization type 3', + code: 'test ntee organization type code 3', + description: 'test ntee organization type 3 description', + }, + { + name: 'test ntee organization type 4', + code: 'test ntee organization type code 4', + description: 'test ntee organization type 4 description', + }, + ], + organizationTags: [ + { + name: 'test organization tag 1', + description: 'test organization tag 1 description', + }, + { + name: 'test organization tag 2', + description: 'test organization tag 2 description', + }, + { + name: 'test organization tag 3', + description: 'test organization tag 3 description', + }, + { + name: 'test organization tag 4', + description: 'test organization tag 4 description', + }, + ], }, { - totalFunded: '40', - totalReceived: '40', + totalFunded: 40, + totalReceived: 40, grantdatesStart: '2001-02-02', grantdatesEnd: '2001-02-02', - organization: { - name: 'test organization 20', - ein: '20', - duns: '20', - stateCorpId: '20', - description: 'test organization 20 description!', - address: { - postalCode: '20', + name: 'test organization 20', + ein: '20', + duns: '20', + stateCorpId: '20', + description: 'test organization 20 description!', + address: { + postalCode: '20', + }, + links: [ + { + url: 'ftp://20', + description: 'a link', }, - links: [ - { - url: 'ftp://20', - description: 'a link', - }, - { - url: 'gopher://20', - description: 'another link', - }, - ], - founded: '2000-10-21', - dissolved: '2001-10-21', - legacyData: { - drupalId: 20, + { + url: 'gopher://20', + description: 'another link', }, - publicFunder: false, - grantsFunded: [ - { - from: { - name: 'test organization 20', - }, - to: { - name: 'test organization 19', - }, - dateFrom: '2001-02-02', - dateTo: '2010-02-02', - amount: 20, - source: 'grant 1 source', - description: 'grant 1 description', - internalNotes: 'grant 1 internal notes', - legacyData: { - drupalId: 1, - }, - federalAwardId: 'grant 1 federal award id', - nteeGrantTypes: [ - { - name: 'test ntee grant type 0', - description: 'test ntee grant type 0 description', - }, - { - name: 'test ntee grant type 1', - description: 'test ntee grant type 1 description', - }, - { - name: 'test ntee grant type 2', - description: 'test ntee grant type 2 description', - }, - ], - grantTags: [ - { - name: 'test grant tag 0', - description: 'test grant tag 0 description', - }, - { - name: 'test grant tag 1', - description: 'test grant tag 1 description', - }, - { - name: 'test grant tag 2', - description: 'test grant tag 2 description', - }, - ], + ], + founded: '2000-10-21', + dissolved: '2001-10-21', + legacyData: { + drupalId: 20, + }, + publicFunder: false, + grantsFunded: [ + { + from: { + name: 'test organization 20', }, - { - from: { - name: 'test organization 20', - }, - to: { - name: 'test organization 21', - }, - dateFrom: '2001-02-02', - dateTo: '2010-02-02', - amount: 20, - source: 'grant 1 source', - description: 'grant 1 description', - internalNotes: 'grant 1 internal notes', - legacyData: { - drupalId: 1, - }, - federalAwardId: 'grant 1 federal award id', - nteeGrantTypes: [ - { - name: 'test ntee grant type 0', - description: 'test ntee grant type 0 description', - }, - { - name: 'test ntee grant type 1', - description: 'test ntee grant type 1 description', - }, - { - name: 'test ntee grant type 2', - description: 'test ntee grant type 2 description', - }, - ], - grantTags: [ - { - name: 'test grant tag 0', - description: 'test grant tag 0 description', - }, - { - name: 'test grant tag 1', - description: 'test grant tag 1 description', - }, - { - name: 'test grant tag 2', - description: 'test grant tag 2 description', - }, - ], + to: { + name: 'test organization 19', }, - ], - grantsReceived: [ - { - from: { - name: 'test organization 19', - }, - to: { - name: 'test organization 20', - }, - dateFrom: '2001-02-02', - dateTo: '2010-02-02', - amount: 20, - source: 'grant 1 source', - description: 'grant 1 description', - internalNotes: 'grant 1 internal notes', - legacyData: { - drupalId: 1, - }, - federalAwardId: 'grant 1 federal award id', - nteeGrantTypes: [ - { - name: 'test ntee grant type 0', - description: 'test ntee grant type 0 description', - }, - { - name: 'test ntee grant type 1', - description: 'test ntee grant type 1 description', - }, - { - name: 'test ntee grant type 2', - description: 'test ntee grant type 2 description', - }, - ], - grantTags: [ - { - name: 'test grant tag 0', - description: 'test grant tag 0 description', - }, - { - name: 'test grant tag 1', - description: 'test grant tag 1 description', - }, - { - name: 'test grant tag 2', - description: 'test grant tag 2 description', - }, - ], + dateFrom: '2001-02-02', + dateTo: '2010-02-02', + amount: 20, + source: 'grant 1 source', + description: 'grant 1 description', + internalNotes: 'grant 1 internal notes', + legacyData: { + drupalId: 1, }, - { - from: { - name: 'test organization 21', - }, - to: { - name: 'test organization 20', - }, - dateFrom: '2001-02-02', - dateTo: '2010-02-02', - amount: 20, - source: 'grant 1 source', - description: 'grant 1 description', - internalNotes: 'grant 1 internal notes', - legacyData: { - drupalId: 1, - }, - federalAwardId: 'grant 1 federal award id', - nteeGrantTypes: [ - { - name: 'test ntee grant type 0', - description: 'test ntee grant type 0 description', - }, - { - name: 'test ntee grant type 1', - description: 'test ntee grant type 1 description', - }, - { - name: 'test ntee grant type 2', - description: 'test ntee grant type 2 description', - }, - ], - grantTags: [ - { - name: 'test grant tag 0', - description: 'test grant tag 0 description', - }, - { - name: 'test grant tag 1', - description: 'test grant tag 1 description', - }, - { - name: 'test grant tag 2', - description: 'test grant tag 2 description', - }, - ], + federalAwardId: 'grant 1 federal award id', + nteeGrantTypes: [ + { + name: 'test ntee grant type 0', + description: 'test ntee grant type 0 description', + }, + { + name: 'test ntee grant type 1', + description: 'test ntee grant type 1 description', + }, + { + name: 'test ntee grant type 2', + description: 'test ntee grant type 2 description', + }, + ], + grantTags: [ + { + name: 'test grant tag 0', + description: 'test grant tag 0 description', + }, + { + name: 'test grant tag 1', + description: 'test grant tag 1 description', + }, + { + name: 'test grant tag 2', + description: 'test grant tag 2 description', + }, + ], + }, + { + from: { + name: 'test organization 20', }, - ], - nteeOrganizationTypes: [ - { - name: 'test ntee organization type 1', - code: 'test ntee organization type code 1', - description: 'test ntee organization type 1 description', + to: { + name: 'test organization 21', }, - { - name: 'test ntee organization type 2', - code: 'test ntee organization type code 2', - description: 'test ntee organization type 2 description', + dateFrom: '2001-02-02', + dateTo: '2010-02-02', + amount: 20, + source: 'grant 1 source', + description: 'grant 1 description', + internalNotes: 'grant 1 internal notes', + legacyData: { + drupalId: 1, }, - { - name: 'test ntee organization type 3', - code: 'test ntee organization type code 3', - description: 'test ntee organization type 3 description', + federalAwardId: 'grant 1 federal award id', + nteeGrantTypes: [ + { + name: 'test ntee grant type 0', + description: 'test ntee grant type 0 description', + }, + { + name: 'test ntee grant type 1', + description: 'test ntee grant type 1 description', + }, + { + name: 'test ntee grant type 2', + description: 'test ntee grant type 2 description', + }, + ], + grantTags: [ + { + name: 'test grant tag 0', + description: 'test grant tag 0 description', + }, + { + name: 'test grant tag 1', + description: 'test grant tag 1 description', + }, + { + name: 'test grant tag 2', + description: 'test grant tag 2 description', + }, + ], + }, + ], + grantsReceived: [ + { + from: { + name: 'test organization 19', }, - { - name: 'test ntee organization type 4', - code: 'test ntee organization type code 4', - description: 'test ntee organization type 4 description', + to: { + name: 'test organization 20', }, - ], - organizationTags: [ - { - name: 'test organization tag 1', - description: 'test organization tag 1 description', + dateFrom: '2001-02-02', + dateTo: '2010-02-02', + amount: 20, + source: 'grant 1 source', + description: 'grant 1 description', + internalNotes: 'grant 1 internal notes', + legacyData: { + drupalId: 1, }, - { - name: 'test organization tag 2', - description: 'test organization tag 2 description', + federalAwardId: 'grant 1 federal award id', + nteeGrantTypes: [ + { + name: 'test ntee grant type 0', + description: 'test ntee grant type 0 description', + }, + { + name: 'test ntee grant type 1', + description: 'test ntee grant type 1 description', + }, + { + name: 'test ntee grant type 2', + description: 'test ntee grant type 2 description', + }, + ], + grantTags: [ + { + name: 'test grant tag 0', + description: 'test grant tag 0 description', + }, + { + name: 'test grant tag 1', + description: 'test grant tag 1 description', + }, + { + name: 'test grant tag 2', + description: 'test grant tag 2 description', + }, + ], + }, + { + from: { + name: 'test organization 21', }, - { - name: 'test organization tag 3', - description: 'test organization tag 3 description', + to: { + name: 'test organization 20', }, - { - name: 'test organization tag 4', - description: 'test organization tag 4 description', + dateFrom: '2001-02-02', + dateTo: '2010-02-02', + amount: 20, + source: 'grant 1 source', + description: 'grant 1 description', + internalNotes: 'grant 1 internal notes', + legacyData: { + drupalId: 1, }, - ], - }, + federalAwardId: 'grant 1 federal award id', + nteeGrantTypes: [ + { + name: 'test ntee grant type 0', + description: 'test ntee grant type 0 description', + }, + { + name: 'test ntee grant type 1', + description: 'test ntee grant type 1 description', + }, + { + name: 'test ntee grant type 2', + description: 'test ntee grant type 2 description', + }, + ], + grantTags: [ + { + name: 'test grant tag 0', + description: 'test grant tag 0 description', + }, + { + name: 'test grant tag 1', + description: 'test grant tag 1 description', + }, + { + name: 'test grant tag 2', + description: 'test grant tag 2 description', + }, + ], + }, + ], + nteeOrganizationTypes: [ + { + name: 'test ntee organization type 1', + code: 'test ntee organization type code 1', + description: 'test ntee organization type 1 description', + }, + { + name: 'test ntee organization type 2', + code: 'test ntee organization type code 2', + description: 'test ntee organization type 2 description', + }, + { + name: 'test ntee organization type 3', + code: 'test ntee organization type code 3', + description: 'test ntee organization type 3 description', + }, + { + name: 'test ntee organization type 4', + code: 'test ntee organization type code 4', + description: 'test ntee organization type 4 description', + }, + ], + organizationTags: [ + { + name: 'test organization tag 1', + description: 'test organization tag 1 description', + }, + { + name: 'test organization tag 2', + description: 'test organization tag 2 description', + }, + { + name: 'test organization tag 3', + description: 'test organization tag 3 description', + }, + { + name: 'test organization tag 4', + description: 'test organization tag 4 description', + }, + ], }, ], }, diff --git a/integration-tests/test-queries/sort-org-grants-by-date.js b/integration-tests/test-queries/sort-org-grants-by-date.js new file mode 100644 index 0000000..f5d7371 --- /dev/null +++ b/integration-tests/test-queries/sort-org-grants-by-date.js @@ -0,0 +1,57 @@ +export const query = ` +query sortOrgGrantsByDate { + organizations( + limit: 1 + offset: 0 + orderBy: countGrantsFrom + orderByDirection: DESC + ) { + name + grantsFunded(orderBy: dateFrom, orderByDirection: ASC) { + dateFrom + } + } +} +`; + +export const expected = { + data: { + organizations: [ + { + name: 'test organization 90', + grantsFunded: [ + { + dateFrom: '2001-02-02', + }, + { + dateFrom: '2001-02-02', + }, + { + dateFrom: '2001-03-03', + }, + { + dateFrom: '2001-03-03', + }, + { + dateFrom: '2001-04-04', + }, + { + dateFrom: '2001-04-04', + }, + { + dateFrom: '2001-05-05', + }, + { + dateFrom: '2001-05-05', + }, + { + dateFrom: '2001-06-06', + }, + { + dateFrom: '2001-06-06', + }, + ], + }, + ], + }, +}; diff --git a/package.json b/package.json index 99b1e72..b90780e 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "dependencies": { "bunyan": "^1.8.12", "config": "^2.0.1", + "dataloader": "1.4.0", "dataloader-sequelize": "1.7.7", + "decamelize": "2.0.0", "google-auth-library": "1.6.1", "graphql": "14.0.2", "graphql-bigint": "1.0.0", @@ -26,7 +28,6 @@ "pg": "^6.1.0", "pg-copy-streams": "1.2.0", "sequelize": "4.38.0", - "sqlite3": "^4.0.2", "toml": "^2.3.3" }, "devDependencies": { diff --git a/src/db/models/grant.ts b/src/db/models/grant.ts index 874cae8..2d234bc 100644 --- a/src/db/models/grant.ts +++ b/src/db/models/grant.ts @@ -1,4 +1,12 @@ import * as Sequelize from 'sequelize'; +import { escape } from 'sequelize/lib/sql-string'; +import * as decamelize from 'decamelize'; + +import { GraphQLInputObjectType, GraphQLNonNull, GraphQLString } from 'graphql'; + +import { Db } from './'; + +import { ledgerListArgs, MAX_LIMIT } from '../../helpers'; import { OrganizationInstance, OrganizationAttributes } from './organization'; import { NewsInstance, NewsAttributes } from './news'; @@ -53,54 +61,56 @@ export interface LegacyData { export type GrantInstance = Sequelize.Instance & GrantAttributes; +const grantColumns = { + uuid: { + type: Sequelize.UUID, + allowNull: true, + defaultValue: Sequelize.UUIDV4, + }, + from: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: 'organization', key: 'id' }, + }, + to: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: 'organization', key: 'id' }, + }, + dateFrom: { + type: Sequelize.DATEONLY, + allowNull: true, + field: 'date_from', + }, + dateTo: { + type: Sequelize.DATEONLY, + allowNull: true, + field: 'date_to', + }, + amount: { type: Sequelize.BIGINT, allowNull: true }, + source: { type: Sequelize.TEXT, allowNull: true }, + description: { type: Sequelize.TEXT, allowNull: true }, + internalNotes: { + type: Sequelize.TEXT, + allowNull: true, + field: 'internal_notes', + }, + legacyData: { + type: Sequelize.JSON, + allowNull: true, + field: 'legacy_data', + }, + federalAwardId: { + type: Sequelize.STRING, + allowNull: true, + field: 'federal_award_id', + }, +}; + export default (sequelize: Sequelize.Sequelize) => { let Grant = sequelize.define( 'Grant', - { - uuid: { - type: Sequelize.UUID, - allowNull: true, - defaultValue: Sequelize.UUIDV4, - }, - from: { - type: Sequelize.INTEGER, - allowNull: false, - references: { model: sequelize.models.organization, key: 'id' }, - }, - to: { - type: Sequelize.INTEGER, - allowNull: false, - references: { model: sequelize.models.organization, key: 'id' }, - }, - dateFrom: { - type: Sequelize.DATEONLY, - allowNull: true, - field: 'date_from', - }, - dateTo: { - type: Sequelize.DATEONLY, - allowNull: true, - field: 'date_to', - }, - amount: { type: Sequelize.BIGINT, allowNull: true }, - source: { type: Sequelize.TEXT, allowNull: true }, - description: { type: Sequelize.TEXT, allowNull: true }, - internalNotes: { - type: Sequelize.TEXT, - allowNull: true, - field: 'internal_notes', - }, - legacyData: { - type: Sequelize.JSON, - allowNull: true, - field: 'legacy_data', - }, - federalAwardId: { - type: Sequelize.STRING, - allowNull: true, - field: 'federal_award_id', - }, - }, + grantColumns, { createdAt: 'created_at', updatedAt: 'updated_at', @@ -161,3 +171,87 @@ export default (sequelize: Sequelize.Sequelize) => { return Grant; }; + +// Add a where stanza to a grant query +interface AddWhere { + (opts: any): string; +} + +export const grantResolver = (db: Db, grantAddWhere?: AddWhere) => async ( + opts, + { limit, offset, orderBy, orderByDirection, textLike = {} }, + context, + info +): Promise => { + let wheres: string[] = []; + + if (grantAddWhere) { + wheres = [grantAddWhere(opts)]; + } + + Object.keys(textLike).forEach(k => + wheres.push(`${decamelize(k)} ILIKE ${escape(textLike[k])}`) + ); + + const whereFragment = + wheres.length > 0 ? `WHERE ${wheres.join(' AND ')}` : ''; + + const results = await db.sequelize.query( + `SELECT g.* +FROM "grant" g +${whereFragment} +ORDER BY "${decamelize(orderBy)}" ${orderByDirection} +LIMIT :limit +OFFSET :offset`, + { + type: db.Sequelize.QueryTypes.SELECT, + model: db.Grant, + mapToModel: true, + replacements: { + limit: Math.min(limit, MAX_LIMIT), + offset, + }, + } + ); + + return results; +}; + +export const singleGrantResolver = getId => async ( + opts, + args, + context, + info +): Promise => { + return context.getGrantById.load(getId(opts)); +}; + +const textColumns = Object.keys(grantColumns).reduce((acc, cur) => { + if ( + grantColumns[cur].type == Sequelize.TEXT || + grantColumns[cur].type === Sequelize.STRING + ) { + return [...acc, cur]; + } + return acc; +}, []); + +const textLikeArgs = textColumns.reduce( + (acc, cur) => ({ + ...acc, + [cur]: { + type: GraphQLString, + }, + }), + {} +); + +export const grantArgs = { + ...ledgerListArgs('Grant', Object.keys(grantColumns)), + textLike: { + type: new GraphQLInputObjectType({ + name: 'TextLike', + fields: textLikeArgs, + }), + }, +}; diff --git a/src/db/models/grantTag.ts b/src/db/models/grantTag.ts index 035351a..d1f775a 100644 --- a/src/db/models/grantTag.ts +++ b/src/db/models/grantTag.ts @@ -1,4 +1,14 @@ import * as Sequelize from 'sequelize'; +import { escape } from 'sequelize/lib/sql-string'; +import * as decamelize from 'decamelize'; + +import { GraphQLFieldConfigMap } from 'graphql'; + +import * as GraphQLBigInt from 'graphql-bigint'; + +import { Db } from './'; + +import { ledgerListArgs, MAX_LIMIT } from '../../helpers'; import { AbstractDrupalTagAttributes } from './abstractDrupalTag'; @@ -10,6 +20,7 @@ export interface GrantTagAttributes extends AbstractDrupalTagAttributes { name: string; description?: string; drupalId?: number; + total?: number; createdAt?: string; updatedAt?: string; } @@ -17,23 +28,29 @@ export interface GrantTagAttributes extends AbstractDrupalTagAttributes { export type GrantTagInstance = Sequelize.Instance & GrantTagAttributes; +const grantTagColumns = { + uuid: { + type: Sequelize.UUID, + allowNull: true, + defaultValue: Sequelize.UUIDV4, + }, + name: { type: Sequelize.STRING, allowNull: false }, + description: { type: Sequelize.STRING, allowNull: true }, + drupalId: { + type: Sequelize.INTEGER, + allowNull: true, + field: 'drupal_id', + }, + total: { + type: new Sequelize.VIRTUAL(Sequelize.BIGINT), + field: 'total', + }, +}; + export default (sequelize: Sequelize.Sequelize) => { let GrantTag = sequelize.define( 'GrantTag', - { - uuid: { - type: Sequelize.UUID, - allowNull: true, - defaultValue: Sequelize.UUIDV4, - }, - name: { type: Sequelize.STRING, allowNull: false }, - description: { type: Sequelize.STRING, allowNull: true }, - drupalId: { - type: Sequelize.INTEGER, - allowNull: true, - field: 'drupal_id', - }, - }, + grantTagColumns, { createdAt: 'created_at', updatedAt: 'updated_at', @@ -59,3 +76,61 @@ export default (sequelize: Sequelize.Sequelize) => { return GrantTag; }; + +export const grantTagSpecialFields: GraphQLFieldConfigMap = { + total: { type: GraphQLBigInt }, +}; + +export const grantTagArgs = ledgerListArgs( + 'GrantTag', + Object.keys(grantTagColumns) +); + +interface GrantTagResolverOptions { + limitToGrantId: boolean; +} + +const defaultGrantTagResolverOptions = { + limitToGrantId: false, +}; + +export const grantTagResolver = ( + db: Db, + resolverOpts: GrantTagResolverOptions = defaultGrantTagResolverOptions +) => async ( + opts, + { limit, offset, orderBy, orderByDirection, uuid = null }, + context, + info +): Promise => { + let where = ''; + // Fetching only grant tags related to a specific grant + if (resolverOpts.limitToGrantId) { + where = `WHERE g.id=${escape(opts.dataValues.id)}`; + } else { + where = uuid ? `WHERE gt.uuid = ${escape(uuid)}` : ''; + } + + const results = await db.sequelize.query( + `SELECT gt.id, gt.uuid, gt.name, gt.description, SUM(g.amount) as total +FROM grant_tag gt +LEFT JOIN grant_grant_tag ggt ON gt.id=ggt.grant_tag_id +LEFT JOIN "grant" g ON ggt.grant_id=g.id +${where} +GROUP BY gt.id +ORDER BY "${decamelize(orderBy)}" ${orderByDirection} +LIMIT :limit +OFFSET :offset`, + { + type: db.Sequelize.QueryTypes.SELECT, + model: db.GrantTag, + mapToModel: true, + replacements: { + limit: Math.min(limit, MAX_LIMIT), + offset, + }, + } + ); + + return results; +}; diff --git a/src/db/models/nteeGrantType.ts b/src/db/models/nteeGrantType.ts index b75a0a5..c127a28 100644 --- a/src/db/models/nteeGrantType.ts +++ b/src/db/models/nteeGrantType.ts @@ -1,4 +1,14 @@ import * as Sequelize from 'sequelize'; +import { escape } from 'sequelize/lib/sql-string'; +import * as decamelize from 'decamelize'; + +import { GraphQLFieldConfigMap } from 'graphql'; + +import * as GraphQLBigInt from 'graphql-bigint'; + +import { Db } from './'; + +import { ledgerListArgs, MAX_LIMIT } from '../../helpers'; import { AbstractDrupalTagAttributes } from './abstractDrupalTag'; @@ -12,6 +22,7 @@ export interface NteeGrantTypeAttributes extends AbstractDrupalTagAttributes { name: string; description?: string; drupalId?: number; + total?: number; createdAt?: string; updatedAt?: string; } @@ -21,34 +32,36 @@ export type NteeGrantTypeInstance = Sequelize.Instance< > & NteeGrantTypeAttributes; +const nteeGrantTypeColumns = { + uuid: { + type: Sequelize.UUID, + allowNull: true, + defaultValue: Sequelize.UUIDV4, + }, + name: { type: Sequelize.STRING, allowNull: false }, + description: { type: Sequelize.STRING, allowNull: true }, + drupalId: { + type: Sequelize.INTEGER, + allowNull: true, + field: 'drupal_id', + }, + total: { + type: new Sequelize.VIRTUAL(Sequelize.BIGINT), + field: 'total', + }, +}; + export default (sequelize: Sequelize.Sequelize) => { let NteeGrantType = sequelize.define< NteeGrantTypeInstance, NteeGrantTypeAttributes - >( - 'NteeGrantType', - { - uuid: { - type: Sequelize.UUID, - allowNull: true, - defaultValue: Sequelize.UUIDV4, - }, - name: { type: Sequelize.STRING, allowNull: false }, - description: { type: Sequelize.STRING, allowNull: true }, - drupalId: { - type: Sequelize.INTEGER, - allowNull: true, - field: 'drupal_id', - }, - }, - { - createdAt: 'created_at', - updatedAt: 'updated_at', - underscored: true, - freezeTableName: true, - tableName: 'ntee_grant_type', - } - ); + >('NteeGrantType', nteeGrantTypeColumns, { + createdAt: 'created_at', + updatedAt: 'updated_at', + underscored: true, + freezeTableName: true, + tableName: 'ntee_grant_type', + }); NteeGrantType.associate = ({ Grant, @@ -66,3 +79,61 @@ export default (sequelize: Sequelize.Sequelize) => { return NteeGrantType; }; + +export const nteeGrantTypeSpecialFields: GraphQLFieldConfigMap = { + total: { type: GraphQLBigInt }, +}; + +export const nteeGrantTypeArgs = ledgerListArgs( + 'NteeGrantType', + Object.keys(nteeGrantTypeColumns) +); + +interface NteeGrantTypeResolverOptions { + limitToGrantId: boolean; +} + +const defaultNteeGrantTypeResolverOptions = { + limitToGrantId: false, +}; + +export const nteeGrantTypeResolver = ( + db: Db, + resolverOpts: NteeGrantTypeResolverOptions = defaultNteeGrantTypeResolverOptions +) => async ( + opts, + { limit, offset, orderBy, orderByDirection, uuid = null }, + context, + info +): Promise => { + let where = ''; + // Fetching only grant tags related to a specific grant + if (resolverOpts.limitToGrantId) { + where = `WHERE g.id=${escape(opts.dataValues.id)}`; + } else { + where = uuid ? `WHERE gt.uuid = ${escape(uuid)}` : ''; + } + + const results = await db.sequelize.query( + `SELECT gt.id, gt.uuid, gt.name, gt.code, gt.description, SUM(g.amount) as total +FROM ntee_grant_type gt +LEFT JOIN grant_ntee_grant_type ggt ON gt.id=ggt.ntee_grant_type_id +LEFT JOIN "grant" g ON ggt.grant_id=g.id +${where} +GROUP BY gt.id +ORDER BY "${decamelize(orderBy)}" ${orderByDirection} +LIMIT :limit +OFFSET :offset`, + { + type: db.Sequelize.QueryTypes.SELECT, + model: db.NteeGrantType, + mapToModel: true, + replacements: { + limit: Math.min(limit, MAX_LIMIT), + offset, + }, + } + ); + + return results; +}; diff --git a/src/db/models/nteeOrganizationType.ts b/src/db/models/nteeOrganizationType.ts index 6370a06..bb8e82e 100644 --- a/src/db/models/nteeOrganizationType.ts +++ b/src/db/models/nteeOrganizationType.ts @@ -1,4 +1,14 @@ import * as Sequelize from 'sequelize'; +import { escape } from 'sequelize/lib/sql-string'; +import * as decamelize from 'decamelize'; + +import { GraphQLFieldConfigMap } from 'graphql'; + +import * as GraphQLBigInt from 'graphql-bigint'; + +import { Db } from './'; + +import { ledgerListArgs, MAX_LIMIT } from '../../helpers'; import { AbstractDrupalTagAttributes } from './abstractDrupalTag'; @@ -15,6 +25,8 @@ export interface NteeOrganizationTypeAttributes code: string; description?: string; drupalId?: number; + totalFunded?: number; + totalReceived?: number; createdAt?: string; updatedAt?: string; } @@ -24,35 +36,41 @@ export type NteeOrganizationTypeInstance = Sequelize.Instance< > & NteeOrganizationTypeAttributes; +const nteeOrganizationTypeColumns = { + uuid: { + type: Sequelize.UUID, + allowNull: true, + defaultValue: Sequelize.UUIDV4, + }, + name: { type: Sequelize.STRING, allowNull: false }, + code: { type: Sequelize.STRING, allowNull: false }, + description: { type: Sequelize.STRING, allowNull: true }, + drupalId: { + type: Sequelize.INTEGER, + allowNull: true, + field: 'drupal_id', + }, + totalFunded: { + type: new Sequelize.VIRTUAL(Sequelize.BIGINT), + field: 'total_funded', + }, + totalReceived: { + type: new Sequelize.VIRTUAL(Sequelize.BIGINT), + field: 'total_received', + }, +}; + export default (sequelize: Sequelize.Sequelize) => { let NteeOrganizationType = sequelize.define< NteeOrganizationTypeInstance, NteeOrganizationTypeAttributes - >( - 'NteeOrganizationType', - { - uuid: { - type: Sequelize.UUID, - allowNull: true, - defaultValue: Sequelize.UUIDV4, - }, - name: { type: Sequelize.STRING, allowNull: false }, - code: { type: Sequelize.STRING, allowNull: false }, - description: { type: Sequelize.STRING, allowNull: true }, - drupalId: { - type: Sequelize.INTEGER, - allowNull: true, - field: 'drupal_id', - }, - }, - { - createdAt: 'created_at', - updatedAt: 'updated_at', - underscored: true, - freezeTableName: true, - tableName: 'ntee_organization_type', - } - ); + >('NteeOrganizationType', nteeOrganizationTypeColumns, { + createdAt: 'created_at', + updatedAt: 'updated_at', + underscored: true, + freezeTableName: true, + tableName: 'ntee_organization_type', + }); NteeOrganizationType.associate = ({ Organization, @@ -73,3 +91,66 @@ export default (sequelize: Sequelize.Sequelize) => { return NteeOrganizationType; }; + +export const nteeOrganizationTypeSpecialFields: GraphQLFieldConfigMap< + never, + never +> = { + totalReceived: { type: GraphQLBigInt }, + totalFunded: { type: GraphQLBigInt }, +}; + +export const nteeOrganizationTypeArgs = ledgerListArgs( + 'NteeOrganizationType', + Object.keys(nteeOrganizationTypeColumns) +); + +interface NteeOrganizationTypeResolverOptions { + limitToOrganizationId: boolean; +} + +const defaultNteeOrganizationTypeResolverOptions = { + limitToOrganizationId: false, +}; + +export const nteeOrganizationTypeResolver = ( + db: Db, + resolverOpts: NteeOrganizationTypeResolverOptions = defaultNteeOrganizationTypeResolverOptions +) => async ( + opts, + { limit, offset, orderBy, orderByDirection, uuid = null }, + context, + info +): Promise => { + let where = ''; + // Fetching only organization tags related to a specific organization + if (resolverOpts.limitToOrganizationId) { + where = `WHERE oot.organization_id=${escape(opts.dataValues.id)}`; + } else { + where = uuid ? `WHERE ot.uuid = ${escape(uuid)}` : ''; + } + + const results = await db.sequelize.query( + `SELECT ot.id, ot.uuid, ot.name, ot.code, ot.description, SUM(gf.amount) as "total_funded", SUM(gr.amount) as "total_received" +FROM ntee_organization_type ot +LEFT JOIN organization_ntee_organization_type oot ON ot.id=oot.ntee_organization_type_id +LEFT JOIN "grant" gf ON gf.from=oot.organization_id +LEFT JOIN "grant" gr ON gr.to=oot.organization_id +${where} +GROUP BY ot.id +ORDER BY "${decamelize(orderBy)}" ${orderByDirection} +LIMIT :limit +OFFSET :offset`, + { + type: db.Sequelize.QueryTypes.SELECT, + model: db.NteeOrganizationType, + mapToModel: true, + replacements: { + limit: Math.min(limit, MAX_LIMIT), + offset, + }, + } + ); + + return results; +}; diff --git a/src/db/models/organization.ts b/src/db/models/organization.ts index 784498e..04c5433 100644 --- a/src/db/models/organization.ts +++ b/src/db/models/organization.ts @@ -1,4 +1,21 @@ import * as Sequelize from 'sequelize'; +import { escape } from 'sequelize/lib/sql-string'; +import * as decamelize from 'decamelize'; +import * as DataLoader from 'dataloader'; + +import { Db } from './'; + +import { + GraphQLFieldConfigMap, + GraphQLInt, + GraphQLList, + GraphQLString, +} from 'graphql'; + +import * as GraphQLBigInt from 'graphql-bigint'; + +import { ledgerListArgs, specialCols, MAX_LIMIT } from '../../helpers'; + import { GrantInstance, GrantAttributes } from './grant'; import { NewsInstance, NewsAttributes } from './news'; import { @@ -31,6 +48,16 @@ export interface OrganizationAttributes { createdAt?: string; updatedAt?: string; + // From meta table join: + countGrantsFrom?: number; + countGrantsTo?: number; + countDistinctFunders?: number; + countDistinctRecipients?: number; + totalReceived?: number; + totalFunded?: number; + grantdatesStart?: Date; + grantdatesEnd?: Date; + // Relationships getOrganizationOrganizationTag?: Sequelize.BelongsToGetAssociationMixin< OrganizationTagInstance[] @@ -78,50 +105,82 @@ export interface LegacyData { export type OrganizationInstance = Sequelize.Instance & OrganizationAttributes; +const organizationColumns = { + uuid: { + type: Sequelize.UUID, + allowNull: true, + defaultValue: Sequelize.UUIDV4, + }, + name: { type: Sequelize.STRING, allowNull: false }, + ein: { type: Sequelize.STRING, allowNull: true }, + duns: { type: Sequelize.STRING, allowNull: true }, + stateCorpId: { + type: Sequelize.STRING, + allowNull: true, + field: 'state_corp_id', + }, + description: { type: Sequelize.TEXT, allowNull: true }, + address: { type: Sequelize.JSON, allowNull: true }, + links: { type: Sequelize.JSON, allowNull: true }, + founded: { type: Sequelize.DATEONLY, allowNull: true }, + dissolved: { type: Sequelize.DATEONLY, allowNull: true }, + legacyData: { + type: Sequelize.JSON, + allowNull: true, + field: 'legacy_data', + }, + publicFunder: { + type: Sequelize.BOOLEAN, + allowNull: true, + field: 'public_funder', + }, + + // From meta table join: + countGrantsFrom: { + type: new Sequelize.VIRTUAL(Sequelize.INTEGER), + field: 'count_grants_from', + }, + countGrantsTo: { + type: new Sequelize.VIRTUAL(Sequelize.INTEGER), + field: 'count_grants_to', + }, + countDistinctFunders: { + type: new Sequelize.VIRTUAL(Sequelize.INTEGER), + field: 'count_distinct_funders', + }, + countDistinctRecipients: { + type: new Sequelize.VIRTUAL(Sequelize.INTEGER), + field: 'count_distinct_recipients', + }, + totalReceived: { + type: new Sequelize.VIRTUAL(Sequelize.BIGINT), + field: 'total_received', + }, + totalFunded: { + type: new Sequelize.VIRTUAL(Sequelize.BIGINT), + field: 'total_funded', + }, + grantdatesStart: { + type: new Sequelize.VIRTUAL(Sequelize.DATEONLY), + field: 'grantdates_start', + }, + grantdatesEnd: { + type: new Sequelize.VIRTUAL(Sequelize.DATEONLY), + field: 'grantdates_end', + }, +}; + export default (sequelize: Sequelize.Sequelize) => { let Organization = sequelize.define< OrganizationInstance, OrganizationAttributes - >( - 'Organization', - { - uuid: { - type: Sequelize.UUID, - allowNull: true, - defaultValue: Sequelize.UUIDV4, - }, - name: { type: Sequelize.STRING, allowNull: false }, - ein: { type: Sequelize.STRING, allowNull: true }, - duns: { type: Sequelize.STRING, allowNull: true }, - stateCorpId: { - type: Sequelize.STRING, - allowNull: true, - field: 'state_corp_id', - }, - description: { type: Sequelize.TEXT, allowNull: true }, - address: { type: Sequelize.JSON, allowNull: true }, - links: { type: Sequelize.JSON, allowNull: true }, - founded: { type: Sequelize.DATEONLY, allowNull: true }, - dissolved: { type: Sequelize.DATEONLY, allowNull: true }, - legacyData: { - type: Sequelize.JSON, - allowNull: true, - field: 'legacy_data', - }, - publicFunder: { - type: Sequelize.BOOLEAN, - allowNull: true, - field: 'public_funder', - }, - }, - { - createdAt: 'created_at', - updatedAt: 'updated_at', - underscored: true, - freezeTableName: true, - tableName: 'organization', - } - ); + >('Organization', organizationColumns, { + createdAt: 'created_at', + updatedAt: 'updated_at', + underscored: true, + freezeTableName: true, + tableName: 'organization', + }); // Set up relations Organization.associate = ({ @@ -202,3 +261,152 @@ export default (sequelize: Sequelize.Sequelize) => { return Organization; }; + +/** + * These fields are defined as "virtual" in Sequelize + * and are the product of a JOIN with the organization_meta table + */ +export const organizationSpecialFields: GraphQLFieldConfigMap = { + countGrantsFrom: { type: GraphQLInt }, + countGrantsTo: { type: GraphQLInt }, + countDistinctFunders: { type: GraphQLInt }, + countDistinctRecipients: { type: GraphQLInt }, + totalReceived: { type: GraphQLBigInt }, + totalFunded: { type: GraphQLBigInt }, + grantdatesStart: { type: GraphQLString }, + grantdatesEnd: { type: GraphQLString }, +}; + +// Add a join to a organization query +interface AddJoin { + (opts: any): string; +} + +export const organizationResolver = (db: Db, orgAddJoin?: AddJoin) => async ( + opts, + { limit, offset, orderBy, orderByDirection, nameLike }, + context, + info +): Promise => { + let where = ''; + if (nameLike) { + where = `WHERE o.name ILIKE ${escape(nameLike)}`; + } + + let addedJoin = ''; + if (opts && orgAddJoin) { + addedJoin = orgAddJoin(opts); + } + + const results = await db.sequelize.query( + `SELECT o.*, ${specialCols(organizationSpecialFields, 'om').join(',')} +FROM organization o +LEFT JOIN organization_meta om ON o.id=om.id +${addedJoin} +${where} +ORDER BY "${decamelize(orderBy)}" ${orderByDirection} +LIMIT :limit +OFFSET :offset`, + { + type: db.Sequelize.QueryTypes.SELECT, + model: db.Organization, + mapToModel: true, + replacements: { + limit: Math.min(limit, MAX_LIMIT), + offset, + }, + } + ); + + return results; +}; + +export const createGetOrganizationByIdDataloader = ( + db: Db +): DataLoader => { + return new DataLoader( + async (ids: number[]): Promise => { + const results = await db.sequelize.query( + `SELECT o.*, ${specialCols(organizationSpecialFields, 'om').join(',')} + FROM organization o + LEFT JOIN organization_meta om ON o.id=om.id + WHERE o.id IN(:ids)`, + { + type: db.Sequelize.QueryTypes.SELECT, + model: db.Organization, + mapToModel: true, + replacements: { ids }, + } + ); + + const ordered = ids.map( + id => + results.find(o => o.id === id) || + new Error(`cannot find organization with id ${id}`) + ); + + return ordered; + } + ); +}; + +export const createGetOrganizationByUuidDataloader = ( + db: Db +): DataLoader => { + return new DataLoader( + async (uuids: number[]): Promise => { + const results = await db.sequelize.query( + `SELECT o.*, ${specialCols(organizationSpecialFields, 'om').join(',')} + FROM organization o + LEFT JOIN organization_meta om ON o.id=om.id + WHERE o.uuid IN(:uuids)`, + { + type: db.Sequelize.QueryTypes.SELECT, + model: db.Organization, + mapToModel: true, + replacements: { uuids }, + } + ); + + const ordered = uuids.map( + uuid => + results.find(o => o.uuid === uuid) || + new Error(`cannot find organization with uuid ${uuid}`) + ); + + return ordered; + } + ); +}; + +interface GetIdFromOpts { + (opts: any): number; +} + +export const singleOrganizationResolver = (getId?: GetIdFromOpts) => async ( + opts, + args, + context, + info +): Promise => { + if (getId) { + return context.getOrganizationById.load(getId(opts)); + } + + if (args.id) { + return context.getOrganizationById.load(args.id); + } + + if (args.uuid) { + return context.getOrganizationByUuid.load(args.uuid); + } + + throw new Error('must supply either getId or args with id or uuid property'); +}; + +export const organizationArgs = { + ...ledgerListArgs('Organization', Object.keys(organizationColumns)), + nameLike: { + type: GraphQLString, + }, +}; diff --git a/src/db/models/organizationTag.ts b/src/db/models/organizationTag.ts index b7036e2..fdeec62 100644 --- a/src/db/models/organizationTag.ts +++ b/src/db/models/organizationTag.ts @@ -1,4 +1,12 @@ import * as Sequelize from 'sequelize'; +import { escape } from 'sequelize/lib/sql-string'; +import * as decamelize from 'decamelize'; + +import { GraphQLFieldConfigMap } from 'graphql'; + +import * as GraphQLBigInt from 'graphql-bigint'; + +import { ledgerListArgs, MAX_LIMIT } from '../../helpers'; import { AbstractDrupalTagAttributes } from './abstractDrupalTag'; @@ -10,6 +18,8 @@ export interface OrganizationTagAttributes extends AbstractDrupalTagAttributes { name: string; description?: string; drupalId?: number; + totalFunded?: number; + totalReceived?: number; createdAt?: string; updatedAt?: string; } @@ -19,34 +29,40 @@ export type OrganizationTagInstance = Sequelize.Instance< > & OrganizationTagAttributes; +const organizationTagColumns = { + uuid: { + type: Sequelize.UUID, + allowNull: true, + defaultValue: Sequelize.UUIDV4, + }, + name: { type: Sequelize.STRING, allowNull: false }, + description: { type: Sequelize.STRING, allowNull: true }, + drupalId: { + type: Sequelize.INTEGER, + allowNull: true, + field: 'drupal_id', + }, + totalFunded: { + type: new Sequelize.VIRTUAL(Sequelize.BIGINT), + field: 'total_funded', + }, + totalReceived: { + type: new Sequelize.VIRTUAL(Sequelize.BIGINT), + field: 'total_received', + }, +}; + export default (sequelize: Sequelize.Sequelize) => { let OrganizationTag = sequelize.define< OrganizationTagInstance, OrganizationTagAttributes - >( - 'OrganizationTag', - { - uuid: { - type: Sequelize.UUID, - allowNull: true, - defaultValue: Sequelize.UUIDV4, - }, - name: { type: Sequelize.STRING, allowNull: false }, - description: { type: Sequelize.STRING, allowNull: true }, - drupalId: { - type: Sequelize.INTEGER, - allowNull: true, - field: 'drupal_id', - }, - }, - { - createdAt: 'created_at', - updatedAt: 'updated_at', - underscored: true, - freezeTableName: true, - tableName: 'organization_tag', - } - ); + >('OrganizationTag', organizationTagColumns, { + createdAt: 'created_at', + updatedAt: 'updated_at', + underscored: true, + freezeTableName: true, + tableName: 'organization_tag', + }); OrganizationTag.associate = ({ Organization, @@ -67,3 +83,66 @@ export default (sequelize: Sequelize.Sequelize) => { return OrganizationTag; }; + +export const organizationTagSpecialFields: GraphQLFieldConfigMap< + never, + never +> = { + totalReceived: { type: GraphQLBigInt }, + totalFunded: { type: GraphQLBigInt }, +}; + +export const organizationTagArgs = ledgerListArgs( + 'OrganizationTag', + Object.keys(organizationTagColumns) +); + +interface OrganizationTagResolverOptions { + limitToOrganizationId: boolean; +} + +const defaultOrganizationTagResolverOptions = { + limitToOrganizationId: false, +}; + +export const organizationTagResolver = ( + db, + resolverOpts: OrganizationTagResolverOptions = defaultOrganizationTagResolverOptions +) => async ( + opts, + { limit, offset, orderBy, orderByDirection, uuid = null }, + context, + info +): Promise => { + let where = ''; + // Fetching only organization tags related to a specific organization + if (resolverOpts.limitToOrganizationId) { + where = `WHERE oot.organization_id=${escape(opts.dataValues.id)}`; + } else { + where = uuid ? `WHERE ot.uuid = ${escape(uuid)}` : ''; + } + + const results = await db.sequelize.query( + `SELECT ot.id, ot.uuid, ot.name, ot.description, SUM(gf.amount) as "total_funded", SUM(gr.amount) as "total_received" +FROM organization_tag ot +LEFT JOIN organization_organization_tag oot ON ot.id=oot.organization_tag_id +LEFT JOIN "grant" gf ON gf.from=oot.organization_id +LEFT JOIN "grant" gr ON gr.to=oot.organization_id +${where} +GROUP BY ot.id +ORDER BY "${decamelize(orderBy)}" ${orderByDirection} +LIMIT :limit +OFFSET :offset`, + { + type: db.Sequelize.QueryTypes.SELECT, + model: db.OrganizationTag, + mapToModel: true, + replacements: { + limit: Math.min(limit, MAX_LIMIT), + offset, + }, + } + ); + + return results; +}; diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..1b4d361 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,82 @@ +import * as decamelize from 'decamelize'; + +import { + GraphQLEnumType, + GraphQLFieldConfigArgumentMap, + GraphQLFieldConfigMap, + GraphQLInt, + GraphQLString, +} from 'graphql'; + +export const MAX_LIMIT = 100; + +/** + * Produces a SQL column select fragment based on the special fields + * defined above. + * + * The provided field config will never have resolve or subscribe props, + * hence the . + */ +export const specialCols = ( + specialFields: GraphQLFieldConfigMap, + alias: string +) => + Object.keys(specialFields).map( + col => `${alias}.${decamelize(col)} AS "${decamelize(col)}"` + ); + +/** + * Helper to generate common list arguments. + * + * A little nicer than what graphql-sequelize offers: + * - order NULLs last + * - enum orderBy options + * + * Does not implement a MAX_LIMIT guard! + * That is the resolver's responsibility. + */ +export const ledgerListArgs = ( + name: string, + tableAttributes: string[] +): GraphQLFieldConfigArgumentMap => ({ + orderBy: { + type: new GraphQLEnumType({ + name: `orderBy${name}`, + values: tableAttributes.reduce( + (acc, cur) => ({ + ...acc, + [cur]: { value: cur }, + }), + {} + ), + }), + defaultValue: 'id', + description: 'sort results by given field', + }, + orderByDirection: { + type: new GraphQLEnumType({ + name: `orderByDirection${name}`, + values: { + ASC: { value: 'ASC NULLS LAST' }, + DESC: { value: 'DESC NULLS LAST' }, + }, + }), + defaultValue: 'ASC NULLS LAST', + description: 'sort direction', + }, + limit: { + type: GraphQLInt, + defaultValue: 10, + description: `Number of items to return, maximum ${MAX_LIMIT}`, + }, + offset: { + type: GraphQLInt, + defaultValue: 0, + }, + uuid: { + type: GraphQLString, + }, + id: { + type: GraphQLInt, + }, +}); diff --git a/src/scripts/grantImporter.ts b/src/scripts/grantImporter.ts index 7ff575d..cc215d9 100644 --- a/src/scripts/grantImporter.ts +++ b/src/scripts/grantImporter.ts @@ -118,4 +118,10 @@ export const doImport = async () => { } }; -doImport(); +doImport() + .then(() => process.exit(0)) + .catch(e => { + console.error('ERROR'); + console.error(e); + process.exit(1); + }); diff --git a/src/scripts/tagImporter.ts b/src/scripts/tagImporter.ts index 78cad8a..d877311 100644 --- a/src/scripts/tagImporter.ts +++ b/src/scripts/tagImporter.ts @@ -42,6 +42,7 @@ function doImport() { chunk = stream.read(); while (chunk !== null) { + // @ts-ignore await cfg.model.create(chunk); chunk = stream.read(); } diff --git a/src/server.ts b/src/server.ts index 3867e69..e91a5fd 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,3 +1,4 @@ +import * as Sequelize from 'sequelize'; import { GraphQLServer } from 'graphql-yoga'; import { createContext, EXPECTED_OPTIONS_KEY } from 'dataloader-sequelize'; import { @@ -11,78 +12,90 @@ import { graphql, GraphQLSchema, GraphQLObjectType, - GraphQLString, GraphQLInt, GraphQLList, } from 'graphql'; +import { escape } from 'sequelize/lib/sql-string'; + import * as GraphQLBigInt from 'graphql-bigint'; import { Db } from './db/models'; -export default function createServer(db: Db): GraphQLServer { - const orderByMultiResolver = (opts, args) => { - const options = { - order: [], - ...opts, - }; - - if (args.orderByMulti) { - options.order = options.order.concat( - args.orderByMulti.map(arg => [ - arg[0], - arg[1] === 'ASC' ? 'ASC NULLS LAST' : 'DESC NULLS LAST', - ]) - ); - } - - return options; - }; - - const organizationNameILikeResolver = (opts, args) => { - if (args.organizationNameILike) - opts.include = [ - { - required: true, - model: db.Organization, - where: { - name: { [db.sequelize.Op.iLike]: args.organizationNameILike }, - }, - }, - ]; - return opts; - }; +import { + organizationResolver, + organizationArgs, + organizationSpecialFields, + singleOrganizationResolver, + createGetOrganizationByIdDataloader, + createGetOrganizationByUuidDataloader, +} from './db/models/organization'; +import { grantResolver, grantArgs } from './db/models/grant'; +import { + grantTagResolver, + grantTagArgs, + grantTagSpecialFields, +} from './db/models/grantTag'; +import { + organizationTagResolver, + organizationTagArgs, + organizationTagSpecialFields, +} from './db/models/organizationTag'; +import { + nteeGrantTypeResolver, + nteeGrantTypeArgs, + nteeGrantTypeSpecialFields, +} from './db/models/nteeGrantType'; +import { + nteeOrganizationTypeResolver, + nteeOrganizationTypeArgs, + nteeOrganizationTypeSpecialFields, +} from './db/models/nteeOrganizationType'; +export default function createServer(db: Db): GraphQLServer { resolver.contextToOptions = { [EXPECTED_OPTIONS_KEY]: EXPECTED_OPTIONS_KEY }; + // Types const shallowOrganizationType = new GraphQLObjectType({ name: 'ShallowOrganization', description: 'An organization, without grants funded or received', fields: attributeFields(db.Organization, { exclude: ['id'] }), }); + const organizationTagType = new GraphQLObjectType({ + name: 'OrganizationTag', + description: 'Tag associated with an organization', + fields: { + ...attributeFields(db.OrganizationTag, { exclude: ['id'] }), + ...organizationTagSpecialFields, + }, + }); + const grantTagType = new GraphQLObjectType({ name: 'GrantTag', description: 'Tag associated with a grant', - fields: attributeFields(db.GrantTag, { exclude: ['id'] }), + fields: { + ...attributeFields(db.GrantTag, { exclude: ['id'] }), + ...grantTagSpecialFields, + }, }); const nteeGrantTypeType = new GraphQLObjectType({ name: 'NteeGrantType', description: 'NTEE classification of a grant', - fields: attributeFields(db.NteeGrantType, { exclude: ['id'] }), - }); - - const organizationTagType = new GraphQLObjectType({ - name: 'OrganizationTag', - description: 'Tag associated with an organization', - fields: attributeFields(db.OrganizationTag, { exclude: ['id'] }), + fields: { + ...attributeFields(db.NteeGrantType, { exclude: ['id'] }), + ...nteeGrantTypeSpecialFields, + }, }); const nteeOrganizationTypeType = new GraphQLObjectType({ name: 'NteeOrganizationType', description: 'NTEE classification of an organization', - fields: attributeFields(db.NteeOrganizationType, { exclude: ['id'] }), + fields: { + ...attributeFields(db.NteeOrganizationType, { exclude: ['id'] }), + ...nteeOrganizationTypeSpecialFields, + }, }); const personType = new GraphQLObjectType({ @@ -104,8 +117,8 @@ export default function createServer(db: Db): GraphQLServer { }, organization: { type: shallowOrganizationType, - // @ts-ignore - resolve: resolver(db.BoardTerm.Organization), + args: organizationArgs, + resolve: singleOrganizationResolver(), }, }, }); @@ -117,23 +130,23 @@ export default function createServer(db: Db): GraphQLServer { ...attributeFields(db.Grant, { exclude: ['id'] }), from: { type: shallowOrganizationType, - // @ts-ignore - resolve: resolver(db.Grant.Funder), + args: organizationArgs, + resolve: singleOrganizationResolver(opts => opts.get('from')), }, to: { type: shallowOrganizationType, - // @ts-ignore - resolve: resolver(db.Grant.Recipient), + args: organizationArgs, + resolve: singleOrganizationResolver(opts => opts.get('to')), }, nteeGrantTypes: { type: new GraphQLList(nteeGrantTypeType), - // @ts-ignore - resolve: resolver(db.Grant.NteeGrantTypes), + args: nteeGrantTypeArgs, + resolve: nteeGrantTypeResolver(db, { limitToGrantId: true }), }, grantTags: { type: new GraphQLList(grantTagType), - // @ts-ignore - resolve: resolver(db.Grant.GrantTags), + args: grantTagArgs, + resolve: grantTagResolver(db, { limitToGrantId: true }), }, amount: { type: GraphQLBigInt }, }, @@ -152,8 +165,14 @@ export default function createServer(db: Db): GraphQLServer { ...attributeFields(db.News, { exclude: ['id'] }), organizations: { type: new GraphQLList(shallowOrganizationType), - // @ts-ignore - resolve: resolver(db.News.Organizations), + args: organizationArgs, + resolve: organizationResolver( + db, + opts => + `INNER JOIN news_organizations no ON no.news_id=${ + opts.id + } AND no.organization_id=o.id` + ), }, grants: { type: new GraphQLList(grantType), @@ -168,6 +187,7 @@ export default function createServer(db: Db): GraphQLServer { description: 'An organization, duh', fields: { ...attributeFields(db.Organization, { exclude: ['id'] }), + ...organizationSpecialFields, boardTerms: { type: new GraphQLList(boardTermType), // @ts-ignore @@ -175,13 +195,13 @@ export default function createServer(db: Db): GraphQLServer { }, grantsFunded: { type: new GraphQLList(grantType), - // @ts-ignore - resolve: resolver(db.Organization.GrantsFunded), + args: grantArgs, + resolve: grantResolver(db, opts => `g.from=${opts.get('id')}`), }, grantsReceived: { type: new GraphQLList(grantType), - // @ts-ignore - resolve: resolver(db.Organization.GrantsReceived), + args: grantArgs, + resolve: grantResolver(db, opts => `g.to=${opts.get('id')}`), }, forms990: { type: new GraphQLList(form990Type), @@ -195,26 +215,15 @@ export default function createServer(db: Db): GraphQLServer { }, nteeOrganizationTypes: { type: new GraphQLList(nteeOrganizationTypeType), - // @ts-ignore - resolve: resolver(db.Organization.NteeOrganizationTypes), + args: nteeOrganizationTypeArgs, + resolve: nteeOrganizationTypeResolver(db, { + limitToOrganizationId: true, + }), }, organizationTags: { type: new GraphQLList(organizationTagType), - // @ts-ignore - resolve: resolver(db.Organization.OrganizationTags), - }, - }, - }); - - const organizationMetaType = new GraphQLObjectType({ - name: 'OrganizationMeta', - description: 'Extra org info', - fields: { - ...attributeFields(db.OrganizationMeta, { exclude: ['id'] }), - organization: { - type: organizationType, - // @ts-ignore - resolve: resolver(db.OrganizationMeta.Organization), + args: organizationTagArgs, + resolve: organizationTagResolver(db, { limitToOrganizationId: true }), }, }, }); @@ -229,17 +238,17 @@ export default function createServer(db: Db): GraphQLServer { name: 'Stats', description: 'gnl stats', fields: { - total_num_grants: { type: GraphQLInt }, - total_num_orgs: { type: GraphQLInt }, - total_grants_dollars: { type: GraphQLBigInt }, + totalNumGrants: { type: GraphQLInt }, + totalNumOrgs: { type: GraphQLInt }, + totalGrantsDollars: { type: GraphQLBigInt }, }, }), resolve: async () => { const results = await Promise.all( [ - 'SELECT COUNT(id) AS total_num_grants FROM "grant"', - 'SELECT COUNT(id) AS total_num_orgs FROM organization', - 'SELECT SUM(amount) AS total_grants_dollars FROM "grant"', + 'SELECT COUNT(id) AS totalNumGrants FROM "grant"', + 'SELECT COUNT(id) AS totalNumOrgs FROM organization', + 'SELECT SUM(amount) AS totalGrantsDollars FROM "grant"', ].map(q => db.sequelize.query(q, { type: db.Sequelize.QueryTypes.SELECT, @@ -254,9 +263,9 @@ export default function createServer(db: Db): GraphQLServer { type: organizationType, args: defaultArgs({ ...db.Organization, - primaryKeyAttributes: ['uuid'], + primaryKeyAttributes: ['id', 'uuid'], }), - resolve: resolver(db.Organization), + resolve: singleOrganizationResolver(), }, news: { type: new GraphQLList(newsType), @@ -268,37 +277,8 @@ export default function createServer(db: Db): GraphQLServer { }, organizations: { type: new GraphQLList(organizationType), - args: { - ...defaultListArgs(), - ...defaultArgs(db.Organization), - orderByMulti: { - type: new GraphQLList(new GraphQLList(GraphQLString)), - }, - }, - resolve: resolver(db.Organization, { - before: orderByMultiResolver, - }), - }, - organizationMetas: { - type: new GraphQLList(organizationMetaType), - args: { - ...defaultListArgs(), - ...defaultArgs(db.OrganizationMeta), - orderByMulti: { - type: new GraphQLList(new GraphQLList(GraphQLString)), - }, - organizationNameILike: { - type: GraphQLString, - }, - }, - resolve: resolver(db.OrganizationMeta, { - before: (opts, args) => { - return organizationNameILikeResolver( - orderByMultiResolver(opts, args), - args - ); - }, - }), + args: organizationArgs, + resolve: organizationResolver(db), }, grant: { type: grantType, @@ -316,17 +296,41 @@ export default function createServer(db: Db): GraphQLServer { }, resolve: resolver(db.Grant), }, + nteeOrganizationTypes: { + type: new GraphQLList(nteeOrganizationTypeType), + args: nteeOrganizationTypeArgs, + resolve: nteeOrganizationTypeResolver(db), + }, + organizationTags: { + type: new GraphQLList(organizationTagType), + args: organizationTagArgs, + resolve: organizationTagResolver(db), + }, + nteeGrantTypes: { + type: new GraphQLList(nteeGrantTypeType), + args: nteeGrantTypeArgs, + resolve: nteeGrantTypeResolver(db), + }, + grantTags: { + type: new GraphQLList(grantTagType), + args: grantTagArgs, + resolve: grantTagResolver(db), + }, }, }), }), context(req) { // For each request, create a DataLoader context for Sequelize to use const dataloaderContext = createContext(db.sequelize); + const getOrganizationById = createGetOrganizationByIdDataloader(db); + const getOrganizationByUuid = createGetOrganizationByUuidDataloader(db); // Using the same EXPECTED_OPTIONS_KEY, store the DataLoader context // in the global request context return { [EXPECTED_OPTIONS_KEY]: dataloaderContext, + getOrganizationById, + getOrganizationByUuid, }; }, }); diff --git a/yarn.lock b/yarn.lock index a09185d..af744b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2197,7 +2197,7 @@ dataloader-sequelize@1.7.7: lru-cache "^4.0.1" shimmer "^1.1.0" -dataloader@^1.2.0: +dataloader@1.4.0, dataloader@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== @@ -2223,6 +2223,13 @@ debug@^3.1.0: dependencies: ms "2.0.0" +decamelize@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" + integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg== + dependencies: + xregexp "4.0.0" + decamelize@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -5203,11 +5210,6 @@ nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== -nan@~2.10.0: - version "2.10.0" - resolved "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" - integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== - nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -5279,7 +5281,7 @@ node-notifier@^5.2.1: shellwords "^0.1.1" which "^1.3.0" -node-pre-gyp@^0.10.0, node-pre-gyp@^0.10.3: +node-pre-gyp@^0.10.0: version "0.10.3" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== @@ -6761,15 +6763,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sqlite3@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.2.tgz#1bbeb68b03ead5d499e42a3a1b140064791c5a64" - integrity sha512-51ferIRwYOhzUEtogqOa/y9supADlAht98bF/gbIi6WkzRJX6Yioldxbzj1MV4yV+LgdKD/kkHwFTeFXOG4htA== - dependencies: - nan "~2.10.0" - node-pre-gyp "^0.10.3" - request "^2.87.0" - sshpk@^1.7.0: version "1.10.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.1.tgz#30e1a5d329244974a1af61511339d595af6638b0" @@ -7475,6 +7468,11 @@ xmlbuilder@~9.0.1: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= +xregexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" + integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== + xtend@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"