From 3701ce7d6611eaa9901347e9b9fac4df5e7f71b4 Mon Sep 17 00:00:00 2001 From: Jesse Bickel Date: Wed, 6 May 2026 10:03:05 -0500 Subject: [PATCH 1/2] Update PDC API calls to use `BaseFieldsBundle` --- package-lock.json | 14 +++++++------- package.json | 2 +- src/pdc-api.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8dab5f4..f838c5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "yargs": "^17.7.2" }, "devDependencies": { - "@pdc/sdk": "^0.26.1", + "@pdc/sdk": "^0.35.1", "@types/node": "^24.12.2", "@types/yargs": "^17.0.32", "@typescript-eslint/eslint-plugin": "^5.62.0", @@ -264,9 +264,9 @@ } }, "node_modules/@pdc/sdk": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/@pdc/sdk/-/sdk-0.26.1.tgz", - "integrity": "sha512-NT/qq/oHGD1izMgJWO3l6H0ClZS5uyD79ux7pgbxKTJERWhhLTGZ6Nfnck6c1xH1q1KWiO4HWZHMtWB1cBZ0xQ==", + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@pdc/sdk/-/sdk-0.35.1.tgz", + "integrity": "sha512-EWUU1NzCJYc6mx04SLmfYzQJHRBcVioQwjQ6N6nEW3td8368qH/qariLhxO9hl9WUlXpmn232zfExS7pSRheXQ==", "dev": true, "license": "AGPL-3.0" }, @@ -4766,9 +4766,9 @@ } }, "@pdc/sdk": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/@pdc/sdk/-/sdk-0.26.1.tgz", - "integrity": "sha512-NT/qq/oHGD1izMgJWO3l6H0ClZS5uyD79ux7pgbxKTJERWhhLTGZ6Nfnck6c1xH1q1KWiO4HWZHMtWB1cBZ0xQ==", + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@pdc/sdk/-/sdk-0.35.1.tgz", + "integrity": "sha512-EWUU1NzCJYc6mx04SLmfYzQJHRBcVioQwjQ6N6nEW3td8368qH/qariLhxO9hl9WUlXpmn232zfExS7pSRheXQ==", "dev": true }, "@pinojs/redact": { diff --git a/package.json b/package.json index c9e6383..c79cfe4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "author": "Open Tech Strategies", "license": "AGPL-3.0-or-later", "devDependencies": { - "@pdc/sdk": "^0.26.1", + "@pdc/sdk": "^0.35.1", "@types/node": "^24.12.2", "@types/yargs": "^17.0.32", "@typescript-eslint/eslint-plugin": "^5.62.0", diff --git a/src/pdc-api.ts b/src/pdc-api.ts index a7fdf93..f8e86cc 100644 --- a/src/pdc-api.ts +++ b/src/pdc-api.ts @@ -2,6 +2,7 @@ import { client } from './client'; import type { AccessTokenSet } from './oidc'; import type { BaseField, ProposalBundle, ChangemakerBundle, SourceBundle, Source, + BaseFieldBundle, } from '@pdc/sdk'; const callPdcApi = async ( @@ -27,10 +28,13 @@ const callPdcApi = async ( }; const getBaseFields = (baseUrl: string, token: AccessTokenSet) => ( - callPdcApi( + callPdcApi( baseUrl, '/baseFields', - {}, + { + _page: '1', + _count: '2147483647', + }, 'get', token, ) From 8cf4ef241b14d81e058903f7e8b79e788c10113b Mon Sep 17 00:00:00 2001 From: Jesse Bickel Date: Wed, 6 May 2026 10:59:14 -0500 Subject: [PATCH 2/2] Add `lookupFromPdc` command In order to run `updateAll`, the ingester needs permission to update all the changemakers. But not all changemakers that are present in PDC have EINs that can be found in Charity Navigator. So before running a modify command (`updateAll`) it is useful to run this new read-only command (`lookupFromPdc`) to list the exact changemakers on which the ingest user needs permissions. --- src/charityNavigator.ts | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/charityNavigator.ts b/src/charityNavigator.ts index 25d544a..cae73d1 100644 --- a/src/charityNavigator.ts +++ b/src/charityNavigator.ts @@ -160,6 +160,12 @@ interface LookupCommandArgs { outputFile?: string; } +interface LookupFromPdcCommandArgs { + 'charity-navigator-api-key'?: string; + 'pdc-api-base-url': string; + outputFile?: string; +} + interface UpdateAllCommandArgs { 'charity-navigator-api-key'?: string; 'oidc-base-url': string, @@ -237,6 +243,66 @@ const getChangemakerByEin = (ein: string, changemakers: ChangemakerBundle): Chan throw new Error('How could this have happened?'); }; +const lookupFromPdcCommand: CommandModule = { + command: 'lookupFromPdc', + describe: 'Fetch and display information about organizations present in PDC', + builder: (y) => (y + .option('charity-navigator-api-key', { + describe: 'CharityNavigator API key; get from account management at https://developer.charitynavigator.org/ (can also be set via DS_CHARITY_NAVIGATOR_API_KEY env var)', + demandOption: false, + type: 'string', + }) + .check((argv) => { + if (!argv.charityNavigatorApiKey) { + throw new Error('Missing required argument: charity-navigator-api-key (set via CLI or DS_CHARITY_NAVIGATOR_API_KEY env var)'); + } + return true; + }) + .option('output-file', { + alias: 'write', + describe: 'Write organization information to the specified JSON file', + normalize: true, + type: 'string', + }) + .option('pdc-api-base-url', { + describe: 'Location of PDC API', + demandOption: true, + type: 'string', + }) + ), + handler: async (args) => { + const { charityNavigatorApiKey: apiKey, pdcApiBaseUrl } = args; + if (!apiKey) { + throw new Error('Missing required argument: charity-navigator-api-key'); + } + if (!pdcApiBaseUrl) { + throw new Error('Missing required argument: pdc-api-base-url'); + } + const changemakers = await getChangemakers(args.pdcApiBaseUrl); + const eins = changemakers.entries.flatMap((c) => c.taxId); + // Charity Navigator expects no hyphens, strip them from EINs after validation. + const validEins = eins.filter(isValidEin).flatMap((e) => e.replace('-', '')); + const invalidEins = eins.filter((e) => !isValidEin(e)); + if (invalidEins.length > 0) { + logger.warn(invalidEins, 'These EINs in PDC are invalid and will not be queried'); + } + logger.info(validEins, 'Found these valid EINs which will be requested from Charity Navigator'); + const charityNavResponse = await getCharityNavigatorProfiles( + apiKey, + validEins, + ); + if (args.outputFile) { + await writeFile( + args.outputFile, + JSON.stringify(charityNavResponse, null, 2), + ); + logger.info(`Wrote CharityNavigator data for ${JSON.stringify(args.ein)} to ${JSON.stringify(args.outputFile)}`); + } else { + logger.info({ charityNavResponse }, 'CharityNavigator result'); + } + }, +}; + const getOrCreateSource = async (baseUrl: string, token: AccessTokenSet): Promise => { const sources = await getSources(baseUrl, token); const filteredSources = sources.entries.filter((s) => s.dataProviderShortCode === CN_SHORT_CODE); @@ -336,6 +402,7 @@ const charityNavigator: CommandModule = { describe: 'Interact with the CharityNavigator Premier API', builder: (y) => (y .command(lookupCommand) + .command(lookupFromPdcCommand) .command(updateAllCommand) .demandCommand(1) ),