Skip to content

Commit

Permalink
create synonym group for eventId if none exists (nasa-gcn#2642)
Browse files Browse the repository at this point in the history
Resolves nasa-gcn#2661.
  • Loading branch information
Courey authored Nov 15, 2024
1 parent 67d176c commit 37f18ac
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 28 deletions.
52 changes: 36 additions & 16 deletions __tests__/synonyms.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import * as awsSDKMock from 'aws-sdk-mock'
import crypto from 'crypto'

import type { Circular } from '~/routes/circulars/circulars.lib'
import { createSynonyms, putSynonyms } from '~/routes/synonyms/synonyms.server'
import {
moderatorCreateSynonyms,
putSynonyms,
} from '~/routes/synonyms/synonyms.server'

jest.mock('@architect/functions')
const synonymId = 'abcde-abcde-abcde-abcde-abcde'
const altSynonymId1 = 'zyxw-zyxw-zyxw-zyxw-zyxw'
const altSynonymId2 = 'lmno-lmno-lmno-lmno-lmno'
const exampleCirculars = [
{
Items: [
Expand Down Expand Up @@ -36,7 +41,7 @@ const exampleCirculars = [
{ Items: [] },
]

describe('createSynonyms', () => {
describe('moderatorCreateSynonyms', () => {
beforeEach(() => {
const mockBatchWrite = jest.fn()
const mockQuery = jest.fn()
Expand Down Expand Up @@ -65,7 +70,7 @@ describe('createSynonyms', () => {
jest.restoreAllMocks()
})

test('createSynonyms should write to DynamoDB', async () => {
test('moderatorCreateSynonyms should write to DynamoDB', async () => {
const mockBatchWriteItem = jest.fn(
(
params: DynamoDB.DocumentClient.BatchWriteItemInput,
Expand All @@ -81,12 +86,12 @@ describe('createSynonyms', () => {
awsSDKMock.mock('DynamoDB', 'batchWriteItem', mockBatchWriteItem)

const synonymousEventIds = ['eventId1', 'eventId2']
const result = await createSynonyms(synonymousEventIds)
const result = await moderatorCreateSynonyms(synonymousEventIds)

expect(result).toBe(synonymId)
})

test('createSynonyms with nonexistent eventId throws Response 400', async () => {
test('moderatorCreateSynonyms with nonexistent eventId throws Response 400', async () => {
const mockBatchWriteItem = jest.fn(
(
params: DynamoDB.DocumentClient.BatchWriteItemInput,
Expand All @@ -103,7 +108,7 @@ describe('createSynonyms', () => {

const synonymousEventIds = ['eventId1', 'nope']
try {
await createSynonyms(synonymousEventIds)
await moderatorCreateSynonyms(synonymousEventIds)
} catch (error) {
// eslint-disable-next-line jest/no-conditional-expect
expect(error).toBeInstanceOf(Response)
Expand All @@ -121,10 +126,6 @@ describe('putSynonyms', () => {
const mockBatchWrite = jest.fn()
const mockQuery = jest.fn()

beforeAll(() => {
jest.spyOn(crypto, 'randomUUID').mockReturnValue(synonymId)
})

afterAll(() => {
jest.restoreAllMocks()
awsSDKMock.restore('DynamoDB')
Expand Down Expand Up @@ -177,6 +178,7 @@ describe('putSynonyms', () => {
})

test('putSynonyms should write to DynamoDB if there are additions', async () => {
jest.spyOn(crypto, 'randomUUID').mockReturnValue(synonymId)
const mockClient = {
batchWrite: mockBatchWrite,
query: mockQuery,
Expand Down Expand Up @@ -222,6 +224,10 @@ describe('putSynonyms', () => {
})

test('putSynonyms should write to DynamoDB if there are subtractions', async () => {
jest
.spyOn(crypto, 'randomUUID')
.mockImplementationOnce(() => altSynonymId1)
.mockImplementationOnce(() => altSynonymId2)
const mockClient = {
batchWrite: mockBatchWrite,
}
Expand All @@ -239,15 +245,27 @@ describe('putSynonyms', () => {
const params = {
RequestItems: {
synonyms: [
{ DeleteRequest: { Key: { eventId: 'eventId3' } } },
{ DeleteRequest: { Key: { eventId: 'eventId4' } } },
{
PutRequest: {
Item: { eventId: 'eventId3', synonymId: altSynonymId1 },
},
},
{
PutRequest: {
Item: { eventId: 'eventId4', synonymId: altSynonymId2 },
},
},
],
},
}
expect(mockBatchWrite).toHaveBeenLastCalledWith(params)
})

test('putSynonyms should write to DynamoDB if there are additions and subtractions', async () => {
jest
.spyOn(crypto, 'randomUUID')
.mockImplementationOnce(() => altSynonymId1)
.mockImplementationOnce(() => altSynonymId2)
const mockClient = {
batchWrite: mockBatchWrite,
query: mockQuery,
Expand Down Expand Up @@ -275,16 +293,18 @@ describe('putSynonyms', () => {
RequestItems: {
synonyms: [
{
DeleteRequest: {
Key: {
PutRequest: {
Item: {
eventId: 'eventId3',
synonymId: altSynonymId1,
},
},
},
{
DeleteRequest: {
Key: {
PutRequest: {
Item: {
eventId: 'eventId4',
synonymId: altSynonymId2,
},
},
},
Expand Down
2 changes: 2 additions & 0 deletions app/email-incoming/circulars/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { sendEmail } from '~/lib/email.server'
import { hostname, origin } from '~/lib/env.server'
import { putRaw, submitterGroup } from '~/routes/circulars/circulars.server'
import { tryInitSynonym } from '~/routes/synonyms/synonyms.server'

interface UserData {
email: string
Expand Down Expand Up @@ -99,6 +100,7 @@ export const handler = createEmailIncomingMessageHandler(
// Removes sub as a property if it is undefined from the legacy users
if (!circular.sub) delete circular.sub
const { circularId } = await putRaw(circular)
if (eventId) await tryInitSynonym(eventId)

// Send a success email
await sendSuccessEmail({
Expand Down
6 changes: 4 additions & 2 deletions app/routes/circulars/circulars.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import memoizee from 'memoizee'
import { dedent } from 'ts-dedent'

import { type User, getUser } from '../_auth/user.server'
import { tryInitSynonym } from '../synonyms/synonyms.server'
import {
bodyIsValid,
formatAuthor,
Expand Down Expand Up @@ -311,8 +312,9 @@ export async function put(

const eventId = parseEventFromSubject(item.subject)
if (eventId) circular.eventId = eventId

return await putRaw(circular)
const result = await putRaw(circular)
if (eventId) await tryInitSynonym(eventId)
return result
}

export async function circularRedirect(query: string) {
Expand Down
4 changes: 2 additions & 2 deletions app/routes/synonyms.new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { getUser } from './_auth/user.server'
import { moderatorGroup } from './circulars/circulars.server'
import {
autoCompleteEventIds,
createSynonyms,
moderatorCreateSynonyms,
} from './synonyms/synonyms.server'
import DetailsDropdownContent from '~/components/DetailsDropdownContent'
import { getFormDataString } from '~/lib/utils'
Expand All @@ -40,7 +40,7 @@ export async function action({ request }: ActionFunctionArgs) {
const data = await request.formData()
const eventIds = getFormDataString(data, 'synonyms')?.split(',')
if (!eventIds) throw new Response(null, { status: 400 })
const synonymId = await createSynonyms(eventIds)
const synonymId = await moderatorCreateSynonyms(eventIds)
return redirect(`/synonyms/${synonymId}`)
}

Expand Down
38 changes: 30 additions & 8 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'
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 @@ -130,17 +130,17 @@ async function getSynonymMembers(eventId: string) {
* BatchWriteItem has a limit of 25 items, so the user may not add more than
* 25 synonyms at a time.
*/
export async function createSynonyms(synonymousEventIds: string[]) {
const uuid = crypto.randomUUID()
export async function moderatorCreateSynonyms(synonymousEventIds: string[]) {
if (!synonymousEventIds.length) {
throw new Response('EventIds are required.', { status: 400 })
}
const uuid = crypto.randomUUID()
const db = await tables()
const client = db._doc as unknown as DynamoDBDocument
const TableName = db.name('synonyms')

const isValid = await validateEventIds({ eventIds: synonymousEventIds })
if (!isValid) throw new Response('eventId does not exist', { status: 400 })

await client.batchWrite({
RequestItems: {
[TableName]: synonymousEventIds.map((eventId) => ({
Expand All @@ -153,6 +153,26 @@ export async function createSynonyms(synonymousEventIds: string[]) {
return uuid
}

export async function tryInitSynonym(eventId: string) {
const db = await tables()

try {
await db.synonyms.update({
Key: { eventId },
UpdateExpression: 'set #synonymId = :synonymId',
ExpressionAttributeNames: {
'#synonymId': 'synonymId',
},
ExpressionAttributeValues: {
':synonymId': crypto.randomUUID(),
},
ConditionExpression: 'attribute_not_exists(eventId)',
})
} catch (error) {
if ((error as Error).name !== 'ConditionalCheckFailedException') throw error
}
}

/*
* If an eventId already has a synonym and is passed in, it will unlink the
* eventId from the old synonym and the only remaining link will be to the
Expand Down Expand Up @@ -181,10 +201,11 @@ export async function putSynonyms({
const writes = []
if (subtractions?.length) {
const subtraction_writes = subtractions.map((eventId) => ({
DeleteRequest: {
Key: { eventId },
PutRequest: {
Item: { synonymId: crypto.randomUUID(), eventId },
},
}))

writes.push(...subtraction_writes)
}
if (additions?.length) {
Expand Down Expand Up @@ -219,9 +240,10 @@ export async function deleteSynonyms(synonymId: string) {
':synonymId': synonymId,
},
})

const writes = results.Items.map(({ eventId }) => ({
DeleteRequest: {
Key: { eventId },
PutRequest: {
Item: { synonymId: crypto.randomUUID(), eventId },
},
}))
const params = {
Expand Down

0 comments on commit 37f18ac

Please sign in to comment.