Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

updated synonymGroups table-stream with tests #2505

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
204 changes: 204 additions & 0 deletions __tests__/table-streams/synonyms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { tables } from '@architect/functions'
import { search } from '@nasa-gcn/architect-functions-search'
import type { DynamoDBRecord } from 'aws-lambda'

import type { Synonym } from '~/routes/synonyms/synonyms.lib'
import { handler } from '~/table-streams/synonyms/index'

const synonymId = 'abcde-abcde-abcde-abcde-abcde'
const eventId = 'GRB 123'
const existingEventId = 'GRB 99999999'

const putData = {
index: 'synonym-groups',
id: synonymId,
body: {
synonymId,
eventIds: [] as string[],
},
}

jest.mock('@nasa-gcn/architect-functions-search', () => ({
search: jest.fn(),
}))

jest.mock('@architect/functions', () => ({
tables: jest.fn(),
}))

const mockIndex = jest.fn()
const mockDelete = jest.fn()
const mockGet = jest.fn()
const mockQuery = jest.fn()

const mockStreamEvent = {
Records: [
{
eventID: '1',
eventName: 'INSERT',
eventVersion: '1.0',
eventSource: 'aws:dynamodb',
awsRegion: 'us-west-2',
dynamodb: {
Keys: {
synonymId: {
S: synonymId,
},
eventId: {
S: eventId,
},
},
NewImage: {
synonymId: {
S: synonymId,
},
eventId: {
S: eventId,
},
},
SequenceNumber: '111',
SizeBytes: 26,
StreamViewType: 'NEW_IMAGE',
},
eventSourceARN:
'arn:aws:dynamodb:us-west-2:123456789012:table/synonym-groups/stream/2020-01-01T00:00:00.000',
} as DynamoDBRecord,
],
}

afterEach(() => {
jest.clearAllMocks()
})

describe('testing put synonymGroup table-stream', () => {
test('insert new synonym group', async () => {
const mockItems = [{ synonymId, eventId }]
const mockClient = {
synonyms: {
query: mockQuery,
},
}
mockQuery.mockResolvedValue({ Items: mockItems })
;(tables as unknown as jest.Mock).mockResolvedValue(mockClient)
putData.body.eventIds = [eventId]
mockGet.mockResolvedValue(null)
;(search as unknown as jest.Mock).mockReturnValue({
index: mockIndex,
delete: mockDelete,
get: mockGet,
})

await handler(mockStreamEvent)

expect(mockIndex).toHaveBeenCalledWith(putData)
})

test('insert into existing synonym group', async () => {
const mockItems = [
{ synonymId, eventId: existingEventId },
{ synonymId, eventId },
]
const mockClient = {
synonyms: {
query: mockQuery,
},
}
mockQuery.mockResolvedValue({ Items: mockItems })
;(tables as unknown as jest.Mock).mockResolvedValue(mockClient)
putData.body.eventIds = [existingEventId, eventId]
mockGet.mockResolvedValue({
body: { _source: { synonymId, eventIds: [existingEventId] } },
})
;(search as unknown as jest.Mock).mockReturnValue({
index: mockIndex,
delete: mockDelete,
get: mockGet,
})

await handler(mockStreamEvent)

expect(mockIndex).toHaveBeenCalledWith(putData)
})

test('insert only once', async () => {
const mockItems = [
{ synonymId, eventId: existingEventId },
{ synonymId, eventId },
]
const mockClient = {
synonyms: {
query: mockQuery,
},
}
mockQuery.mockResolvedValue({ Items: mockItems })
;(tables as unknown as jest.Mock).mockResolvedValue(mockClient)
putData.body.eventIds = [existingEventId, eventId]
mockGet.mockResolvedValue({
body: { _source: { synonymId, eventIds: [existingEventId, eventId] } },
})
;(search as unknown as jest.Mock).mockReturnValue({
index: mockIndex,
delete: mockDelete,
get: mockGet,
})

await handler(mockStreamEvent)

expect(mockIndex).toHaveBeenCalledWith(putData)
})
})

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]
mockGet.mockResolvedValue({
body: { _source: { synonymId, eventIds: [existingEventId, eventId] } },
})
;(search as unknown as jest.Mock).mockReturnValue({
index: mockIndex,
delete: mockDelete,
get: mockGet,
})

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,
},
}
mockQuery.mockResolvedValue({ Items: mockItems })
;(tables as unknown as jest.Mock).mockResolvedValue(mockClient)
mockStreamEvent.Records[0].eventName = 'REMOVE'
const deleteData = {
index: 'synonym-groups',
id: synonymId,
}
mockGet.mockResolvedValue({
body: { _source: { synonymId, eventIds: [eventId] } },
})
;(search as unknown as jest.Mock).mockReturnValue({
index: mockIndex,
delete: mockDelete,
get: mockGet,
})

await handler(mockStreamEvent)

expect(mockDelete).toHaveBeenCalledWith(deleteData)
})
})
8 changes: 8 additions & 0 deletions app/routes/synonyms/synonyms.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
*
* SPDX-License-Identifier: Apache-2.0
*/

/* Data structure in DynamoDB */
export interface Synonym {
Courey marked this conversation as resolved.
Show resolved Hide resolved
eventId: string
synonymId: string
}

/* Layout of materialized view in OpenSearch */
export interface SynonymGroup {
Courey marked this conversation as resolved.
Show resolved Hide resolved
synonymId: string
eventIds: string[]
}
6 changes: 3 additions & 3 deletions app/routes/synonyms/synonyms.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { tables } from '@architect/functions'
import type { DynamoDBDocument } from '@aws-sdk/lib-dynamodb/dist-types/DynamoDBDocument'
import type { DynamoDBDocument } from '@aws-sdk/lib-dynamodb'
import { search as getSearchClient } from '@nasa-gcn/architect-functions-search'
import crypto from 'crypto'

Expand Down Expand Up @@ -54,7 +54,7 @@ export async function searchSynonymsByEventId({
if (eventId) {
query.bool.should.push({
match: {
eventId: {
eventIds: {
query: eventId,
fuzziness: 'AUTO',
},
Expand All @@ -70,7 +70,7 @@ export async function searchSynonymsByEventId({
},
},
} = await client.search({
index: 'synonyms',
index: 'synonym-groups',
from: page && limit && (page - 1) * limit,
size: limit,
body: {
Expand Down
26 changes: 16 additions & 10 deletions app/table-streams/synonyms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import type {
} from 'aws-lambda'

import { createTriggerHandler } from '~/lib/lambdaTrigger.server'
import type { Synonym } from '~/routes/synonyms/synonyms.lib'
import type { Synonym, SynonymGroup } from '~/routes/synonyms/synonyms.lib'
import { getSynonymsByUuid } from '~/routes/synonyms/synonyms.server'

const index = 'synonyms'
const index = 'synonym-groups'

function unmarshallTrigger(item?: Record<string, LambdaTriggerAttributeValue>) {
return unmarshall(item as Record<string, AttributeValue>)
Expand All @@ -34,26 +35,31 @@ async function removeIndex(id: string) {
}
}

async function putIndex(synonym: Synonym) {
async function putIndex(synonymGroup: SynonymGroup) {
const client = await getSearchClient()
await client.index({
index,
body: synonym,
id: synonymGroup.synonymId,
body: synonymGroup,
})
}

export const handler = createTriggerHandler(
async ({ eventName, dynamodb }: DynamoDBRecord) => {
const id = unmarshallTrigger(dynamodb!.Keys).id as string
if (!eventName || !dynamodb) return
const promises = []

if (eventName === 'REMOVE') {
const id = unmarshallTrigger(dynamodb!.Keys).synonymId
Courey marked this conversation as resolved.
Show resolved Hide resolved
const synonym = unmarshallTrigger(dynamodb!.NewImage) as Synonym
Courey marked this conversation as resolved.
Show resolved Hide resolved
const dynamoSynonyms = await getSynonymsByUuid(synonym.synonymId)
const group = {
synonymId: synonym.synonymId,
eventIds: dynamoSynonyms.map((synonym) => synonym.eventId),
}
if (eventName === 'REMOVE' && group.eventIds.length === 0) {
promises.push(removeIndex(id))
} /* (eventName === 'INSERT' || eventName === 'MODIFY') */ else {
const synonym = unmarshallTrigger(dynamodb!.NewImage) as Synonym
promises.push(putIndex(synonym))
promises.push(putIndex(group))
Courey marked this conversation as resolved.
Show resolved Hide resolved
}

await Promise.all(promises)
}
)
14 changes: 14 additions & 0 deletions sandbox-invoke-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ export default {
body: 'This is a test',
},
},
synonyms: {
INSERT: {
eventId: 'GRB 230727A',
synonymId: 'fe00636d-ae9a-45fe-9941-9cbc6e84da04',
},
REMOVE: {
eventId: 'GRB 230727A',
synonymId: 'fe00636d-ae9a-45fe-9941-9cbc6e84da04',
},
MODIFY: {
eventId: 'GRB 230727A',
synonymId: 'fe00636d-ae9a-45fe-9941-9cbc6e84da04',
},
},
'circulars-kafka-distribution': {
INSERT: {
circularId: 40000,
Expand Down
14 changes: 12 additions & 2 deletions sandbox-search.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@ export default async function () {
const text = await readFile('sandbox-seed.json', { encoding: 'utf-8' })
const { circulars, synonyms } = JSON.parse(text)

const groupedSynonyms = synonyms.reduce((accumulator, synonym) => {
Copy link
Member

Choose a reason for hiding this comment

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

I was thinking something more along the lines of https://lodash.com/docs/#groupBy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

is there a reason or is it just preference?

Copy link
Member

Choose a reason for hiding this comment

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

It's terser and it's more obvious what the code does. I think it is more idiomatic. I would have suggested Object.groupBy, but it is a relatively recent addition to ECMAScript and requires a newer version of Node.js than we are targeting.

;(accumulator[synonym.synonymId] ??= []).push(synonym.eventId)
return accumulator
}, {})

const groups = Object.keys(groupedSynonyms).map((id) => ({
synonymId: id,
eventIds: groupedSynonyms[id],
}))

return [
...circulars.flatMap((item) => [
{ index: { _index: 'circulars', _id: item.circularId.toString() } },
item,
]),
...synonyms.flatMap((item) => [
{ index: { _index: 'synonyms', _id: item.id } },
...groups.flatMap((item) => [
Courey marked this conversation as resolved.
Show resolved Hide resolved
{ index: { _index: 'synonym-groups', _id: item.synonymId.toString() } },
item,
]),
]
Expand Down
Loading