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

10252 migration #4811

Draft
wants to merge 7 commits into
base: 10252-notifications-endpoint-504s
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions web-api/src/persistence/dynamo/dynamoTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type TDynamoRecord<T = Record<string, any>> = {
pk: string;
sk: string;
gsi1pk?: string;
gsi2pk?: string; // deprecated; can remove after #10252 is deployed
gsiUserBox?: string;
gsiSectionBox?: string;
ttl?: number;
Expand Down
57 changes: 17 additions & 40 deletions web-api/src/persistence/dynamo/messages/upsertMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export const upsertMessage = async ({
}): Promise<void> => {
let gsiUserBox, gsiSectionBox;

await putMessageInOutbox({ applicationContext, message });

if (!message.completedAt) {
await putMessageInOutbox({ applicationContext, message });
// user inbox
gsiUserBox = message.toUserId
? `assigneeId|${message.toUserId}`
Expand All @@ -22,8 +23,6 @@ export const upsertMessage = async ({
gsiSectionBox = message.toSection
? `section|${message.toSection}`
: undefined;
} else {
await putMessageInCompletedBox({ applicationContext, message });
}

await put({
Expand All @@ -46,23 +45,33 @@ const putMessageInOutbox = async ({
applicationContext: IApplicationContext;
message: RawMessage;
}): Promise<void> => {
const sk = message.createdAt;
const sk = message.completedAt ? message.completedAt : message.createdAt;
const ttl = calculateTimeToLive({
numDays: 8,
timestamp: message.createdAt,
});

const box = message.completedAt ? 'completed' : 'outbox';
const buckets = [
{ bucket: 'user', identifier: message.fromUserId },
{ bucket: 'section', identifier: message.fromSection },
{
bucket: 'user',
identifier: message.completedAt
? message.completedByUserId
: message.fromUserId,
},
{
bucket: 'section',
identifier: message.completedAt
? message.completedBySection
: message.fromSection,
},
];

await Promise.all(
buckets.map(({ bucket, identifier }) =>
put({
Item: {
...message,
pk: `message|outbox|${bucket}|${identifier}`,
pk: `message|${box}|${bucket}|${identifier}`,
sk,
ttl: ttl.expirationTimestamp,
},
Expand All @@ -71,35 +80,3 @@ const putMessageInOutbox = async ({
),
);
};

const putMessageInCompletedBox = async ({
applicationContext,
message,
}: {
applicationContext: IApplicationContext;
message: RawMessage;
}): Promise<void> => {
const ttl = calculateTimeToLive({
numDays: 8,
timestamp: message.completedAt!,
});

const buckets = [
{ bucket: 'user', identifier: message.completedByUserId },
{ bucket: 'section', identifier: message.completedBySection },
];

await Promise.all(
buckets.map(({ bucket, identifier }) =>
put({
Item: {
...message,
pk: `message|completed|${bucket}|${identifier}`,
sk: message.completedAt!,
ttl: ttl.expirationTimestamp,
},
applicationContext,
}),
),
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import {
CASE_STATUS_TYPES,
DOCKET_SECTION,
PETITIONS_SECTION,
} from '@shared/business/entities/EntityConstants';
import { RawMessage } from '@shared/business/entities/Message';
import { TDynamoRecord } from '@web-api/persistence/dynamo/dynamoTypes';
import { migrateItems } from './10252-add-gsis-to-messages';

describe('migrateItems', () => {
const gsiUserBoxKey = 'gsiUserBox';
const gsiSectionBoxKey = 'gsiSectionBox';

let mockMessage: TDynamoRecord<RawMessage>;
beforeEach(() => {
mockMessage = {
caseStatus: CASE_STATUS_TYPES.generalDocket,
caseTitle: 'The Land Before Time',
createdAt: '2019-03-01T21:40:46.415Z',
docketNumber: '123-20',
docketNumberWithSuffix: '123-45S',
entityName: 'Message',
from: 'Test Petitionsclerk',
fromSection: PETITIONS_SECTION,
fromUserId: '4791e892-14ee-4ab1-8468-0c942ec379d2',
isCompleted: false,
isRead: false,
isRepliedTo: false,
message: 'hey there',
messageId: 'a10d6855-f3ee-4c11-861c-c7f11cba4dff',
parentMessageId: '31687a1e-3640-42cd-8e7e-a8e6df39ce9a',
pk: 'case|101-45',
sk: 'message|a10d6855-f3ee-4c11-861c-c7f11cba4dff',
subject: 'hello',
to: 'Test Petitionsclerk2',
toSection: PETITIONS_SECTION,
toUserId: '449b916e-3362-4a5d-bf56-b2b94ba29c12',
};
});

describe('completed', () => {
beforeEach(() => {
mockMessage = {
...mockMessage,
completedAt: '2024-03-01T00:00:00.000Z',
completedBy: 'someone',
completedBySection: 'section-name',
completedByUserId: 'user-id',
};
});

it('does not add a gsiUserBox or gsiSectionBox on a message record that has completedAt', () => {
const migratedItems = migrateItems([mockMessage]);
expect(migratedItems).toEqual(
expect.arrayContaining([
{
...mockMessage,
[gsiSectionBoxKey]: undefined,
[gsiUserBoxKey]: undefined,
},
]),
);
});

it('adds a record for user completed box', () => {
const migratedItems = migrateItems([mockMessage]);
expect(migratedItems).toEqual(
expect.arrayContaining([
{
...mockMessage,
gsi1pk: undefined,
[gsiSectionBoxKey]: undefined,
[gsiUserBoxKey]: undefined,
pk: 'message|completed|user|user-id',
sk: mockMessage.completedAt,
ttl: expect.anything(),
},
]),
);
});

it('adds a record for the section completed box', () => {
const migratedItems = migrateItems([mockMessage]);
expect(migratedItems).toEqual(
expect.arrayContaining([
{
...mockMessage,
gsi1pk: undefined,
[gsiSectionBoxKey]: undefined,
[gsiUserBoxKey]: undefined,
pk: 'message|completed|section|section-name',
sk: mockMessage.completedAt,
ttl: expect.anything(),
},
]),
);
});
});

describe('inbox', () => {
it('adds gsiUserBox on a message record that has a toUserId', async () => {
mockMessage = {
...mockMessage,
completedAt: undefined,
toUserId: '123',
};

const migratedItems = await migrateItems([mockMessage]);

expect(migratedItems).toEqual(
expect.arrayContaining([
{
...mockMessage,
[gsiUserBoxKey]: 'assigneeId|123',
},
]),
);
});

it('adds gsiSectionBox on a message record that has a section', async () => {
mockMessage = {
...mockMessage,
completedAt: undefined,
toSection: DOCKET_SECTION,
};

const migratedItems = await migrateItems([mockMessage]);

expect(migratedItems).toEqual(
expect.arrayContaining([
{
...mockMessage,
[gsiSectionBoxKey]: `section|${DOCKET_SECTION}`,
},
]),
);
});

it('does not add a gsiUserBox on a message record that does not have a toUserId', async () => {
mockMessage = {
...mockMessage,
toUserId: undefined,
} as any;
const records = [mockMessage];

const migratedItems = await migrateItems(records);

expect(migratedItems).toEqual(
expect.arrayContaining([
{
...mockMessage,
[gsiUserBoxKey]: undefined,
},
]),
);
});

it('does not add a gsiSectionBox on a message record that does not have a toSection specified', async () => {
mockMessage = {
...mockMessage,
toSection: undefined,
} as any;
const records = [mockMessage];

const migratedItems = await migrateItems(records);

expect(migratedItems).toEqual(
expect.arrayContaining([
{
...mockMessage,
[gsiSectionBoxKey]: undefined,
},
]),
);
});

it('adds a record for user outbox box', () => {
const migratedItems = migrateItems([mockMessage]);
expect(migratedItems).toEqual(
expect.arrayContaining([
{
...mockMessage,
gsi1pk: undefined,
[gsiSectionBoxKey]: undefined,
[gsiUserBoxKey]: undefined,
pk: 'message|outbox|user|4791e892-14ee-4ab1-8468-0c942ec379d2',
sk: mockMessage.createdAt,
ttl: expect.anything(),
},
]),
);
});

it('adds a record for the section outbox box', () => {
const migratedItems = migrateItems([mockMessage]);
expect(migratedItems).toEqual(
expect.arrayContaining([
{
...mockMessage,
gsi1pk: undefined,
[gsiSectionBoxKey]: undefined,
[gsiUserBoxKey]: undefined,
pk: `message|outbox|section|${PETITIONS_SECTION}`,
sk: mockMessage.createdAt,
ttl: expect.anything(),
},
]),
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { TDynamoRecord } from '@web-api/persistence/dynamo/dynamoTypes';
import { calculateTimeToLive } from '@web-api/persistence/dynamo/calculateTimeToLive';

const isMessage = item => {
return item.pk.startsWith('case|') && item.sk.startsWith('message|');
};

export const migrateItems = items => {
const itemsAfter: TDynamoRecord[] = [];
for (const item of items) {
if (isMessage(item)) {
if (!item.completedAt) {
const ttl = calculateTimeToLive({
numDays: 8,
timestamp: item.createdAt!,
});

// add outbox records
itemsAfter.push({
...item,
gsi1pk: undefined,
pk: `message|outbox|user|${item.fromUserId}`,
sk: item.createdAt,
ttl: ttl.expirationTimestamp,
});
itemsAfter.push({
...item,
gsi1pk: undefined,
pk: `message|outbox|section|${item.fromSection}`,
sk: item.createdAt,
ttl: ttl.expirationTimestamp,
});

// add global secondary indexes
item.gsiUserBox = item.toUserId
? `assigneeId|${item.toUserId}`
: undefined;
item.gsiSectionBox = item.toSection
? `section|${item.toSection}`
: undefined;
itemsAfter.push(item);
} else {
const ttl = calculateTimeToLive({
numDays: 8,
timestamp: item.completedAt!,
});

// add completed box records
itemsAfter.push({
...item,
gsi1pk: undefined,
pk: `message|completed|user|${item.completedByUserId}`,
sk: item.completedAt!,
ttl: ttl.expirationTimestamp,
});
itemsAfter.push({
...item,
gsi1pk: undefined,
pk: `message|completed|section|${item.completedBySection}`,
sk: item.completedAt!,
ttl: ttl.expirationTimestamp,
});

// completed message does not get global secondary indexes
itemsAfter.push({
...item,
gsiSectionBox: undefined,
gsiUserBox: undefined,
});
}
}
}

return itemsAfter;
};
Loading
Loading