Skip to content

Commit

Permalink
Merge branch 'staging' into 10554-data-cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
jimlerza authored Dec 16, 2024
2 parents 10d8acd + 545dd03 commit fb8ace6
Show file tree
Hide file tree
Showing 64 changed files with 1,333 additions and 350 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
BatchWriteCommand,
DynamoDBDocumentClient,
} from '@aws-sdk/lib-dynamodb';

export async function batchDeleteDynamoItems(
itemsToDelete: { DeleteRequest: { Key: { pk: string; sk: string } } }[],
client: DynamoDBDocumentClient,
tableNameInput: string,
): Promise<number> {
const BATCH_SIZE = 25;
const RETRY_DELAY_MS = 5000; // Set the delay between retries (in milliseconds)
let totalItemsDeleted = 0;

for (let i = 0; i < itemsToDelete.length; i += BATCH_SIZE) {
const batch = itemsToDelete.slice(i, i + BATCH_SIZE);

const batchWriteParams = {
RequestItems: {
[tableNameInput]: batch,
},
};

try {
let unprocessedItems: any[] = batch;
let retryCount = 0;
const MAX_RETRIES = 5;

// Retry logic for unprocessed items
while (unprocessedItems.length > 0 && retryCount < MAX_RETRIES) {
const response = await client.send(
new BatchWriteCommand(batchWriteParams),
);

totalItemsDeleted +=
unprocessedItems.length -
(response.UnprocessedItems?.[tableNameInput]?.length || 0);

unprocessedItems = response.UnprocessedItems?.[tableNameInput] ?? [];

if (unprocessedItems.length > 0) {
console.log(
`Retrying unprocessed items: ${unprocessedItems.length}, attempt ${retryCount + 1}`,
);
batchWriteParams.RequestItems[tableNameInput] = unprocessedItems;
retryCount++;

// Add delay before the next retry
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
}
}

if (unprocessedItems.length > 0) {
console.error(
`Failed to delete ${unprocessedItems.length} items after ${MAX_RETRIES} retries.`,
);
}
} catch (error) {
console.error('Error in batch delete:', error);
}
}
return totalItemsDeleted;
}
60 changes: 60 additions & 0 deletions scripts/run-once-scripts/postgres-migration/delete-case-notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* HOW TO RUN
* npx ts-node --transpileOnly scripts/run-once-scripts/postgres-migration/delete-case-notes.ts
*/

import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { getDbReader } from '../../../web-api/src/database';
import { isEmpty } from 'lodash';
import { batchDeleteDynamoItems } from './batch-delete-dynamo-items';
import { environment } from '../../../web-api/src/environment';

const caseUserNotesPageSize = 10000;
const dynamoDbClient = new DynamoDBClient({ region: 'us-east-1' });
const dynamoDbDocClient = DynamoDBDocumentClient.from(dynamoDbClient);

// We set the environment as 'production' (= "a deployed environment") to get the RDS connection to work properly
environment.nodeEnv = 'production';

const getCaseNotesToDelete = async (offset: number) => {
const caseNotes = await getDbReader(reader =>
reader
.selectFrom('dwUserCaseNote')
.select(['docketNumber', 'userId'])
.orderBy(['docketNumber', 'userId'])
.limit(caseUserNotesPageSize)
.offset(offset)
.execute(),
);
return caseNotes;
};

let totalItemsDeleted = 0;

async function main() {
let offset = 0;
let caseNotesToDelete = await getCaseNotesToDelete(offset);

while (!isEmpty(caseNotesToDelete)) {
const dynamoItemsToDelete = caseNotesToDelete.map(c => ({
DeleteRequest: {
Key: {
pk: `user-case-note|${c.docketNumber}`,
sk: `user|${c.userId}`,
},
},
}));
totalItemsDeleted += await batchDeleteDynamoItems(
dynamoItemsToDelete,
dynamoDbDocClient,
environment.dynamoDbTableName,
);
console.log(`Total case notes deleted so far: ${totalItemsDeleted}`);
offset += caseUserNotesPageSize;
caseNotesToDelete = await getCaseNotesToDelete(offset);
}
console.log('Done deleting case notes from Dynamo');
}

main().catch(console.error);
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ScanCommand,
} from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient, ScanCommandInput } from '@aws-sdk/client-dynamodb';
import { requireEnvVars } from '../../shared/admin-tools/util';
import { requireEnvVars } from '../../../shared/admin-tools/util';

requireEnvVars(['TABLE_NAME']);

Expand Down
36 changes: 36 additions & 0 deletions shared/src/business/entities/factories/UserFactory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Practitioner } from '@shared/business/entities/Practitioner';
import { ROLES } from '@shared/business/entities/EntityConstants';
import { User } from '@shared/business/entities/User';
import { UserFactory } from '@shared/business/entities/factories/UserFactory';

describe('UserFactory', () => {
describe('getClass', () => {
it('should return "Practitioner" class type if role is "privatePractitioner"', () => {
const TEST_USER = { role: ROLES.privatePractitioner };
const userFactory = new UserFactory(TEST_USER);
const classInstance = userFactory.getClass();
expect(classInstance).toEqual(Practitioner);
});

it('should return "Practitioner" class type if role is "irsPractitioner"', () => {
const TEST_USER = { role: ROLES.irsPractitioner };
const userFactory = new UserFactory(TEST_USER);
const classInstance = userFactory.getClass();
expect(classInstance).toEqual(Practitioner);
});

it('should return "Practitioner" class type if role is "inactivePractitioner"', () => {
const TEST_USER = { role: ROLES.inactivePractitioner };
const userFactory = new UserFactory(TEST_USER);
const classInstance = userFactory.getClass();
expect(classInstance).toEqual(Practitioner);
});

it('should return "User" class type if role is "admin"', () => {
const TEST_USER = { role: ROLES.admin };
const userFactory = new UserFactory(TEST_USER);
const classInstance = userFactory.getClass();
expect(classInstance).toEqual(User);
});
});
});
27 changes: 27 additions & 0 deletions shared/src/business/entities/factories/UserFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Practitioner } from '@shared/business/entities/Practitioner';
import { ROLES, Role } from '@shared/business/entities/EntityConstants';
import { User } from '@shared/business/entities/User';

type MinimalFactoryInfo = {
role: Role;
};

export class UserFactory {
private rawUser: MinimalFactoryInfo;

constructor(rawUser: MinimalFactoryInfo) {
this.rawUser = rawUser;
}

public getClass(): typeof User | typeof Practitioner {
if (
this.rawUser.role === ROLES.privatePractitioner ||
this.rawUser.role === ROLES.irsPractitioner ||
this.rawUser.role === ROLES.inactivePractitioner
) {
return Practitioner;
}

return User;
}
}
4 changes: 2 additions & 2 deletions shared/src/proxies/users/verifyUserPendingEmailProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { put } from '../requests';
*/
export const verifyUserPendingEmailInteractor = (
applicationContext,
{ token },
) => {
{ token }: { token: string },
): Promise<void> => {
return put({
applicationContext,
body: {
Expand Down
6 changes: 0 additions & 6 deletions types/TEntity.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@ type TPetitioner = {
hasConsentedToEService?: boolean;
};

type TCaseNote = {
userId: string;
docketNumber: string;
notes: string;
};

interface IValidateRawCollection<I> {
(collection: I[], options: { applicationContext: IApplicationContext }): I[];
}
Expand Down
9 changes: 9 additions & 0 deletions web-api/elasticsearch/efcms-case-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export const efcmsCaseMappings = {
'indexedTimestamp.N': {
type: 'text',
},
'irsPractitioners.L.M.email.S': {
type: 'keyword',
},
'irsPractitioners.L.M.userId.S': {
type: 'keyword',
},
Expand All @@ -89,6 +92,9 @@ export const efcmsCaseMappings = {
'petitioners.L.M.countryType.S': {
type: 'keyword',
},
'petitioners.L.M.email.S': {
type: 'keyword',
},
'petitioners.L.M.name.S': {
type: 'text',
},
Expand All @@ -104,6 +110,9 @@ export const efcmsCaseMappings = {
'preferredTrialCity.S': {
type: 'keyword',
},
'privatePractitioners.L.M.email.S': {
type: 'keyword',
},
'privatePractitioners.L.M.userId.S': {
type: 'keyword',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import {
UserStatusType,
} from '@aws-sdk/client-cognito-identity-provider';
import { MESSAGE_TYPES } from '@web-api/gateways/worker/workerRouter';
import { MOCK_PRACTITIONER } from '@shared/test/mockUsers';
import { MOCK_PRACTITIONER, petitionerUser } from '@shared/test/mockUsers';
import {
ROLES,
Role,
SERVICE_INDICATOR_TYPES,
} from '../../../../../shared/src/business/entities/EntityConstants';
import { UserRecord } from '@web-api/persistence/dynamo/dynamoTypes';
import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext';
import { changePasswordInteractor } from './changePasswordInteractor';
import {
changePasswordInteractor,
updateUserPendingEmailRecord,
} from './changePasswordInteractor';
import jwt from 'jsonwebtoken';

describe('changePasswordInteractor', () => {
Expand Down Expand Up @@ -326,3 +329,45 @@ describe('changePasswordInteractor', () => {
});
});
});

describe('updateUserPendingEmailRecord', () => {
beforeEach(() => {
applicationContext
.getPersistenceGateway()
.updateUser.mockResolvedValue(null);
});

it('should set isUpdatingInformation to true if flag is enabled', async () => {
await updateUserPendingEmailRecord(applicationContext, {
setIsUpdatingInformation: true,
user: {
...petitionerUser,
isUpdatingInformation: false,
},
});

const updateUserCalls =
applicationContext.getPersistenceGateway().updateUser.mock.calls;
expect(updateUserCalls.length).toEqual(1);
expect(updateUserCalls[0][0].user).toMatchObject({
isUpdatingInformation: true,
});
});

it('should not update isUpdatingInformation property when flag is disabled', async () => {
await updateUserPendingEmailRecord(applicationContext, {
setIsUpdatingInformation: false,
user: {
...petitionerUser,
isUpdatingInformation: false,
},
});

const updateUserCalls =
applicationContext.getPersistenceGateway().updateUser.mock.calls;
expect(updateUserCalls.length).toEqual(1);
expect(updateUserCalls[0][0].user).toMatchObject({
isUpdatingInformation: false,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ export const changePasswordInteractor = async (

export const updateUserPendingEmailRecord = async (
applicationContext: ServerApplicationContext,
{ user }: { user: RawUser },
{
setIsUpdatingInformation = false,
user,
}: { user: RawUser; setIsUpdatingInformation?: boolean },
): Promise<{ updatedUser: RawPractitioner | RawUser }> => {
let userEntity;

Expand All @@ -150,6 +153,8 @@ export const updateUserPendingEmailRecord = async (
});
}

if (setIsUpdatingInformation) userEntity.isUpdatingInformation = true;

const rawUser = userEntity.validate().toRawObject();
await applicationContext.getPersistenceGateway().updateUser({
applicationContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import '@web-api/persistence/postgres/userCaseNotes/mocks.jest';
import { ROLES } from '../../../../../shared/src/business/entities/EntityConstants';
import { UnauthorizedError } from '@web-api/errors/errors';
import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser';
import { User } from '../../../../../shared/src/business/entities/User';
import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext';
import { deleteUserCaseNoteInteractor } from './deleteUserCaseNoteInteractor';
import { deleteUserCaseNote as deleteUserCaseNoteMock } from '@web-api/persistence/postgres/userCaseNotes/deleteUserCaseNote';
import { mockJudgeUser } from '@shared/test/mockAuthUsers';
import { omit } from 'lodash';

describe('deleteUserCaseNoteInteractor', () => {
const deleteUserCaseNote = deleteUserCaseNoteMock as jest.Mock;

it('throws an error if the user is not valid or authorized', async () => {
let user = {} as UnknownAuthUser;

Expand All @@ -33,7 +37,7 @@ describe('deleteUserCaseNoteInteractor', () => {
applicationContext
.getPersistenceGateway()
.getUserById.mockReturnValue(mockUser);
applicationContext.getPersistenceGateway().deleteUserCaseNote = v => v;
deleteUserCaseNote.mockImplementation(v => v);
applicationContext
.getUseCaseHelpers()
.getJudgeInSectionHelper.mockReturnValue({
Expand All @@ -60,7 +64,6 @@ describe('deleteUserCaseNoteInteractor', () => {
applicationContext
.getPersistenceGateway()
.getUserById.mockReturnValue(mockUser);
applicationContext.getPersistenceGateway().deleteUserCaseNote = jest.fn();
applicationContext
.getUseCaseHelpers()
.getJudgeInSectionHelper.mockReturnValue(null);
Expand All @@ -72,9 +75,8 @@ describe('deleteUserCaseNoteInteractor', () => {
omit(mockUser, 'section'),
);

expect(
applicationContext.getPersistenceGateway().deleteUserCaseNote.mock
.calls[0][0].userId,
).toEqual(mockJudgeUser.userId);
expect(deleteUserCaseNote.mock.calls[0][0].userId).toEqual(
mockJudgeUser.userId,
);
});
});
Loading

0 comments on commit fb8ace6

Please sign in to comment.