Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 108 additions & 3 deletions resources-domain/lambdas/s3-importer/src/uschMappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,69 @@ import type { AccountSID } from '@tech-matters/types';
import type { FlatResource } from '@tech-matters/resources-types';
import { parse } from 'date-fns';

// https://gist.github.com/mshafrir/2646763
const US_STATE_CODE_MAPPING = {
AL: 'Alabama',
AK: 'Alaska',
AS: 'American Samoa',
AZ: 'Arizona',
AR: 'Arkansas',
CA: 'California',
CO: 'Colorado',
CT: 'Connecticut',
DE: 'Delaware',
DC: 'District Of Columbia',
FM: 'Federated States Of Micronesia',
FL: 'Florida',
GA: 'Georgia',
GU: 'Guam',
HI: 'Hawaii',
ID: 'Idaho',
IL: 'Illinois',
IN: 'Indiana',
IA: 'Iowa',
KS: 'Kansas',
KY: 'Kentucky',
LA: 'Louisiana',
ME: 'Maine',
MH: 'Marshall Islands',
MD: 'Maryland',
MA: 'Massachusetts',
MI: 'Michigan',
MN: 'Minnesota',
MS: 'Mississippi',
MO: 'Missouri',
MT: 'Montana',
NE: 'Nebraska',
NV: 'Nevada',
NH: 'New Hampshire',
NJ: 'New Jersey',
NM: 'New Mexico',
NY: 'New York',
NC: 'North Carolina',
ND: 'North Dakota',
MP: 'Northern Mariana Islands',
OH: 'Ohio',
OK: 'Oklahoma',
OR: 'Oregon',
PW: 'Palau',
PA: 'Pennsylvania',
PR: 'Puerto Rico',
RI: 'Rhode Island',
SC: 'South Carolina',
SD: 'South Dakota',
TN: 'Tennessee',
TX: 'Texas',
UT: 'Utah',
VT: 'Vermont',
VI: 'Virgin Islands',
VA: 'Virginia',
WA: 'Washington',
WV: 'West Virginia',
WI: 'Wisconsin',
WY: 'Wyoming',
} as Record<string, string>;

/*
* This defines all the mapping logic to convert Childhelp resource to an Aselo resource.
* The mapping is defined as a tree of nodes.
Expand Down Expand Up @@ -93,6 +156,20 @@ export type UschExpandedResource = Partial<
}
>;

const lookupUsStateNameFromCode = ({
Country: country,
StateProvince: stateProvince,
}: UschExpandedResource): string | undefined => {
if (
['us', 'usa', 'unitedstates'].includes(
(country ?? '').toLowerCase().replace(/[.\s]/, ''),
)
) {
return US_STATE_CODE_MAPPING[stateProvince ?? ''] ?? stateProvince;
}
return stateProvince;
};

export const expandCsvLine = (csv: UschCsvResource): UschExpandedResource => {
const expanded = {
...csv,
Expand All @@ -113,8 +190,27 @@ export const USCH_MAPPING_NODE: MappingNode = {
Name: resourceFieldMapping('name'),
AlternateName: translatableAttributeMapping('alternateName', { language: 'en' }),
Address: attributeMapping('stringAttributes', 'address/street'),
City: attributeMapping('stringAttributes', 'address/city'),
StateProvince: attributeMapping('stringAttributes', 'address/province'),
City: attributeMapping('stringAttributes', 'address/city', {
value: ({ currentValue, rootResource }) =>
[
(rootResource as UschExpandedResource).Country,
(rootResource as UschExpandedResource).StateProvince,
currentValue,
].join('/'),
info: ({ currentValue, rootResource }) => ({
country: (rootResource as UschExpandedResource).Country,
stateProvince: lookupUsStateNameFromCode(rootResource as UschExpandedResource),
name: currentValue,
}),
}),
StateProvince: attributeMapping('stringAttributes', 'address/province', {
value: ({ currentValue, rootResource }) =>
`${(rootResource as UschExpandedResource).Country}/${currentValue}`,
info: ({ rootResource }) => ({
country: (rootResource as UschExpandedResource).Country,
name: lookupUsStateNameFromCode(rootResource as UschExpandedResource),
}),
}),
PostalCode: attributeMapping('stringAttributes', 'address/postalCode'),
Country: attributeMapping('stringAttributes', 'address/country'),
HoursOfOperation: translatableAttributeMapping('hoursOfOperation'),
Expand Down Expand Up @@ -189,7 +285,16 @@ export const USCH_MAPPING_NODE: MappingNode = {
Coverage: {
children: {
'{coverageIndex}': translatableAttributeMapping('coverage/{coverageIndex}', {
value: ({ currentValue }) => currentValue,
value: ({ currentValue }) =>
`United States/${currentValue.replace(/\s+-\s+/, '/')}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this treating all resources as United States resources? I don't get what's the intention of this templated string 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first look there weren't coverages for international resources so I was assuming the ones that existed were US. But there are some international coverages, but they are formatted '{country} - {stateProvince}' not '{stateProvince} - {city}'

Updated the mapping code to account for this

info: ({ currentValue }) => {
const [stateProvince, city] = currentValue.toString().split(/\s+-\s+/);
return {
country: 'United States',
stateProvince,
city,
};
},
language: 'en',
}),
},
Expand Down
2 changes: 1 addition & 1 deletion resources-domain/resources-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"test:service": "cross-env POSTGRES_PORT=5433 RDS_USERNAME=hrm RDS_PASSWORD=postgres RESOURCES_PASSWORD=resources-password run-s -c docker:compose:test:up db:create:schema test:service:ci:migrate test:service:ci:run docker:compose:test:down",
"test:service:ci": "RDS_USERNAME=rdsadmin RDS_PASSWORD=postgres RESOURCES_PASSWORD=resources-password run-s db:create:schema test:service:ci:migrate test:service:ci:run",
"test:service:ci:migrate": "node ./db-migrate",
"test:service:ci:run": "cross-env AWS_REGION=us-east-1 CI=true TWILIO_ACCOUNT_SID=ACxxx TWILIO_AUTH_TOKEN=xxxxxx SSM_ENDPOINT=http://mock-ssm/ jest --verbose --maxWorkers=1 --forceExit --coverage tests/service/adminSearch.test.ts",
"test:service:ci:run": "cross-env AWS_REGION=us-east-1 CI=true TWILIO_ACCOUNT_SID=ACxxx TWILIO_AUTH_TOKEN=xxxxxx SSM_ENDPOINT=http://mock-ssm/ jest --verbose --maxWorkers=1 --forceExit --coverage tests/service",
"test:coverage": "run-s docker:compose:test:up test:service:migrate test:coverage:run docker:compose:test:down",
"test:coverage:run": "cross-env POSTGRES_PORT=5433 AWS_REGION=us-east-1 jest --verbose --maxWorkers=1 --coverage",
"test:migrate": "run-s test:service:migrate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const referenceAttributeRoutes = () => {
const router: IRouter = Router();
const { getResourceReferenceAttributeList } = referenceAttributeService();

router.get('/:list', async (req, res) => {
router.get('/*', async (req, res) => {
const { valueStartsWith, language } = req.query;
const { list } = req.params;
const list = (req as any).params[0];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document what's this doing? Is params now a list or how is this working? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes /* returns a list, which is documented in Express, I'm not sure we should document standard API behaviour

const result = await getResourceReferenceAttributeList(
req.hrmAccountId as AccountSID,
list,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ export const getDistinctStringAttributes = async (
accountSid: AccountSID,
key: string,
language: string | undefined,
valueStartsWith: string | undefined,
): Promise<string[]> => {
const res = await db.task(async t =>
t.manyOrNone(SELECT_DISTINCT_RESOURCE_STRING_ATTRIBUTES_SQL, {
accountSid,
key,
language: language || undefined, // Ensure any falsy value is converted to undefined so to be NULL for the query
valueLikePattern: valueStartsWith ? `${valueStartsWith}%` : undefined,
}),
);
console.debug(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ const resourceRoutes = () => {
res.json(suggestions);
});

router.get('/list-string-attributes', async (req, res) => {
const { key, language } = req.query;
const listStringAttributesHandler = async (req: any, res: any) => {
const { key: queryKey, language, valueStartsWith } = req.query;
console.debug('PATH PARAMS', req.params);
const key = req.params[0] || queryKey;

if (!key || typeof key !== 'string') {
res.status(400).json({
Expand All @@ -84,10 +86,14 @@ const resourceRoutes = () => {
<AccountSID>req.hrmAccountId,
key,
language,
valueStartsWith,
);

res.json(attributes);
});
};

router.get('/list-string-attributes/*', listStringAttributesHandler);
router.get('/list-string-attributes', listStringAttributesHandler);
Comment on lines +99 to +100
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference of this two endpoints, when should each be used?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One specifies the key on the path, the other in a query item. I prefer using the path because it's more consistent with the reference-string-attributes API, but the query item form might be needed if we ever need to specify multiple keys for the same request.

Just being indecisive really, and it wasn't much extra code to support both forms


return router;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ export const resourceService = () => {
getDistinctResourceStringAttributes: async (
accountSid: AccountSID,
key: string,
language: string,
) => getDistinctStringAttributes(accountSid, key, language),
language: string | undefined,
valueStartsWith: string | undefined,
) => getDistinctStringAttributes(accountSid, key, language, valueStartsWith),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ WHERE r."accountSid" = $<accountSid> AND r."id" IN ($<resourceIds:csv>) AND r."d
`;

export const SELECT_DISTINCT_RESOURCE_STRING_ATTRIBUTES_SQL = `
SELECT DISTINCT("value") FROM "ResourceStringAttributes"
SELECT DISTINCT "value", "info" FROM "ResourceStringAttributes"
WHERE "accountSid" = $<accountSid> AND
"key" = $<key> AND
($<language> IS NULL OR "language"=$<language>)`;
($<language> IS NULL OR "language"=$<language>) AND
($<valueLikePattern> IS NULL OR "value" LIKE $<valueLikePattern>)`;
29 changes: 29 additions & 0 deletions resources-domain/resources-service/tests/service/clearDb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2021-2025 Technology Matters
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import { db } from '../../src/connection-pool';

export const clearDb = async () => {
await db.none(`
TRUNCATE resources."ResourceReferenceStringAttributeValues" CASCADE;
`);
await db.none(`
TRUNCATE resources."Resources" CASCADE;
`);
await db.none(`
TRUNCATE resources."Accounts";
`);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { db } from '../../src/connection-pool';
import range from './range';
import { parseISO, addHours, subHours, addSeconds, subSeconds } from 'date-fns';
import { AccountSID } from '@tech-matters/types';
import type {
import {
FlatResource,
ImportBatch,
ImportProgress,
Expand Down
Loading
Loading