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

Spsh-1574: Fortschrittsanzeige & Bulk-Transaktionen #844

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b77d61d
SPSH-1286: Implemented the repository, entity & aggregate for the his…
phaelcg Nov 27, 2024
fece2b8
SPSH-1286: Refactored the import workflow to update the status of imp…
phaelcg Nov 28, 2024
ff4eff8
SPSH-1286: Implemented unit tests and integration tests
phaelcg Nov 29, 2024
4f9ccae
SPSH-1286: Fixed test coverage
phaelcg Nov 29, 2024
e89db13
SPSH-1286: Fixed code coverage
phaelcg Nov 29, 2024
b841bee
Merge branch 'main' into SPSH-1286
phaelcg Nov 29, 2024
fd0d026
SPSH-1286: Added a reference to the ImportDataItem table and refactor…
phaelcg Nov 29, 2024
28f928c
First draft in progress
phaelcg Dec 2, 2024
174a4cd
SPSH-1553: Workaround for import-workflow
phaelcg Dec 4, 2024
5e5344f
SPSH-1553: Fixed issues with the import-event.handler and added migra…
phaelcg Dec 4, 2024
c8e9768
Merge branch 'main' into SPSH-1553
phaelcg Dec 6, 2024
d9837ee
SPSH-1553: Fixed issued after merge with main
phaelcg Dec 6, 2024
98a9802
SPSH-1553: More code refactoring and foxed tests for the new import w…
phaelcg Dec 10, 2024
af99463
SPSH-1553: Fixed the config loader tests
phaelcg Dec 10, 2024
7e0078a
SPSH-1553: Fixed integration tests
phaelcg Dec 10, 2024
de3c00b
SPSH-1553: Fixed code coverage
phaelcg Dec 10, 2024
50595c1
SPSH-1553: Begrenzung der Anzahl der Nutzer beim Import über die Konf…
phaelcg Dec 11, 2024
958ffcc
SPSH-1553: Fixed issue with last empty line in the CSV file when the …
phaelcg Dec 11, 2024
426bdfc
SPSH-1553: PR Review
phaelcg Dec 12, 2024
95db629
SPSH-1553: Fixed test coverage
phaelcg Dec 12, 2024
d8889ba
Merge branch 'main' into SPSH-1553
casparneumann-cap Dec 12, 2024
4d2b7cf
SPSH-1553: Merged two migration scripts
phaelcg Dec 12, 2024
9402b13
SPSH-1574: Implemented insertMany in import-data-repository
phaelcg Dec 13, 2024
6a3d4c1
SPSH-1553: Reversed mistake in the values.yaml
phaelcg Dec 13, 2024
1a2f74a
Merge branch 'SPSH-1553' into SPSH-1574
phaelcg Dec 13, 2024
0690e7c
SPSH-1553: Added chart configuration and renamed import config variab…
phaelcg Dec 13, 2024
32d7c35
Merge branch 'SPSH-1553' into SPSH-1574
phaelcg Dec 13, 2024
8aceada
SPSH-1574: Implemented create many for the saving of multiple data-it…
phaelcg Dec 16, 2024
eba55c3
SPSH-1574: Fixed the issue with data-items being saved two times
phaelcg Dec 16, 2024
7137875
SPSH-1553: Changed the response type for the import status
phaelcg Dec 16, 2024
5f9f882
Merge branch 'SPSH-1553' into SPSH-1574
phaelcg Dec 16, 2024
37d8e6b
SPSH-1574: Added updateAll in the import-data-repository and fixed th…
phaelcg Dec 17, 2024
d014414
Merge branch 'main' into SPSH-1574
phaelcg Dec 17, 2024
32263fb
SPSH-1574: Renamed updateAll to replaceAll in import-data-repository
phaelcg Dec 17, 2024
1308536
SPSH-1574: Added integration tests and error handling for the replace…
phaelcg Dec 18, 2024
6cd2cf4
SPSH-1574: Fixed sonar cloud error
phaelcg Dec 18, 2024
9eb9b03
Merge branch 'main' into SPSH-1574
phaelcg Dec 18, 2024
6081c6b
SPSH-1574: PR Review
phaelcg Dec 18, 2024
32224a4
New migration for the new column
YoussefBouch Dec 18, 2024
9318e2f
Merge branch 'SPSH-1574' of https://github.com/dBildungsplattform/dbi…
YoussefBouch Dec 18, 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
9 changes: 7 additions & 2 deletions src/modules/import/api/import.controller.integration-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,15 +786,20 @@ describe('Import API', () => {
describe('/GET importstatus by id', () => {
it('should return 200 OK with import ststus', async () => {
const importVorgang: ImportVorgang<true> = await importVorgangRepository.save(
DoFactory.createImportVorgang(false, { status: ImportStatus.COMPLETED }),
DoFactory.createImportVorgang(false, { status: ImportStatus.COMPLETED, totalDataItemImported: 100 }),
);

const response: Response = await request(app.getHttpServer() as App)
.get(`/import/${importVorgang.id}/status`)
.send();

expect(response.status).toBe(200);
expect(response.body).toEqual({ status: ImportStatus.COMPLETED } as ImportVorgangStatusResponse);
expect(response.body).toBeInstanceOf(Object);
expect(response.body).toEqual({
dataItemCount: 100,
status: ImportStatus.COMPLETED,
totalDataItemImported: 100,
} as ImportVorgangStatusResponse);
});

it('should return 404 if importvorgang does not exist', async () => {
Expand Down
8 changes: 8 additions & 0 deletions src/modules/import/api/importvorgang-status.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ import { ImportStatus, ImportStatusName } from '../domain/import.enums.js';
import { ImportVorgang } from '../domain/import-vorgang.js';

export class ImportVorgangStatusResponse {
@ApiProperty()
public dataItemCount: number;

@ApiProperty({ enum: ImportStatus, enumName: ImportStatusName })
public status: ImportStatus;

@ApiProperty()
public totalDataItemImported: number;

public constructor(importVorgang: ImportVorgang<true>) {
this.dataItemCount = importVorgang.dataItemCount;
this.status = importVorgang.status;
this.totalDataItemImported = importVorgang.totalDataItemImported;
}
}
19 changes: 15 additions & 4 deletions src/modules/import/domain/import-event-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ClassLogger } from '../../../core/logging/class-logger.js';
import { ImportExecutedEvent } from '../../../shared/events/import-executed.event.js';
import { RolleNurAnPassendeOrganisationError } from '../../personenkontext/specification/error/rolle-nur-an-passende-organisation.js';
import { ImportPasswordEncryptor } from './import-password-encryptor.js';
import { ImportDomainError } from './import-domain.error.js';

describe('ImportEventHandler', () => {
let module: TestingModule;
Expand Down Expand Up @@ -167,7 +168,12 @@ describe('ImportEventHandler', () => {
personenkontextCreationServiceMock.createPersonWithPersonenkontexte.mockResolvedValueOnce(error);
organisationRepoMock.findById.mockResolvedValueOnce(schule);

await sut.handleExecuteImport(event);
const importDomainError: DomainError = new ImportDomainError(
`The creation of person with personenkontexte for the import transaction:${importvorgang.id} failed`,
importvorgang.id,
);

await expect(sut.handleExecuteImport(event)).rejects.toThrowError(importDomainError);

expect(loggerMock.error).toHaveBeenCalledWith(
`System hat versucht einen neuen Benutzer für ${importDataItem.vorname} ${importDataItem.nachname} anzulegen. Fehler: ${error.message}`,
Expand Down Expand Up @@ -206,7 +212,12 @@ describe('ImportEventHandler', () => {
personenkontextCreationServiceMock.createPersonWithPersonenkontexte.mockResolvedValueOnce(pks);
organisationRepoMock.findById.mockResolvedValueOnce(schule);

await sut.handleExecuteImport(event);
const error: DomainError = new ImportDomainError(
`The creation for a password for the person with ID ${person.id} for the import transaction:${importvorgang.id} has failed`,
importvorgang.id,
);

await expect(sut.handleExecuteImport(event)).rejects.toThrowError(error);

expect(person.newPassword).toBeUndefined();
expect(loggerMock.error).toHaveBeenCalledWith(`Person with ID ${person.id} has no start password!`);
Expand Down Expand Up @@ -258,8 +269,8 @@ describe('ImportEventHandler', () => {
expect(loggerMock.info).toHaveBeenCalledWith(
`System hat einen neuen Benutzer ${person.referrer} (${person.id}) angelegt.`,
);
expect(importDataRepositoryMock.save).toHaveBeenCalled();
expect(importVorgangRepositoryMock.save).toHaveBeenCalledTimes(1);
expect(importDataRepositoryMock.replaceAll).toHaveBeenCalled();
expect(importVorgangRepositoryMock.save).toHaveBeenCalledTimes(2);
});
});
});
61 changes: 46 additions & 15 deletions src/modules/import/domain/import-event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import { OrganisationByIdAndName } from './import-workflow.js';
import { Injectable } from '@nestjs/common';
import { ClassLogger } from '../../../core/logging/class-logger.js';
import { ImportPasswordEncryptor } from './import-password-encryptor.js';
import { PersonPermissions } from '../../authentication/domain/person-permissions.js';
import { ImportDomainError } from './import-domain.error.js';
@Injectable()
export class ImportEventHandler {
public selectedOrganisationId!: string;

public selectedRolleId!: string;

private readonly NUMBER_OF_USERS_PRO_REQUEST: number = 25;
phaelcg marked this conversation as resolved.
Show resolved Hide resolved

public constructor(
private readonly organisationRepository: OrganisationRepository,
private readonly importDataRepository: ImportDataRepository,
Expand Down Expand Up @@ -51,7 +55,6 @@ export class ImportEventHandler {
}
});

const importDataItemsWithLoginInfo: ImportDataItem<true>[] = [];
const importvorgangId: string = event.importVorgangId;
const importVorgang: Option<ImportVorgang<true>> = await this.importVorgangRepository.findById(importvorgangId);
if (!importVorgang) {
Expand All @@ -64,9 +67,37 @@ export class ImportEventHandler {
return this.logger.error(`No import data itemns found for Importvorgang:${importvorgangId}`);
}

/* eslint-disable no-await-in-loop */
phaelcg marked this conversation as resolved.
Show resolved Hide resolved
while (importDataItems.length > 0) {
const dataItemsToImport: ImportDataItem<true>[] = importDataItems.splice(
0,
this.NUMBER_OF_USERS_PRO_REQUEST,
);
await this.savePersonWithPersonenkontext(
importVorgang,
dataItemsToImport,
klassenByIDandName,
event.permissions,
);

importVorgang.incrementTotalImportDataItems(dataItemsToImport.length);
await this.importVorgangRepository.save(importVorgang);
}

importVorgang.finish();
await this.importVorgangRepository.save(importVorgang);
}

private async savePersonWithPersonenkontext(
importVorgang: ImportVorgang<true>,
dataItems: ImportDataItem<true>[],
klassenByIDandName: OrganisationByIdAndName[],
permissions: PersonPermissions,
): Promise<void> {
const importDataItemsWithLoginInfo: ImportDataItem<true>[] = [];
//We must create every peron individually otherwise it cannot assign the correct username when we have multiple users with the same name
/* eslint-disable no-await-in-loop */
for (const importDataItem of importDataItems) {
for (const importDataItem of dataItems) {
phaelcg marked this conversation as resolved.
Show resolved Hide resolved
const klasse: OrganisationByIdAndName | undefined = klassenByIDandName.find(
(organisationByIdAndName: OrganisationByIdAndName) =>
organisationByIdAndName.name === importDataItem.klasse,
Expand All @@ -93,7 +124,7 @@ export class ImportEventHandler {

const savedPersonWithPersonenkontext: DomainError | PersonPersonenkontext =
await this.personenkontextCreationService.createPersonWithPersonenkontexte(
event.permissions,
permissions,
importDataItem.vorname,
importDataItem.nachname,
createPersonenkontexte,
Expand All @@ -104,14 +135,22 @@ export class ImportEventHandler {
`System hat einen neuen Benutzer ${savedPersonWithPersonenkontext.person.referrer} (${savedPersonWithPersonenkontext.person.id}) angelegt.`,
);
} else {
return this.logger.error(
this.logger.error(
`System hat versucht einen neuen Benutzer für ${importDataItem.vorname} ${importDataItem.nachname} anzulegen. Fehler: ${savedPersonWithPersonenkontext.message}`,
);

throw new ImportDomainError(
`The creation of person with personenkontexte for the import transaction:${importVorgang.id} failed`,
importVorgang.id,
);
}

if (!savedPersonWithPersonenkontext.person.newPassword) {
return this.logger.error(
`Person with ID ${savedPersonWithPersonenkontext.person.id} has no start password!`,
this.logger.error(`Person with ID ${savedPersonWithPersonenkontext.person.id} has no start password!`);

throw new ImportDomainError(
`The creation for a password for the person with ID ${savedPersonWithPersonenkontext.person.id} for the import transaction:${importVorgang.id} has failed`,
importVorgang.id,
Copy link
Contributor

Choose a reason for hiding this comment

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

If these errors occur, they aren't handled at all as far as I can see. They will just be logged and the import will never finish?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great observation, i set the status to FAILED, when the FE requests the import status it will see that the import has failed

);
}

Expand All @@ -134,14 +173,6 @@ export class ImportEventHandler {
);
}
/* eslint-disable no-await-in-loop */

await Promise.allSettled(
importDataItemsWithLoginInfo.map(async (importDataItem: ImportDataItem<true>) =>
this.importDataRepository.save(importDataItem),
),
);

importVorgang.finish();
await this.importVorgangRepository.save(importVorgang);
await this.importDataRepository.replaceAll(importDataItemsWithLoginInfo);
}
}
8 changes: 8 additions & 0 deletions src/modules/import/domain/import-vorgang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class ImportVorgang<WasPersisted extends boolean> {
public organisationsname: string,
public dataItemCount: number,
public status: Persisted<ImportStatus, WasPersisted>,
public totalDataItemImported: number,
public importByPersonId?: string,
public rolleId?: string,
public organisationId?: string,
Expand All @@ -24,6 +25,7 @@ export class ImportVorgang<WasPersisted extends boolean> {
organisationsname: string,
dataItemCount: number,
status: ImportStatus,
totalDataItemImported: number,
importByPersonId?: string,
rolleId?: string,
organisationId?: string,
Expand All @@ -37,6 +39,7 @@ export class ImportVorgang<WasPersisted extends boolean> {
organisationsname,
dataItemCount,
status,
totalDataItemImported,
importByPersonId,
rolleId,
organisationId,
Expand All @@ -61,6 +64,7 @@ export class ImportVorgang<WasPersisted extends boolean> {
organisationsname,
dataItemCount,
undefined,
0,
importByPersonId,
rolleId,
organisationId,
Expand Down Expand Up @@ -90,4 +94,8 @@ export class ImportVorgang<WasPersisted extends boolean> {
public finish(): void {
this.status = ImportStatus.FINISHED;
}

public incrementTotalImportDataItems(totalDataItemImported: number): void {
this.totalDataItemImported += totalDataItemImported;
}
}
76 changes: 39 additions & 37 deletions src/modules/import/domain/import-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,56 +186,63 @@ export class ImportWorkflow {
);

const savedImportvorgang: ImportVorgang<true> = await this.importVorgangRepository.save(importVorgang);
const totalImportDataItems: number = parsedDataItems.length;
/* eslint-disable no-await-in-loop */
while (parsedDataItems.length > 0) {
const dataItems: CSVImportDataItemDTO[] = parsedDataItems.splice(0, 50);

const promises: Promise<ImportDataItem<true>>[] = parsedDataItems.map((value: CSVImportDataItemDTO) => {
const importDataItemErrors: string[] = [];
const importDataItems: ImportDataItem<false>[] = dataItems.map((value: CSVImportDataItemDTO) => {
const importDataItemErrors: string[] = [];

// Validate object
for (const error of validateSync(value, { forbidUnknownValues: true })) {
if (error.constraints) {
for (const message of Object.values(error.constraints)) {
importDataItemErrors.push(message);
// Validate object
for (const error of validateSync(value, { forbidUnknownValues: true })) {
if (error.constraints) {
for (const message of Object.values(error.constraints)) {
importDataItemErrors.push(message);
}
}
}
}

if (value.klasse) {
const klasse: OrganisationByIdAndName | undefined = klassenByIDandName.find(
(organisationByIdAndName: OrganisationByIdAndName) => organisationByIdAndName.name === value.klasse, //Klassennamen sind case sensitive
);
if (value.klasse) {
const klasse: OrganisationByIdAndName | undefined = klassenByIDandName.find(
(organisationByIdAndName: OrganisationByIdAndName) =>
organisationByIdAndName.name === value.klasse, //Klassennamen sind case sensitive
);

//Only check if the Klasse exists
//Do not need to check if the Klasse can be assigned to rolle for now, because we only impport RollenArt=LERN
if (!klasse) {
importDataItemErrors.push(ImportDomainErrorI18nTypes.IMPORT_DATA_ITEM_KLASSE_NOT_FOUND);
//Only check if the Klasse exists
//Do not need to check if the Klasse can be assigned to rolle for now, because we only impport RollenArt=LERN
if (!klasse) {
importDataItemErrors.push(ImportDomainErrorI18nTypes.IMPORT_DATA_ITEM_KLASSE_NOT_FOUND);
}
}
}

const importDataItem: ImportDataItem<false> = ImportDataItem.createNew(
savedImportvorgang.id,
value.nachname,
value.vorname,
value.klasse,
value.personalnummer,
importDataItemErrors,
);
const importDataItem: ImportDataItem<false> = ImportDataItem.createNew(
savedImportvorgang.id,
value.nachname,
value.vorname,
value.klasse,
value.personalnummer,
importDataItemErrors,
);

if (importDataItemErrors.length > 0) {
invalidImportDataItems.push(importDataItem);
}
if (importDataItemErrors.length > 0) {
invalidImportDataItems.push(importDataItem);
}

return this.importDataRepository.save(importDataItem);
});
return importDataItem;
});

await Promise.all(promises);
await this.importDataRepository.createAll(importDataItems);
}
/* eslint-disable no-await-in-loop */

savedImportvorgang.validate(invalidImportDataItems.length);
await this.importVorgangRepository.save(savedImportvorgang);

return {
importVorgangId: savedImportvorgang.id,
isValid: invalidImportDataItems.length === 0,
totalImportDataItems: parsedDataItems.length,
totalImportDataItems: totalImportDataItems,
totalInvalidImportDataItems: invalidImportDataItems.length,
invalidImportDataItems,
};
Expand All @@ -250,11 +257,6 @@ export class ImportWorkflow {
};
}

// Get all import data items with importvorgangId
//Optimierung: für das folgeTicket mit z.B. 800 Lehrer , muss der thread so manipuliert werden (sobald ein Resultat da ist, wird der nächste request abgeschickt)
//Optimierung: Process 10 dataItems at time for createPersonWithPersonenkontexte
// const offset: number = 0;
// const limit: number = 10;
const importVorgang: Option<ImportVorgang<true>> = await this.importVorgangRepository.findById(importvorgangId);
if (!importVorgang) {
this.logger.warning(`Importvorgang: ${importvorgangId} not found`);
Expand Down
Loading
Loading