diff --git a/__tests__/table-streams/synonyms.ts b/__tests__/table-streams/synonyms.ts index 67ecbf6c0..3f9e366f1 100644 --- a/__tests__/table-streams/synonyms.ts +++ b/__tests__/table-streams/synonyms.ts @@ -8,13 +8,14 @@ import { tables } from '@architect/functions' import { search } from '@nasa-gcn/architect-functions-search' import type { DynamoDBRecord } from 'aws-lambda' +import crypto from 'crypto' -import type { Synonym } from '~/routes/synonyms/synonyms.lib' import { handler } from '~/table-streams/synonyms/index' -const synonymId = 'abcde-abcde-abcde-abcde-abcde' +const synonymId = crypto.randomUUID() const eventId = 'GRB 123' const existingEventId = 'GRB 99999999' +const previousSynonymId = crypto.randomUUID() const putData = { index: 'synonym-groups', @@ -48,6 +49,7 @@ const mockIndex = jest.fn() const mockDelete = jest.fn() const mockQuery = jest.fn() +const eventSlug = eventId.replace(' ', '-').toLowerCase() const mockStreamEvent = { Records: [ { @@ -64,6 +66,9 @@ const mockStreamEvent = { eventId: { S: eventId, }, + slug: { + S: eventSlug, + }, }, NewImage: { synonymId: { @@ -72,6 +77,9 @@ const mockStreamEvent = { eventId: { S: eventId, }, + slug: { + S: eventSlug, + }, }, SequenceNumber: '111', SizeBytes: 26, @@ -88,8 +96,12 @@ afterEach(() => { }) describe('testing put synonymGroup table-stream', () => { - test('insert new synonym group', async () => { - const mockItems = [{ synonymId, eventId }] + test('insert initial synonym record where no existing opensearch record exists', async () => { + const mockSearch = jest + .fn() + .mockReturnValue({ body: { hits: { hits: [] } } }) + const slug = eventId.replace(' ', '-').toLowerCase() + const mockItems = [{ synonymId, eventId, slug }] const mockClient = { synonyms: { query: mockQuery, @@ -101,103 +113,79 @@ describe('testing put synonymGroup table-stream', () => { ;(search as unknown as jest.Mock).mockReturnValue({ index: mockIndex, delete: mockDelete, + search: mockSearch, }) await handler(mockStreamEvent) - + putData.body.slugs.push(slug) expect(mockIndex).toHaveBeenCalledWith(putData) }) - test('insert into existing synonym group', async () => { - const mockItems = [ - { synonymId, eventId: existingEventId }, - { synonymId, eventId }, - ] - const mockClient = { - synonyms: { - query: mockQuery, - }, + test('insert into existing synonym group with removal of now unused group', async () => { + const existingEventSlug = existingEventId.replace(' ', '-').toLowerCase() + const deleteData = { + index: 'synonym-groups', + id: previousSynonymId, } - mockQuery.mockResolvedValue({ Items: mockItems }) - ;(tables as unknown as jest.Mock).mockResolvedValue(mockClient) - putData.body.eventIds = [existingEventId, eventId] - ;(search as unknown as jest.Mock).mockReturnValue({ - index: mockIndex, - delete: mockDelete, - }) - await handler(mockStreamEvent) - - expect(mockIndex).toHaveBeenCalledWith(putData) - }) - - test('insert only once', async () => { - const mockItems = [ - { synonymId, eventId: existingEventId }, - { synonymId, eventId }, - ] - const mockClient = { - synonyms: { - query: mockQuery, + const mockSearch = jest.fn().mockReturnValue({ + body: { + hits: { + hits: [ + { + _source: { + synonymId: previousSynonymId, + eventIds: [eventId], + slugs: [eventSlug], + }, + }, + ], + }, }, - } - mockQuery.mockResolvedValue({ Items: mockItems }) - ;(tables as unknown as jest.Mock).mockResolvedValue(mockClient) - putData.body.eventIds = [existingEventId, eventId] - ;(search as unknown as jest.Mock).mockReturnValue({ - index: mockIndex, - delete: mockDelete, }) - await handler(mockStreamEvent) + const mockItems = [ + { synonymId, eventId: existingEventId, slug: existingEventSlug }, + { synonymId, eventId, slug: eventSlug }, + ] - expect(mockIndex).toHaveBeenCalledWith(putData) - }) -}) + const mockPreviousItems = [ + { synonymId: previousSynonymId, eventId, slug: eventSlug }, + ] -describe('testing delete synonymGroup table-stream', () => { - test('remove one eventId while leaving others', async () => { - const mockItems = [{ synonymId, eventId: existingEventId }] - const mockClient = { - synonyms: { - query: mockQuery, - }, - } - mockQuery.mockResolvedValue({ Items: mockItems }) - ;(tables as unknown as jest.Mock).mockResolvedValue(mockClient) - mockStreamEvent.Records[0].eventName = 'REMOVE' - putData.body.eventIds = [existingEventId] + const implementedMockQuery = mockQuery.mockImplementation((query) => { + if (query.ExpressionAttributeValues[':synonymId'] == previousSynonymId) { + return { Items: mockPreviousItems } + } else { + return { Items: mockItems } + } + }) ;(search as unknown as jest.Mock).mockReturnValue({ index: mockIndex, delete: mockDelete, + search: mockSearch, }) - - await handler(mockStreamEvent) - - expect(mockIndex).toHaveBeenCalledWith(putData) - }) - - test('remove final synonym and delete synonym group', async () => { - const mockItems = [] as Synonym[] const mockClient = { synonyms: { - query: mockQuery, + query: implementedMockQuery, }, } - mockQuery.mockResolvedValue({ Items: mockItems }) + ;(tables as unknown as jest.Mock).mockResolvedValue(mockClient) - mockStreamEvent.Records[0].eventName = 'REMOVE' - const deleteData = { - index: 'synonym-groups', - id: synonymId, - } - ;(search as unknown as jest.Mock).mockReturnValue({ - index: mockIndex, - delete: mockDelete, + putData.body.eventIds = [existingEventId, eventId] + putData.body.slugs = [existingEventSlug, eventSlug] + + mockQuery.mockImplementation((query) => { + if (query.ExpressionAttributeValues[':synonymId'] == previousSynonymId) { + return { Items: mockPreviousItems } + } else { + return { Items: mockItems } + } }) - await handler(mockStreamEvent) - + // the new group opensearch record is updated + expect(mockIndex).toHaveBeenCalledWith(putData) + // the old group opensearch record is deleted expect(mockDelete).toHaveBeenCalledWith(deleteData) }) }) diff --git a/app/routes/synonyms/synonyms.server.ts b/app/routes/synonyms/synonyms.server.ts index a19acdec5..e5f1fc3ae 100644 --- a/app/routes/synonyms/synonyms.server.ts +++ b/app/routes/synonyms/synonyms.server.ts @@ -106,7 +106,6 @@ export async function searchSynonymsByEventId({ fields: { eventIds: string[]; synonymId: string; slugs: string[] } }) => body ) - return { items: results, totalItems, @@ -139,7 +138,7 @@ export async function opensearchKeywordSearch({ if (response.body.hits.hits.length > 0) { return response.body.hits.hits[0]._source } else { - return null + return [] } } diff --git a/app/table-streams/synonyms/index.ts b/app/table-streams/synonyms/index.ts index fc3b81563..68ecd4ae4 100644 --- a/app/table-streams/synonyms/index.ts +++ b/app/table-streams/synonyms/index.ts @@ -50,12 +50,24 @@ export const handler = createTriggerHandler( ) as Synonym const dynamoSynonyms = await getSynonymsByUuid(synonymId) const opensearchSynonym = await opensearchKeywordSearch({ eventId }) - const previousSynonymId = opensearchSynonym.synonymId - const dynamoPreviousGroup = ( - await getSynonymsByUuid(previousSynonymId) - ).filter((synonym) => synonym.eventId != eventId) + const previousSynonymId = opensearchSynonym + ? opensearchSynonym.synonymId + : null + const dynamoPreviousGroup = previousSynonymId + ? (await getSynonymsByUuid(previousSynonymId)).filter( + (synonym) => synonym.eventId != eventId + ) + : [] - if (dynamoPreviousGroup.length === 0) await removeIndex(previousSynonymId) + if (dynamoPreviousGroup.length === 0 && previousSynonymId) { + await removeIndex(previousSynonymId) + } else { + await putIndex({ + synonymId: previousSynonymId, + eventIds: dynamoPreviousGroup.map((synonym) => synonym.eventId), + slugs: dynamoPreviousGroup.map((synonym) => synonym.slug), + }) + } if (dynamoSynonyms.length > 0) { await putIndex({