diff --git a/hrm-domain/hrm-core/case/caseReindexService.ts b/hrm-domain/hrm-core/case/caseReindexService.ts index 09e47635..92ed5c79 100644 --- a/hrm-domain/hrm-core/case/caseReindexService.ts +++ b/hrm-domain/hrm-core/case/caseReindexService.ts @@ -68,14 +68,14 @@ export const reindexCasesStream = async ( }); this.push( - `${new Date().toISOString()},${accountSid},contad id: ${ + `${new Date().toISOString()}, ${accountSid}, case id: ${ caseObj.id } Success, MessageId ${MessageId} \n`, ); } catch (err) { this.push( - `${new Date().toISOString()},${accountSid},contad id: ${caseObj.id} Error: ${ + `${new Date().toISOString()}, ${accountSid}, case id: ${caseObj.id} Error: ${ err.message?.replace('"', '""') || String(err) }\n`, ); diff --git a/hrm-domain/hrm-core/contact/canPerformContactAction.ts b/hrm-domain/hrm-core/contact/canPerformContactAction.ts index 681873f8..756eddc7 100644 --- a/hrm-domain/hrm-core/contact/canPerformContactAction.ts +++ b/hrm-domain/hrm-core/contact/canPerformContactAction.ts @@ -168,11 +168,11 @@ export const canPerformViewContactAction = canPerformActionOnContact( ); const canRemoveFromCase = async ( - originalCaseId: string, + originalCaseId: number, { can, user, hrmAccountId, permissions }, ): Promise => { if (originalCaseId) { - const originalCaseObj = await getCase(parseInt(originalCaseId), hrmAccountId, { + const originalCaseObj = await getCase(originalCaseId, hrmAccountId, { can, user, permissions, @@ -190,11 +190,16 @@ const canConnectContact = canPerformActionOnContact( { can, user, hrmAccountId, body: { caseId: targetCaseId }, permissions }, ) => { if ( - !(await canRemoveFromCase(originalCaseId, { can, user, hrmAccountId, permissions })) + !(await canRemoveFromCase(originalCaseId, { + can, + user, + hrmAccountId, + permissions, + })) ) { return false; } - const targetCaseObj = await getCase(parseInt(targetCaseId), hrmAccountId, { + const targetCaseObj = await getCase(parseInt(targetCaseId, 10), hrmAccountId, { can, user, permissions, diff --git a/hrm-domain/hrm-core/contact/contactsReindexService.ts b/hrm-domain/hrm-core/contact/contactsReindexService.ts index 491b6e85..eb545d7d 100644 --- a/hrm-domain/hrm-core/contact/contactsReindexService.ts +++ b/hrm-domain/hrm-core/contact/contactsReindexService.ts @@ -60,16 +60,16 @@ export const reindexContactsStream = async ( }); this.push( - `${new Date().toISOString()},${accountSid},contad id: ${ + `${new Date().toISOString()}, ${accountSid}, contact id: ${ contact.id } Success, MessageId ${MessageId} \n`, ); } catch (err) { this.push( - `${new Date().toISOString()},${accountSid},contad id: ${contact.id} Error: ${ - err.message?.replace('"', '""') || String(err) - }\n`, + `${new Date().toISOString()}, ${accountSid}, contact id: ${ + contact.id + } Error: ${err.message?.replace('"', '""') || String(err)}\n`, ); } callback(); diff --git a/hrm-domain/hrm-core/unit-tests/contact/canPerformContactAction.test.ts b/hrm-domain/hrm-core/unit-tests/contact/canPerformContactAction.test.ts index 6c8c30d0..7327923b 100644 --- a/hrm-domain/hrm-core/unit-tests/contact/canPerformContactAction.test.ts +++ b/hrm-domain/hrm-core/unit-tests/contact/canPerformContactAction.test.ts @@ -253,7 +253,7 @@ describe('canDisconnectContact', () => { let contact = new ContactBuilder().withFinalizedAt(BASELINE_DATE).build(); beforeEach(() => { mockGetContactById.mockResolvedValue(contact); - contact.caseId = '123'; + contact.caseId = 123; }); test('can returns true to permit & case id not set on contact - permits', async () => { delete contact.caseId; diff --git a/hrm-domain/lambdas/search-index-consumer/messagesToPayloads.ts b/hrm-domain/lambdas/search-index-consumer/messagesToPayloads.ts index 2f38f83c..18a1eb5c 100644 --- a/hrm-domain/lambdas/search-index-consumer/messagesToPayloads.ts +++ b/hrm-domain/lambdas/search-index-consumer/messagesToPayloads.ts @@ -141,7 +141,7 @@ const generatePayloadFromContact = ( ...(ps[HRM_CASES_INDEX_TYPE] ?? []), { ...m, - documentId: parseInt(m.message.contact.caseId, 10), + documentId: m.message.contact.caseId, payload: { ...m.message, transcript: m.transcript }, indexHandler: 'updateScript', }, diff --git a/hrm-domain/packages/hrm-search-config/convertToIndexDocument.ts b/hrm-domain/packages/hrm-search-config/convertToIndexDocument.ts index 5cc37fab..1870dc4a 100644 --- a/hrm-domain/packages/hrm-search-config/convertToIndexDocument.ts +++ b/hrm-domain/packages/hrm-search-config/convertToIndexDocument.ts @@ -55,7 +55,7 @@ export const convertContactToContactDocument = ({ const contactDocument: ContactDocument = { accountSid, - id, + id: id.toString(), createdAt, updatedAt, createdBy, @@ -115,7 +115,7 @@ const convertCaseToCaseDocument = ({ const caseDocument: CaseDocument = { accountSid, - id, + id: id.toString(), createdAt, updatedAt, createdBy, diff --git a/hrm-domain/packages/hrm-search-config/convertToScriptUpdate.ts b/hrm-domain/packages/hrm-search-config/convertToScriptUpdate.ts index e5c6695c..21cb30c5 100644 --- a/hrm-domain/packages/hrm-search-config/convertToScriptUpdate.ts +++ b/hrm-domain/packages/hrm-search-config/convertToScriptUpdate.ts @@ -38,7 +38,7 @@ const convertContactToCaseScriptUpdate = ( const contactDocument = convertContactToContactDocument(payload); const documentUpdate: CreateIndexConvertedDocument = { - id: parseInt(caseId!, 10), + id: caseId!.toString(), accountSid, contacts: [contactDocument], }; @@ -66,9 +66,9 @@ const convertContactToCaseScriptUpdate = ( case 'remove': { const scriptUpdate: Script = { source: - 'def removeContact(int contactId, List contacts) { contacts.removeIf(contact -> contact.id == contactId); } removeContact(params.contactId, ctx._source.contacts);', + 'def removeContact(String contactId, List contacts) { contacts.removeIf(contact -> contact.id == contactId); } removeContact(params.contactId, ctx._source.contacts);', params: { - contactId: payload.contact.id, + contactId: payload.contact.id.toString(), }, }; diff --git a/hrm-domain/packages/hrm-search-config/generateElasticsearchQuery.ts b/hrm-domain/packages/hrm-search-config/generateElasticsearchQuery.ts index 8c4026e9..4d8f1e72 100644 --- a/hrm-domain/packages/hrm-search-config/generateElasticsearchQuery.ts +++ b/hrm-domain/packages/hrm-search-config/generateElasticsearchQuery.ts @@ -33,7 +33,6 @@ const BOOST_FACTORS = { case: 3, }; const MIN_SCORE = 0.1; -const MAX_INT = 2147483648 - 1; // 2^31 - 1, the max integer allowed by ElasticSearch integer type export const FILTER_ALL_CLAUSE: QueryDslQueryContainer[][] = [ [ @@ -61,21 +60,36 @@ export type DocumentTypeQueryParams = { [DocumentType.Case]: GenerateTDocQueryParams; }; -type GenerateTermQueryParams = { +type NestableQueryParams = { + parentPath?: string; +}; + +type GenerateTermQueryParams = { type: 'term'; term: string | boolean | number; + field: keyof DocumentTypeToDocument[TDoc]; boost?: number; -}; -type GenerateRangeQueryParams = { +} & NestableQueryParams; +type GenerateRangeQueryParams = { type: 'range'; + field: keyof DocumentTypeToDocument[TDoc]; ranges: { lt?: string; lte?: string; gt?: string; gte?: string }; -}; -type GenerateQueryStringQuery = { type: 'queryString'; query: string; boost?: number }; -type GenerateSimpleQueryStringQuery = { - type: 'simpleQueryString'; +} & NestableQueryParams; +type GenerateQueryStringQuery = { + type: 'queryString'; query: string; + field: keyof DocumentTypeToDocument[TDoc]; boost?: number; -}; +} & NestableQueryParams; +type SimpleQueryStringFields = { + field: keyof DocumentTypeToDocument[TDoc]; + boost?: number; +}[]; +type GenerateSimpleQueryStringQuery = { + type: 'simpleQueryString'; + query: string; + fields: SimpleQueryStringFields; +} & NestableQueryParams; type GenerateMustNotQueryParams = { type: 'mustNot'; innerQuery: DocumentTypeQueryParams[TDoc]; @@ -98,12 +112,12 @@ type GenerateNestedQueryParams = { }[keyof NestedDocumentsQueryParams[TDoc]]; type GenerateQueryParams = - | ({ field: keyof DocumentTypeToDocument[TDoc]; parentPath?: string } & ( - | GenerateTermQueryParams - | GenerateRangeQueryParams - | GenerateQueryStringQuery - | GenerateSimpleQueryStringQuery - )) + | ( + | GenerateTermQueryParams + | GenerateRangeQueryParams + | GenerateQueryStringQuery + | GenerateSimpleQueryStringQuery + ) | GenerateMustNotQueryParams | GenerateNestedQueryParams; @@ -113,6 +127,14 @@ const getFieldName = (p: { field: keyof T; parentPath?: string }) return `${prefix}${String(p.field)}`; }; +const getSimpleQueryStringFields = ( + p: GenerateSimpleQueryStringQuery, +) => { + const prefix = p.parentPath ? `${p.parentPath}.` : ''; + + return p.fields.map(f => `${prefix}${String(f.field)}${f.boost ? `^${f.boost}` : ''}`); +}; + /** Utility function that creates a filter based on a more human-readable representation */ export const generateESQuery = ( p: DocumentTypeQueryParams[TDoc], @@ -144,10 +166,9 @@ export const generateESQuery = ( case 'simpleQueryString': { return { simple_query_string: { - fields: [getFieldName(p)], + fields: getSimpleQueryStringFields(p as GenerateSimpleQueryStringQuery), // typecast to conform TS, only valid parameters should be accept query: p.query, flags: 'FUZZY|NEAR|PHRASE|WHITESPACE', - ...(p.boost && { boost: p.boost }), }, }; } @@ -193,64 +214,20 @@ type SearchParametersContact = { }; } & SearchPagination; -const generateQueriesFromId = ({ - searchTerm, - documentType, - parentPath, - boostFactor, - queryWrapper = p => p, -}: { - searchTerm: string; - boostFactor: number; - queryWrapper?: ( - p: DocumentTypeQueryParams[DocumentType], - ) => DocumentTypeQueryParams[DocumentType]; -} & { - documentType: TDoc; - parentPath?: string; -}): QueryDslQueryContainer[] => { - const terms = searchTerm.split(' '); - - if (terms.length > 1) { - return []; - } - - const parsed = Number.parseInt(searchTerm, 10); - // Ignore terms that are not entirely a number or greater than max int, as that breaks term queries against integer fields - if (Number.isNaN(parsed) || !Number.isInteger(parsed) || parsed > MAX_INT) { - return []; - } - - return [ - generateESQuery( - queryWrapper({ - documentType, - type: 'term', - term: parsed, - boost: boostFactor * BOOST_FACTORS.id, - field: 'id' as any, // typecast to conform TS, only valid parameters should be accept - parentPath, - }), - ), - ]; -}; - const generateQueryFromSearchTerms = ({ searchTerm, documentType, - field, + fields, parentPath, - boostFactor, queryWrapper = p => p, }: { searchTerm: string; - boostFactor: number; queryWrapper?: ( p: DocumentTypeQueryParams[DocumentType], ) => DocumentTypeQueryParams[DocumentType]; } & { documentType: TDoc; - field: keyof DocumentTypeToDocument[TDoc]; + fields: SimpleQueryStringFields; parentPath?: string; }): QueryDslQueryContainer => { const query = generateESQuery( @@ -258,8 +235,7 @@ const generateQueryFromSearchTerms = ({ documentType, type: 'simpleQueryString', query: searchTerm, - boost: boostFactor, - field: field as any, // typecast to conform TS, only valid parameters should be accept + fields: fields as any, // typecast to conform TS, only valid parameters should be accept parentPath, }), ); @@ -282,10 +258,9 @@ const generateTranscriptQueriesFromFilters = ({ }): QueryDslQueryContainer[] => { const query = generateQueryFromSearchTerms({ documentType: DocumentType.Contact, - field: 'transcript', + fields: [{ field: 'transcript', boost: BOOST_FACTORS.transcript }], searchTerm: searchParameters.searchTerm, parentPath: buildParams.parentPath, - boostFactor: BOOST_FACTORS.transcript, queryWrapper, }); @@ -331,17 +306,18 @@ const generateContactsQueriesFromFilters = ({ const queries = [ generateQueryFromSearchTerms({ documentType: DocumentType.Contact, - field: 'content', - searchTerm: searchParameters.searchTerm, - parentPath: buildParams.parentPath, - boostFactor: BOOST_FACTORS.contact, - queryWrapper, - }), - ...generateQueriesFromId({ - documentType: DocumentType.Contact, + fields: [ + { + field: 'content', + boost: BOOST_FACTORS.contact, + }, + { + field: 'id', + boost: BOOST_FACTORS.id * BOOST_FACTORS.contact, + }, + ], searchTerm: searchParameters.searchTerm, parentPath: buildParams.parentPath, - boostFactor: BOOST_FACTORS.contact, queryWrapper, }), ]; @@ -434,22 +410,29 @@ const generateCasesQueriesFromFilters = ({ const queries = [ generateQueryFromSearchTerms({ documentType: DocumentType.Case, - field: 'content', - searchTerm: searchParameters.searchTerm, - boostFactor: BOOST_FACTORS.case, - }), - ...generateQueriesFromId({ - documentType: DocumentType.Case, + fields: [ + { + field: 'content', + boost: BOOST_FACTORS.case, + }, + { + field: 'id', + boost: BOOST_FACTORS.id * BOOST_FACTORS.case, + }, + ], searchTerm: searchParameters.searchTerm, - boostFactor: BOOST_FACTORS.case, }), ]; const sectionsQuery = generateQueryFromSearchTerms({ documentType: DocumentType.CaseSection, - field: 'content', + fields: [ + { + field: 'content', + boost: BOOST_FACTORS.case, + }, + ], searchTerm: searchParameters.searchTerm, - boostFactor: BOOST_FACTORS.case, queryWrapper: p => ({ documentType: DocumentType.Case, type: 'nested', diff --git a/hrm-domain/packages/hrm-search-config/hrmIndexDocumentMappings/mappings.ts b/hrm-domain/packages/hrm-search-config/hrmIndexDocumentMappings/mappings.ts index ce9bff5b..cf088f97 100644 --- a/hrm-domain/packages/hrm-search-config/hrmIndexDocumentMappings/mappings.ts +++ b/hrm-domain/packages/hrm-search-config/hrmIndexDocumentMappings/mappings.ts @@ -39,7 +39,7 @@ const commonProperties = { // Properties shared by both types of documents, cases and contacts const rootProperties = { id: { - type: 'integer', + type: 'keyword', }, twilioWorkerId: { type: 'keyword', diff --git a/hrm-domain/packages/hrm-types/Contact.ts b/hrm-domain/packages/hrm-types/Contact.ts index 8351fcdd..563fc4e2 100644 --- a/hrm-domain/packages/hrm-types/Contact.ts +++ b/hrm-domain/packages/hrm-types/Contact.ts @@ -16,6 +16,7 @@ import { HrmAccountId, TwilioUserIdentifier, WorkerSID } from '@tech-matters/types'; import { ConversationMedia } from './ConversationMedia'; import { Referral } from './Referral'; +import { CaseService } from './Case'; /** * This and contained types are copied from Flex @@ -53,7 +54,7 @@ export type NewContactRecord = { taskId: string; channelSid?: string; serviceSid?: string; - caseId?: string; + caseId?: CaseService['id']; profileId?: number; identifierId?: number; }; diff --git a/package.json b/package.json index f8829cb3..e05613ba 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "scripts": { "admin-cli": "npx tsx hrm-domain/hrm-service/scripts/admin-cli.ts", "build": "tsc -b --verbose", + "clean": "tsc -b --clean", "build-and-start": "run-s build start", "build-and-start:service": "run-s build start:service", "start": "run-s start:db start:localstack start:es start:service", diff --git a/packages/elasticsearch-client/src/client.ts b/packages/elasticsearch-client/src/client.ts index 77b1fb92..8ccf8420 100644 --- a/packages/elasticsearch-client/src/client.ts +++ b/packages/elasticsearch-client/src/client.ts @@ -67,17 +67,12 @@ export type PassThroughConfig = { client: EsClient; }; -const getConfigSsmParameterKey = (indexType: string) => - `/${process.env.NODE_ENV}/${indexType}/${process.env.AWS_REGION}/elasticsearch_config`; - //TODO: type for config const getEsConfig = async ({ config, - indexType, ssmConfigParameter, }: { - config: ClientOptions | undefined; - indexType: string; + config?: ClientOptions | undefined; ssmConfigParameter?: string; }) => { console.log('config', config); @@ -102,8 +97,7 @@ const getEsConfig = async ({ return JSON.parse(await getSsmParameter(ssmConfigParameter)); } - // TODO: remove this if it's not used anymore? - return JSON.parse(await getSsmParameter(getConfigSsmParameterKey(indexType))); + throw new Error(`getEsConfig error: no valid config provided to initialize client`); }; /** @@ -123,7 +117,6 @@ export type IndexClient = { const getClientOrMock = async ({ config, index, - indexType, ssmConfigParameter, }: GetClientOrMockArgs) => { // TODO: mock client for unit tests @@ -132,9 +125,7 @@ const getClientOrMock = async ({ // return mock; // } - const client = new EsClient( - await getEsConfig({ config, indexType, ssmConfigParameter }), - ); + const client = new EsClient(await getEsConfig({ config, ssmConfigParameter })); return { client, index,