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

10492 dxox #5518

Merged
merged 21 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4668df8
10492: Move User Case Notes to Postgres
pixiwyn Sep 27, 2024
449da7d
Merge branch '10391-dynamo-migrations-kysely-aurora-rds-tokens' of gi…
pixiwyn Oct 4, 2024
5e49404
10492: update api tests with mocks + add stream for user case notes
pixiwyn Oct 4, 2024
b09299f
10492: quick fix
pixiwyn Oct 4, 2024
1313dfe
10492: quick fix
pixiwyn Oct 4, 2024
7b5cb98
10492: fix api tests
pixiwyn Oct 4, 2024
78e0cf3
10492: add test user case note
pixiwyn Oct 4, 2024
485c741
10492: fix api tests
pixiwyn Oct 4, 2024
4b7cc83
Merge branch 'staging' of github.com:ustaxcourt/ef-cms into 10492-dxox
pixiwyn Nov 6, 2024
3147b33
10492-dxox: rename updateUserCaseNote.ts to upsertUserCaseNote.ts
Mwindo Nov 6, 2024
0a3c981
10492-dxox: fix missed rename in updateUserCaseNoteInteractor
Mwindo Nov 6, 2024
dd0f4ff
10492-dxox: add delete script
Mwindo Nov 20, 2024
75884f5
10492-dxox: fix typo in delete-case-notes
Mwindo Nov 20, 2024
e2a8ace
Merge branch 'staging' into 10492-dxox
pixiwyn Nov 20, 2024
38417e8
10492-dxox: switch to podman compose in run-local.sh
Mwindo Nov 20, 2024
726d1a7
Merge branch '10492-dxox' of github.com:flexion/ef-cms into 10492-dxox
Mwindo Nov 20, 2024
207b254
Merge branch 'staging' into 10492-dxox
pixiwyn Nov 26, 2024
8c2ba35
Merge branch 'staging' of github.com:ustaxcourt/ef-cms into 10492-dxox
pixiwyn Dec 3, 2024
e5a2749
10492-dxox: fix delete script to avoid unstable sort and to get RDS c…
Mwindo Dec 3, 2024
ecb7eb3
Merge branch 'staging' into 10492-dxox
pixiwyn Dec 12, 2024
13bf9f3
10492-dxox: rename 0004 to 0003
Mwindo Dec 12, 2024
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
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
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
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,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { ServerApplicationContext } from '@web-api/applicationContext';
import { UnauthorizedError } from '@web-api/errors/errors';
import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser';
import { deleteUserCaseNote } from '@web-api/persistence/postgres/userCaseNotes/deleteUserCaseNote';

/**
* deleteUserCaseNoteInteractor
Expand All @@ -31,8 +32,7 @@ export const deleteUserCaseNoteInteractor = async (
userIdMakingRequest: authorizedUser.userId,
});

return await applicationContext.getPersistenceGateway().deleteUserCaseNote({
applicationContext,
return await deleteUserCaseNote({
docketNumber,
userId,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import '@web-api/persistence/postgres/userCaseNotes/mocks.jest';
import { MOCK_CASE } from '../../../../../shared/src/test/mockCase';
import { UnauthorizedError } from '@web-api/errors/errors';
import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser';
import { UserCaseNote } from '@shared/business/entities/notes/UserCaseNote';
import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext';
import { getUserCaseNoteForCasesInteractor } from './getUserCaseNoteForCasesInteractor';
import { getUserCaseNoteForCases as getUserCaseNoteForCasesMock } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNoteForCases';
import { mockJudgeUser } from '@shared/test/mockAuthUsers';
import { omit } from 'lodash';

Expand All @@ -21,15 +24,15 @@ describe('getUserCaseNoteForCasesInteractor', () => {
section: 'colvinChambers',
} as UnknownAuthUser;

const getUserCaseNoteForCases = getUserCaseNoteForCasesMock as jest.Mock;

beforeEach(() => {
mockCurrentUser = mockJudge;
mockNote = MOCK_NOTE;
applicationContext
.getPersistenceGateway()
.getUserById.mockImplementation(() => mockCurrentUser);
applicationContext
.getPersistenceGateway()
.getUserCaseNoteForCases.mockResolvedValue([mockNote]);
getUserCaseNoteForCases.mockResolvedValue([new UserCaseNote(mockNote)]);
applicationContext
.getUseCaseHelpers()
.getJudgeInSectionHelper.mockReturnValue(mockJudge);
Expand All @@ -52,9 +55,9 @@ describe('getUserCaseNoteForCasesInteractor', () => {
});

it('throws an error if the entity returned from persistence is invalid', async () => {
applicationContext
.getPersistenceGateway()
.getUserCaseNoteForCases.mockResolvedValue([omit(MOCK_NOTE, 'userId')]);
getUserCaseNoteForCases.mockResolvedValue([
new UserCaseNote([omit(MOCK_NOTE, 'userId')]),
]);

await expect(
getUserCaseNoteForCasesInteractor(
Expand Down Expand Up @@ -100,9 +103,8 @@ describe('getUserCaseNoteForCasesInteractor', () => {
omit(mockUser, 'section'),
);

expect(
applicationContext.getPersistenceGateway().getUserCaseNoteForCases.mock
.calls[0][0].userId,
).toEqual(userIdToExpect);
expect(getUserCaseNoteForCases.mock.calls[0][0].userId).toEqual(
userIdToExpect,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
} from '../../../../../shared/src/authorization/authorizationClientService';
import { UnauthorizedError } from '@web-api/errors/errors';
import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser';
import { UserCaseNote } from '../../../../../shared/src/business/entities/notes/UserCaseNote';
import { getUserCaseNoteForCases } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNoteForCases';

export const getUserCaseNoteForCasesInteractor = async (
applicationContext,
Expand All @@ -21,13 +21,10 @@ export const getUserCaseNoteForCasesInteractor = async (
userIdMakingRequest: authorizedUser.userId,
});

const caseNotes = await applicationContext
.getPersistenceGateway()
.getUserCaseNoteForCases({
applicationContext,
docketNumbers,
userId,
});
const caseNotes = await getUserCaseNoteForCases({
docketNumbers,
userId,
});

return caseNotes.map(note => new UserCaseNote(note).validate().toRawObject());
return caseNotes.map(note => note.validate().toRawObject());
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import '@web-api/persistence/postgres/userCaseNotes/mocks.jest';
import { MOCK_CASE } from '../../../../../shared/src/test/mockCase';
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 { getUserCaseNoteInteractor } from './getUserCaseNoteInteractor';
import { getUserCaseNote as getUserCaseNoteMock } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNote';
import { mockJudgeUser } from '@shared/test/mockAuthUsers';
import { omit } from 'lodash';

Expand All @@ -19,13 +21,13 @@ describe('Get case note', () => {
userId: 'unauthorizedUser',
} as unknown as UnknownAuthUser;

const getUserCaseNote = getUserCaseNoteMock as jest.Mock;

it('throws error if user is unauthorized', async () => {
applicationContext
.getPersistenceGateway()
.getUserById.mockImplementation(() => new User(mockUnauthorizedUser));
applicationContext
.getPersistenceGateway()
.getUserCaseNote.mockReturnValue({});
getUserCaseNote.mockReturnValue({});
applicationContext
.getUseCaseHelpers()
.getJudgeInSectionHelper.mockReturnValue(null);
Expand All @@ -45,9 +47,7 @@ describe('Get case note', () => {
applicationContext
.getPersistenceGateway()
.getUserById.mockImplementation(() => new User(mockJudgeUser));
applicationContext
.getPersistenceGateway()
.getUserCaseNote.mockResolvedValue(omit(MOCK_NOTE, 'userId'));
getUserCaseNote.mockResolvedValue(omit(MOCK_NOTE, 'userId'));
applicationContext
.getUseCaseHelpers()
.getJudgeInSectionHelper.mockReturnValue(mockJudgeUser);
Expand All @@ -67,9 +67,7 @@ describe('Get case note', () => {
applicationContext
.getPersistenceGateway()
.getUserById.mockImplementation(() => new User(mockJudgeUser));
applicationContext
.getPersistenceGateway()
.getUserCaseNote.mockResolvedValue(MOCK_NOTE);
getUserCaseNote.mockResolvedValue(MOCK_NOTE);
applicationContext
.getUseCaseHelpers()
.getJudgeInSectionHelper.mockReturnValue(mockJudgeUser);
Expand All @@ -89,9 +87,7 @@ describe('Get case note', () => {
applicationContext
.getPersistenceGateway()
.getUserById.mockImplementation(() => new User(mockJudgeUser));
applicationContext
.getPersistenceGateway()
.getUserCaseNote.mockResolvedValue(MOCK_NOTE);
getUserCaseNote.mockResolvedValue(MOCK_NOTE);
applicationContext
.getUseCaseHelpers()
.getJudgeInSectionHelper.mockReturnValue(null);
Expand All @@ -114,9 +110,7 @@ describe('Get case note', () => {
applicationContext
.getPersistenceGateway()
.getUserById.mockImplementation(() => new User(mockJudgeUser));
applicationContext
.getPersistenceGateway()
.getUserCaseNote.mockReturnValue(null);
getUserCaseNote.mockReturnValue(null);
applicationContext
.getUseCaseHelpers()
.getJudgeInSectionHelper.mockReturnValue(mockJudgeUser);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ServerApplicationContext } from '@web-api/applicationContext';
import { UnauthorizedError } from '@web-api/errors/errors';
import { UnknownAuthUser } from '@shared/business/entities/authUser/AuthUser';
import { UserCaseNote } from '../../../../../shared/src/business/entities/notes/UserCaseNote';
import { getUserCaseNote } from '@web-api/persistence/postgres/userCaseNotes/getUserCaseNote';

/**
* getUserCaseNoteInteractor
Expand All @@ -32,13 +33,10 @@ export const getUserCaseNoteInteractor = async (
userIdMakingRequest: authorizedUser.userId,
});

const caseNote = await applicationContext
.getPersistenceGateway()
.getUserCaseNote({
applicationContext,
docketNumber,
userId,
});
const caseNote = await getUserCaseNote({
docketNumber,
userId,
});

if (caseNote) {
return new UserCaseNote(caseNote).validate().toRawObject();
Expand Down
Loading
Loading