From 73165caea2fc981f05fc6cd93169786e86efecb7 Mon Sep 17 00:00:00 2001 From: mkreuzkam-cap <144103168+mkreuzkam-cap@users.noreply.github.com> Date: Wed, 23 Oct 2024 09:02:03 +0200 Subject: [PATCH] EW-1005: Add school data to TSP sync. (#5296) * Add school data to TSP sync. --------- Co-authored-by: Alexander Weber <103171324+alweber-cap@users.noreply.github.com> --- apps/server/src/infra/sync/sync.module.ts | 10 +- .../tsp-data-fetched.loggable.spec.ts | 29 +++ .../tsp/loggable/tsp-data-fetched.loggable.ts | 24 ++ .../tsp-missing-external-id.loggable.spec.ts | 26 ++ .../tsp-missing-external-id.loggable.ts | 16 ++ .../tsp-synced-users.loggable.spec.ts | 26 ++ .../tsp/loggable/tsp-synced-users.loggable.ts | 16 ++ .../tsp-syncing-users.loggable.spec.ts | 26 ++ .../loggable/tsp-syncing-users.loggable.ts | 16 ++ .../sync/tsp/tsp-oauth-data.mapper.spec.ts | 224 ++++++++++++++++++ .../infra/sync/tsp/tsp-oauth-data.mapper.ts | 133 +++++++++++ .../src/infra/sync/tsp/tsp-sync.config.ts | 2 + .../infra/sync/tsp/tsp-sync.service.spec.ts | 224 +++++++++++++++--- .../src/infra/sync/tsp/tsp-sync.service.ts | 59 ++++- .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 89 +++++++ .../src/infra/sync/tsp/tsp-sync.strategy.ts | 50 +++- .../service/tsp-provisioning.service.ts | 2 +- .../src/modules/server/server.config.ts | 2 + config/default.schema.json | 10 + 19 files changed, 941 insertions(+), 43 deletions(-) create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts create mode 100644 apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index 5295e22a922..40e02c83966 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -1,14 +1,16 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { ConsoleWriterModule } from '@infra/console'; +import { RabbitMQWrapperModule } from '@infra/rabbitmq'; import { TspClientModule } from '@infra/tsp-client/tsp-client.module'; import { LegacySchoolModule } from '@modules/legacy-school'; import { SchoolModule } from '@modules/school'; import { SystemModule } from '@modules/system'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { RabbitMQWrapperModule } from '@infra/rabbitmq'; +import { ProvisioningModule } from '@src/modules/provisioning'; import { SyncConsole } from './console/sync.console'; import { SyncService } from './service/sync.service'; +import { TspOauthDataMapper } from './tsp/tsp-oauth-data.mapper'; import { TspSyncService } from './tsp/tsp-sync.service'; import { TspSyncStrategy } from './tsp/tsp-sync.strategy'; import { SyncUc } from './uc/sync.uc'; @@ -18,14 +20,16 @@ import { SyncUc } from './uc/sync.uc'; LoggerModule, ConsoleWriterModule, ...((Configuration.get('FEATURE_TSP_SYNC_ENABLED') as boolean) - ? [TspClientModule, SystemModule, SchoolModule, LegacySchoolModule, RabbitMQWrapperModule] + ? [TspClientModule, SystemModule, SchoolModule, LegacySchoolModule, RabbitMQWrapperModule, ProvisioningModule] : []), ], providers: [ SyncConsole, SyncUc, SyncService, - ...((Configuration.get('FEATURE_TSP_SYNC_ENABLED') as boolean) ? [TspSyncStrategy, TspSyncService] : []), + ...((Configuration.get('FEATURE_TSP_SYNC_ENABLED') as boolean) + ? [TspSyncStrategy, TspSyncService, TspOauthDataMapper] + : []), ], exports: [SyncConsole], }) diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.spec.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.spec.ts new file mode 100644 index 00000000000..cc21a93af02 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.spec.ts @@ -0,0 +1,29 @@ +import { TspDataFetchedLoggable } from './tsp-data-fetched.loggable'; + +describe(TspDataFetchedLoggable.name, () => { + let loggable: TspDataFetchedLoggable; + + beforeAll(() => { + loggable = new TspDataFetchedLoggable(1, 2, 3, 4); + }); + + describe('when loggable is initialized', () => { + it('should be defined', () => { + expect(loggable).toBeDefined(); + }); + }); + + describe('getLogMessage', () => { + it('should return a log message', () => { + expect(loggable.getLogMessage()).toEqual({ + message: `Fetched 1 teachers, 2 students and 3 classes for the last 4 days from TSP`, + data: { + tspTeacherCount: 1, + tspStudentCount: 2, + tspClassesCount: 3, + daysFetched: 4, + }, + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts new file mode 100644 index 00000000000..ae9e62d96cc --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts @@ -0,0 +1,24 @@ +import { Loggable, LogMessage } from '@src/core/logger'; + +export class TspDataFetchedLoggable implements Loggable { + constructor( + private readonly tspTeacherCount: number, + private readonly tspStudentCount: number, + private readonly tspClassesCount: number, + private readonly daysFetched: number + ) {} + + getLogMessage(): LogMessage { + const message: LogMessage = { + message: `Fetched ${this.tspTeacherCount} teachers, ${this.tspStudentCount} students and ${this.tspClassesCount} classes for the last ${this.daysFetched} days from TSP`, + data: { + tspTeacherCount: this.tspTeacherCount, + tspStudentCount: this.tspStudentCount, + tspClassesCount: this.tspClassesCount, + daysFetched: this.daysFetched, + }, + }; + + return message; + } +} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.spec.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.spec.ts new file mode 100644 index 00000000000..0d1ff41fbad --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.spec.ts @@ -0,0 +1,26 @@ +import { TspMissingExternalIdLoggable } from './tsp-missing-external-id.loggable'; + +describe(TspMissingExternalIdLoggable.name, () => { + let loggable: TspMissingExternalIdLoggable; + + beforeAll(() => { + loggable = new TspMissingExternalIdLoggable('teacher'); + }); + + describe('when loggable is initialized', () => { + it('should be defined', () => { + expect(loggable).toBeDefined(); + }); + }); + + describe('getLogMessage', () => { + it('should return a log message', () => { + expect(loggable.getLogMessage()).toEqual({ + message: `A teacher is missing an id. It is skipped.`, + data: { + objectType: 'teacher', + }, + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts new file mode 100644 index 00000000000..d8362100247 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts @@ -0,0 +1,16 @@ +import { Loggable, LogMessage } from '@src/core/logger'; + +export class TspMissingExternalIdLoggable implements Loggable { + constructor(private readonly objectType: string) {} + + getLogMessage(): LogMessage { + const message: LogMessage = { + message: `A ${this.objectType} is missing an id. It is skipped.`, + data: { + objectType: this.objectType, + }, + }; + + return message; + } +} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.spec.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.spec.ts new file mode 100644 index 00000000000..b97c2d8865e --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.spec.ts @@ -0,0 +1,26 @@ +import { TspSyncedUsersLoggable } from './tsp-synced-users.loggable'; + +describe(TspSyncedUsersLoggable.name, () => { + let loggable: TspSyncedUsersLoggable; + + beforeAll(() => { + loggable = new TspSyncedUsersLoggable(10); + }); + + describe('when loggable is initialized', () => { + it('should be defined', () => { + expect(loggable).toBeDefined(); + }); + }); + + describe('getLogMessage', () => { + it('should return a log message', () => { + expect(loggable.getLogMessage()).toEqual({ + message: `Synced 10 users from TSP.`, + data: { + syncedUsers: 10, + }, + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts new file mode 100644 index 00000000000..535a9ce0cb7 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts @@ -0,0 +1,16 @@ +import { Loggable, LogMessage } from '@src/core/logger'; + +export class TspSyncedUsersLoggable implements Loggable { + constructor(private readonly syncedUsers: number) {} + + getLogMessage(): LogMessage { + const message: LogMessage = { + message: `Synced ${this.syncedUsers} users from TSP.`, + data: { + syncedUsers: this.syncedUsers, + }, + }; + + return message; + } +} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.spec.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.spec.ts new file mode 100644 index 00000000000..61c4f93d21d --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.spec.ts @@ -0,0 +1,26 @@ +import { TspSyncingUsersLoggable } from './tsp-syncing-users.loggable'; + +describe(TspSyncingUsersLoggable.name, () => { + let loggable: TspSyncingUsersLoggable; + + beforeAll(() => { + loggable = new TspSyncingUsersLoggable(10); + }); + + describe('when loggable is initialized', () => { + it('should be defined', () => { + expect(loggable).toBeDefined(); + }); + }); + + describe('getLogMessage', () => { + it('should return a log message', () => { + expect(loggable.getLogMessage()).toEqual({ + message: `Syncing 10 users from TSP.`, + data: { + syncingUsers: 10, + }, + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts new file mode 100644 index 00000000000..a6ecfe0aeff --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts @@ -0,0 +1,16 @@ +import { Loggable, LogMessage } from '@src/core/logger'; + +export class TspSyncingUsersLoggable implements Loggable { + constructor(private readonly syncingUsers: number) {} + + getLogMessage(): LogMessage { + const message: LogMessage = { + message: `Syncing ${this.syncingUsers} users from TSP.`, + data: { + syncingUsers: this.syncingUsers, + }, + }; + + return message; + } +} diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts new file mode 100644 index 00000000000..821a88d432a --- /dev/null +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -0,0 +1,224 @@ +import { faker } from '@faker-js/faker'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { + ExternalClassDto, + ExternalSchoolDto, + ExternalUserDto, + OauthDataDto, + ProvisioningSystemDto, +} from '@modules/provisioning'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RoleName } from '@shared/domain/interface'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { Logger } from '@src/core/logger'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '@src/infra/tsp-client'; +import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; +import { schoolFactory } from '@src/modules/school/testing'; +import { systemFactory } from '@src/modules/system/testing'; +import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; +import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; + +describe(TspOauthDataMapper.name, () => { + let module: TestingModule; + let sut: TspOauthDataMapper; + let logger: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + TspOauthDataMapper, + { + provide: Logger, + useValue: createMock(), + }, + ], + }).compile(); + + sut = module.get(TspOauthDataMapper); + logger = module.get(Logger); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('when mapper is initialized', () => { + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + }); + + describe('mapTspDataToOauthData', () => { + describe('when mapping tsp data to oauth data', () => { + const setup = () => { + const system = systemFactory.build(); + + const school = schoolFactory.build({ + externalId: faker.string.alpha(), + }); + + const lehrerUid = faker.string.alpha(); + + const tspTeachers: RobjExportLehrer[] = [ + { + lehrerUid, + lehrerNachname: faker.string.alpha(), + lehrerVorname: faker.string.alpha(), + schuleNummer: school.externalId, + }, + ]; + + const klasseId = faker.string.alpha(); + + const tspClasses: RobjExportKlasse[] = [ + { + klasseId, + klasseName: faker.string.alpha(), + lehrerUid, + }, + ]; + + const tspStudents: RobjExportSchueler[] = [ + { + schuelerUid: faker.string.alpha(), + schuelerNachname: faker.string.alpha(), + schuelerVorname: faker.string.alpha(), + schuleNummer: school.externalId, + klasseId, + }, + ]; + + const provisioningSystemDto = new ProvisioningSystemDto({ + systemId: system.id, + provisioningStrategy: SystemProvisioningStrategy.TSP, + }); + + const externalSchool = new ExternalSchoolDto({ + externalId: school.externalId ?? '', + }); + + const externalClass = new ExternalClassDto({ + externalId: klasseId, + name: tspClasses[0].klasseName, + }); + + const expected: OauthDataDto[] = [ + new OauthDataDto({ + system: provisioningSystemDto, + externalUser: new ExternalUserDto({ + externalId: tspTeachers[0].lehrerUid ?? '', + firstName: tspTeachers[0].lehrerNachname, + lastName: tspTeachers[0].lehrerNachname, + roles: [RoleName.TEACHER], + }), + externalSchool, + externalClasses: [externalClass], + }), + new OauthDataDto({ + system: provisioningSystemDto, + externalUser: new ExternalUserDto({ + externalId: tspStudents[0].schuelerUid ?? '', + firstName: tspStudents[0].schuelerNachname, + lastName: tspStudents[0].schuelerNachname, + roles: [RoleName.STUDENT], + }), + externalSchool, + externalClasses: [externalClass], + }), + ]; + + return { system, school, tspTeachers, tspStudents, tspClasses, expected }; + }; + + it('should return an array of oauth data dtos', () => { + const { system, school, tspTeachers, tspStudents, tspClasses, expected } = setup(); + + const result = sut.mapTspDataToOauthData(system, [school], tspTeachers, tspStudents, tspClasses); + + expect(result).toStrictEqual(expected); + }); + }); + + describe('when school has to externalId', () => { + const setup = () => { + const system = systemFactory.build(); + const school = schoolFactory.build({ + externalId: undefined, + }); + + return { system, school }; + }; + + it('should throw BadDataLoggableException', () => { + const { system, school } = setup(); + + expect(() => sut.mapTspDataToOauthData(system, [school], [], [], [])).toThrow(BadDataLoggableException); + }); + }); + + describe('when tsp class has to id', () => { + const setup = () => { + const system = systemFactory.build(); + + const tspClass: RobjExportKlasse = { + klasseId: undefined, + }; + + return { system, tspClass }; + }; + + it('should log TspMissingExternalIdLoggable', () => { + const { system, tspClass } = setup(); + + sut.mapTspDataToOauthData(system, [], [], [], [tspClass]); + + expect(logger.info).toHaveBeenCalledWith(new TspMissingExternalIdLoggable('class')); + }); + }); + + describe('when tsp teacher has to id', () => { + const setup = () => { + const system = systemFactory.build(); + + const tspTeacher: RobjExportLehrer = { + lehrerUid: undefined, + }; + + return { system, tspTeacher }; + }; + + it('should log TspMissingExternalIdLoggable', () => { + const { system, tspTeacher } = setup(); + + sut.mapTspDataToOauthData(system, [], [tspTeacher], [], []); + + expect(logger.info).toHaveBeenCalledWith(new TspMissingExternalIdLoggable('teacher')); + }); + }); + + describe('when tsp student has to id', () => { + const setup = () => { + const system = systemFactory.build(); + + const tspStudent: RobjExportSchueler = { + schuelerUid: undefined, + }; + + return { system, tspStudent }; + }; + + it('should log TspMissingExternalIdLoggable', () => { + const { system, tspStudent } = setup(); + + sut.mapTspDataToOauthData(system, [], [], [tspStudent], []); + + expect(logger.info).toHaveBeenCalledWith(new TspMissingExternalIdLoggable('student')); + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts new file mode 100644 index 00000000000..3237388d07f --- /dev/null +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts @@ -0,0 +1,133 @@ +import { + ExternalClassDto, + ExternalSchoolDto, + ExternalUserDto, + OauthDataDto, + ProvisioningSystemDto, +} from '@modules/provisioning'; +import { School } from '@modules/school'; +import { System } from '@modules/system'; +import { Injectable } from '@nestjs/common'; +import { RoleName } from '@shared/domain/interface'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { Logger } from '@src/core/logger'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '@src/infra/tsp-client'; +import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; +import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; + +@Injectable() +export class TspOauthDataMapper { + constructor(private readonly logger: Logger) { + this.logger.setContext(TspOauthDataMapper.name); + } + + public mapTspDataToOauthData( + system: System, + schools: School[], + tspTeachers: RobjExportLehrer[], + tspStudents: RobjExportSchueler[], + tspClasses: RobjExportKlasse[] + ): OauthDataDto[] { + const systemDto = new ProvisioningSystemDto({ + systemId: system.id, + provisioningStrategy: SystemProvisioningStrategy.TSP, + }); + + const externalSchools = new Map(); + const externalClasses = new Map(); + const teacherForClasses = new Map>(); + const oauthDataDtos: OauthDataDto[] = []; + + schools.forEach((school) => { + if (!school.externalId) { + throw new BadDataLoggableException(`School ${school.id} has no externalId`); + } + + externalSchools.set( + school.externalId, + new ExternalSchoolDto({ + externalId: school.externalId, + }) + ); + }); + + tspClasses.forEach((tspClass) => { + if (!tspClass.klasseId) { + this.logger.info(new TspMissingExternalIdLoggable('class')); + return; + } + + const externalClass = new ExternalClassDto({ + externalId: tspClass.klasseId, + name: tspClass.klasseName, + }); + + externalClasses.set(tspClass.klasseId, externalClass); + + if (tspClass.lehrerUid) { + const classSet = teacherForClasses.get(tspClass.lehrerUid) ?? []; + classSet.push(tspClass.klasseId); + teacherForClasses.set(tspClass.lehrerUid, classSet); + } + }); + + tspTeachers.forEach((tspTeacher) => { + if (!tspTeacher.lehrerUid) { + this.logger.info(new TspMissingExternalIdLoggable('teacher')); + return; + } + + const externalUser = new ExternalUserDto({ + externalId: tspTeacher.lehrerUid, + firstName: tspTeacher.lehrerNachname, + lastName: tspTeacher.lehrerNachname, + roles: [RoleName.TEACHER], + }); + + const classIds = teacherForClasses.get(tspTeacher.lehrerUid) ?? []; + const classes = classIds + .map((classId) => externalClasses.get(classId)) + .filter((externalClass) => !!externalClass); + + const externalSchool = tspTeacher.schuleNummer == null ? undefined : externalSchools.get(tspTeacher.schuleNummer); + + const oauthDataDto = new OauthDataDto({ + system: systemDto, + externalUser, + externalSchool, + externalClasses: classes, + }); + + oauthDataDtos.push(oauthDataDto); + }); + + tspStudents.forEach((tspStudent) => { + if (!tspStudent.schuelerUid) { + this.logger.info(new TspMissingExternalIdLoggable('student')); + return; + } + + const externalUser = new ExternalUserDto({ + externalId: tspStudent.schuelerUid, + firstName: tspStudent.schuelerNachname, + lastName: tspStudent.schuelerNachname, + roles: [RoleName.STUDENT], + }); + + const classStudent = tspStudent.klasseId == null ? undefined : externalClasses.get(tspStudent.klasseId); + + const externalSchool = tspStudent.schuleNummer == null ? undefined : externalSchools.get(tspStudent.schuleNummer); + + const oauthDataDto = new OauthDataDto({ + system: systemDto, + externalUser, + externalSchool, + externalClasses: classStudent ? [classStudent] : [], + }); + + oauthDataDtos.push(oauthDataDto); + }); + + return oauthDataDtos; + } +} diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.config.ts b/apps/server/src/infra/sync/tsp/tsp-sync.config.ts index 9b1e8064337..d6ac4204c9c 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.config.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.config.ts @@ -1,4 +1,6 @@ export interface TspSyncConfig { TSP_SYNC_SCHOOL_LIMIT: number; TSP_SYNC_SCHOOL_DAYS_TO_FETCH: number; + TSP_SYNC_DATA_LIMIT: number; + TSP_SYNC_DATA_DAYS_TO_FETCH: number; } diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts index 486d4c2bb57..fe8bcc4c4a3 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts @@ -1,6 +1,13 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ExportApiInterface, RobjExportSchule, TspClientFactory } from '@infra/tsp-client'; +import { + ExportApiInterface, + RobjExportKlasse, + RobjExportLehrer, + RobjExportSchueler, + RobjExportSchule, + TspClientFactory, +} from '@infra/tsp-client'; import { School, SchoolService } from '@modules/school'; import { SystemService, SystemType } from '@modules/system'; import { Test, TestingModule } from '@nestjs/testing'; @@ -106,38 +113,75 @@ describe(TspSyncService.name, () => { }); }); - describe('fetchTspSchools', () => { - describe('when tsp schools are fetched', () => { - const setup = () => { - const clientId = faker.string.alpha(); - const clientSecret = faker.string.alpha(); - const tokenEndpoint = faker.internet.url(); - const system = systemFactory.build({ - oauthConfig: { - clientId, - clientSecret, - tokenEndpoint, - }, - }); + const setupTspClient = () => { + const clientId = faker.string.alpha(); + const clientSecret = faker.string.alpha(); + const tokenEndpoint = faker.internet.url(); + const system = systemFactory.build({ + oauthConfig: { + clientId, + clientSecret, + tokenEndpoint, + }, + }); - const tspSchool: RobjExportSchule = { - schuleName: faker.string.alpha(), - schuleNummer: faker.string.alpha(), - }; - const schools = [tspSchool]; - const response = createMock>>({ - data: schools, - }); + const tspSchool: RobjExportSchule = { + schuleName: faker.string.alpha(), + schuleNummer: faker.string.alpha(), + }; + const schools = [tspSchool]; + const responseSchools = createMock>>({ + data: schools, + }); + + const tspTeacher: RobjExportLehrer = { + schuleNummer: faker.string.alpha(), + lehrerVorname: faker.string.alpha(), + lehrerNachname: faker.string.alpha(), + lehrerUid: faker.string.alpha(), + }; + const teachers = [tspTeacher]; + const responseTeachers = createMock>>({ + data: teachers, + }); - const exportApiMock = createMock(); - exportApiMock.exportSchuleList.mockResolvedValueOnce(response); - tspClientFactory.createExportClient.mockReturnValueOnce(exportApiMock); + const tspStudent: RobjExportSchueler = { + schuleNummer: faker.string.alpha(), + schuelerVorname: faker.string.alpha(), + schuelerNachname: faker.string.alpha(), + schuelerUid: faker.string.alpha(), + }; + const students = [tspStudent]; + const responseStudents = createMock>>({ + data: students, + }); - return { clientId, clientSecret, tokenEndpoint, system, exportApiMock, schools }; - }; + const tspClass: RobjExportKlasse = { + schuleNummer: faker.string.alpha(), + klasseId: faker.string.alpha(), + klasseName: faker.string.alpha(), + lehrerUid: faker.string.alpha(), + }; + const classes = [tspClass]; + const responseClasses = createMock>>({ + data: classes, + }); + + const exportApiMock = createMock(); + exportApiMock.exportSchuleList.mockResolvedValueOnce(responseSchools); + exportApiMock.exportLehrerList.mockResolvedValueOnce(responseTeachers); + exportApiMock.exportSchuelerList.mockResolvedValueOnce(responseStudents); + exportApiMock.exportKlasseList.mockResolvedValueOnce(responseClasses); + + tspClientFactory.createExportClient.mockReturnValueOnce(exportApiMock); + return { clientId, clientSecret, tokenEndpoint, system, exportApiMock, schools, teachers, students, classes }; + }; + + describe('fetchTspSchools', () => { + describe('when tsp schools are fetched', () => { it('should use the oauthConfig to create the client', async () => { - const { clientId, clientSecret, tokenEndpoint, system } = setup(); + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); await sut.fetchTspSchools(system, 1); @@ -149,7 +193,7 @@ describe(TspSyncService.name, () => { }); it('should call exportSchuleList', async () => { - const { system, exportApiMock } = setup(); + const { system, exportApiMock } = setupTspClient(); await sut.fetchTspSchools(system, 1); @@ -157,7 +201,7 @@ describe(TspSyncService.name, () => { }); it('should return an array of schools', async () => { - const { system } = setup(); + const { system } = setupTspClient(); const schools = await sut.fetchTspSchools(system, 1); @@ -167,6 +211,105 @@ describe(TspSyncService.name, () => { }); }); + describe('fetchTspTeachers', () => { + describe('when tsp teachers are fetched', () => { + it('should use the oauthConfig to create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); + + await sut.fetchTspTeachers(system, 1); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportLehrerList', async () => { + const { system, exportApiMock } = setupTspClient(); + + await sut.fetchTspTeachers(system, 1); + + expect(exportApiMock.exportLehrerList).toHaveBeenCalledTimes(1); + }); + + it('should return an array of teachers', async () => { + const { system } = setupTspClient(); + + const teachers = await sut.fetchTspTeachers(system, 1); + + expect(teachers).toBeDefined(); + expect(teachers).toBeInstanceOf(Array); + }); + }); + }); + + describe('fetchTspStudents', () => { + describe('when tsp students are fetched', () => { + it('should use the oauthConfig to create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); + + await sut.fetchTspStudents(system, 1); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportSchuelerList', async () => { + const { system, exportApiMock } = setupTspClient(); + + await sut.fetchTspStudents(system, 1); + + expect(exportApiMock.exportSchuelerList).toHaveBeenCalledTimes(1); + }); + + it('should return an array of students', async () => { + const { system } = setupTspClient(); + + const students = await sut.fetchTspStudents(system, 1); + + expect(students).toBeDefined(); + expect(students).toBeInstanceOf(Array); + }); + }); + }); + + describe('fetchTspClasses', () => { + describe('when tsp classes are fetched', () => { + it('should use the oauthConfig to create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); + + await sut.fetchTspClasses(system, 1); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportKlasseList', async () => { + const { system, exportApiMock } = setupTspClient(); + + await sut.fetchTspClasses(system, 1); + + expect(exportApiMock.exportKlasseList).toHaveBeenCalledTimes(1); + }); + + it('should return an array of classes', async () => { + const { system } = setupTspClient(); + + const classes = await sut.fetchTspClasses(system, 1); + + expect(classes).toBeDefined(); + expect(classes).toBeInstanceOf(Array); + }); + }); + }); + describe('findSchool', () => { describe('when school is found', () => { const setup = () => { @@ -208,6 +351,27 @@ describe(TspSyncService.name, () => { }); }); + describe('findSchoolsForSystem', () => { + describe('when findSchoolsForSystem is called', () => { + const setup = () => { + const system = systemFactory.build(); + const school = schoolFactory.build(); + + schoolService.getSchools.mockResolvedValueOnce([school]); + + return { system, school }; + }; + + it('should return an array of schools', async () => { + const { system, school } = setup(); + + const schools = await sut.findSchoolsForSystem(system); + + expect(schools).toEqual([school]); + }); + }); + }); + describe('updateSchool', () => { describe('when school is updated', () => { const setup = () => { diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts index 00ff1d2429e..084990d8f8e 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts @@ -1,4 +1,4 @@ -import { RobjExportSchule, TspClientFactory } from '@infra/tsp-client'; +import { TspClientFactory } from '@infra/tsp-client'; import { FederalStateService, SchoolYearService } from '@modules/legacy-school'; import { School, SchoolService } from '@modules/school'; import { System, SystemService, SystemType } from '@modules/system'; @@ -40,18 +40,45 @@ export class TspSyncService { } public async fetchTspSchools(system: System, daysToFetch: number) { - const client = this.tspClientFactory.createExportClient({ - clientId: system.oauthConfig?.clientId ?? '', - clientSecret: system.oauthConfig?.clientSecret ?? '', - tokenEndpoint: system.oauthConfig?.tokenEndpoint ?? '', - }); + const client = this.createClient(system); const lastChangeDate = this.formatChangeDate(daysToFetch); - const schools: RobjExportSchule[] = (await client.exportSchuleList(lastChangeDate)).data; + const schoolsResponse = await client.exportSchuleList(lastChangeDate); + const schools = schoolsResponse.data; return schools; } + public async fetchTspTeachers(system: System, daysToFetch: number) { + const client = this.createClient(system); + + const lastChangeDate = this.formatChangeDate(daysToFetch); + const teachersResponse = await client.exportLehrerList(lastChangeDate); + const teachers = teachersResponse.data; + + return teachers; + } + + public async fetchTspStudents(system: System, daysToFetch: number) { + const client = this.createClient(system); + + const lastChangeDate = this.formatChangeDate(daysToFetch); + const studentsResponse = await client.exportSchuelerList(lastChangeDate); + const students = studentsResponse.data; + + return students; + } + + public async fetchTspClasses(system: System, daysToFetch: number) { + const client = this.createClient(system); + + const lastChangeDate = this.formatChangeDate(daysToFetch); + const classesResponse = await client.exportKlasseList(lastChangeDate); + const classes = classesResponse.data; + + return classes; + } + public async findSchool(system: System, identifier: string): Promise { const schools = await this.schoolService.getSchools({ externalId: identifier, @@ -64,6 +91,14 @@ export class TspSyncService { return schools[0]; } + public async findSchoolsForSystem(system: System): Promise { + const schools = await this.schoolService.getSchools({ + systemId: system.id, + }); + + return schools; + } + public async updateSchool(school: School, name?: string): Promise { if (!name) { return school; @@ -111,4 +146,14 @@ export class TspSyncService { private formatChangeDate(daysToFetch: number): string { return moment(new Date()).subtract(daysToFetch, 'days').subtract(1, 'hours').format('YYYY-MM-DD HH:mm:ss.SSS'); } + + private createClient(system: System) { + const client = this.tspClientFactory.createExportClient({ + clientId: system.oauthConfig?.clientId ?? '', + clientSecret: system.oauthConfig?.clientSecret ?? '', + tokenEndpoint: system.oauthConfig?.tokenEndpoint ?? '', + }); + + return client; + } } diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index ae1767a934c..c7877e9977c 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -3,9 +3,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { RobjExportSchule } from '@infra/tsp-client'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { Logger } from '@src/core/logger'; +import { ExternalUserDto, OauthDataDto, ProvisioningService, ProvisioningSystemDto } from '@src/modules/provisioning'; import { schoolFactory } from '@src/modules/school/testing'; import { SyncStrategyTarget } from '../sync-strategy.types'; +import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; import { TspSyncConfig } from './tsp-sync.config'; import { TspSyncService } from './tsp-sync.service'; import { TspSyncStrategy } from './tsp-sync.strategy'; @@ -14,6 +17,8 @@ describe(TspSyncStrategy.name, () => { let module: TestingModule; let sut: TspSyncStrategy; let tspSyncService: DeepMocked; + let provisioningService: DeepMocked; + let tspOauthDataMapper: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -36,17 +41,31 @@ describe(TspSyncStrategy.name, () => { return 10; case 'TSP_SYNC_SCHOOL_DAYS_TO_FETCH': return 1; + case 'TSP_SYNC_DATA_LIMIT': + return 10; + case 'TSP_SYNC_DATA_DAYS_TO_FETCH': + return 1; default: throw new Error(`Unknown key: ${key}`); } }, }), }, + { + provide: ProvisioningService, + useValue: createMock(), + }, + { + provide: TspOauthDataMapper, + useValue: createMock(), + }, ], }).compile(); sut = module.get(TspSyncStrategy); tspSyncService = module.get(TspSyncService); + provisioningService = module.get(ProvisioningService); + tspOauthDataMapper = module.get(TspOauthDataMapper); }); afterEach(() => { @@ -76,6 +95,24 @@ describe(TspSyncStrategy.name, () => { describe('when sync is called', () => { const setup = () => { tspSyncService.fetchTspSchools.mockResolvedValueOnce([]); + tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); + tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); + tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); + + const oauthDataDto = new OauthDataDto({ + system: new ProvisioningSystemDto({ + systemId: faker.string.alpha(), + provisioningStrategy: SystemProvisioningStrategy.TSP, + }), + externalUser: new ExternalUserDto({ + externalId: faker.string.alpha(), + }), + }); + + tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([oauthDataDto]); + + return { oauthDataDto }; }; it('should find the tsp system', async () => { @@ -91,6 +128,40 @@ describe(TspSyncStrategy.name, () => { expect(tspSyncService.fetchTspSchools).toHaveBeenCalled(); }); + + it('should fetch the data', async () => { + setup(); + + await sut.sync(); + + expect(tspSyncService.fetchTspTeachers).toHaveBeenCalled(); + expect(tspSyncService.fetchTspStudents).toHaveBeenCalled(); + expect(tspSyncService.fetchTspClasses).toHaveBeenCalled(); + }); + + it('should load all schools', async () => { + setup(); + + await sut.sync(); + + expect(tspSyncService.findSchoolsForSystem).toHaveBeenCalled(); + }); + + it('should map to OauthDataDto', async () => { + setup(); + + await sut.sync(); + + expect(tspOauthDataMapper.mapTspDataToOauthData).toHaveBeenCalled(); + }); + + it('should call provisioning service with mapped OauthDataDtos', async () => { + const { oauthDataDto } = setup(); + + await sut.sync(); + + expect(provisioningService.provisionData).toHaveBeenCalledWith(oauthDataDto); + }); }); describe('when school does not exist', () => { @@ -103,6 +174,12 @@ describe(TspSyncStrategy.name, () => { tspSyncService.fetchTspSchools.mockResolvedValueOnce(tspSchools); tspSyncService.findSchool.mockResolvedValueOnce(undefined); + + tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); + tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); + tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); + tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); }; it('should create the school', async () => { @@ -125,6 +202,12 @@ describe(TspSyncStrategy.name, () => { const school = schoolFactory.build(); tspSyncService.findSchool.mockResolvedValueOnce(school); + + tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); + tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); + tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); + tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); }; it('should update the school', async () => { @@ -144,6 +227,12 @@ describe(TspSyncStrategy.name, () => { }; const tspSchools = [tspSchool]; tspSyncService.fetchTspSchools.mockResolvedValueOnce(tspSchools); + + tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); + tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); + tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); + tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); }; it('should skip the school', async () => { diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts index c0cb551c3e3..b7273d7cfdc 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts @@ -2,13 +2,18 @@ import { School } from '@modules/school'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Logger } from '@src/core/logger'; +import { ProvisioningService } from '@src/modules/provisioning'; import { System } from '@src/modules/system'; import pLimit from 'p-limit'; import { SyncStrategy } from '../strategy/sync-strategy'; import { SyncStrategyTarget } from '../sync-strategy.types'; +import { TspDataFetchedLoggable } from './loggable/tsp-data-fetched.loggable'; import { TspSchoolsFetchedLoggable } from './loggable/tsp-schools-fetched.loggable'; import { TspSchoolsSyncedLoggable } from './loggable/tsp-schools-synced.loggable'; import { TspSchulnummerMissingLoggable } from './loggable/tsp-schulnummer-missing.loggable'; +import { TspSyncedUsersLoggable } from './loggable/tsp-synced-users.loggable'; +import { TspSyncingUsersLoggable } from './loggable/tsp-syncing-users.loggable'; +import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; import { TspSyncConfig } from './tsp-sync.config'; import { TspSyncService } from './tsp-sync.service'; @@ -16,17 +21,27 @@ import { TspSyncService } from './tsp-sync.service'; export class TspSyncStrategy extends SyncStrategy { private readonly schoolLimit: pLimit.Limit; + private readonly dataLimit: pLimit.Limit; + private readonly schoolDaysToFetch: number; + private readonly schoolDataDaysToFetch: number; + constructor( private readonly logger: Logger, private readonly tspSyncService: TspSyncService, - configService: ConfigService + private readonly tspOauthDataMapper: TspOauthDataMapper, + configService: ConfigService, + private readonly provisioningService: ProvisioningService ) { super(); this.logger.setContext(TspSyncStrategy.name); + this.schoolLimit = pLimit(configService.getOrThrow('TSP_SYNC_SCHOOL_LIMIT')); this.schoolDaysToFetch = configService.get('TSP_SYNC_SCHOOL_DAYS_TO_FETCH', 1); + + this.dataLimit = pLimit(configService.getOrThrow('TSP_SYNC_DATA_LIMIT')); + this.schoolDataDaysToFetch = configService.get('TSP_SYNC_DATA_DAYS_TO_FETCH', 1); } public override getType(): SyncStrategyTarget { @@ -37,6 +52,10 @@ export class TspSyncStrategy extends SyncStrategy { const system = await this.tspSyncService.findTspSystemOrFail(); await this.syncSchools(system); + + const schools = await this.tspSyncService.findSchoolsForSystem(system); + + await this.syncData(system, schools); } private async syncSchools(system: System): Promise { @@ -46,7 +65,7 @@ export class TspSyncStrategy extends SyncStrategy { const schoolPromises = tspSchools.map((tspSchool) => this.schoolLimit(async () => { if (!tspSchool.schuleNummer) { - this.logger.warning(new TspSchulnummerMissingLoggable()); + this.logger.warning(new TspSchulnummerMissingLoggable(tspSchool.schuleName)); return null; } @@ -76,4 +95,31 @@ export class TspSyncStrategy extends SyncStrategy { return scSchools.filter((scSchool) => scSchool != null).map((scSchool) => scSchool.school); } + + private async syncData(system: System, schools: School[]): Promise { + const tspTeachers = await this.tspSyncService.fetchTspTeachers(system, this.schoolDataDaysToFetch); + const tspStudents = await this.tspSyncService.fetchTspStudents(system, this.schoolDataDaysToFetch); + const tspClasses = await this.tspSyncService.fetchTspClasses(system, this.schoolDataDaysToFetch); + this.logger.info( + new TspDataFetchedLoggable(tspTeachers.length, tspStudents.length, tspClasses.length, this.schoolDataDaysToFetch) + ); + + const oauthDataDtos = this.tspOauthDataMapper.mapTspDataToOauthData( + system, + schools, + tspTeachers, + tspStudents, + tspClasses + ); + + this.logger.info(new TspSyncingUsersLoggable(oauthDataDtos.length)); + + const dataPromises = oauthDataDtos.map((oauthDataDto) => + this.dataLimit(() => this.provisioningService.provisionData(oauthDataDto)) + ); + + const results = await Promise.allSettled(dataPromises); + + this.logger.info(new TspSyncedUsersLoggable(results.length)); + } } diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts index dae91d92d72..1efb774d9de 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts @@ -14,7 +14,7 @@ import { BadDataLoggableException } from '../loggable'; export class TspProvisioningService { private ENTITY_SOURCE = 'tsp'; // used as source attribute in created users and classes - private TSP_EMAIL_DOMAIN = 'tsp.de'; + private TSP_EMAIL_DOMAIN = 'schul-cloud.org'; constructor( private readonly schoolService: SchoolService, diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index cdda04bf236..c3594739d84 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -319,6 +319,8 @@ const config: ServerConfig = { TSP_API_TOKEN_LIFETIME_MS: Configuration.get('TSP_API_TOKEN_LIFETIME_MS') as number, TSP_SYNC_SCHOOL_LIMIT: Configuration.get('TSP_SYNC_SCHOOL_LIMIT') as number, TSP_SYNC_SCHOOL_DAYS_TO_FETCH: Configuration.get('TSP_SYNC_SCHOOL_DAYS_TO_FETCH') as number, + TSP_SYNC_DATA_LIMIT: Configuration.get('TSP_SYNC_DATA_LIMIT') as number, + TSP_SYNC_DATA_DAYS_TO_FETCH: Configuration.get('TSP_SYNC_DATA_DAYS_TO_FETCH') as number, ROCKET_CHAT_URI: Configuration.get('ROCKET_CHAT_URI') as string, ROCKET_CHAT_ADMIN_ID: Configuration.get('ROCKET_CHAT_ADMIN_ID') as string, ROCKET_CHAT_ADMIN_TOKEN: Configuration.get('ROCKET_CHAT_ADMIN_TOKEN') as string, diff --git a/config/default.schema.json b/config/default.schema.json index 572afde4e44..272c2e5bce2 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -189,6 +189,16 @@ "default": "1", "description": "The amount of days for which the sync fetches schools from the TSP." }, + "TSP_SYNC_DATA_LIMIT": { + "type": "number", + "default": "150", + "description": "The amount of school data updates the sync handles at once." + }, + "TSP_SYNC_DATA_DAYS_TO_FETCH": { + "type": "number", + "default": "1", + "description": "The amount of days for which the sync fetches school data from the TSP." + }, "FEATURE_TSP_ENABLED": { "type": "boolean", "default": false,