diff --git a/Dockerfile b/Dockerfile index 8663c9b8d85..77ed4a1088b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,5 @@ COPY scripts/ldapSync.sh /schulcloud-server/scripts/ RUN npm run build ENV NODE_ENV=production +ENV NO_COLOR="true" CMD npm run start diff --git a/TODO.md b/TODO.md index 90f2bcfd358..7fa92508759 100644 --- a/TODO.md +++ b/TODO.md @@ -88,4 +88,4 @@ - naming of dtos and dto-files: api vs domain, we leave out "dto" suffix for simplicity (we know that they are dtos) and instead append a specific suffix: e.g. api: , , - domain: , + domain: , diff --git a/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 index 17384845b71..e3843effc48 100644 --- a/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: amqp-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: amqp-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ AMQP_FILE_STORAGE_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: amqp-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: amqp-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 index 317d37dcda9..2eb0fdd4094 100644 --- a/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 @@ -5,14 +5,18 @@ metadata: labels: app: api cronjob: delete-s3-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: delete-s3-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} name: api-delete-s3-files-cronjob spec: concurrencyPolicy: Forbid schedule: "{{ SERVER_FILE_DELETION_CRONJOB_SCHEDULE|default("@hourly", true) }}" jobTemplate: - labels: - app: api - cronjob: delete-s3-files spec: template: spec: @@ -34,3 +38,14 @@ spec: cpu: {{ API_CPU_REQUESTS|default("100m", true) }} memory: {{ API_MEMORY_REQUESTS|default("150Mi", true) }} restartPolicy: OnFailure + metadata: + labels: + app: api + cronjob: delete-s3-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: delete-s3-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} diff --git a/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 index 727b5a9f4f0..7866e23dcc3 100644 --- a/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_FILE_STORAGE_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-files + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-files + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 index c8d7e6f894c..5fe3f528bde 100644 --- a/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-fwu + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-fwu + app.kubernetes.io/component: fwu + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_FWU_LEARNING_CONTENTS_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-fwu + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-fwu + app.kubernetes.io/component: fwu + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 index 0750bf91495..a18102bb55d 100644 --- a/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api + app.kubernetes.io/component: server + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api + app.kubernetes.io/component: server + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/preview-generator-configmap.yml.j2 b/ansible/roles/schulcloud-server-core/templates/preview-generator-configmap.yml.j2 index 457a5e47364..8d3a4b1f4c2 100644 --- a/ansible/roles/schulcloud-server-core/templates/preview-generator-configmap.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/preview-generator-configmap.yml.j2 @@ -6,3 +6,4 @@ metadata: labels: app: preview-generator data: + NEST_LOG_LEVEL: "info" diff --git a/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 index 51d87b88755..cffa1dc02b8 100644 --- a/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: preview-generator + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: preview-generator + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ AMQP_FILE_PREVIEW_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: preview-generator + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: preview-generator + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-core/templates/preview-generator-scaled-object.yml.j2 b/ansible/roles/schulcloud-server-core/templates/preview-generator-scaled-object.yml.j2 index 2f8f9091b8e..6058b1b0ccb 100644 --- a/ansible/roles/schulcloud-server-core/templates/preview-generator-scaled-object.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/preview-generator-scaled-object.yml.j2 @@ -6,6 +6,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: preview-generator + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: preview-generator + app.kubernetes.io/component: files + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: scaleTargetRef: name: preview-generator-deployment diff --git a/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 b/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 index 4e41f454ed9..e5923a2e1b9 100644 --- a/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-h5p + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-h5p + app.kubernetes.io/component: h5p + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_H5P_EDITOR_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-h5p + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-h5p + app.kubernetes.io/component: h5p + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 b/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 index 9d99c672be8..93378069cc9 100644 --- a/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: management-deployment + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: management-deployment + app.kubernetes.io/component: management + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_MANAGEMENT_REPLICAS|default("1", true) }} strategy: @@ -20,6 +27,13 @@ spec: metadata: labels: app: management-deployment + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: management-deployment + app.kubernetes.io/component: management + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 index 74cc37d75b6..db852a7081f 100644 --- a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 @@ -4,6 +4,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-ldapsync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-ldapsync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} name: api-ldapsync-full-cronjob spec: schedule: "{{ SERVER_LDAP_SYNC_FULL_CRONJOB|default("0 3 * * 3,6", true) }}" @@ -30,3 +37,13 @@ spec: cpu: {{ API_CPU_REQUESTS|default("100m", true) }} memory: {{ API_MEMORY_REQUESTS|default("150Mi", true) }} restartPolicy: OnFailure + metadata: + labels: + app: api-ldapsync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-ldapsync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} diff --git a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 index 838009883d4..9f03dbee883 100644 --- a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-worker + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-worker + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_WORKER_REPLICAS|default("2", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-worker + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-worker + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-base-cronjob.yml.j2 b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-base-cronjob.yml.j2 index f5a3d0751f4..e46dac14330 100644 --- a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-base-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-base-cronjob.yml.j2 @@ -4,15 +4,19 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} name: api-tsp-sync-base-cronjob spec: schedule: "{{ SERVER_TSP_SYNC_BASE_CRONJOB|default("9 3 * * *", true) }}" jobTemplate: spec: template: - metadata: - labels: - app: api-tsp-sync-cronjob spec: containers: - name: api-tsp-sync-base-cronjob @@ -23,3 +27,13 @@ spec: command: ['/bin/sh','-c'] args: ['curl -H "X-API-Key: $SYNC_API_KEY" "http://{{ API_TSP_SYNC_SVC|default("api-tsp-sync-svc", true) }}:3030/api/v1/sync?target=tsp-base" | python3 -m json.tool'] restartPolicy: OnFailure + metadata: + labels: + app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} diff --git a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 index 54595985dd4..614db8cf1f9 100644 --- a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 @@ -5,6 +5,13 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-tsp-sync + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: replicas: {{ API_TSP_REPLICAS|default("1", true) }} strategy: @@ -21,6 +28,13 @@ spec: metadata: labels: app: api-tsp-sync + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: securityContext: runAsUser: 1000 diff --git a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-school-cronjob.yml.j2 b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-school-cronjob.yml.j2 index a92bd92560e..e47ae85fab1 100644 --- a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-school-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-school-cronjob.yml.j2 @@ -4,15 +4,19 @@ metadata: namespace: {{ NAMESPACE }} labels: app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} name: api-tsp-sync-school-cronjob spec: schedule: "{{ SERVER_TSP_SYNC_SCHOOL_CRONJOB|default("39 3 * * *", true) }}" jobTemplate: spec: template: - metadata: - labels: - app: api-tsp-sync-cronjob spec: containers: - name: api-tsp-sync-school-cronjob @@ -23,3 +27,13 @@ spec: command: ['/bin/sh','-c'] args: ['curl -H "X-API-Key: $SYNC_API_KEY" "http://{{ API_TSP_SYNC_SVC|default("api-tsp-sync-svc", true) }}:3030/api/v1/sync?target=tsp-school" | python3 -m json.tool'] restartPolicy: OnFailure + metadata: + labels: + app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} diff --git a/apps/server/src/apps/deletion-console.app.ts b/apps/server/src/apps/deletion-console.app.ts new file mode 100644 index 00000000000..cafb137e160 --- /dev/null +++ b/apps/server/src/apps/deletion-console.app.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +import { BootstrapConsole } from 'nestjs-console'; +import { DeletionConsoleModule } from '@modules/deletion'; + +async function run() { + const bootstrap = new BootstrapConsole({ + module: DeletionConsoleModule, + useDecorators: true, + }); + + const app = await bootstrap.init(); + + try { + await app.init(); + + // Execute console application with provided arguments. + await bootstrap.boot(); + } catch (err) { + // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-call + console.error(err); + + // Set the exit code to 1 to indicate a console app failure. + process.exitCode = 1; + } + + // Always close the app, even if some exception + // has been thrown from the console app. + await app.close(); +} + +void run(); diff --git a/apps/server/src/core/error/filter/global-error.filter.ts b/apps/server/src/core/error/filter/global-error.filter.ts index 314c8247d18..068e7bcca8a 100644 --- a/apps/server/src/core/error/filter/global-error.filter.ts +++ b/apps/server/src/core/error/filter/global-error.filter.ts @@ -1,6 +1,6 @@ +import { IError, RpcMessage } from '@infra/rabbitmq/rpc-message'; import { ArgumentsHost, Catch, ExceptionFilter, HttpException, InternalServerErrorException } from '@nestjs/common'; import { ApiValidationError, BusinessError } from '@shared/common'; -import { IError, RpcMessage } from '@infra/rabbitmq/rpc-message'; import { ErrorLogger, Loggable } from '@src/core/logger'; import { LoggingUtils } from '@src/core/logger/logging.utils'; import { Response } from 'express'; diff --git a/apps/server/src/core/error/interface/error-type.interface.ts b/apps/server/src/core/error/interface/error-type.interface.ts index 9ea4939da89..006fa18c793 100644 --- a/apps/server/src/core/error/interface/error-type.interface.ts +++ b/apps/server/src/core/error/interface/error-type.interface.ts @@ -1,4 +1,4 @@ -export interface IErrorType { +export interface ErrorType { readonly type: string; readonly title: string; readonly defaultMessage: string; diff --git a/apps/server/src/core/error/server-error-types.ts b/apps/server/src/core/error/server-error-types.ts index 3224aec5016..91538d75cc3 100644 --- a/apps/server/src/core/error/server-error-types.ts +++ b/apps/server/src/core/error/server-error-types.ts @@ -1,13 +1,13 @@ /** * all errors defined in the application require a definition, - * implementing the @IErrorType where their type is unambigious within of the application. + * implementing the @ErrorType where their type is unambigious within of the application. * */ import legacyErrorTypes = require('../../../../../src/errors/commonErrorTypes'); -import { IErrorType } from './interface/error-type.interface'; +import { ErrorType } from './interface/error-type.interface'; -// check legacy error typing is matching IErrorType -const serverErrorTypes: { [index: string]: IErrorType } = legacyErrorTypes; +// check legacy error typing is matching ErrorType +const serverErrorTypes: { [index: string]: ErrorType } = legacyErrorTypes; // re-use legacy error types export const { INTERNAL_SERVER_ERROR_TYPE, ASSERTION_ERROR_TYPE, API_VALIDATION_ERROR_TYPE, FORBIDDEN_ERROR_TYPE } = @@ -15,7 +15,7 @@ export const { INTERNAL_SERVER_ERROR_TYPE, ASSERTION_ERROR_TYPE, API_VALIDATION_ // further error types -export const NOT_FOUND_ERROR_TYPE: IErrorType = { +export const NOT_FOUND_ERROR_TYPE: ErrorType = { type: 'NOT_FOUND_ERROR', title: 'Not Found', defaultMessage: 'The requested ressource has not been found.', diff --git a/apps/server/src/core/interceptor/interceptor.module.ts b/apps/server/src/core/interceptor/interceptor.module.ts index 6815e44a867..12f12306335 100644 --- a/apps/server/src/core/interceptor/interceptor.module.ts +++ b/apps/server/src/core/interceptor/interceptor.module.ts @@ -1,7 +1,7 @@ import { ClassSerializerInterceptor, Module } from '@nestjs/common'; -import { APP_INTERCEPTOR } from '@nestjs/core'; -import { IInterceptorConfig, TimeoutInterceptor } from '@shared/common'; import { ConfigService } from '@nestjs/config'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { InterceptorConfig, TimeoutInterceptor } from '@shared/common'; /** ********************************************* * Global Interceptor setup @@ -18,7 +18,7 @@ import { ConfigService } from '@nestjs/config'; }, { provide: APP_INTERCEPTOR, // TODO remove (for testing) - useFactory: (configService: ConfigService) => { + useFactory: (configService: ConfigService) => { const timeout = configService.get('INCOMING_REQUEST_TIMEOUT'); return new TimeoutInterceptor(timeout); }, diff --git a/apps/server/src/core/interfaces/core-module-config.ts b/apps/server/src/core/interfaces/core-module-config.ts index 21f4cd51b7d..a51eb7bd08d 100644 --- a/apps/server/src/core/interfaces/core-module-config.ts +++ b/apps/server/src/core/interfaces/core-module-config.ts @@ -1,4 +1,4 @@ -import { IInterceptorConfig } from '@shared/common'; -import { ILoggerConfig } from '../logger'; +import { InterceptorConfig } from '@shared/common'; +import { LoggerConfig } from '../logger'; -export interface ICoreModuleConfig extends IInterceptorConfig, ILoggerConfig {} +export interface CoreModuleConfig extends InterceptorConfig, LoggerConfig {} diff --git a/apps/server/src/core/logger/interfaces/logger-config.ts b/apps/server/src/core/logger/interfaces/logger-config.ts index d9fe1e86538..0d9c651ec70 100644 --- a/apps/server/src/core/logger/interfaces/logger-config.ts +++ b/apps/server/src/core/logger/interfaces/logger-config.ts @@ -1,3 +1,3 @@ -export interface ILoggerConfig { +export interface LoggerConfig { NEST_LOG_LEVEL: string; } diff --git a/apps/server/src/core/logger/logger.module.ts b/apps/server/src/core/logger/logger.module.ts index 7751a48b8aa..477037b7cfa 100644 --- a/apps/server/src/core/logger/logger.module.ts +++ b/apps/server/src/core/logger/logger.module.ts @@ -3,14 +3,14 @@ import { ConfigService } from '@nestjs/config'; import { utilities, WinstonModule } from 'nest-winston'; import winston from 'winston'; import { ErrorLogger } from './error-logger'; -import { ILoggerConfig } from './interfaces'; +import { LoggerConfig } from './interfaces'; import { LegacyLogger } from './legacy-logger.service'; import { Logger } from './logger'; @Module({ imports: [ WinstonModule.forRootAsync({ - useFactory: (configService: ConfigService) => { + useFactory: (configService: ConfigService) => { return { levels: winston.config.syslog.levels, level: configService.get('NEST_LOG_LEVEL'), diff --git a/apps/server/src/infra/calendar/interface/calendar-event.interface.ts b/apps/server/src/infra/calendar/interface/calendar-event.interface.ts index b9c6a1e6293..1ce70acdf87 100644 --- a/apps/server/src/infra/calendar/interface/calendar-event.interface.ts +++ b/apps/server/src/infra/calendar/interface/calendar-event.interface.ts @@ -1,4 +1,4 @@ -export interface ICalendarEvent { +export interface CalendarEvent { data: { attributes: { summary: string; diff --git a/apps/server/src/infra/calendar/mapper/calendar.mapper.spec.ts b/apps/server/src/infra/calendar/mapper/calendar.mapper.spec.ts index 9679ce0a4fa..f84427791b9 100644 --- a/apps/server/src/infra/calendar/mapper/calendar.mapper.spec.ts +++ b/apps/server/src/infra/calendar/mapper/calendar.mapper.spec.ts @@ -1,4 +1,4 @@ -import { ICalendarEvent } from '@infra/calendar/interface/calendar-event.interface'; +import { CalendarEvent } from '@infra/calendar/interface/calendar-event.interface'; import { Test, TestingModule } from '@nestjs/testing'; import { CalendarMapper } from './calendar.mapper'; @@ -6,7 +6,7 @@ describe('CalendarMapper', () => { let module: TestingModule; let mapper: CalendarMapper; - const event: ICalendarEvent = { + const event: CalendarEvent = { data: [ { attributes: { diff --git a/apps/server/src/infra/calendar/mapper/calendar.mapper.ts b/apps/server/src/infra/calendar/mapper/calendar.mapper.ts index a75ff01eae6..c229df3c278 100644 --- a/apps/server/src/infra/calendar/mapper/calendar.mapper.ts +++ b/apps/server/src/infra/calendar/mapper/calendar.mapper.ts @@ -1,10 +1,10 @@ -import { ICalendarEvent } from '@infra/calendar/interface/calendar-event.interface'; +import { CalendarEvent } from '@infra/calendar/interface/calendar-event.interface'; import { Injectable } from '@nestjs/common'; import { CalendarEventDto } from '../dto/calendar-event.dto'; @Injectable() export class CalendarMapper { - mapToDto(event: ICalendarEvent): CalendarEventDto { + mapToDto(event: CalendarEvent): CalendarEventDto { const { attributes } = event.data[0]; return new CalendarEventDto({ teamId: attributes['x-sc-teamid'], diff --git a/apps/server/src/infra/calendar/service/calendar.service.spec.ts b/apps/server/src/infra/calendar/service/calendar.service.spec.ts index ed6bb4620bc..95d1fa95e22 100644 --- a/apps/server/src/infra/calendar/service/calendar.service.spec.ts +++ b/apps/server/src/infra/calendar/service/calendar.service.spec.ts @@ -1,14 +1,14 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { CalendarEventDto, CalendarService } from '@infra/calendar'; import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { CalendarEventDto, CalendarService } from '@infra/calendar'; import { axiosResponseFactory } from '@shared/testing'; import { AxiosResponse } from 'axios'; import { of, throwError } from 'rxjs'; +import { CalendarEvent } from '../interface/calendar-event.interface'; import { CalendarMapper } from '../mapper/calendar.mapper'; -import { ICalendarEvent } from '../interface/calendar-event.interface'; describe('CalendarServiceSpec', () => { let module: TestingModule; @@ -56,7 +56,7 @@ describe('CalendarServiceSpec', () => { const title = 'eventTitle'; const teamId = 'teamId'; - const event: ICalendarEvent = { + const event: CalendarEvent = { data: [ { attributes: { @@ -66,7 +66,7 @@ describe('CalendarServiceSpec', () => { }, ], }; - const axiosResponse: AxiosResponse = axiosResponseFactory.build({ + const axiosResponse: AxiosResponse = axiosResponseFactory.build({ data: event, }); httpService.get.mockReturnValue(of(axiosResponse)); diff --git a/apps/server/src/infra/calendar/service/calendar.service.ts b/apps/server/src/infra/calendar/service/calendar.service.ts index 3bf2a6576be..7f39c21ae58 100644 --- a/apps/server/src/infra/calendar/service/calendar.service.ts +++ b/apps/server/src/infra/calendar/service/calendar.service.ts @@ -4,11 +4,11 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { ErrorUtils } from '@src/core/error/utils'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; -import { Observable, firstValueFrom } from 'rxjs'; +import { firstValueFrom, Observable } from 'rxjs'; import { URL, URLSearchParams } from 'url'; -import { CalendarMapper } from '../mapper/calendar.mapper'; import { CalendarEventDto } from '../dto/calendar-event.dto'; -import { ICalendarEvent } from '../interface/calendar-event.interface'; +import { CalendarEvent } from '../interface/calendar-event.interface'; +import { CalendarMapper } from '../mapper/calendar.mapper'; @Injectable() export class CalendarService { @@ -34,7 +34,7 @@ export class CalendarService { timeout: this.timeoutMs, }) ) - .then((resp: AxiosResponse) => this.calendarMapper.mapToDto(resp.data)) + .then((resp: AxiosResponse) => this.calendarMapper.mapToDto(resp.data)) .catch((error) => { throw new InternalServerErrorException( null, @@ -47,7 +47,7 @@ export class CalendarService { path: string, queryParams: URLSearchParams, config: AxiosRequestConfig - ): Observable> { + ): Observable> { const url: URL = new URL(this.baseURL); url.pathname = path; url.search = queryParams.toString(); diff --git a/apps/server/src/infra/collaborative-storage/collaborative-storage-adapter.module.ts b/apps/server/src/infra/collaborative-storage/collaborative-storage-adapter.module.ts index f60ff664654..3415d0dbdc3 100644 --- a/apps/server/src/infra/collaborative-storage/collaborative-storage-adapter.module.ts +++ b/apps/server/src/infra/collaborative-storage/collaborative-storage-adapter.module.ts @@ -1,18 +1,18 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { PseudonymModule } from '@modules/pseudonym'; +import { ToolModule } from '@modules/tool'; +import { UserModule } from '@modules/user'; import { HttpModule } from '@nestjs/axios'; import { Module, Provider } from '@nestjs/common'; import { LtiToolRepo } from '@shared/repo/ltitool/'; import { LoggerModule } from '@src/core/logger'; -import { ToolModule } from '@modules/tool'; -import { PseudonymModule } from '@modules/pseudonym'; -import { UserModule } from '@modules/user'; -import { NextcloudStrategy } from './strategy/nextcloud/nextcloud.strategy'; -import { NextcloudClient } from './strategy/nextcloud/nextcloud.client'; -import { CollaborativeStorageAdapterMapper } from './mapper'; import { CollaborativeStorageAdapter } from './collaborative-storage.adapter'; +import { CollaborativeStorageAdapterMapper } from './mapper'; +import { NextcloudClient } from './strategy/nextcloud/nextcloud.client'; +import { NextcloudStrategy } from './strategy/nextcloud/nextcloud.strategy'; const storageStrategy: Provider = { - provide: 'ICollaborativeStorageStrategy', + provide: 'CollaborativeStorageStrategy', useExisting: NextcloudStrategy, }; diff --git a/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.spec.ts b/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.spec.ts index 3b9ba2175e6..3742c3ff408 100644 --- a/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.spec.ts +++ b/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.spec.ts @@ -1,14 +1,14 @@ import { createMock } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; +import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; // invalid import please fix import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; -import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; // invalid import please fix import { CollaborativeStorageAdapter } from './collaborative-storage.adapter'; import { CollaborativeStorageAdapterMapper } from './mapper/collaborative-storage-adapter.mapper'; -import { ICollaborativeStorageStrategy } from './strategy/base.interface.strategy'; +import { CollaborativeStorageStrategy } from './strategy/base.interface.strategy'; -class TestStrategy implements ICollaborativeStorageStrategy { +class TestStrategy implements CollaborativeStorageStrategy { baseURL: string; constructor() { @@ -38,7 +38,7 @@ class TestStrategy implements ICollaborativeStorageStrategy { describe('CollaborativeStorage Adapter', () => { let module: TestingModule; let adapter: CollaborativeStorageAdapter; - let strategy: ICollaborativeStorageStrategy; + let strategy: CollaborativeStorageStrategy; beforeAll(async () => { module = await Test.createTestingModule({ @@ -50,8 +50,8 @@ describe('CollaborativeStorage Adapter', () => { useValue: createMock(), }, { - provide: 'ICollaborativeStorageStrategy', - useValue: createMock(), + provide: 'CollaborativeStorageStrategy', + useValue: createMock(), }, ], }).compile(); diff --git a/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.ts b/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.ts index b50657f393e..27cfdd1b201 100644 --- a/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.ts +++ b/apps/server/src/infra/collaborative-storage/collaborative-storage.adapter.ts @@ -1,10 +1,10 @@ import { TeamPermissionsDto } from '@modules/collaborative-storage/services/dto/team-permissions.dto'; import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; import { Inject, Injectable } from '@nestjs/common'; import { LegacyLogger } from '@src/core/logger'; -import { RoleDto } from '@modules/role/service/dto/role.dto'; import { CollaborativeStorageAdapterMapper } from './mapper/collaborative-storage-adapter.mapper'; -import { ICollaborativeStorageStrategy } from './strategy/base.interface.strategy'; +import { CollaborativeStorageStrategy } from './strategy/base.interface.strategy'; /** * Provides an Adapter to an external collaborative storage. @@ -12,10 +12,10 @@ import { ICollaborativeStorageStrategy } from './strategy/base.interface.strateg */ @Injectable() export class CollaborativeStorageAdapter { - strategy: ICollaborativeStorageStrategy; + strategy: CollaborativeStorageStrategy; constructor( - @Inject('ICollaborativeStorageStrategy') strategy: ICollaborativeStorageStrategy, + @Inject('CollaborativeStorageStrategy') strategy: CollaborativeStorageStrategy, private mapper: CollaborativeStorageAdapterMapper, private logger: LegacyLogger ) { @@ -27,7 +27,7 @@ export class CollaborativeStorageAdapter { * Set the strategy that should be used by the adapter * @param strategy The strategy */ - setStrategy(strategy: ICollaborativeStorageStrategy) { + setStrategy(strategy: CollaborativeStorageStrategy) { this.strategy = strategy; } diff --git a/apps/server/src/infra/collaborative-storage/strategy/base.interface.strategy.ts b/apps/server/src/infra/collaborative-storage/strategy/base.interface.strategy.ts index f960452df5f..3021d53dcb7 100644 --- a/apps/server/src/infra/collaborative-storage/strategy/base.interface.strategy.ts +++ b/apps/server/src/infra/collaborative-storage/strategy/base.interface.strategy.ts @@ -4,7 +4,7 @@ import { TeamRolePermissionsDto } from '../dto/team-role-permissions.dto'; /** * base interface for all CollaborativeStorage Strategies */ -export interface ICollaborativeStorageStrategy { +export interface CollaborativeStorageStrategy { /** * Updates The Permissions for the given Role in the given Team * @param dto The DTO to be processed diff --git a/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts index 6b75d6ec76f..b7312f9158e 100644 --- a/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts +++ b/apps/server/src/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts @@ -1,24 +1,24 @@ +import { TeamDto, TeamUserDto } from '@modules/collaborative-storage'; +import { PseudonymService } from '@modules/pseudonym'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { ExternalToolService } from '@modules/tool/external-tool/service'; +import { UserService } from '@modules/user'; import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { Pseudonym, UserDO } from '@shared/domain/'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; import { LtiToolRepo } from '@shared/repo/ltitool/'; import { LegacyLogger } from '@src/core/logger'; -import { TeamDto, TeamUserDto } from '@modules/collaborative-storage'; -import { PseudonymService } from '@modules/pseudonym'; -import { UserService } from '@modules/user'; -import { ExternalToolService } from '@modules/tool/external-tool/service'; -import { ExternalTool } from '@modules/tool/external-tool/domain'; import { TeamRolePermissionsDto } from '../../dto/team-role-permissions.dto'; -import { ICollaborativeStorageStrategy } from '../base.interface.strategy'; +import { CollaborativeStorageStrategy } from '../base.interface.strategy'; import { NextcloudClient } from './nextcloud.client'; /** * Nextcloud Strategy Implementation for Collaborative Storage * - * @implements {ICollaborativeStorageStrategy} + * @implements {CollaborativeStorageStrategy} */ @Injectable() -export class NextcloudStrategy implements ICollaborativeStorageStrategy { +export class NextcloudStrategy implements CollaborativeStorageStrategy { constructor( private readonly logger: LegacyLogger, private readonly client: NextcloudClient, diff --git a/apps/server/src/infra/encryption/encryption.interface.ts b/apps/server/src/infra/encryption/encryption.interface.ts index b8a26d7d145..062e7184d0e 100644 --- a/apps/server/src/infra/encryption/encryption.interface.ts +++ b/apps/server/src/infra/encryption/encryption.interface.ts @@ -1,7 +1,7 @@ export const DefaultEncryptionService = Symbol('DefaultEncryptionService'); export const LdapEncryptionService = Symbol('LdapEncryptionService'); -export interface IEncryptionService { +export interface EncryptionService { encrypt(data: string): string; decrypt(data: string): string; } diff --git a/apps/server/src/infra/encryption/encryption.module.spec.ts b/apps/server/src/infra/encryption/encryption.module.spec.ts index 2625f37a321..c65896efeaf 100644 --- a/apps/server/src/infra/encryption/encryption.module.spec.ts +++ b/apps/server/src/infra/encryption/encryption.module.spec.ts @@ -1,11 +1,11 @@ import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { DefaultEncryptionService, EncryptionModule, IEncryptionService, LdapEncryptionService } from '.'; +import { DefaultEncryptionService, EncryptionModule, EncryptionService, LdapEncryptionService } from '.'; describe('EncryptionModule', () => { let module: TestingModule; - let defaultService: IEncryptionService; - let ldapService: IEncryptionService; + let defaultService: EncryptionService; + let ldapService: EncryptionService; beforeAll(async () => { module = await Test.createTestingModule({ diff --git a/apps/server/src/infra/encryption/encryption.service.ts b/apps/server/src/infra/encryption/encryption.service.ts index a443503e9ef..d23a7395675 100644 --- a/apps/server/src/infra/encryption/encryption.service.ts +++ b/apps/server/src/infra/encryption/encryption.service.ts @@ -2,10 +2,10 @@ import CryptoJs from 'crypto-js'; import { Injectable } from '@nestjs/common'; import { LegacyLogger } from '@src/core/logger'; -import { IEncryptionService } from './encryption.interface'; +import { EncryptionService } from './encryption.interface'; @Injectable() -export class SymetricKeyEncryptionService implements IEncryptionService { +export class SymetricKeyEncryptionService implements EncryptionService { constructor(private logger: LegacyLogger, private key?: string) { if (!this.key) { this.logger.warn('No AES key defined. Encryption will no work'); diff --git a/apps/server/src/infra/identity-management/identity-management.config.ts b/apps/server/src/infra/identity-management/identity-management.config.ts index 39ff137fb2b..5c4d8953813 100644 --- a/apps/server/src/infra/identity-management/identity-management.config.ts +++ b/apps/server/src/infra/identity-management/identity-management.config.ts @@ -1,4 +1,4 @@ -export interface IIdentityManagementConfig { +export interface IdentityManagementConfig { FEATURE_IDENTITY_MANAGEMENT_ENABLED: boolean; FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: boolean; FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: boolean; diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts b/apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts index 85d3f7a5a3c..10daef11317 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts @@ -5,18 +5,18 @@ import { KeycloakConfigurationUc } from '../uc/keycloak-configuration.uc'; const defaultError = new Error('IDM is not reachable or authentication failed.'); -interface IRetryOptions { +interface RetryOptions { retryCount?: number; retryDelay?: number; } -interface IMigrationOptions { +interface MigrationOptions { skip?: number; query?: string; verbose?: boolean; } -interface ICleanOptions { +interface CleanOptions { pageSize?: number; } @Console({ command: 'idm', description: 'Prefixes all Identity Management (IDM) related console commands.' }) @@ -74,7 +74,7 @@ export class KeycloakConsole { }, ], }) - async clean(options: IRetryOptions & ICleanOptions): Promise { + async clean(options: RetryOptions & CleanOptions): Promise { await this.repeatCommand( 'clean', async () => { @@ -96,7 +96,7 @@ export class KeycloakConsole { description: 'Add all seed users to the IDM.', options: KeycloakConsole.retryFlags, }) - async seed(options: IRetryOptions): Promise { + async seed(options: RetryOptions): Promise { await this.repeatCommand( 'seed', async () => { @@ -118,7 +118,7 @@ export class KeycloakConsole { description: 'Configures Keycloak identity providers.', options: [...KeycloakConsole.retryFlags], }) - async configure(options: IRetryOptions): Promise { + async configure(options: RetryOptions): Promise { await this.repeatCommand( 'configure', async () => { @@ -153,7 +153,7 @@ export class KeycloakConsole { }, ], }) - async migrate(options: IRetryOptions & IMigrationOptions): Promise { + async migrate(options: RetryOptions & MigrationOptions): Promise { await this.repeatCommand( 'migrate', async () => { diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-account.interface.ts b/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-account.interface.ts index 7a284b70a6b..2b8d74a1006 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-account.interface.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-account.interface.ts @@ -1,4 +1,4 @@ -export interface IJsonAccount { +export interface JsonAccount { _id: { $oid: string; }; diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-user.interface.ts b/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-user.interface.ts index 79ff1d11708..2e4f3f89c9d 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-user.interface.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/interface/json-user.interface.ts @@ -1,4 +1,4 @@ -export interface IJsonUser { +export interface JsonUser { _id: { $oid: string; }; diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts b/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts index 6573ed35a5b..a7f9e360074 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts @@ -1,10 +1,10 @@ +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; -import { Inject } from '@nestjs/common'; -import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; import { OidcConfigDto } from '@modules/system/service'; +import { Inject } from '@nestjs/common'; export class OidcIdentityProviderMapper { - constructor(@Inject(DefaultEncryptionService) private readonly defaultEncryptionService: IEncryptionService) {} + constructor(@Inject(DefaultEncryptionService) private readonly defaultEncryptionService: EncryptionService) {} public mapToKeycloakIdentityProvider(oidcConfig: OidcConfigDto, flowAlias: string): IdentityProviderRepresentation { return { diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts index 61e475fe3e3..0b47e876ba0 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { SymetricKeyEncryptionService } from '@infra/encryption'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client-cjs/keycloak-admin-client-cjs-index'; import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; @@ -6,14 +7,13 @@ import { AuthenticationManagement } from '@keycloak/keycloak-admin-client/lib/re import { Clients } from '@keycloak/keycloak-admin-client/lib/resources/clients'; import { IdentityProviders } from '@keycloak/keycloak-admin-client/lib/resources/identityProviders'; import { Realms } from '@keycloak/keycloak-admin-client/lib/resources/realms'; +import { SystemOidcMapper } from '@modules/system/mapper/system-oidc.mapper'; +import { SystemOidcService } from '@modules/system/service/system-oidc.service'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { SymetricKeyEncryptionService } from '@infra/encryption'; import { systemFactory } from '@shared/testing'; -import { SystemOidcMapper } from '@modules/system/mapper/system-oidc.mapper'; -import { SystemOidcService } from '@modules/system/service/system-oidc.service'; import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; import { v1 } from 'uuid'; diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts index ae7f2631bce..89389a5318d 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts @@ -4,11 +4,11 @@ import ClientRepresentation from '@keycloak/keycloak-admin-client/lib/defs/clien import IdentityProviderMapperRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperRepresentation'; import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; import ProtocolMapperRepresentation from '@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation'; -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { IServerConfig } from '@modules/server/server.config'; +import { ServerConfig } from '@modules/server/server.config'; import { OidcConfigDto } from '@modules/system/service'; import { SystemOidcService } from '@modules/system/service/system-oidc.service'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; import { OidcIdentityProviderMapper } from '../mapper/identity-provider.mapper'; @@ -26,7 +26,7 @@ const oidcExternalSubMapperName = 'External Sub Mapper'; export class KeycloakConfigurationService { constructor( private readonly kcAdmin: KeycloakAdministrationService, - private readonly configService: ConfigService, + private readonly configService: ConfigService, private readonly oidcIdentityProviderMapper: OidcIdentityProviderMapper, private readonly systemOidcService: SystemOidcService ) {} diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts index 19a3b28326e..f94513c1bd5 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts @@ -1,18 +1,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { LegacyLogger } from '@src/core/logger'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client-cjs/keycloak-admin-client-cjs-index'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { AuthenticationManagement } from '@keycloak/keycloak-admin-client/lib/resources/authenticationManagement'; import { Users } from '@keycloak/keycloak-admin-client/lib/resources/users'; import { Test, TestingModule } from '@nestjs/testing'; +import { LegacyLogger } from '@src/core/logger'; import { v1 } from 'uuid'; import { IKeycloakSettings, KeycloakSettings, } from '../../keycloak-administration/interface/keycloak-settings.interface'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; -import { IJsonAccount } from '../interface/json-account.interface'; -import { IJsonUser } from '../interface/json-user.interface'; +import { JsonAccount } from '../interface/json-account.interface'; +import { JsonUser } from '../interface/json-user.interface'; import { IKeycloakConfigurationInputFiles, KeycloakConfigurationInputFiles, @@ -21,8 +21,8 @@ import { KeycloakSeedService } from './keycloak-seed.service'; const accountsFile = 'accounts.json'; const usersFile = 'users.json'; -let jsonAccounts: IJsonAccount[]; -let jsonUsers: IJsonUser[]; +let jsonAccounts: JsonAccount[]; +let jsonUsers: JsonUser[]; jest.mock('node:fs/promises', () => { return { @@ -52,8 +52,8 @@ describe('KeycloakSeedService', () => { const kcApiAuthenticationManagementMock = createMock(); const adminUsername = 'admin'; - let validAccountsNoDuplicates: IJsonAccount[]; - let validAccounts: IJsonAccount[]; + let validAccountsNoDuplicates: JsonAccount[]; + let validAccounts: JsonAccount[]; const adminUser: UserRepresentation = { id: v1(), diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts index eaf08cebe27..5ba3071c2b4 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts @@ -2,12 +2,12 @@ import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRep import { Inject } from '@nestjs/common'; import { LegacyLogger } from '@src/core/logger'; import fs from 'node:fs/promises'; -import { IJsonAccount } from '../interface/json-account.interface'; -import { IJsonUser } from '../interface/json-user.interface'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; +import { JsonAccount } from '../interface/json-account.interface'; +import { JsonUser } from '../interface/json-user.interface'; import { - KeycloakConfigurationInputFiles, IKeycloakConfigurationInputFiles, + KeycloakConfigurationInputFiles, } from '../interface/keycloak-configuration-input-files.interface'; export class KeycloakSeedService { @@ -57,7 +57,7 @@ export class KeycloakSeedService { return deletedUsers; } - private async createOrUpdateIdmAccount(account: IJsonAccount, user: IJsonUser): Promise { + private async createOrUpdateIdmAccount(account: JsonAccount, user: JsonUser): Promise { const idmUserRepresentation: UserRepresentation = { username: account.username, firstName: user.firstName, @@ -91,13 +91,13 @@ export class KeycloakSeedService { return false; } - private async loadAccounts(): Promise { + private async loadAccounts(): Promise { const data = await fs.readFile(this.inputFiles.accountsFile, { encoding: 'utf-8' }); - return JSON.parse(data) as IJsonAccount[]; + return JSON.parse(data) as JsonAccount[]; } - private async loadUsers(): Promise { + private async loadUsers(): Promise { const data = await fs.readFile(this.inputFiles.usersFile, { encoding: 'utf-8' }); - return JSON.parse(data) as IJsonUser[]; + return JSON.parse(data) as JsonUser[]; } } diff --git a/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts index 9ccd20b6f99..8bd354ff379 100644 --- a/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts @@ -1,9 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; @@ -38,7 +38,7 @@ describe('KeycloakIdentityManagementService', () => { }, { provide: DefaultEncryptionService, - useValue: createMock(), + useValue: createMock(), }, ], }).compile(); diff --git a/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts index 9eab3a4f60a..cbfa289165e 100644 --- a/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts +++ b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts @@ -1,8 +1,8 @@ +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; +import { OauthConfigDto } from '@modules/system/service'; import { HttpService } from '@nestjs/axios'; import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; -import { OauthConfigDto } from '@modules/system/service'; import qs from 'qs'; import { lastValueFrom } from 'rxjs'; import { IdentityManagementOauthService } from '../../identity-management-oauth.service'; @@ -16,7 +16,7 @@ export class KeycloakIdentityManagementOauthService extends IdentityManagementOa private readonly kcAdminService: KeycloakAdministrationService, private readonly configService: ConfigService, private readonly httpService: HttpService, - @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: IEncryptionService + @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: EncryptionService ) { super(); } diff --git a/apps/server/src/infra/mail/interfaces/mail-config.ts b/apps/server/src/infra/mail/interfaces/mail-config.ts index 6dbb0c7864d..d4baab878e7 100644 --- a/apps/server/src/infra/mail/interfaces/mail-config.ts +++ b/apps/server/src/infra/mail/interfaces/mail-config.ts @@ -1,3 +1,3 @@ -export interface IMailConfig { +export interface MailConfig { ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS: string[]; } diff --git a/apps/server/src/infra/mail/mail.module.ts b/apps/server/src/infra/mail/mail.module.ts index ee6d50d59e7..0f30be221a7 100644 --- a/apps/server/src/infra/mail/mail.module.ts +++ b/apps/server/src/infra/mail/mail.module.ts @@ -1,7 +1,7 @@ -import { Module, DynamicModule } from '@nestjs/common'; +import { DynamicModule, Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { MailConfig } from './interfaces/mail-config'; import { MailService } from './mail.service'; -import { IMailConfig } from './interfaces/mail-config'; interface MailModuleOptions { exchange: string; @@ -19,7 +19,7 @@ export class MailModule { provide: 'MAIL_SERVICE_OPTIONS', useValue: { exchange: options.exchange, routingKey: options.routingKey }, }, - ConfigService, + ConfigService, ], exports: [MailService], }; diff --git a/apps/server/src/infra/mail/mail.service.spec.ts b/apps/server/src/infra/mail/mail.service.spec.ts index ebc77030252..c72f182ee40 100644 --- a/apps/server/src/infra/mail/mail.service.spec.ts +++ b/apps/server/src/infra/mail/mail.service.spec.ts @@ -1,10 +1,10 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; -import { Test, TestingModule } from '@nestjs/testing'; import { createMock } from '@golevelup/ts-jest'; import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MailConfig } from './interfaces/mail-config'; import { Mail } from './mail.interface'; import { MailService } from './mail.service'; -import { IMailConfig } from './interfaces/mail-config'; describe('MailService', () => { let module: TestingModule; @@ -24,7 +24,7 @@ describe('MailService', () => { { provide: 'MAIL_SERVICE_OPTIONS', useValue: mailServiceOptions }, { provide: ConfigService, - useValue: createMock>({ get: () => ['schul-cloud.org', 'example.com'] }), + useValue: createMock>({ get: () => ['schul-cloud.org', 'example.com'] }), }, ], }).compile(); diff --git a/apps/server/src/infra/mail/mail.service.ts b/apps/server/src/infra/mail/mail.service.ts index 432f0746934..ce8f68ceddb 100644 --- a/apps/server/src/infra/mail/mail.service.ts +++ b/apps/server/src/infra/mail/mail.service.ts @@ -1,8 +1,8 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { MailConfig } from './interfaces/mail-config'; import { Mail } from './mail.interface'; -import { IMailConfig } from './interfaces/mail-config'; interface MailServiceOptions { exchange: string; @@ -16,7 +16,7 @@ export class MailService { constructor( private readonly amqpConnection: AmqpConnection, @Inject('MAIL_SERVICE_OPTIONS') private readonly options: MailServiceOptions, - private readonly configService: ConfigService + private readonly configService: ConfigService ) { this.domainBlacklist = this.configService.get('ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS'); } diff --git a/apps/server/src/infra/preview-generator/preview-generator.consumer.ts b/apps/server/src/infra/preview-generator/preview-generator.consumer.ts index d34fc8bc37c..7e1a5c523ae 100644 --- a/apps/server/src/infra/preview-generator/preview-generator.consumer.ts +++ b/apps/server/src/infra/preview-generator/preview-generator.consumer.ts @@ -1,7 +1,7 @@ import { RabbitPayload, RabbitRPC } from '@golevelup/nestjs-rabbitmq'; +import { FilesPreviewEvents, FilesPreviewExchange } from '@infra/rabbitmq'; import { Injectable } from '@nestjs/common'; import { Logger } from '@src/core/logger'; -import { FilesPreviewEvents, FilesPreviewExchange } from '@infra/rabbitmq'; import { PreviewFileOptions } from './interface'; import { PreviewActionsLoggable } from './loggable/preview-actions.loggable'; import { PreviewGeneratorService } from './preview-generator.service'; @@ -18,10 +18,12 @@ export class PreviewGeneratorConsumer { queue: FilesPreviewEvents.GENERATE_PREVIEW, }) public async generatePreview(@RabbitPayload() payload: PreviewFileOptions) { - this.logger.debug(new PreviewActionsLoggable('PreviewGeneratorConsumer.generatePreview', payload)); + this.logger.info(new PreviewActionsLoggable('PreviewGeneratorConsumer.generatePreview:start', payload)); const response = await this.previewGeneratorService.generatePreview(payload); + this.logger.info(new PreviewActionsLoggable('PreviewGeneratorConsumer.generatePreview:end', payload)); + return { message: response }; } } diff --git a/apps/server/src/infra/preview-generator/preview-generator.service.ts b/apps/server/src/infra/preview-generator/preview-generator.service.ts index 83dca461a2f..35b52d5e174 100644 --- a/apps/server/src/infra/preview-generator/preview-generator.service.ts +++ b/apps/server/src/infra/preview-generator/preview-generator.service.ts @@ -1,5 +1,5 @@ -import { Injectable } from '@nestjs/common'; import { GetFile, S3ClientAdapter } from '@infra/s3-client'; +import { Injectable } from '@nestjs/common'; import { Logger } from '@src/core/logger'; import { subClass } from 'gm'; import { PassThrough } from 'stream'; @@ -16,7 +16,7 @@ export class PreviewGeneratorService { } public async generatePreview(params: PreviewFileOptions): Promise { - this.logger.debug(new PreviewActionsLoggable('PreviewGeneratorService.generatePreview:start', params)); + this.logger.info(new PreviewActionsLoggable('PreviewGeneratorService.generatePreview:start', params)); const { originFilePath, previewFilePath, previewOptions } = params; const original = await this.downloadOriginFile(originFilePath); @@ -26,7 +26,7 @@ export class PreviewGeneratorService { await this.storageClient.create(previewFilePath, file); - this.logger.debug(new PreviewActionsLoggable('PreviewGeneratorService.generatePreview:end', params)); + this.logger.info(new PreviewActionsLoggable('PreviewGeneratorService.generatePreview:end', params)); return { previewFilePath, diff --git a/apps/server/src/infra/preview-generator/preview.producer.ts b/apps/server/src/infra/preview-generator/preview.producer.ts index 28cf6930830..8c84e93b295 100644 --- a/apps/server/src/infra/preview-generator/preview.producer.ts +++ b/apps/server/src/infra/preview-generator/preview.producer.ts @@ -1,7 +1,7 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { FilesPreviewEvents, FilesPreviewExchange, RpcMessageProducer } from '@infra/rabbitmq'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { FilesPreviewEvents, FilesPreviewExchange, RpcMessageProducer } from '@infra/rabbitmq'; import { Logger } from '@src/core/logger'; import { PreviewFileOptions, PreviewResponseMessage } from './interface'; import { PreviewModuleConfig } from './interface/preview-consumer-config'; @@ -21,10 +21,11 @@ export class PreviewProducer extends RpcMessageProducer { } async generate(payload: PreviewFileOptions): Promise { - this.logger.debug(new PreviewActionsLoggable('PreviewProducer.generate:started', payload)); + this.logger.info(new PreviewActionsLoggable('PreviewProducer.generate:started', payload)); + const response = await this.request(FilesPreviewEvents.GENERATE_PREVIEW, payload); - this.logger.debug(new PreviewActionsLoggable('PreviewProducer.generate:finished', payload)); + this.logger.info(new PreviewActionsLoggable('PreviewProducer.generate:finished', payload)); return response; } diff --git a/apps/server/src/infra/rabbitmq/error.mapper.spec.ts b/apps/server/src/infra/rabbitmq/error.mapper.spec.ts index 884dfb35158..33dabb8acf8 100644 --- a/apps/server/src/infra/rabbitmq/error.mapper.spec.ts +++ b/apps/server/src/infra/rabbitmq/error.mapper.spec.ts @@ -1,10 +1,10 @@ +import { IError } from '@infra/rabbitmq'; import { BadRequestException, ConflictException, ForbiddenException, InternalServerErrorException, } from '@nestjs/common'; -import { IError } from '@infra/rabbitmq'; import _ from 'lodash'; import { ErrorMapper } from './error.mapper'; diff --git a/apps/server/src/infra/rabbitmq/error.mapper.ts b/apps/server/src/infra/rabbitmq/error.mapper.ts index 6f7083d3ad9..dcee0654e10 100644 --- a/apps/server/src/infra/rabbitmq/error.mapper.ts +++ b/apps/server/src/infra/rabbitmq/error.mapper.ts @@ -1,6 +1,6 @@ +import { IError } from '@infra/rabbitmq'; import { BadRequestException, ForbiddenException, InternalServerErrorException } from '@nestjs/common'; import { ErrorUtils } from '@src/core/error/utils'; -import { IError } from '@infra/rabbitmq'; export class ErrorMapper { static mapRpcErrorResponseToDomainError( diff --git a/apps/server/src/infra/rabbitmq/exchange/files-storage.ts b/apps/server/src/infra/rabbitmq/exchange/files-storage.ts index 080bcfebae5..86d2f710a57 100644 --- a/apps/server/src/infra/rabbitmq/exchange/files-storage.ts +++ b/apps/server/src/infra/rabbitmq/exchange/files-storage.ts @@ -27,25 +27,25 @@ export enum FileRecordParentType { 'BoardNode' = 'boardnodes', } -export interface ICopyFilesOfParentParams { +export interface CopyFilesOfParentParams { userId: EntityId; - source: IFileRecordParams; - target: IFileRecordParams; + source: FileRecordParams; + target: FileRecordParams; } -export interface IFileRecordParams { +export interface FileRecordParams { schoolId: EntityId; parentId: EntityId; parentType: FileRecordParentType; } -export interface ICopyFileDO { +export interface CopyFileDO { id?: EntityId; sourceId: EntityId; name: string; } -export interface IFileDO { +export interface FileDO { id: string; name: string; parentId: string; diff --git a/apps/server/src/infra/s3-client/s3-client.adapter.ts b/apps/server/src/infra/s3-client/s3-client.adapter.ts index 3c83fce7413..1ed693a4d52 100644 --- a/apps/server/src/infra/s3-client/s3-client.adapter.ts +++ b/apps/server/src/infra/s3-client/s3-client.adapter.ts @@ -33,7 +33,7 @@ export class S3ClientAdapter { // is public but only used internally public async createBucket() { try { - this.logger.log({ action: 'create bucket', params: { bucket: this.config.bucket } }); + this.logger.debug({ action: 'create bucket', params: { bucket: this.config.bucket } }); const req = new CreateBucketCommand({ Bucket: this.config.bucket }); await this.client.send(req); @@ -50,7 +50,7 @@ export class S3ClientAdapter { public async get(path: string, bytesRange?: string): Promise { try { - this.logger.log({ action: 'get', params: { path, bucket: this.config.bucket } }); + this.logger.debug({ action: 'get', params: { path, bucket: this.config.bucket } }); const req = new GetObjectCommand({ Bucket: this.config.bucket, @@ -73,8 +73,8 @@ export class S3ClientAdapter { } catch (err) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (err?.Code === 'NoSuchKey') { - this.logger.log(`could not find one of the files for deletion with id ${path}`); - throw new NotFoundException('NoSuchKey'); + this.logger.warn(`could not find one of the files for deletion with id ${path}`); + throw new NotFoundException('NoSuchKey', ErrorUtils.createHttpExceptionOptions(err)); } else { throw new InternalServerErrorException('S3ClientAdapter:get', ErrorUtils.createHttpExceptionOptions(err)); } @@ -83,7 +83,7 @@ export class S3ClientAdapter { public async create(path: string, file: File): Promise { try { - this.logger.log({ action: 'create', params: { path, bucket: this.config.bucket } }); + this.logger.debug({ action: 'create', params: { path, bucket: this.config.bucket } }); const req = { Body: file.data, @@ -126,7 +126,7 @@ export class S3ClientAdapter { } catch (err) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (err?.cause?.name === 'NoSuchKey') { - this.logger.log(`could not find one of the files for deletion with ids ${paths.join(',')}`); + this.logger.warn(`could not find one of the files for deletion with ids ${paths.join(',')}`); return []; } throw new InternalServerErrorException('S3ClientAdapter:delete', ErrorUtils.createHttpExceptionOptions(err)); @@ -135,7 +135,7 @@ export class S3ClientAdapter { public async restore(paths: string[]): Promise { try { - this.logger.log({ action: 'restore', params: { paths, bucket: this.config.bucket } }); + this.logger.debug({ action: 'restore', params: { paths, bucket: this.config.bucket } }); const copyPaths = paths.map((path) => { return { sourcePath: `${this.deletedFolderName}/${path}`, targetPath: path }; @@ -156,7 +156,7 @@ export class S3ClientAdapter { public async copy(paths: CopyFiles[]) { try { - this.logger.log({ action: 'copy', params: { paths, bucket: this.config.bucket } }); + this.logger.debug({ action: 'copy', params: { paths, bucket: this.config.bucket } }); const copyRequests = paths.map(async (path) => { const req = new CopyObjectCommand({ @@ -180,7 +180,7 @@ export class S3ClientAdapter { public async delete(paths: string[]) { try { - this.logger.log({ action: 'delete', params: { paths, bucket: this.config.bucket } }); + this.logger.debug({ action: 'delete', params: { paths, bucket: this.config.bucket } }); const pathObjects = paths.map((p) => { return { Key: p }; @@ -200,7 +200,7 @@ export class S3ClientAdapter { public async list(params: ListFiles): Promise { try { - this.logger.log({ action: 'list', params }); + this.logger.debug({ action: 'list', params }); const result = await this.listObjectKeysRecursive(params); @@ -242,7 +242,7 @@ export class S3ClientAdapter { public async head(path: string): Promise { try { - this.logger.log({ action: 'head', params: { path, bucket: this.config.bucket } }); + this.logger.debug({ action: 'head', params: { path, bucket: this.config.bucket } }); const req = new HeadObjectCommand({ Bucket: this.config.bucket, @@ -255,7 +255,7 @@ export class S3ClientAdapter { } catch (err) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (err.message && err.message === 'NoSuchKey') { - this.logger.log(`could not find the file for head with id ${path}`); + this.logger.warn(`could not find the file for head with id ${path}`); throw new NotFoundException(null, ErrorUtils.createHttpExceptionOptions(err, 'NoSuchKey')); } throw new InternalServerErrorException(null, ErrorUtils.createHttpExceptionOptions(err, 'S3ClientAdapter:head')); @@ -264,7 +264,7 @@ export class S3ClientAdapter { public async deleteDirectory(path: string) { try { - this.logger.log({ action: 'deleteDirectory', params: { path, bucket: this.config.bucket } }); + this.logger.debug({ action: 'deleteDirectory', params: { path, bucket: this.config.bucket } }); const req = new ListObjectsV2Command({ Bucket: this.config.bucket, diff --git a/apps/server/src/modules/account/account-config.ts b/apps/server/src/modules/account/account-config.ts index cb2bd64e517..38b6acf7e58 100644 --- a/apps/server/src/modules/account/account-config.ts +++ b/apps/server/src/modules/account/account-config.ts @@ -1,4 +1,4 @@ -export interface IAccountConfig { +export interface AccountConfig { LOGIN_BLOCK_TIME: number; TEACHER_STUDENT_VISIBILITY__IS_CONFIGURABLE: boolean; } diff --git a/apps/server/src/modules/account/account.module.ts b/apps/server/src/modules/account/account.module.ts index 2e11af11c6d..9a57ba37a44 100644 --- a/apps/server/src/modules/account/account.module.ts +++ b/apps/server/src/modules/account/account.module.ts @@ -1,19 +1,19 @@ +import { IdentityManagementModule } from '@infra/identity-management'; import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PermissionService } from '@shared/domain'; import { SystemRepo, UserRepo } from '@shared/repo'; -import { IdentityManagementModule } from '@infra/identity-management'; import { LoggerModule } from '@src/core/logger/logger.module'; +import { ServerConfig } from '../server/server.config'; +import { AccountIdmToDtoMapper, AccountIdmToDtoMapperDb, AccountIdmToDtoMapperIdm } from './mapper'; import { AccountRepo } from './repo/account.repo'; -import { AccountService } from './services/account.service'; -import { AccountValidationService } from './services/account.validation.service'; import { AccountServiceDb } from './services/account-db.service'; import { AccountServiceIdm } from './services/account-idm.service'; -import { AccountIdmToDtoMapper, AccountIdmToDtoMapperDb, AccountIdmToDtoMapperIdm } from './mapper'; -import { IServerConfig } from '../server/server.config'; import { AccountLookupService } from './services/account-lookup.service'; +import { AccountService } from './services/account.service'; +import { AccountValidationService } from './services/account.validation.service'; -function accountIdmToDtoMapperFactory(configService: ConfigService): AccountIdmToDtoMapper { +function accountIdmToDtoMapperFactory(configService: ConfigService): AccountIdmToDtoMapper { if (configService.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED') === true) { return new AccountIdmToDtoMapperIdm(); } diff --git a/apps/server/src/modules/account/controller/account.controller.ts b/apps/server/src/modules/account/controller/account.controller.ts index 2256cb9fc90..3265693915c 100644 --- a/apps/server/src/modules/account/controller/account.controller.ts +++ b/apps/server/src/modules/account/controller/account.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Delete, Get, Param, Patch, Query } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; import { EntityNotFoundError, ForbiddenOperationError, ValidationError } from '@shared/common'; import { ICurrentUser } from '@src/modules/authentication'; +import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; import { AccountUc } from '../uc/account.uc'; import { AccountByIdBodyParams, diff --git a/apps/server/src/modules/account/controller/api-test/account.api.spec.ts b/apps/server/src/modules/account/controller/api-test/account.api.spec.ts index fefdb006bf8..7a920a40a97 100644 --- a/apps/server/src/modules/account/controller/api-test/account.api.spec.ts +++ b/apps/server/src/modules/account/controller/api-test/account.api.spec.ts @@ -2,7 +2,6 @@ import { EntityManager } from '@mikro-orm/core'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, Permission, RoleName, User } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; import { accountFactory, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { AccountByIdBodyParams, @@ -11,6 +10,7 @@ import { PatchMyAccountParams, PatchMyPasswordParams, } from '@src/modules/account/controller/dto'; +import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@src/modules/server/server.module'; import { Request } from 'express'; diff --git a/apps/server/src/modules/account/services/account-db.service.spec.ts b/apps/server/src/modules/account/services/account-db.service.spec.ts index 107273797f9..e847cdf9eb7 100644 --- a/apps/server/src/modules/account/services/account-db.service.spec.ts +++ b/apps/server/src/modules/account/services/account-db.service.spec.ts @@ -1,14 +1,14 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { IdentityManagementService } from '@infra/identity-management/identity-management.service'; import { ObjectId } from '@mikro-orm/mongodb'; +import { AccountEntityToDtoMapper } from '@modules/account/mapper'; +import { AccountDto } from '@modules/account/services/dto'; +import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; import { Account, EntityId, Permission, Role, RoleName, SchoolEntity, User } from '@shared/domain'; -import { IdentityManagementService } from '@infra/identity-management/identity-management.service'; import { accountFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; -import { AccountEntityToDtoMapper } from '@modules/account/mapper'; -import { AccountDto } from '@modules/account/services/dto'; -import { IServerConfig } from '@modules/server'; import bcrypt from 'bcryptjs'; import { LegacyLogger } from '../../../core/logger'; import { AccountRepo } from '../repo/account.repo'; @@ -117,7 +117,7 @@ describe('AccountDbService', () => { }, { provide: ConfigService, - useValue: createMock>(), + useValue: createMock>(), }, { provide: IdentityManagementService, diff --git a/apps/server/src/modules/account/services/account-lookup.service.ts b/apps/server/src/modules/account/services/account-lookup.service.ts index ed67d03232d..41064d077e9 100644 --- a/apps/server/src/modules/account/services/account-lookup.service.ts +++ b/apps/server/src/modules/account/services/account-lookup.service.ts @@ -1,8 +1,8 @@ +import { IdentityManagementService } from '@infra/identity-management'; +import { ServerConfig } from '@modules/server/server.config'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { EntityId } from '@shared/domain'; -import { IdentityManagementService } from '@infra/identity-management'; -import { IServerConfig } from '@modules/server/server.config'; import { ObjectId } from 'bson'; /** @@ -15,7 +15,7 @@ import { ObjectId } from 'bson'; export class AccountLookupService { constructor( private readonly idmService: IdentityManagementService, - private readonly configService: ConfigService + private readonly configService: ConfigService ) {} /** diff --git a/apps/server/src/modules/account/services/account.service.spec.ts b/apps/server/src/modules/account/services/account.service.spec.ts index 4cb95d96a36..1d7c7d43398 100644 --- a/apps/server/src/modules/account/services/account.service.spec.ts +++ b/apps/server/src/modules/account/services/account.service.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { IServerConfig } from '@modules/server'; import { LegacyLogger } from '../../../core/logger'; import { AccountServiceDb } from './account-db.service'; import { AccountServiceIdm } from './account-idm.service'; @@ -40,7 +40,7 @@ describe('AccountService', () => { }, { provide: ConfigService, - useValue: createMock>(), + useValue: createMock>(), }, { provide: AccountValidationService, diff --git a/apps/server/src/modules/account/services/account.service.ts b/apps/server/src/modules/account/services/account.service.ts index 6c2070550ab..6e6107954ce 100644 --- a/apps/server/src/modules/account/services/account.service.ts +++ b/apps/server/src/modules/account/services/account.service.ts @@ -5,7 +5,7 @@ import { ValidationError } from '@shared/common'; import { Counted } from '@shared/domain'; import { isEmail, validateOrReject } from 'class-validator'; import { LegacyLogger } from '../../../core/logger'; -import { IServerConfig } from '../../server/server.config'; +import { ServerConfig } from '../../server/server.config'; import { AccountServiceDb } from './account-db.service'; import { AccountServiceIdm } from './account-idm.service'; import { AbstractAccountService } from './account.service.abstract'; @@ -19,7 +19,7 @@ export class AccountService extends AbstractAccountService { constructor( private readonly accountDb: AccountServiceDb, private readonly accountIdm: AccountServiceIdm, - private readonly configService: ConfigService, + private readonly configService: ConfigService, private readonly accountValidationService: AccountValidationService, private readonly logger: LegacyLogger ) { diff --git a/apps/server/src/modules/account/uc/account.uc.spec.ts b/apps/server/src/modules/account/uc/account.uc.spec.ts index aa4cdf56a82..6810c2baa5c 100644 --- a/apps/server/src/modules/account/uc/account.uc.spec.ts +++ b/apps/server/src/modules/account/uc/account.uc.spec.ts @@ -1,4 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountSaveDto } from '@modules/account/services/dto'; +import { AccountDto } from '@modules/account/services/dto/account.dto'; +import { ICurrentUser } from '@modules/authentication'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { AuthorizationError, EntityNotFoundError, ForbiddenOperationError, ValidationError } from '@shared/common'; @@ -18,10 +22,6 @@ import { import { UserRepo } from '@shared/repo'; import { accountFactory, schoolFactory, setupEntities, systemFactory, userFactory } from '@shared/testing'; import { BruteForcePrevention } from '@src/imports-from-feathers'; -import { AccountService } from '@modules/account/services/account.service'; -import { AccountSaveDto } from '@modules/account/services/dto'; -import { AccountDto } from '@modules/account/services/dto/account.dto'; -import { ICurrentUser } from '@modules/authentication'; import { ObjectId } from 'bson'; import { AccountByIdBodyParams, diff --git a/apps/server/src/modules/account/uc/account.uc.ts b/apps/server/src/modules/account/uc/account.uc.ts index ac4ac053dae..dac0b936ba5 100644 --- a/apps/server/src/modules/account/uc/account.uc.ts +++ b/apps/server/src/modules/account/uc/account.uc.ts @@ -1,3 +1,5 @@ +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto/account.dto'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { @@ -8,13 +10,11 @@ import { } from '@shared/common/error'; import { Account, EntityId, Permission, PermissionService, Role, RoleName, SchoolEntity, User } from '@shared/domain'; import { UserRepo } from '@shared/repo'; -import { AccountService } from '@modules/account/services/account.service'; -import { AccountDto } from '@modules/account/services/dto/account.dto'; -import { BruteForcePrevention } from '@src/imports-from-feathers'; import { ICurrentUser } from '@modules/authentication'; +import { BruteForcePrevention } from '@src/imports-from-feathers'; import { ObjectId } from 'bson'; -import { IAccountConfig } from '../account-config'; +import { AccountConfig } from '../account-config'; import { AccountByIdBodyParams, AccountByIdParams, @@ -39,7 +39,7 @@ export class AccountUc { private readonly userRepo: UserRepo, private readonly permissionService: PermissionService, private readonly accountValidationService: AccountValidationService, - private readonly configService: ConfigService + private readonly configService: ConfigService ) {} /** diff --git a/apps/server/src/modules/authentication/decorator/auth.decorator.spec.ts b/apps/server/src/modules/authentication/decorator/auth.decorator.spec.ts index f193bd67516..cc6006228b8 100644 --- a/apps/server/src/modules/authentication/decorator/auth.decorator.spec.ts +++ b/apps/server/src/modules/authentication/decorator/auth.decorator.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Controller, ExecutionContext, ForbiddenException, Get, INestApplication } from '@nestjs/common'; -import request from 'supertest'; -import { Test, TestingModule } from '@nestjs/testing'; import { ICurrentUser } from '@modules/authentication'; import { ServerTestModule } from '@modules/server/server.module'; +import { Controller, ExecutionContext, ForbiddenException, Get, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; import { JwtAuthGuard } from '../guard/jwt-auth.guard'; import { Authenticate, CurrentUser, JWT } from './auth.decorator'; diff --git a/apps/server/src/modules/authentication/decorator/auth.decorator.ts b/apps/server/src/modules/authentication/decorator/auth.decorator.ts index 4f0167ec854..6d36784af0d 100644 --- a/apps/server/src/modules/authentication/decorator/auth.decorator.ts +++ b/apps/server/src/modules/authentication/decorator/auth.decorator.ts @@ -6,11 +6,11 @@ import { UnauthorizedException, UseGuards, } from '@nestjs/common'; -import { Request } from 'express'; import { ApiBearerAuth } from '@nestjs/swagger'; +import { Request } from 'express'; import { ExtractJwt } from 'passport-jwt'; -import { ICurrentUser } from '../interface/user'; import { JwtAuthGuard } from '../guard/jwt-auth.guard'; +import { ICurrentUser } from '../interface/user'; import { JwtExtractor } from '../strategy/jwt-extractor'; const STRATEGIES = ['jwt'] as const; diff --git a/apps/server/src/modules/authentication/index.ts b/apps/server/src/modules/authentication/index.ts index 3a3ea0c2755..80e9d64ab69 100644 --- a/apps/server/src/modules/authentication/index.ts +++ b/apps/server/src/modules/authentication/index.ts @@ -1,4 +1,4 @@ -export { ICurrentUser } from './interface'; -export { JWT, CurrentUser, Authenticate } from './decorator'; export { AuthenticationModule } from './authentication.module'; +export { Authenticate, CurrentUser, JWT } from './decorator'; +export { ICurrentUser } from './interface'; export { AuthenticationService } from './services'; diff --git a/apps/server/src/modules/authentication/services/authentication.service.spec.ts b/apps/server/src/modules/authentication/services/authentication.service.spec.ts index 1e5c69ecfb1..de80540a65a 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.spec.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.spec.ts @@ -1,11 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto'; +import { ICurrentUser } from '@modules/authentication'; import { UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; -import { AccountService } from '@modules/account/services/account.service'; -import { AccountDto } from '@modules/account/services/dto'; -import { ICurrentUser } from '@modules/authentication'; import jwt from 'jsonwebtoken'; import { BruteForceError } from '../errors/brute-force.error'; import { JwtValidationAdapter } from '../strategy/jwt-validation.adapter'; diff --git a/apps/server/src/modules/authentication/services/authentication.service.ts b/apps/server/src/modules/authentication/services/authentication.service.ts index 41aab6153ea..efa8ec35b27 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.ts @@ -1,16 +1,16 @@ +import { AccountService } from '@modules/account'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; -import { AccountService } from '@modules/account'; // invalid import import { AccountDto } from '@modules/account/services/dto'; // invalid import, can produce dependency cycles -import type { IServerConfig } from '@modules/server'; +import type { ServerConfig } from '@modules/server'; import { randomUUID } from 'crypto'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { JwtValidationAdapter } from '../strategy/jwt-validation.adapter'; import { BruteForceError, UnauthorizedLoggableException } from '../errors'; import { CreateJwtPayload } from '../interface/jwt-payload'; +import { JwtValidationAdapter } from '../strategy/jwt-validation.adapter'; import { LoginDto } from '../uc/dto'; @Injectable() @@ -19,7 +19,7 @@ export class AuthenticationService { private readonly jwtService: JwtService, private readonly jwtValidationAdapter: JwtValidationAdapter, private readonly accountService: AccountService, - private readonly configService: ConfigService + private readonly configService: ConfigService ) {} async loadAccount(username: string, systemId?: string): Promise { diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts index 5b23be9eb74..b3067de04eb 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AccountDto } from '@modules/account/services/dto'; import { UnauthorizedException } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; import { Test, TestingModule } from '@nestjs/testing'; @@ -15,7 +16,6 @@ import { userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AccountDto } from '@modules/account/services/dto'; import { LdapAuthorizationBodyParams } from '../controllers/dto'; import { ICurrentUser } from '../interface'; import { AuthenticationService } from '../services/authentication.service'; diff --git a/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts index 121d2874fe9..44db3ea4b21 100644 --- a/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts @@ -1,13 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { IdentityManagementOauthService } from '@infra/identity-management'; +import { AccountEntityToDtoMapper } from '@modules/account/mapper'; +import { AccountDto } from '@modules/account/services/dto'; +import { ServerConfig } from '@modules/server'; import { UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { RoleName, User } from '@shared/domain'; -import { IdentityManagementOauthService } from '@infra/identity-management'; import { UserRepo } from '@shared/repo'; import { accountFactory, setupEntities, userFactory } from '@shared/testing'; -import { AccountEntityToDtoMapper } from '@modules/account/mapper'; -import { AccountDto } from '@modules/account/services/dto'; -import { IServerConfig } from '@modules/server'; import bcrypt from 'bcryptjs'; import { AuthenticationService } from '../services/authentication.service'; import { LocalStrategy } from './local.strategy'; @@ -28,7 +28,7 @@ describe('LocalStrategy', () => { await setupEntities(); authenticationServiceMock = createMock(); idmOauthServiceMock = createMock(); - configServiceMock = createMock>(); + configServiceMock = createMock>(); userRepoMock = createMock(); strategy = new LocalStrategy(authenticationServiceMock, idmOauthServiceMock, configServiceMock, userRepoMock); mockUser = userFactory.withRoleByName(RoleName.STUDENT).buildWithId(); diff --git a/apps/server/src/modules/authentication/strategy/local.strategy.ts b/apps/server/src/modules/authentication/strategy/local.strategy.ts index 1d31a86d833..c423fc396ff 100644 --- a/apps/server/src/modules/authentication/strategy/local.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/local.strategy.ts @@ -1,14 +1,14 @@ -import { Strategy } from 'passport-local'; +import { IdentityManagementConfig, IdentityManagementOauthService } from '@infra/identity-management'; +import { AccountDto } from '@modules/account/services/dto'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import bcrypt from 'bcryptjs'; -import { UserRepo } from '@shared/repo'; -import { AccountDto } from '@modules/account/services/dto'; import { GuardAgainst } from '@shared/common/utils/guard-against'; -import { IdentityManagementOauthService, IIdentityManagementConfig } from '@infra/identity-management'; -import { CurrentUserMapper } from '../mapper'; +import { UserRepo } from '@shared/repo'; +import bcrypt from 'bcryptjs'; +import { Strategy } from 'passport-local'; import { ICurrentUser } from '../interface'; +import { CurrentUserMapper } from '../mapper'; import { AuthenticationService } from '../services/authentication.service'; @Injectable() @@ -16,7 +16,7 @@ export class LocalStrategy extends PassportStrategy(Strategy) { constructor( private readonly authenticationService: AuthenticationService, private readonly idmOauthService: IdentityManagementOauthService, - private readonly configService: ConfigService, + private readonly configService: ConfigService, private readonly userRepo: UserRepo ) { super(); diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts index 428f3c5e048..9f47f0e1d6a 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts @@ -1,14 +1,17 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto'; -import { OAuthTokenDto, OAuthService } from '@modules/oauth'; +import { OAuthService, OAuthTokenDto } from '@modules/oauth'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, RoleName } from '@shared/domain'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { userDoFactory } from '@shared/testing'; + import { ICurrentUser, OauthCurrentUser } from '../interface'; + import { SchoolInMigrationLoggableException } from '../loggable'; + import { Oauth2Strategy } from './oauth2.strategy'; describe('Oauth2Strategy', () => { diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts index e5bc6f942f8..320387a7d00 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts @@ -1,6 +1,6 @@ import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto'; -import { OAuthTokenDto, OAuthService } from '@modules/oauth'; +import { OAuthService, OAuthTokenDto } from '@modules/oauth'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { UserDO } from '@shared/domain/domainobject/user.do'; diff --git a/apps/server/src/modules/authorization/authorization-reference.module.ts b/apps/server/src/modules/authorization/authorization-reference.module.ts index e253587af7b..a115d0433c3 100644 --- a/apps/server/src/modules/authorization/authorization-reference.module.ts +++ b/apps/server/src/modules/authorization/authorization-reference.module.ts @@ -1,20 +1,20 @@ +import { BoardModule } from '@modules/board'; +import { ToolModule } from '@modules/tool'; import { forwardRef, Module } from '@nestjs/common'; import { CourseGroupRepo, CourseRepo, - LessonRepo, - SchoolExternalToolRepo, LegacySchoolRepo, + SchoolExternalToolRepo, SubmissionRepo, TaskRepo, TeamsRepo, UserRepo, } from '@shared/repo'; -import { ToolModule } from '@modules/tool'; import { LoggerModule } from '@src/core/logger'; -import { BoardModule } from '@modules/board'; -import { ReferenceLoader, AuthorizationReferenceService, AuthorizationHelper } from './domain'; +import { LessonModule } from '../lesson'; import { AuthorizationModule } from './authorization.module'; +import { AuthorizationHelper, AuthorizationReferenceService, ReferenceLoader } from './domain'; /** * This module is part of an intermediate state. In the future it should be replaced by an AuthorizationApiModule. @@ -23,7 +23,13 @@ import { AuthorizationModule } from './authorization.module'; */ @Module({ // TODO: remove forwardRef to TooModule N21-1055 - imports: [AuthorizationModule, forwardRef(() => ToolModule), forwardRef(() => BoardModule), LoggerModule], + imports: [ + AuthorizationModule, + LessonModule, + forwardRef(() => ToolModule), + forwardRef(() => BoardModule), + LoggerModule, + ], providers: [ AuthorizationHelper, ReferenceLoader, @@ -32,7 +38,6 @@ import { AuthorizationModule } from './authorization.module'; CourseGroupRepo, TaskRepo, LegacySchoolRepo, - LessonRepo, TeamsRepo, SubmissionRepo, SchoolExternalToolRepo, diff --git a/apps/server/src/modules/authorization/authorization.module.ts b/apps/server/src/modules/authorization/authorization.module.ts index d01cd9363f4..f734a72ed8a 100644 --- a/apps/server/src/modules/authorization/authorization.module.ts +++ b/apps/server/src/modules/authorization/authorization.module.ts @@ -1,23 +1,23 @@ +import { FeathersModule } from '@infra/feathers'; import { Module } from '@nestjs/common'; import { UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { FeathersModule } from '@infra/feathers'; +import { AuthorizationHelper, AuthorizationService, RuleManager } from './domain'; import { BoardDoRule, ContextExternalToolRule, CourseGroupRule, CourseRule, + GroupRule, + LegacySchoolRule, LessonRule, SchoolExternalToolRule, SubmissionRule, TaskRule, TeamRule, - UserRule, UserLoginMigrationRule, - LegacySchoolRule, - GroupRule, + UserRule, } from './domain/rules'; -import { AuthorizationHelper, AuthorizationService, RuleManager } from './domain'; import { FeathersAuthorizationService, FeathersAuthProvider } from './feathers'; @Module({ diff --git a/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts b/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts index e2e77212cab..11f553423b6 100644 --- a/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts @@ -1,24 +1,24 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardDoAuthorizableService } from '@modules/board'; +import { ContextExternalToolAuthorizableService } from '@modules/tool'; import { NotImplementedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId } from '@shared/domain'; import { CourseGroupRepo, CourseRepo, - LessonRepo, - SchoolExternalToolRepo, LegacySchoolRepo, + SchoolExternalToolRepo, SubmissionRepo, TaskRepo, TeamsRepo, UserRepo, } from '@shared/repo'; import { setupEntities, userFactory } from '@shared/testing'; -import { BoardDoAuthorizableService } from '@modules/board'; -import { ContextExternalToolAuthorizableService } from '@modules/tool/context-external-tool/service/context-external-tool-authorizable.service'; -import { ReferenceLoader } from './reference.loader'; +import { LessonService } from '@modules/lesson'; import { AuthorizableReferenceType } from '../type'; +import { ReferenceLoader } from './reference.loader'; describe('reference.loader', () => { let service: ReferenceLoader; @@ -27,7 +27,7 @@ describe('reference.loader', () => { let courseGroupRepo: DeepMocked; let taskRepo: DeepMocked; let schoolRepo: DeepMocked; - let lessonRepo: DeepMocked; + let lessonService: DeepMocked; let teamsRepo: DeepMocked; let submissionRepo: DeepMocked; let schoolExternalToolRepo: DeepMocked; @@ -62,8 +62,8 @@ describe('reference.loader', () => { useValue: createMock(), }, { - provide: LessonRepo, - useValue: createMock(), + provide: LessonService, + useValue: createMock(), }, { provide: TeamsRepo, @@ -94,7 +94,7 @@ describe('reference.loader', () => { courseGroupRepo = await module.get(CourseGroupRepo); taskRepo = await module.get(TaskRepo); schoolRepo = await module.get(LegacySchoolRepo); - lessonRepo = await module.get(LessonRepo); + lessonService = await module.get(LessonService); teamsRepo = await module.get(TeamsRepo); submissionRepo = await module.get(SubmissionRepo); schoolExternalToolRepo = await module.get(SchoolExternalToolRepo); @@ -144,7 +144,7 @@ describe('reference.loader', () => { it('should call lessonRepo.findById', async () => { await service.loadAuthorizableObject(AuthorizableReferenceType.Lesson, entityId); - expect(lessonRepo.findById).toBeCalledWith(entityId); + expect(lessonService.findById).toBeCalledWith(entityId); }); it('should call teamsRepo.findById', async () => { diff --git a/apps/server/src/modules/authorization/domain/service/reference.loader.ts b/apps/server/src/modules/authorization/domain/service/reference.loader.ts index d584561be9e..7fb574a4d6e 100644 --- a/apps/server/src/modules/authorization/domain/service/reference.loader.ts +++ b/apps/server/src/modules/authorization/domain/service/reference.loader.ts @@ -1,19 +1,20 @@ +import { BoardDoAuthorizableService } from '@modules/board'; + +import { LessonService } from '@modules/lesson'; +import { ContextExternalToolAuthorizableService } from '@modules/tool'; import { Injectable, NotImplementedException } from '@nestjs/common'; import { BaseDO, EntityId } from '@shared/domain'; import { AuthorizableObject } from '@shared/domain/domain-object'; import { CourseGroupRepo, CourseRepo, - LessonRepo, - SchoolExternalToolRepo, LegacySchoolRepo, + SchoolExternalToolRepo, SubmissionRepo, TaskRepo, TeamsRepo, UserRepo, } from '@shared/repo'; -import { BoardDoAuthorizableService } from '@modules/board'; -import { ContextExternalToolAuthorizableService } from '@modules/tool/context-external-tool/service'; import { AuthorizableReferenceType } from '../type'; type RepoType = @@ -21,22 +22,22 @@ type RepoType = | CourseRepo | UserRepo | LegacySchoolRepo - | LessonRepo | TeamsRepo | CourseGroupRepo | SubmissionRepo | SchoolExternalToolRepo | BoardDoAuthorizableService - | ContextExternalToolAuthorizableService; + | ContextExternalToolAuthorizableService + | LessonService; -interface IRepoLoader { +interface RepoLoader { repo: RepoType; populate?: boolean; } @Injectable() export class ReferenceLoader { - private repos: Map = new Map(); + private repos: Map = new Map(); constructor( private readonly userRepo: UserRepo, @@ -44,7 +45,7 @@ export class ReferenceLoader { private readonly courseGroupRepo: CourseGroupRepo, private readonly taskRepo: TaskRepo, private readonly schoolRepo: LegacySchoolRepo, - private readonly lessonRepo: LessonRepo, + private readonly lessonService: LessonService, private readonly teamsRepo: TeamsRepo, private readonly submissionRepo: SubmissionRepo, private readonly schoolExternalToolRepo: SchoolExternalToolRepo, @@ -56,7 +57,7 @@ export class ReferenceLoader { this.repos.set(AuthorizableReferenceType.CourseGroup, { repo: this.courseGroupRepo }); this.repos.set(AuthorizableReferenceType.User, { repo: this.userRepo }); this.repos.set(AuthorizableReferenceType.School, { repo: this.schoolRepo }); - this.repos.set(AuthorizableReferenceType.Lesson, { repo: this.lessonRepo }); + this.repos.set(AuthorizableReferenceType.Lesson, { repo: this.lessonService }); this.repos.set(AuthorizableReferenceType.Team, { repo: this.teamsRepo, populate: true }); this.repos.set(AuthorizableReferenceType.Submission, { repo: this.submissionRepo }); this.repos.set(AuthorizableReferenceType.SchoolExternalToolEntity, { repo: this.schoolExternalToolRepo }); @@ -66,7 +67,7 @@ export class ReferenceLoader { }); } - private resolveRepo(type: AuthorizableReferenceType): IRepoLoader { + private resolveRepo(type: AuthorizableReferenceType): RepoLoader { const repo = this.repos.get(type); if (repo) { return repo; @@ -78,7 +79,7 @@ export class ReferenceLoader { objectName: AuthorizableReferenceType, objectId: EntityId ): Promise { - const repoLoader: IRepoLoader = this.resolveRepo(objectName); + const repoLoader: RepoLoader = this.resolveRepo(objectName); let object: AuthorizableObject | BaseDO; if (repoLoader.populate) { diff --git a/apps/server/src/modules/authorization/index.ts b/apps/server/src/modules/authorization/index.ts index e129df2cd11..13ae209f13d 100644 --- a/apps/server/src/modules/authorization/index.ts +++ b/apps/server/src/modules/authorization/index.ts @@ -1,15 +1,17 @@ export { AuthorizationModule } from './authorization.module'; export { - AuthorizationService, - AuthorizationHelper, - AuthorizationContextBuilder, - ForbiddenLoggableException, - Rule, - AuthorizationContext, // Action should not be exported, but hard to solve for now. The AuthorizationContextBuilder is the prefared way Action, + AuthorizableReferenceType, + AuthorizationContext, + AuthorizationContextBuilder, + AuthorizationHelper, AuthorizationLoaderService, AuthorizationLoaderServiceGeneric, + AuthorizationReferenceService, + AuthorizationService, + ForbiddenLoggableException, + Rule, } from './domain'; // Should not used anymore export { FeathersAuthorizationService } from './feathers'; diff --git a/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts index e92bb645872..5d7633e4c99 100644 --- a/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -11,9 +14,6 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server'; import { Request } from 'express'; import request from 'supertest'; import { BoardResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts index 8423d68f793..c4f685b9a6a 100644 --- a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -12,9 +15,6 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; import { BoardResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts index 4b0f57c361d..de006926e8f 100644 --- a/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -11,9 +14,6 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; import { CardResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts index 3fff41b3660..d2b9a02efda 100644 --- a/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -13,9 +16,6 @@ import { richTextElementNodeFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts index 711ccf1ac8f..f43fb954e68 100644 --- a/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -15,9 +18,6 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; import { CardIdsParams, CardListResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts index e9e55b27dd2..fa67220072a 100644 --- a/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -12,9 +15,6 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts index 52aaa4608ca..dd63155d4a5 100644 --- a/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -10,9 +13,6 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; import { ColumnResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts index 86e3e666c84..8b7dc463f98 100644 --- a/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -12,9 +15,6 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts index 6fda90f739a..f467646766a 100644 --- a/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -12,9 +15,6 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts index 6b99a64e7ab..b19aa8ff11c 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -13,9 +16,6 @@ import { richTextElementNodeFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts index dbcd9acbc31..f913a547f59 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts @@ -1,4 +1,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; @@ -13,9 +16,6 @@ import { richTextElementNodeFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/board-submission.controller.ts b/apps/server/src/modules/board/controller/board-submission.controller.ts index 4e81d342849..bc6f7298668 100644 --- a/apps/server/src/modules/board/controller/board-submission.controller.ts +++ b/apps/server/src/modules/board/controller/board-submission.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, @@ -11,8 +12,6 @@ import { } from '@nestjs/common'; import { ApiExtraModels, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; -import { SubmissionsResponse } from './dto/submission-item/submissions.response'; import { CardUc } from '../uc'; import { ElementUc } from '../uc/element.uc'; import { SubmissionItemUc } from '../uc/submission-item.uc'; @@ -24,6 +23,7 @@ import { SubmissionItemUrlParams, UpdateSubmissionItemBodyParams, } from './dto'; +import { SubmissionsResponse } from './dto/submission-item/submissions.response'; import { ContentElementResponseFactory, SubmissionItemResponseMapper } from './mapper'; @ApiTags('Board Submission') diff --git a/apps/server/src/modules/board/controller/board.controller.ts b/apps/server/src/modules/board/controller/board.controller.ts index 0d77aa80b3d..55a54e8f77b 100644 --- a/apps/server/src/modules/board/controller/board.controller.ts +++ b/apps/server/src/modules/board/controller/board.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, @@ -12,7 +13,6 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { BoardUc } from '../uc'; import { BoardResponse, BoardUrlParams, ColumnResponse, RenameBodyParams } from './dto'; import { BoardContextResponse } from './dto/board/board-context.reponse'; diff --git a/apps/server/src/modules/board/controller/card.controller.ts b/apps/server/src/modules/board/controller/card.controller.ts index e75d7afc7a5..71a6d2ab0f9 100644 --- a/apps/server/src/modules/board/controller/card.controller.ts +++ b/apps/server/src/modules/board/controller/card.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, @@ -14,7 +15,6 @@ import { } from '@nestjs/common'; import { ApiExtraModels, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { CardUc, ColumnUc } from '../uc'; import { AnyContentElementResponse, diff --git a/apps/server/src/modules/board/controller/column.controller.ts b/apps/server/src/modules/board/controller/column.controller.ts index 870bcc5dc06..9a3da989159 100644 --- a/apps/server/src/modules/board/controller/column.controller.ts +++ b/apps/server/src/modules/board/controller/column.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, @@ -12,11 +13,10 @@ import { } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { BoardUc, ColumnUc } from '../uc'; import { CardResponse, ColumnUrlParams, MoveColumnBodyParams, RenameBodyParams } from './dto'; -import { CardResponseMapper } from './mapper'; import { CreateCardBodyParams } from './dto/card/create-card.body.params'; +import { CardResponseMapper } from './mapper'; @ApiTags('Board Column') @Authenticate('jwt') diff --git a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts index 23ce88b904c..7d0314208c6 100644 --- a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts @@ -2,7 +2,7 @@ import { ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger import { ContentElementType } from '@shared/domain'; import { InputFormat } from '@shared/domain/types'; import { Type } from 'class-transformer'; -import { IsDate, IsEnum, IsMongoId, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator'; +import { IsDate, IsEnum, IsMongoId, IsOptional, IsString, ValidateNested } from 'class-validator'; export abstract class ElementContentBody { @IsEnum(ContentElementType) @@ -34,7 +34,7 @@ export class FileElementContentBody extends ElementContentBody { } export class LinkContentBody { - @IsUrl() + @IsString() @ApiProperty({}) url!: string; diff --git a/apps/server/src/modules/board/controller/element.controller.ts b/apps/server/src/modules/board/controller/element.controller.ts index 2bed9006a0f..71ade95db66 100644 --- a/apps/server/src/modules/board/controller/element.controller.ts +++ b/apps/server/src/modules/board/controller/element.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, @@ -12,7 +13,6 @@ import { } from '@nestjs/common'; import { ApiBody, ApiExtraModels, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { CardUc } from '../uc'; import { ElementUc } from '../uc/element.uc'; import { diff --git a/apps/server/src/modules/class/entity/class.entity.ts b/apps/server/src/modules/class/entity/class.entity.ts index 3f08ba49582..e5b2dce908e 100644 --- a/apps/server/src/modules/class/entity/class.entity.ts +++ b/apps/server/src/modules/class/entity/class.entity.ts @@ -1,10 +1,10 @@ import { Embedded, Entity, Index, Property } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { EntityId } from '@shared/domain'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { ClassSourceOptionsEntity } from './class-source-options.entity'; -export interface IClassEntityProps { +export interface ClassEntityProps { id?: EntityId; name: string; schoolId: ObjectId; @@ -59,13 +59,13 @@ export class ClassEntity extends BaseEntityWithTimestamps { @Embedded(() => ClassSourceOptionsEntity, { object: true, nullable: true }) sourceOptions?: ClassSourceOptionsEntity; - private validate(props: IClassEntityProps) { + private validate(props: ClassEntityProps) { if (props.gradeLevel !== undefined && (props.gradeLevel < 1 || props.gradeLevel > 13)) { throw new Error('gradeLevel must be value beetween 1 and 13'); } } - constructor(props: IClassEntityProps) { + constructor(props: ClassEntityProps) { super(); this.validate(props); diff --git a/apps/server/src/modules/class/entity/testing/factory/class.entity.factory.ts b/apps/server/src/modules/class/entity/testing/factory/class.entity.factory.ts index b98d20853fc..d122a5933d6 100644 --- a/apps/server/src/modules/class/entity/testing/factory/class.entity.factory.ts +++ b/apps/server/src/modules/class/entity/testing/factory/class.entity.factory.ts @@ -1,11 +1,11 @@ -import { DeepPartial } from 'fishery'; +import { ClassEntity, ClassEntityProps, ClassSourceOptionsEntity } from '@modules/class/entity'; import { BaseFactory } from '@shared/testing/factory/base.factory'; -import { ClassEntity, ClassSourceOptionsEntity, IClassEntityProps } from '@modules/class/entity'; import { ObjectId } from 'bson'; +import { DeepPartial } from 'fishery'; -class ClassEntityFactory extends BaseFactory { +class ClassEntityFactory extends BaseFactory { withUserIds(userIds: ObjectId[]): this { - const params: DeepPartial = { + const params: DeepPartial = { userIds, }; diff --git a/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.spec.ts b/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.spec.ts index 4622fed4c00..77b958f9c7a 100644 --- a/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.spec.ts +++ b/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.spec.ts @@ -1,8 +1,8 @@ -import { CollaborativeStorageController } from '@modules/collaborative-storage/controller/collaborative-storage.controller'; -import { Test, TestingModule } from '@nestjs/testing'; -import { CollaborativeStorageUc } from '@modules/collaborative-storage/uc/collaborative-storage.uc'; import { createMock } from '@golevelup/ts-jest'; import { ICurrentUser } from '@modules/authentication'; +import { CollaborativeStorageController } from '@modules/collaborative-storage/controller/collaborative-storage.controller'; +import { CollaborativeStorageUc } from '@modules/collaborative-storage/uc/collaborative-storage.uc'; +import { Test, TestingModule } from '@nestjs/testing'; import { LegacyLogger } from '@src/core/logger'; describe('CollaborativeStorage Controller', () => { diff --git a/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.ts b/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.ts index 19fbccbed1c..4e11b3c46bd 100644 --- a/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.ts +++ b/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.ts @@ -1,6 +1,6 @@ -import { ApiResponse, ApiTags } from '@nestjs/swagger'; -import { Body, Controller, Param, Patch } from '@nestjs/common'; import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { Body, Controller, Param, Patch } from '@nestjs/common'; +import { ApiResponse, ApiTags } from '@nestjs/swagger'; import { LegacyLogger } from '@src/core/logger'; import { CollaborativeStorageUc } from '../uc/collaborative-storage.uc'; import { TeamPermissionsBody } from './dto/team-permissions.body.params'; diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts new file mode 100644 index 00000000000..bd49bb841e6 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts @@ -0,0 +1,33 @@ +import { ObjectId } from 'bson'; +import { DeletionRequestInput } from '../interface'; +import { DeletionRequestInputBuilder } from './deletion-request-input.builder'; + +describe(DeletionRequestInputBuilder.name, () => { + describe(DeletionRequestInputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const targetRefDomain = 'school'; + const targetRefId = new ObjectId().toHexString(); + const deleteInMinutes = 43200; + + const expectedOutput: DeletionRequestInput = { + targetRef: { + domain: targetRefDomain, + id: targetRefId, + }, + deleteInMinutes, + }; + + return { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput } = setup(); + + const output = DeletionRequestInputBuilder.build(targetRefDomain, targetRefId, deleteInMinutes); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts new file mode 100644 index 00000000000..28091418065 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts @@ -0,0 +1,11 @@ +import { DeletionRequestInput } from '../interface'; +import { DeletionRequestTargetRefInputBuilder } from './deletion-request-target-ref-input.builder'; + +export class DeletionRequestInputBuilder { + static build(targetRefDomain: string, targetRefId: string, deleteInMinutes?: number): DeletionRequestInput { + return { + targetRef: DeletionRequestTargetRefInputBuilder.build(targetRefDomain, targetRefId), + deleteInMinutes, + }; + } +} diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts new file mode 100644 index 00000000000..399821f33ff --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts @@ -0,0 +1,29 @@ +import { ObjectId } from 'bson'; +import { DeletionRequestOutput } from '../interface'; +import { DeletionRequestOutputBuilder } from './deletion-request-output.builder'; + +describe(DeletionRequestOutputBuilder.name, () => { + describe(DeletionRequestOutputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const requestId = new ObjectId().toHexString(); + const deletionPlannedAt = new Date(); + + const expectedOutput: DeletionRequestOutput = { + requestId, + deletionPlannedAt, + }; + + return { requestId, deletionPlannedAt, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { requestId, deletionPlannedAt, expectedOutput } = setup(); + + const output = DeletionRequestOutputBuilder.build(requestId, deletionPlannedAt); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts new file mode 100644 index 00000000000..9192c1a47ce --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts @@ -0,0 +1,10 @@ +import { DeletionRequestOutput } from '../interface'; + +export class DeletionRequestOutputBuilder { + static build(requestId: string, deletionPlannedAt: Date): DeletionRequestOutput { + return { + requestId, + deletionPlannedAt, + }; + } +} diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts new file mode 100644 index 00000000000..74b0631e49d --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts @@ -0,0 +1,26 @@ +import { ObjectId } from 'bson'; +import { DeletionRequestTargetRefInput } from '../interface'; +import { DeletionRequestTargetRefInputBuilder } from './deletion-request-target-ref-input.builder'; + +describe(DeletionRequestTargetRefInputBuilder.name, () => { + describe(DeletionRequestTargetRefInputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const domain = 'user'; + const id = new ObjectId().toHexString(); + + const expectedOutput: DeletionRequestTargetRefInput = { domain, id }; + + return { domain, id, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { domain, id, expectedOutput } = setup(); + + const output = DeletionRequestTargetRefInputBuilder.build(domain, id); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts new file mode 100644 index 00000000000..ed3c0219993 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts @@ -0,0 +1,7 @@ +import { DeletionRequestTargetRefInput } from '../interface'; + +export class DeletionRequestTargetRefInputBuilder { + static build(domain: string, id: string): DeletionRequestTargetRefInput { + return { domain, id }; + } +} diff --git a/apps/server/src/modules/deletion/client/builder/index.ts b/apps/server/src/modules/deletion/client/builder/index.ts new file mode 100644 index 00000000000..85644a6b2ee --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/index.ts @@ -0,0 +1,3 @@ +export * from './deletion-request-target-ref-input.builder'; +export * from './deletion-request-input.builder'; +export * from './deletion-request-output.builder'; diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.spec.ts b/apps/server/src/modules/deletion/client/deletion-client.config.spec.ts new file mode 100644 index 00000000000..a3cae21e425 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion-client.config.spec.ts @@ -0,0 +1,41 @@ +import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DeletionClientConfig } from './interface'; +import { getDeletionClientConfig } from './deletion-client.config'; + +describe(getDeletionClientConfig.name, () => { + let configBefore: IConfig; + + beforeAll(() => { + configBefore = Configuration.toObject({ plainSecrets: true }); + }); + + afterEach(() => { + Configuration.reset(configBefore); + }); + + describe('when called', () => { + const setup = () => { + const baseUrl = 'http://api-admin:4030'; + const apiKey = '652559c2-93da-42ad-94e1-640e3afbaca0'; + + Configuration.set('ADMIN_API_CLIENT__BASE_URL', baseUrl); + Configuration.set('ADMIN_API_CLIENT__API_KEY', apiKey); + + const expectedConfig: DeletionClientConfig = { + ADMIN_API_CLIENT_BASE_URL: baseUrl, + ADMIN_API_CLIENT_API_KEY: apiKey, + }; + + return { expectedConfig }; + }; + + it('should return config with proper values', () => { + const { expectedConfig } = setup(); + + const config = getDeletionClientConfig(); + + expect(config).toEqual(expectedConfig); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.ts b/apps/server/src/modules/deletion/client/deletion-client.config.ts new file mode 100644 index 00000000000..db5bf7ff226 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion-client.config.ts @@ -0,0 +1,9 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DeletionClientConfig } from './interface'; + +export const getDeletionClientConfig = (): DeletionClientConfig => { + return { + ADMIN_API_CLIENT_BASE_URL: Configuration.get('ADMIN_API_CLIENT__BASE_URL') as string, + ADMIN_API_CLIENT_API_KEY: Configuration.get('ADMIN_API_CLIENT__API_KEY') as string, + }; +}; diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts new file mode 100644 index 00000000000..096b1f9b082 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -0,0 +1,154 @@ +import { of } from 'rxjs'; +import { AxiosResponse } from 'axios'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { axiosResponseFactory } from '@shared/testing'; +import { DeletionRequestInputBuilder, DeletionRequestOutputBuilder } from '.'; +import { DeletionRequestOutput } from './interface'; +import { DeletionClient } from './deletion.client'; + +describe(DeletionClient.name, () => { + let module: TestingModule; + let client: DeletionClient; + let httpService: DeepMocked; + + beforeEach(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionClient, + { + provide: ConfigService, + useValue: createMock({ + get: jest.fn((key: string) => { + if (key === 'ADMIN_API_CLIENT_BASE_URL') { + return 'http://localhost:4030'; + } + + // Default is for the Admin APIs API Key. + return '6b3df003-61e9-467c-9e6b-579634801896'; + }), + }), + }, + { + provide: HttpService, + useValue: createMock(), + }, + ], + }).compile(); + + client = module.get(DeletionClient); + httpService = module.get(HttpService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('queueDeletionRequest', () => { + describe('when received valid response with expected HTTP status code', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const output: DeletionRequestOutput = DeletionRequestOutputBuilder.build( + '6536ce29b595d7c8e5faf200', + new Date('2024-10-15T12:42:50.521Z') + ); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: output, + status: 202, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input, output }; + }; + + it('should return proper output', async () => { + const { input, output } = setup(); + + const result = await client.queueDeletionRequest(input); + + expect(result).toEqual(output); + }); + }); + + describe('when received invalid HTTP status code in a response', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const output: DeletionRequestOutput = DeletionRequestOutputBuilder.build('', new Date()); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: output, + status: 200, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input }; + }; + + it('should throw an exception', async () => { + const { input } = setup(); + + await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); + }); + }); + + describe('when received no requestId in a response', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const output: DeletionRequestOutput = DeletionRequestOutputBuilder.build( + '', + new Date('2024-10-15T12:42:50.521Z') + ); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: output, + status: 202, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input }; + }; + + it('should throw an exception', async () => { + const { input } = setup(); + + await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); + }); + }); + + describe('when received no deletionPlannedAt in a response', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: { + requestId: '6536ce29b595d7c8e5faf200', + }, + status: 202, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input }; + }; + + it('should throw an exception', async () => { + const { input } = setup(); + + await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/deletion.client.ts b/apps/server/src/modules/deletion/client/deletion.client.ts new file mode 100644 index 00000000000..66bb267d070 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion.client.ts @@ -0,0 +1,66 @@ +import { firstValueFrom } from 'rxjs'; +import { AxiosResponse } from 'axios'; +import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { DeletionRequestInput, DeletionRequestOutput, DeletionClientConfig } from './interface'; + +@Injectable() +export class DeletionClient { + private readonly baseUrl: string; + + private readonly apiKey: string; + + private readonly postDeletionRequestsEndpoint: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService + ) { + this.baseUrl = this.configService.get('ADMIN_API_CLIENT_BASE_URL'); + this.apiKey = this.configService.get('ADMIN_API_CLIENT_API_KEY'); + + // Prepare the POST /deletionRequests endpoint beforehand to not do it on every client call. + this.postDeletionRequestsEndpoint = new URL('/admin/api/v1/deletionRequests', this.baseUrl).toString(); + } + + async queueDeletionRequest(input: DeletionRequestInput): Promise { + const request = this.httpService.post(this.postDeletionRequestsEndpoint, input, this.defaultHeaders()); + + return firstValueFrom(request) + .then((resp: AxiosResponse) => { + // Throw an error if any other status code (other than expected "202 Accepted" is returned). + if (resp.status !== 202) { + throw new Error(`invalid HTTP status code in a response from the server - ${resp.status} instead of 202`); + } + + // Throw an error if server didn't return a requestId in a response (and it is + // required as it gives client the reference to the created deletion request). + if (!resp.data.requestId) { + throw new Error('no valid requestId returned from the server'); + } + + // Throw an error if server didn't return a deletionPlannedAt timestamp so the user + // will not be aware after which date the deletion request's execution will begin. + if (!resp.data.deletionPlannedAt) { + throw new Error('no valid deletionPlannedAt returned from the server'); + } + + return resp.data; + }) + .catch((err: Error) => { + // Throw an error if sending/processing deletion request by the client failed in any way. + throw new Error(`failed to send/process a deletion request: ${err.toString()}`); + }); + } + + private apiKeyHeader() { + return { 'X-Api-Key': this.apiKey }; + } + + private defaultHeaders() { + return { + headers: this.apiKeyHeader(), + }; + } +} diff --git a/apps/server/src/modules/deletion/client/index.ts b/apps/server/src/modules/deletion/client/index.ts new file mode 100644 index 00000000000..fde3db98f3b --- /dev/null +++ b/apps/server/src/modules/deletion/client/index.ts @@ -0,0 +1,3 @@ +export * from './interface'; +export * from './builder'; +export * from './deletion.client'; diff --git a/apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts new file mode 100644 index 00000000000..8178515b6d4 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts @@ -0,0 +1,4 @@ +export interface DeletionClientConfig { + ADMIN_API_CLIENT_BASE_URL: string; + ADMIN_API_CLIENT_API_KEY: string; +} diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts new file mode 100644 index 00000000000..4879ce4d972 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts @@ -0,0 +1,6 @@ +import { DeletionRequestTargetRefInput } from './deletion-request-target-ref-input.interface'; + +export interface DeletionRequestInput { + targetRef: DeletionRequestTargetRefInput; + deleteInMinutes?: number; +} diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts new file mode 100644 index 00000000000..b61a372d5a5 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts @@ -0,0 +1,4 @@ +export interface DeletionRequestOutput { + requestId: string; + deletionPlannedAt: Date; +} diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts new file mode 100644 index 00000000000..603bb0c13ec --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts @@ -0,0 +1,4 @@ +export interface DeletionRequestTargetRefInput { + domain: string; + id: string; +} diff --git a/apps/server/src/modules/deletion/client/interface/index.ts b/apps/server/src/modules/deletion/client/interface/index.ts new file mode 100644 index 00000000000..38f0f639731 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/index.ts @@ -0,0 +1,4 @@ +export * from './deletion-client-config.interface'; +export * from './deletion-request-target-ref-input.interface'; +export * from './deletion-request-input.interface'; +export * from './deletion-request-output.interface'; diff --git a/apps/server/src/modules/deletion/console/builder/index.ts b/apps/server/src/modules/deletion/console/builder/index.ts new file mode 100644 index 00000000000..12fd0997ebe --- /dev/null +++ b/apps/server/src/modules/deletion/console/builder/index.ts @@ -0,0 +1 @@ +export * from './push-delete-requests-options.builder'; diff --git a/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts new file mode 100644 index 00000000000..5c83defdd1e --- /dev/null +++ b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts @@ -0,0 +1,43 @@ +import { PushDeletionRequestsOptions } from '../interface'; +import { PushDeleteRequestsOptionsBuilder } from './push-delete-requests-options.builder'; + +describe(PushDeleteRequestsOptionsBuilder.name, () => { + describe(PushDeleteRequestsOptionsBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const refsFilePath = '/tmp/ids.txt'; + const targetRefDomain = 'school'; + const deleteInMinutes = 43200; + const callsDelayMs = 100; + + const expectedOutput: PushDeletionRequestsOptions = { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + }; + + return { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + expectedOutput, + }; + }; + + it('should return valid object with expected values', () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, callsDelayMs, expectedOutput } = setup(); + + const output = PushDeleteRequestsOptionsBuilder.build( + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs + ); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts new file mode 100644 index 00000000000..f8ceae9263d --- /dev/null +++ b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts @@ -0,0 +1,17 @@ +import { PushDeletionRequestsOptions } from '../interface'; + +export class PushDeleteRequestsOptionsBuilder { + static build( + refsFilePath: string, + targetRefDomain: string, + deleteInMinutes: number, + callsDelayMs: number + ): PushDeletionRequestsOptions { + return { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + }; + } +} diff --git a/apps/server/src/modules/deletion/console/deletion-console.module.ts b/apps/server/src/modules/deletion/console/deletion-console.module.ts new file mode 100644 index 00000000000..0585b3631da --- /dev/null +++ b/apps/server/src/modules/deletion/console/deletion-console.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; +import { ConsoleModule } from 'nestjs-console'; +import { ConsoleWriterModule } from '@infra/console'; +import { createConfigModuleOptions } from '@src/config'; +import { DeletionClient } from '../client'; +import { getDeletionClientConfig } from '../client/deletion-client.config'; +import { BatchDeletionService } from '../services'; +import { BatchDeletionUc } from '../uc'; +import { DeletionQueueConsole } from './deletion-queue.console'; + +@Module({ + imports: [ + ConsoleModule, + ConsoleWriterModule, + HttpModule, + ConfigModule.forRoot(createConfigModuleOptions(getDeletionClientConfig)), + ], + providers: [DeletionClient, BatchDeletionService, BatchDeletionUc, DeletionQueueConsole], +}) +export class DeletionConsoleModule {} diff --git a/apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts b/apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts new file mode 100644 index 00000000000..61f9cf0ff53 --- /dev/null +++ b/apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts @@ -0,0 +1,79 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConsoleWriterService } from '@infra/console'; +import { createMock } from '@golevelup/ts-jest'; +import { BatchDeletionUc } from '../uc'; +import { DeletionQueueConsole } from './deletion-queue.console'; +import { PushDeleteRequestsOptionsBuilder } from './builder'; + +describe(DeletionQueueConsole.name, () => { + let module: TestingModule; + let console: DeletionQueueConsole; + let batchDeletionUc: BatchDeletionUc; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionQueueConsole, + { + provide: ConsoleWriterService, + useValue: createMock(), + }, + { + provide: BatchDeletionUc, + useValue: createMock(), + }, + ], + }).compile(); + + console = module.get(DeletionQueueConsole); + batchDeletionUc = module.get(BatchDeletionUc); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('console should be defined', () => { + expect(console).toBeDefined(); + }); + + describe('pushDeletionRequests', () => { + describe('when called with valid options', () => { + const setup = () => { + const refsFilePath = '/tmp/ids.txt'; + const targetRefDomain = 'school'; + const deleteInMinutes = 43200; + const callsDelayMs = 100; + + const options = PushDeleteRequestsOptionsBuilder.build( + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs + ); + + return { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + options, + }; + }; + + it(`should call ${BatchDeletionUc.name} with proper arguments`, async () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, callsDelayMs, options } = setup(); + + const spy = jest.spyOn(batchDeletionUc, 'deleteRefsFromTxtFile'); + + await console.pushDeletionRequests(options); + + expect(spy).toBeCalledWith(refsFilePath, targetRefDomain, deleteInMinutes, callsDelayMs); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/console/deletion-queue.console.ts b/apps/server/src/modules/deletion/console/deletion-queue.console.ts new file mode 100644 index 00000000000..c8b133dba84 --- /dev/null +++ b/apps/server/src/modules/deletion/console/deletion-queue.console.ts @@ -0,0 +1,46 @@ +import { Console, Command } from 'nestjs-console'; +import { ConsoleWriterService } from '@infra/console'; +import { BatchDeletionUc } from '../uc'; +import { PushDeletionRequestsOptions } from './interface'; + +@Console({ command: 'queue', description: 'Console providing an access to the deletion queue.' }) +export class DeletionQueueConsole { + constructor(private consoleWriter: ConsoleWriterService, private batchDeletionUc: BatchDeletionUc) {} + + @Command({ + command: 'push', + description: 'Push new deletion requests to the deletion queue.', + options: [ + { + flags: '-rfp, --refsFilePath ', + description: 'Path of the file containing all the references to the data that should be deleted.', + required: true, + }, + { + flags: '-trd, --targetRefDomain ', + description: 'Name of the target ref domain.', + required: false, + }, + { + flags: '-dim, --deleteInMinutes ', + description: 'Number of minutes after which the data deletion process should begin.', + required: false, + }, + { + flags: '-cdm, --callsDelayMs ', + description: 'Delay between all the performed client calls, in milliseconds.', + required: false, + }, + ], + }) + async pushDeletionRequests(options: PushDeletionRequestsOptions): Promise { + const summary = await this.batchDeletionUc.deleteRefsFromTxtFile( + options.refsFilePath, + options.targetRefDomain, + options.deleteInMinutes ? Number(options.deleteInMinutes) : undefined, + options.callsDelayMs ? Number(options.callsDelayMs) : undefined + ); + + this.consoleWriter.info(JSON.stringify(summary)); + } +} diff --git a/apps/server/src/modules/deletion/console/index.ts b/apps/server/src/modules/deletion/console/index.ts new file mode 100644 index 00000000000..db47bd8c99f --- /dev/null +++ b/apps/server/src/modules/deletion/console/index.ts @@ -0,0 +1 @@ +export * from './deletion-console.module'; diff --git a/apps/server/src/modules/deletion/console/interface/index.ts b/apps/server/src/modules/deletion/console/interface/index.ts new file mode 100644 index 00000000000..2fcb281430f --- /dev/null +++ b/apps/server/src/modules/deletion/console/interface/index.ts @@ -0,0 +1 @@ +export * from './push-delete-requests-options.interface'; diff --git a/apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts b/apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts new file mode 100644 index 00000000000..a54f652cf94 --- /dev/null +++ b/apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts @@ -0,0 +1,6 @@ +export interface PushDeletionRequestsOptions { + refsFilePath: string; + targetRefDomain: string; + deleteInMinutes: number; + callsDelayMs: number; +} diff --git a/apps/server/src/modules/deletion/index.ts b/apps/server/src/modules/deletion/index.ts index bd89c1e8d84..793f306e7a0 100644 --- a/apps/server/src/modules/deletion/index.ts +++ b/apps/server/src/modules/deletion/index.ts @@ -1,2 +1,4 @@ export * from './deletion.module'; export * from './services'; +export * from './client'; +export * from './console'; diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts new file mode 100644 index 00000000000..640e290af1a --- /dev/null +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts @@ -0,0 +1,97 @@ +import { ObjectId } from 'bson'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DeletionClient, DeletionRequestOutput, DeletionRequestOutputBuilder } from '../client'; +import { QueueDeletionRequestInputBuilder, QueueDeletionRequestOutputBuilder } from './builder'; +import { BatchDeletionService } from './batch-deletion.service'; + +describe(BatchDeletionService.name, () => { + let module: TestingModule; + let service: BatchDeletionService; + let deletionClient: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BatchDeletionService, + { + provide: DeletionClient, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BatchDeletionService); + deletionClient = module.get(DeletionClient); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('service should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('queueDeletionRequests', () => { + describe('when called with valid inputs array and a requested delay between the client calls', () => { + describe("when client doesn't throw any error", () => { + const setup = () => { + const inputs = [QueueDeletionRequestInputBuilder.build('user', new ObjectId().toHexString(), 60)]; + + const requestId = new ObjectId().toHexString(); + const deletionPlannedAt = new Date(); + + const queueDeletionRequestOutput: DeletionRequestOutput = DeletionRequestOutputBuilder.build( + requestId, + deletionPlannedAt + ); + + deletionClient.queueDeletionRequest.mockResolvedValueOnce(queueDeletionRequestOutput); + + const expectedOutput = QueueDeletionRequestOutputBuilder.buildSuccess(requestId, deletionPlannedAt); + + const expectedOutputs = [expectedOutput]; + + return { inputs, expectedOutputs }; + }; + + it('should return an output object with successful status info', async () => { + const { inputs, expectedOutputs } = setup(); + + const outputs = await service.queueDeletionRequests(inputs, 1); + + expect(outputs).toStrictEqual(expectedOutputs); + }); + }); + + describe('when client throws an error', () => { + const setup = () => { + const inputs = [QueueDeletionRequestInputBuilder.build('user', new ObjectId().toHexString(), 60)]; + + const error = new Error('connection error'); + + deletionClient.queueDeletionRequest.mockRejectedValueOnce(error); + + const expectedOutput = QueueDeletionRequestOutputBuilder.buildError(error); + + const expectedOutputs = [expectedOutput]; + + return { inputs, expectedOutputs }; + }; + + it('should return an output object with failure status info', async () => { + const { inputs, expectedOutputs } = setup(); + + const outputs = await service.queueDeletionRequests(inputs); + + expect(outputs).toStrictEqual(expectedOutputs); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.ts new file mode 100644 index 00000000000..d8c14f315a0 --- /dev/null +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { QueueDeletionRequestOutputBuilder } from './builder'; +import { DeletionClient, DeletionRequestInputBuilder } from '../client'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from './interface'; + +@Injectable() +export class BatchDeletionService { + constructor(private readonly deletionClient: DeletionClient) {} + + async queueDeletionRequests( + inputs: QueueDeletionRequestInput[], + callsDelayMilliseconds?: number + ): Promise { + const outputs: QueueDeletionRequestOutput[] = []; + + // For every provided deletion request input, try to queue it via deletion client. + // In any case, add the result of the trial to the outputs - it will be either a valid + // response in a form of a requestId + deletionPlannedAt values pair or some error + // returned from the client. In any case, every input should be processed. + for (const input of inputs) { + const deletionRequestInput = DeletionRequestInputBuilder.build( + input.targetRefDomain, + input.targetRefId, + input.deleteInMinutes + ); + + try { + // eslint-disable-next-line no-await-in-loop + const deletionRequestOutput = await this.deletionClient.queueDeletionRequest(deletionRequestInput); + + // In case of a successful client response, add the + // requestId + deletionPlannedAt values pair to the outputs. + outputs.push( + QueueDeletionRequestOutputBuilder.buildSuccess( + deletionRequestOutput.requestId, + deletionRequestOutput.deletionPlannedAt + ) + ); + } catch (err) { + // In case of a failure client response, add the full error message to the outputs. + outputs.push(QueueDeletionRequestOutputBuilder.buildError(err as Error)); + } + + // If any delay between the client calls has been requested, "sleep" for the specified amount of time. + if (callsDelayMilliseconds && callsDelayMilliseconds > 0) { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, callsDelayMilliseconds); + }); + } + } + + return outputs; + } +} diff --git a/apps/server/src/modules/deletion/services/builder/index.ts b/apps/server/src/modules/deletion/services/builder/index.ts new file mode 100644 index 00000000000..acd85a37989 --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/index.ts @@ -0,0 +1,2 @@ +export * from './queue-deletion-request-input.builder'; +export * from './queue-deletion-request-output.builder'; diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts new file mode 100644 index 00000000000..e5d87858156 --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts @@ -0,0 +1,27 @@ +import { ObjectId } from 'bson'; +import { QueueDeletionRequestInput } from '../interface'; +import { QueueDeletionRequestInputBuilder } from './queue-deletion-request-input.builder'; + +describe(QueueDeletionRequestInputBuilder.name, () => { + describe(QueueDeletionRequestInputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const targetRefDomain = 'user'; + const targetRefId = new ObjectId().toHexString(); + const deleteInMinutes = 60; + + const expectedOutput: QueueDeletionRequestInput = { targetRefDomain, targetRefId, deleteInMinutes }; + + return { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput } = setup(); + + const output = QueueDeletionRequestInputBuilder.build(targetRefDomain, targetRefId, deleteInMinutes); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts new file mode 100644 index 00000000000..a7fff2152b9 --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts @@ -0,0 +1,7 @@ +import { QueueDeletionRequestInput } from '../interface'; + +export class QueueDeletionRequestInputBuilder { + static build(targetRefDomain: string, targetRefId: string, deleteInMinutes: number): QueueDeletionRequestInput { + return { targetRefDomain, targetRefId, deleteInMinutes }; + } +} diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts new file mode 100644 index 00000000000..cd835a9cf4a --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts @@ -0,0 +1,46 @@ +import { ObjectId } from 'bson'; +import { QueueDeletionRequestOutput } from '../interface'; +import { QueueDeletionRequestOutputBuilder } from './queue-deletion-request-output.builder'; + +describe(QueueDeletionRequestOutputBuilder.name, () => { + describe(QueueDeletionRequestOutputBuilder.buildSuccess.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const requestId = new ObjectId().toHexString(); + const deletionPlannedAt = new Date(); + + const expectedOutput: QueueDeletionRequestOutput = { requestId, deletionPlannedAt }; + + return { requestId, deletionPlannedAt, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { requestId, deletionPlannedAt, expectedOutput } = setup(); + + const output = QueueDeletionRequestOutputBuilder.buildSuccess(requestId, deletionPlannedAt); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); + + describe(QueueDeletionRequestOutputBuilder.buildError.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const error = new Error('test error message'); + + const expectedOutput: QueueDeletionRequestOutput = { error: error.toString() }; + + return { error, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { error, expectedOutput } = setup(); + + const output = QueueDeletionRequestOutputBuilder.buildError(error); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts new file mode 100644 index 00000000000..c44e503cbaf --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts @@ -0,0 +1,29 @@ +import { QueueDeletionRequestOutput } from '../interface'; + +export class QueueDeletionRequestOutputBuilder { + private static build(requestId?: string, deletionPlannedAt?: Date, error?: string): QueueDeletionRequestOutput { + const output: QueueDeletionRequestOutput = {}; + + if (requestId) { + output.requestId = requestId; + } + + if (deletionPlannedAt) { + output.deletionPlannedAt = deletionPlannedAt; + } + + if (error) { + output.error = error.toString(); + } + + return output; + } + + static buildSuccess(requestId: string, deletionPlannedAt: Date): QueueDeletionRequestOutput { + return this.build(requestId, deletionPlannedAt); + } + + static buildError(err: Error): QueueDeletionRequestOutput { + return this.build(undefined, undefined, err.toString()); + } +} diff --git a/apps/server/src/modules/deletion/services/index.ts b/apps/server/src/modules/deletion/services/index.ts index 9661354718c..52e0b6ba22d 100644 --- a/apps/server/src/modules/deletion/services/index.ts +++ b/apps/server/src/modules/deletion/services/index.ts @@ -1 +1,5 @@ +export * from './interface'; +export * from './builder'; +export * from './references.service'; +export * from './batch-deletion.service'; export * from './deletion-request.service'; diff --git a/apps/server/src/modules/deletion/services/interface/index.ts b/apps/server/src/modules/deletion/services/interface/index.ts new file mode 100644 index 00000000000..8a455440798 --- /dev/null +++ b/apps/server/src/modules/deletion/services/interface/index.ts @@ -0,0 +1,2 @@ +export * from './queue-deletion-request-input.interface'; +export * from './queue-deletion-request-output.interface'; diff --git a/apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts new file mode 100644 index 00000000000..b421943bce9 --- /dev/null +++ b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts @@ -0,0 +1,5 @@ +export interface QueueDeletionRequestInput { + targetRefDomain: string; + targetRefId: string; + deleteInMinutes: number; +} diff --git a/apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts new file mode 100644 index 00000000000..375ff811857 --- /dev/null +++ b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts @@ -0,0 +1,5 @@ +export interface QueueDeletionRequestOutput { + requestId?: string; + deletionPlannedAt?: Date; + error?: string; +} diff --git a/apps/server/src/modules/deletion/services/references.service.spec.ts b/apps/server/src/modules/deletion/services/references.service.spec.ts new file mode 100644 index 00000000000..26eb6e0c8d7 --- /dev/null +++ b/apps/server/src/modules/deletion/services/references.service.spec.ts @@ -0,0 +1,74 @@ +import fs from 'fs'; +import { ReferencesService } from './references.service'; + +describe(ReferencesService.name, () => { + describe(ReferencesService.loadFromTxtFile.name, () => { + const setup = (mockedFileContent: string) => { + jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockedFileContent); + }; + + describe('when passed a completely empty file (without any content)', () => { + it('should return an empty references array', () => { + setup(''); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([]); + }); + }); + + describe('when passed a file without any references (just some empty lines)', () => { + it('should return an empty references array', () => { + setup('\n\n \n \n\n\n \n\n\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([]); + }); + }); + + describe('when passed a file with 3 references on a few separate lines', () => { + describe('split with CRs', () => { + it('should return an array with all the references present in a file', () => { + setup('653fd3b784ca851b17e98579\r653fd3b784ca851b17e9857a\r653fd3b784ca851b17e9857b\n\n\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([ + '653fd3b784ca851b17e98579', + '653fd3b784ca851b17e9857a', + '653fd3b784ca851b17e9857b', + ]); + }); + }); + + describe('split with LFs', () => { + it('should return an array with all the references present in a file', () => { + setup('653fd3b784ca851b17e98579\n653fd3b784ca851b17e9857a\n653fd3b784ca851b17e9857b\n\n\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([ + '653fd3b784ca851b17e98579', + '653fd3b784ca851b17e9857a', + '653fd3b784ca851b17e9857b', + ]); + }); + }); + + describe('split with CRLFs', () => { + it('should return an array with all the references present in a file', () => { + setup('653fd3b784ca851b17e98579\r\n653fd3b784ca851b17e9857a\r\n653fd3b784ca851b17e9857b\r\n\r\n\r\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([ + '653fd3b784ca851b17e98579', + '653fd3b784ca851b17e9857a', + '653fd3b784ca851b17e9857b', + ]); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/references.service.ts b/apps/server/src/modules/deletion/services/references.service.ts new file mode 100644 index 00000000000..991c36c02af --- /dev/null +++ b/apps/server/src/modules/deletion/services/references.service.ts @@ -0,0 +1,27 @@ +import fs from 'fs'; + +export class ReferencesService { + static loadFromTxtFile(filePath: string): string[] { + let fileContent = fs.readFileSync(filePath).toString(); + + // Replace all the CRLF occurrences with just a LF. + fileContent = fileContent.replace(/\r\n?/g, '\n'); + + // Split the whole file content by a line feed (LF) char (\n). + const fileLines = fileContent.split('\n'); + + const references: string[] = []; + + // Iterate over all the file lines and if it contains a valid id (which is + // basically any non-empty string), add it to the loaded references array. + fileLines.forEach((fileLine) => { + const reference = fileLine.trim(); + + if (reference && reference.length > 0) { + references.push(reference); + } + }); + + return references; + } +} diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts new file mode 100644 index 00000000000..7292f36efb0 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts @@ -0,0 +1,195 @@ +import { ObjectId } from 'bson'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { + BatchDeletionService, + QueueDeletionRequestInputBuilder, + QueueDeletionRequestOutput, + QueueDeletionRequestOutputBuilder, + ReferencesService, +} from '../services'; +import { BatchDeletionSummaryDetail, BatchDeletionSummaryOverallStatus } from './interface'; +import { BatchDeletionSummaryDetailBuilder } from './builder'; +import { BatchDeletionUc } from './batch-deletion.uc'; + +describe(BatchDeletionUc.name, () => { + let module: TestingModule; + let uc: BatchDeletionUc; + let batchDeletionService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BatchDeletionUc, + { + provide: BatchDeletionService, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(BatchDeletionUc); + batchDeletionService = module.get(BatchDeletionService); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('uc should be defined', () => { + expect(uc).toBeDefined(); + }); + + describe('deleteRefsFromTxtFile', () => { + describe('when called with valid arguments', () => { + describe('when batch deletion service returns an expected amount of outputs', () => { + describe('when only successful executions took place', () => { + const setup = () => { + const targetRefsCount = 3; + + const targetRefIds: string[] = []; + const outputs: QueueDeletionRequestOutput[] = []; + + for (let i = 0; i < targetRefsCount; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + outputs.push(QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date())); + } + + ReferencesService.loadFromTxtFile = jest.fn().mockReturnValueOnce(targetRefIds); + + batchDeletionService.queueDeletionRequests.mockResolvedValueOnce([outputs[0], outputs[1], outputs[2]]); + + const targetRefDomain = 'school'; + const deleteInMinutes = 60; + + const expectedSummaryFieldsDetails: BatchDeletionSummaryDetail[] = []; + + for (let i = 0; i < targetRefIds.length; i += 1) { + expectedSummaryFieldsDetails.push( + BatchDeletionSummaryDetailBuilder.build( + QueueDeletionRequestInputBuilder.build(targetRefDomain, targetRefIds[i], deleteInMinutes), + outputs[i] + ) + ); + } + + const expectedSummaryFields = { + overallStatus: BatchDeletionSummaryOverallStatus.SUCCESS, + successCount: 3, + failureCount: 0, + details: expectedSummaryFieldsDetails, + }; + + const refsFilePath = '/tmp/ids.txt'; + + return { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields }; + }; + + it('should return proper summary with all the successes and a successful overall status', async () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields } = setup(); + + const summary = await uc.deleteRefsFromTxtFile(refsFilePath, targetRefDomain, deleteInMinutes); + + expect(summary.executionTimeMilliseconds).toBeGreaterThan(0); + expect(summary).toMatchObject(expectedSummaryFields); + }); + }); + + describe('when both successful and failed executions took place', () => { + const setup = () => { + const targetRefsCount = 3; + + const targetRefIds: string[] = []; + + for (let i = 0; i < targetRefsCount; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + } + + const targetRefDomain = 'school'; + const deleteInMinutes = 60; + + ReferencesService.loadFromTxtFile = jest.fn().mockReturnValueOnce(targetRefIds); + + const outputs = [ + QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date()), + QueueDeletionRequestOutputBuilder.buildError(new Error('some error occurred...')), + QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date()), + ]; + + batchDeletionService.queueDeletionRequests.mockResolvedValueOnce([outputs[0], outputs[1], outputs[2]]); + + const expectedSummaryFieldsDetails: BatchDeletionSummaryDetail[] = []; + + for (let i = 0; i < targetRefIds.length; i += 1) { + expectedSummaryFieldsDetails.push( + BatchDeletionSummaryDetailBuilder.build( + QueueDeletionRequestInputBuilder.build(targetRefDomain, targetRefIds[i], deleteInMinutes), + outputs[i] + ) + ); + } + + const expectedSummaryFields = { + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, + successCount: 2, + failureCount: 1, + details: expectedSummaryFieldsDetails, + }; + + const refsFilePath = '/tmp/ids.txt'; + + return { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields }; + }; + + it('should return proper summary with all the successes and failures', async () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields } = setup(); + + const summary = await uc.deleteRefsFromTxtFile(refsFilePath, targetRefDomain, deleteInMinutes); + + expect(summary.executionTimeMilliseconds).toBeGreaterThan(0); + expect(summary).toMatchObject(expectedSummaryFields); + }); + }); + }); + + describe('when batch deletion service returns an invalid amount of outputs', () => { + const setup = () => { + const targetRefsCount = 3; + + const targetRefIds: string[] = []; + + for (let i = 0; i < targetRefsCount; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + } + + ReferencesService.loadFromTxtFile = jest.fn().mockReturnValueOnce(targetRefIds); + + const outputs: QueueDeletionRequestOutput[] = []; + + for (let i = 0; i < targetRefsCount - 1; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + outputs.push(QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date())); + } + + batchDeletionService.queueDeletionRequests.mockResolvedValueOnce(outputs); + + const refsFilePath = '/tmp/ids.txt'; + + return { refsFilePath }; + }; + + it('should throw an error', async () => { + const { refsFilePath } = setup(); + + const func = () => uc.deleteRefsFromTxtFile(refsFilePath); + + await expect(func()).rejects.toThrow(); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts new file mode 100644 index 00000000000..258b1b53f65 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; +import { BatchDeletionSummaryBuilder, BatchDeletionSummaryDetailBuilder } from './builder'; +import { + ReferencesService, + BatchDeletionService, + QueueDeletionRequestInput, + QueueDeletionRequestInputBuilder, +} from '../services'; +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from './interface'; + +@Injectable() +export class BatchDeletionUc { + constructor(private readonly batchDeletionService: BatchDeletionService) {} + + async deleteRefsFromTxtFile( + refsFilePath: string, + targetRefDomain = 'user', + deleteInMinutes = 43200, // 43200 minutes = 720 hours = 30 days + callsDelayMilliseconds?: number + ): Promise { + // First, load all the references from the provided text file (with given path). + const refsFromTxtFile = ReferencesService.loadFromTxtFile(refsFilePath); + + const inputs: QueueDeletionRequestInput[] = []; + + // For each reference found in a given file, add it to the inputs + // array (with added targetRefDomain and deleteInMinutes fields). + refsFromTxtFile.forEach((ref) => + inputs.push(QueueDeletionRequestInputBuilder.build(targetRefDomain, ref, deleteInMinutes)) + ); + + // Measure the overall queueing execution time by setting the start... + const startTime = performance.now(); + + const outputs = await this.batchDeletionService.queueDeletionRequests(inputs, callsDelayMilliseconds); + + // ...and end timestamps before and after the batch deletion service method execution. + const endTime = performance.now(); + + // Throw an error if the returned outputs number doesn't match the returned inputs number. + if (outputs.length !== inputs.length) { + throw new Error( + 'invalid result from the batch deletion service - expected to ' + + 'receive the same amount of outputs as the provided inputs, ' + + `instead received ${outputs.length} outputs for ${inputs.length} inputs` + ); + } + + const summary: BatchDeletionSummary = BatchDeletionSummaryBuilder.build(endTime - startTime); + + // Go through every received output and, in case of an error presence increase + // a failure count or, in case of no error, increase a success count. + for (let i = 0; i < outputs.length; i += 1) { + if (outputs[i].error) { + summary.failureCount += 1; + } else { + summary.successCount += 1; + } + + // Also add all the processed inputs and outputs details to the overall summary. + summary.details.push(BatchDeletionSummaryDetailBuilder.build(inputs[i], outputs[i])); + } + + // If no failure has been spotted, assume an overall success. + if (summary.failureCount === 0) { + summary.overallStatus = BatchDeletionSummaryOverallStatus.SUCCESS; + } + + return summary; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts new file mode 100644 index 00000000000..43ea82e86d5 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts @@ -0,0 +1,69 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BatchDeletionSummaryDetail } from '..'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; +import { BatchDeletionSummaryDetailBuilder } from './batch-deletion-summary-detail.builder'; + +describe(BatchDeletionSummaryDetailBuilder.name, () => { + describe(BatchDeletionSummaryDetailBuilder.build.name, () => { + describe('when called with proper arguments for', () => { + describe('a successful output case', () => { + const setup = () => { + const deletionRequestInput: QueueDeletionRequestInput = { + targetRefDomain: 'user', + targetRefId: new ObjectId().toHexString(), + deleteInMinutes: 1440, + }; + + const deletionRequestOutput: QueueDeletionRequestOutput = { + requestId: new ObjectId().toHexString(), + deletionPlannedAt: new Date(), + }; + + const expectedOutput: BatchDeletionSummaryDetail = { + input: deletionRequestInput, + output: deletionRequestOutput, + }; + + return { deletionRequestInput, deletionRequestOutput, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { deletionRequestInput, deletionRequestOutput, expectedOutput } = setup(); + + const output = BatchDeletionSummaryDetailBuilder.build(deletionRequestInput, deletionRequestOutput); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + + describe('an error output case', () => { + const setup = () => { + const deletionRequestInput: QueueDeletionRequestInput = { + targetRefDomain: 'user', + targetRefId: new ObjectId().toHexString(), + deleteInMinutes: 1440, + }; + + const deletionRequestOutput: QueueDeletionRequestOutput = { + error: 'some error occurred...', + }; + + const expectedOutput: BatchDeletionSummaryDetail = { + input: deletionRequestInput, + output: deletionRequestOutput, + }; + + return { deletionRequestInput, deletionRequestOutput, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { deletionRequestInput, deletionRequestOutput, expectedOutput } = setup(); + + const output = BatchDeletionSummaryDetailBuilder.build(deletionRequestInput, deletionRequestOutput); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts new file mode 100644 index 00000000000..9ebbce66171 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts @@ -0,0 +1,8 @@ +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; +import { BatchDeletionSummaryDetail } from '../interface'; + +export class BatchDeletionSummaryDetailBuilder { + static build(input: QueueDeletionRequestInput, output: QueueDeletionRequestOutput): BatchDeletionSummaryDetail { + return { input, output }; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts new file mode 100644 index 00000000000..a2b534602d4 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts @@ -0,0 +1,30 @@ +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from '../interface'; +import { BatchDeletionSummaryBuilder } from './batch-deletion-summary.builder'; + +describe(BatchDeletionSummaryBuilder.name, () => { + describe(BatchDeletionSummaryBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const executionTimeMilliseconds = 142; + + const expectedOutput: BatchDeletionSummary = { + executionTimeMilliseconds: 142, + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, + successCount: 0, + failureCount: 0, + details: [], + }; + + return { executionTimeMilliseconds, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { executionTimeMilliseconds, expectedOutput } = setup(); + + const output = BatchDeletionSummaryBuilder.build(executionTimeMilliseconds); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts new file mode 100644 index 00000000000..57fa2bcccd9 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts @@ -0,0 +1,13 @@ +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from '../interface'; + +export class BatchDeletionSummaryBuilder { + static build(executionTimeMilliseconds: number): BatchDeletionSummary { + return { + executionTimeMilliseconds, + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, + successCount: 0, + failureCount: 0, + details: [], + }; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/index.ts b/apps/server/src/modules/deletion/uc/builder/index.ts new file mode 100644 index 00000000000..46733980f94 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/index.ts @@ -0,0 +1,2 @@ +export * from './batch-deletion-summary-detail.builder'; +export * from './batch-deletion-summary.builder'; diff --git a/apps/server/src/modules/deletion/uc/index.ts b/apps/server/src/modules/deletion/uc/index.ts new file mode 100644 index 00000000000..cf74de969e5 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/index.ts @@ -0,0 +1,2 @@ +export * from './interface'; +export * from './batch-deletion.uc'; diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts new file mode 100644 index 00000000000..4fe99c13fad --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts @@ -0,0 +1,6 @@ +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; + +export interface BatchDeletionSummaryDetail { + input: QueueDeletionRequestInput; + output: QueueDeletionRequestOutput; +} diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts new file mode 100644 index 00000000000..4ae91bdf70e --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts @@ -0,0 +1,4 @@ +export const enum BatchDeletionSummaryOverallStatus { + SUCCESS = 'success', + FAILURE = 'failure', +} diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts new file mode 100644 index 00000000000..ce633e164f1 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts @@ -0,0 +1,9 @@ +import { BatchDeletionSummaryDetail } from './batch-deletion-summary-detail.interface'; + +export interface BatchDeletionSummary { + executionTimeMilliseconds: number; + overallStatus: string; + successCount: number; + failureCount: number; + details: BatchDeletionSummaryDetail[]; +} diff --git a/apps/server/src/modules/deletion/uc/interface/index.ts b/apps/server/src/modules/deletion/uc/interface/index.ts index 95786098275..9dcf644f410 100644 --- a/apps/server/src/modules/deletion/uc/interface/index.ts +++ b/apps/server/src/modules/deletion/uc/interface/index.ts @@ -1 +1,4 @@ +export * from './batch-deletion-summary-overall-status.enum'; +export * from './batch-deletion-summary-detail.interface'; +export * from './batch-deletion-summary.interface'; export * from './interfaces'; diff --git a/apps/server/src/modules/files-storage-client/dto/copy-file.dto.ts b/apps/server/src/modules/files-storage-client/dto/copy-file.dto.ts index eaddf253791..8c6936f33fa 100644 --- a/apps/server/src/modules/files-storage-client/dto/copy-file.dto.ts +++ b/apps/server/src/modules/files-storage-client/dto/copy-file.dto.ts @@ -1,5 +1,5 @@ import { EntityId } from '@shared/domain'; -import { ICopyFileDomainObjectProps } from '../interfaces'; +import { CopyFileDomainObjectProps } from '../interfaces'; export class CopyFileDto { id?: EntityId | undefined; @@ -8,7 +8,7 @@ export class CopyFileDto { name: string; - constructor(data: ICopyFileDomainObjectProps) { + constructor(data: CopyFileDomainObjectProps) { this.id = data.id; this.sourceId = data.sourceId; this.name = data.name; diff --git a/apps/server/src/modules/files-storage-client/dto/file.dto.ts b/apps/server/src/modules/files-storage-client/dto/file.dto.ts index 38d6daf4c3b..c5a5118ceae 100644 --- a/apps/server/src/modules/files-storage-client/dto/file.dto.ts +++ b/apps/server/src/modules/files-storage-client/dto/file.dto.ts @@ -1,6 +1,6 @@ -import { EntityId } from '@shared/domain'; import { FileRecordParentType } from '@infra/rabbitmq'; -import { IFileDomainObjectProps } from '../interfaces'; +import { EntityId } from '@shared/domain'; +import { FileDomainObjectProps } from '../interfaces'; export class FileDto { id: EntityId; @@ -11,7 +11,7 @@ export class FileDto { parentId: EntityId; - constructor(props: IFileDomainObjectProps) { + constructor(props: FileDomainObjectProps) { this.id = props.id; this.name = props.name; this.parentType = props.parentType; diff --git a/apps/server/src/modules/files-storage-client/index.ts b/apps/server/src/modules/files-storage-client/index.ts index 7b242946d8f..ae847364123 100644 --- a/apps/server/src/modules/files-storage-client/index.ts +++ b/apps/server/src/modules/files-storage-client/index.ts @@ -1,6 +1,6 @@ export { FileDto } from './dto'; export * from './files-storage-client.module'; -export { IFilesStorageClientConfig } from './interfaces'; +export { FilesStorageClientConfig } from './interfaces'; export { FileParamBuilder } from './mapper/files-storage-param.builder'; -export { CopyFilesService } from './service/copy-files.service'; +export * from './service/copy-files.service'; export { FilesStorageClientAdapterService } from './service/files-storage-client.service'; diff --git a/apps/server/src/modules/files-storage-client/interfaces/copy-file-domain-object-props.ts b/apps/server/src/modules/files-storage-client/interfaces/copy-file-domain-object-props.ts index 89348ea8c68..0dfa8455094 100644 --- a/apps/server/src/modules/files-storage-client/interfaces/copy-file-domain-object-props.ts +++ b/apps/server/src/modules/files-storage-client/interfaces/copy-file-domain-object-props.ts @@ -1,6 +1,6 @@ import { EntityId } from '@shared/domain'; -export interface ICopyFileDomainObjectProps { +export interface CopyFileDomainObjectProps { id?: EntityId | undefined; sourceId: EntityId; name: string; diff --git a/apps/server/src/modules/files-storage-client/interfaces/copy-file-request-info.ts b/apps/server/src/modules/files-storage-client/interfaces/copy-file-request-info.ts index e52bbce9b1a..42c971d9a0d 100644 --- a/apps/server/src/modules/files-storage-client/interfaces/copy-file-request-info.ts +++ b/apps/server/src/modules/files-storage-client/interfaces/copy-file-request-info.ts @@ -1,8 +1,8 @@ import { EntityId } from '@shared/domain'; -import { IFileRequestInfo } from './file-request-info'; +import { FileRequestInfo } from './file-request-info'; -export interface ICopyFilesRequestInfo { +export interface CopyFilesRequestInfo { userId: EntityId; - source: IFileRequestInfo; - target: IFileRequestInfo; + source: FileRequestInfo; + target: FileRequestInfo; } diff --git a/apps/server/src/modules/files-storage-client/interfaces/file-domain-object-props.ts b/apps/server/src/modules/files-storage-client/interfaces/file-domain-object-props.ts index f302e53e9a0..6a0fca7ec2e 100644 --- a/apps/server/src/modules/files-storage-client/interfaces/file-domain-object-props.ts +++ b/apps/server/src/modules/files-storage-client/interfaces/file-domain-object-props.ts @@ -1,7 +1,7 @@ import { EntityId } from '@shared/domain'; import { FileRecordParentType } from '@infra/rabbitmq'; -export interface IFileDomainObjectProps { +export interface FileDomainObjectProps { id: EntityId; name: string; parentType: FileRecordParentType; diff --git a/apps/server/src/modules/files-storage-client/interfaces/file-request-info.ts b/apps/server/src/modules/files-storage-client/interfaces/file-request-info.ts index 12a7898d9cf..4d00caf356b 100644 --- a/apps/server/src/modules/files-storage-client/interfaces/file-request-info.ts +++ b/apps/server/src/modules/files-storage-client/interfaces/file-request-info.ts @@ -1,7 +1,7 @@ import { EntityId } from '@shared/domain'; import { FileRecordParentType } from '@infra/rabbitmq'; -export interface IFileRequestInfo { +export interface FileRequestInfo { schoolId: EntityId; parentType: FileRecordParentType; parentId: EntityId; diff --git a/apps/server/src/modules/files-storage-client/interfaces/files-storage-client-config.ts b/apps/server/src/modules/files-storage-client/interfaces/files-storage-client-config.ts index 8e8457f7c4b..8e746ecdd8d 100644 --- a/apps/server/src/modules/files-storage-client/interfaces/files-storage-client-config.ts +++ b/apps/server/src/modules/files-storage-client/interfaces/files-storage-client-config.ts @@ -1,3 +1,3 @@ -export interface IFilesStorageClientConfig { +export interface FilesStorageClientConfig { INCOMING_REQUEST_TIMEOUT_COPY_API: number; } diff --git a/apps/server/src/modules/files-storage-client/mapper/copy-files-of-parent-param.builder.ts b/apps/server/src/modules/files-storage-client/mapper/copy-files-of-parent-param.builder.ts index 9edee50870a..fe1bbf09c08 100644 --- a/apps/server/src/modules/files-storage-client/mapper/copy-files-of-parent-param.builder.ts +++ b/apps/server/src/modules/files-storage-client/mapper/copy-files-of-parent-param.builder.ts @@ -1,9 +1,9 @@ import { EntityId } from '@shared/domain'; -import { IFileRequestInfo } from '../interfaces'; -import { ICopyFilesRequestInfo } from '../interfaces/copy-file-request-info'; +import { FileRequestInfo } from '../interfaces'; +import { CopyFilesRequestInfo } from '../interfaces/copy-file-request-info'; export class CopyFilesOfParentParamBuilder { - static build(userId: EntityId, source: IFileRequestInfo, target: IFileRequestInfo): ICopyFilesRequestInfo { + static build(userId: EntityId, source: FileRequestInfo, target: FileRequestInfo): CopyFilesRequestInfo { const fileRequestInfo = { userId, source, diff --git a/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.spec.ts b/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.spec.ts index f48d66dce48..a33ef506e51 100644 --- a/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.spec.ts +++ b/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.spec.ts @@ -1,5 +1,5 @@ import { FileRecordParentType } from '@infra/rabbitmq'; -import { ICopyFileDomainObjectProps, IFileDomainObjectProps } from '../interfaces'; +import { CopyFileDomainObjectProps, FileDomainObjectProps } from '../interfaces'; import { FilesStorageClientMapper } from './files-storage-client.mapper'; describe('FilesStorageClientMapper', () => { @@ -15,7 +15,7 @@ describe('FilesStorageClientMapper', () => { parentType: FileRecordParentType.Task, }; - const response: IFileDomainObjectProps[] = [record]; + const response: FileDomainObjectProps[] = [record]; describe('mapfileRecordListResponseToDomainFilesDto', () => { it('Should map to valid file Dtos.', () => { @@ -83,13 +83,13 @@ describe('FilesStorageClientMapper', () => { }); describe('copyFileDto mapper', () => { - const copyFileResponse: ICopyFileDomainObjectProps = { + const copyFileResponse: CopyFileDomainObjectProps = { id: 'id123', sourceId: 'sourceId123', name: 'name', }; - const list: ICopyFileDomainObjectProps[] = [copyFileResponse]; + const list: CopyFileDomainObjectProps[] = [copyFileResponse]; describe('mapCopyFileListResponseToCopyFilesDto', () => { it('Should map to valid file Dtos.', () => { diff --git a/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.ts b/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.ts index 233e47fd4c8..dcc2f927b5f 100644 --- a/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.ts +++ b/apps/server/src/modules/files-storage-client/mapper/files-storage-client.mapper.ts @@ -1,11 +1,11 @@ import { LessonEntity, Submission, Task } from '@shared/domain'; import { FileRecordParentType } from '@infra/rabbitmq'; import { CopyFileDto, FileDto } from '../dto'; -import { EntitiesWithFiles, ICopyFileDomainObjectProps, IFileDomainObjectProps } from '../interfaces'; +import { CopyFileDomainObjectProps, EntitiesWithFiles, FileDomainObjectProps } from '../interfaces'; export class FilesStorageClientMapper { - static mapfileRecordListResponseToDomainFilesDto(fileRecordListResponse: IFileDomainObjectProps[]): FileDto[] { - const filesDto = fileRecordListResponse.map((record: IFileDomainObjectProps) => { + static mapfileRecordListResponseToDomainFilesDto(fileRecordListResponse: FileDomainObjectProps[]): FileDto[] { + const filesDto = fileRecordListResponse.map((record: FileDomainObjectProps) => { const fileDto = FilesStorageClientMapper.mapFileRecordResponseToFileDto(record); return fileDto; @@ -14,7 +14,7 @@ export class FilesStorageClientMapper { return filesDto; } - static mapCopyFileListResponseToCopyFilesDto(copyFileListResponse: ICopyFileDomainObjectProps[]): CopyFileDto[] { + static mapCopyFileListResponseToCopyFilesDto(copyFileListResponse: CopyFileDomainObjectProps[]): CopyFileDto[] { const filesDto = copyFileListResponse.map((response) => { const fileDto = FilesStorageClientMapper.mapCopyFileResponseToCopyFileDto(response); @@ -24,7 +24,7 @@ export class FilesStorageClientMapper { return filesDto; } - static mapFileRecordResponseToFileDto(fileRecordResponse: IFileDomainObjectProps) { + static mapFileRecordResponseToFileDto(fileRecordResponse: FileDomainObjectProps) { const parentType = FilesStorageClientMapper.mapStringToParentType(fileRecordResponse.parentType); const fileDto = new FileDto({ id: fileRecordResponse.id, @@ -36,7 +36,7 @@ export class FilesStorageClientMapper { return fileDto; } - static mapCopyFileResponseToCopyFileDto(response: ICopyFileDomainObjectProps) { + static mapCopyFileResponseToCopyFileDto(response: CopyFileDomainObjectProps) { const dto = new CopyFileDto({ id: response.id, sourceId: response.sourceId, diff --git a/apps/server/src/modules/files-storage-client/mapper/files-storage-param.builder.ts b/apps/server/src/modules/files-storage-client/mapper/files-storage-param.builder.ts index bd37c97ceb5..08ae8552bf0 100644 --- a/apps/server/src/modules/files-storage-client/mapper/files-storage-param.builder.ts +++ b/apps/server/src/modules/files-storage-client/mapper/files-storage-param.builder.ts @@ -1,9 +1,9 @@ import { EntityId } from '@shared/domain'; -import { EntitiesWithFiles, IFileRequestInfo } from '../interfaces'; +import { EntitiesWithFiles, FileRequestInfo } from '../interfaces'; import { FilesStorageClientMapper } from './files-storage-client.mapper'; export class FileParamBuilder { - static build(schoolId: EntityId, parent: EntitiesWithFiles): IFileRequestInfo { + static build(schoolId: EntityId, parent: EntitiesWithFiles): FileRequestInfo { const parentType = FilesStorageClientMapper.mapEntityToParentType(parent); const fileRequestInfo = { parentType, diff --git a/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts b/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts index 5bfc98a361a..24de3426bc9 100644 --- a/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts +++ b/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts @@ -1,14 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { CopyElementType, CopyHelperService } from '@modules/copy-helper'; import { Test, TestingModule } from '@nestjs/testing'; -import { ComponentType, IComponentProperties } from '@shared/domain'; +import { ComponentProperties, ComponentType } from '@shared/domain'; import { courseFactory, + legacyFileEntityMockFactory, lessonFactory, schoolFactory, - legacyFileEntityMockFactory, setupEntities, } from '@shared/testing'; -import { CopyElementType, CopyHelperService } from '@modules/copy-helper'; import { CopyFilesService } from './copy-files.service'; import { FilesStorageClientAdapterService } from './files-storage-client.service'; @@ -69,13 +69,13 @@ describe('copy files service', () => { const lessonSetup = () => { const { school, imageHTML1, imageHTML2 } = setup(); const originalCourse = courseFactory.build({ school }); - const textContent: IComponentProperties = { + const textContent: ComponentProperties = { title: '', hidden: false, component: ComponentType.TEXT, content: { text: `${imageHTML1} test abschnitt ${imageHTML2}` }, }; - const geoGebraContent: IComponentProperties = { + const geoGebraContent: ComponentProperties = { title: 'geoGebra component 1', hidden: false, component: ComponentType.GEOGEBRA, diff --git a/apps/server/src/modules/files-storage-client/service/files-storage-client.service.ts b/apps/server/src/modules/files-storage-client/service/files-storage-client.service.ts index c632631b884..6cb47305248 100644 --- a/apps/server/src/modules/files-storage-client/service/files-storage-client.service.ts +++ b/apps/server/src/modules/files-storage-client/service/files-storage-client.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; import { CopyFileDto, FileDto } from '../dto'; -import { IFileRequestInfo } from '../interfaces'; -import { ICopyFilesRequestInfo } from '../interfaces/copy-file-request-info'; +import { FileRequestInfo } from '../interfaces'; +import { CopyFilesRequestInfo } from '../interfaces/copy-file-request-info'; import { FilesStorageClientMapper } from '../mapper'; import { FilesStorageProducer } from './files-storage.producer'; @@ -13,14 +13,14 @@ export class FilesStorageClientAdapterService { this.logger.setContext(FilesStorageClientAdapterService.name); } - async copyFilesOfParent(param: ICopyFilesRequestInfo): Promise { + async copyFilesOfParent(param: CopyFilesRequestInfo): Promise { const response = await this.fileStorageMQProducer.copyFilesOfParent(param); const fileInfos = FilesStorageClientMapper.mapCopyFileListResponseToCopyFilesDto(response); return fileInfos; } - async listFilesOfParent(param: IFileRequestInfo): Promise { + async listFilesOfParent(param: FileRequestInfo): Promise { const response = await this.fileStorageMQProducer.listFilesOfParent(param); const fileInfos = FilesStorageClientMapper.mapfileRecordListResponseToDomainFilesDto(response); diff --git a/apps/server/src/modules/files-storage-client/service/files-storage.producer.ts b/apps/server/src/modules/files-storage-client/service/files-storage.producer.ts index 34927c01831..5f122263a14 100644 --- a/apps/server/src/modules/files-storage-client/service/files-storage.producer.ts +++ b/apps/server/src/modules/files-storage-client/service/files-storage.producer.ts @@ -1,51 +1,51 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { EntityId } from '@shared/domain'; -import { LegacyLogger } from '@src/core/logger'; import { + CopyFileDO, + CopyFilesOfParentParams, + FileDO, + FileRecordParams, FilesStorageEvents, FilesStorageExchange, - ICopyFileDO, - ICopyFilesOfParentParams, - IFileDO, - IFileRecordParams, RpcMessageProducer, } from '@infra/rabbitmq'; -import { IFilesStorageClientConfig } from '../interfaces'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EntityId } from '@shared/domain'; +import { LegacyLogger } from '@src/core/logger'; +import { FilesStorageClientConfig } from '../interfaces'; @Injectable() export class FilesStorageProducer extends RpcMessageProducer { constructor( protected readonly amqpConnection: AmqpConnection, private readonly logger: LegacyLogger, - protected readonly configService: ConfigService + protected readonly configService: ConfigService ) { super(amqpConnection, FilesStorageExchange, configService.get('INCOMING_REQUEST_TIMEOUT_COPY_API')); this.logger.setContext(FilesStorageProducer.name); } - async copyFilesOfParent(payload: ICopyFilesOfParentParams): Promise { + async copyFilesOfParent(payload: CopyFilesOfParentParams): Promise { this.logger.debug({ action: 'copyFilesOfParent:started', payload }); - const response = await this.request(FilesStorageEvents.COPY_FILES_OF_PARENT, payload); + const response = await this.request(FilesStorageEvents.COPY_FILES_OF_PARENT, payload); this.logger.debug({ action: 'copyFilesOfParent:finished', payload }); return response; } - async listFilesOfParent(payload: IFileRecordParams): Promise { + async listFilesOfParent(payload: FileRecordParams): Promise { this.logger.debug({ action: 'listFilesOfParent:started', payload }); - const response = await this.request(FilesStorageEvents.LIST_FILES_OF_PARENT, payload); + const response = await this.request(FilesStorageEvents.LIST_FILES_OF_PARENT, payload); this.logger.debug({ action: 'listFilesOfParent:finished', payload }); return response; } - async deleteFilesOfParent(payload: EntityId): Promise { + async deleteFilesOfParent(payload: EntityId): Promise { this.logger.debug({ action: 'deleteFilesOfParent:started', payload }); - const response = await this.request(FilesStorageEvents.DELETE_FILES_OF_PARENT, payload); + const response = await this.request(FilesStorageEvents.DELETE_FILES_OF_PARENT, payload); this.logger.debug({ action: 'deleteFilesOfParent:finished', payload }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts index 4a966165633..1711acba050 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts @@ -1,4 +1,6 @@ import { createMock } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ICurrentUser } from '@modules/authentication'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; @@ -6,8 +8,6 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, courseFactory, diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts index 6c1087ce371..ea85d9b656b 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts @@ -1,4 +1,6 @@ import { createMock } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ICurrentUser } from '@modules/authentication'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; @@ -6,8 +8,6 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, fileRecordFactory, diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts index 27661af51f8..8d8e7dbf912 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts @@ -1,4 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { EntityManager } from '@mikro-orm/mongodb'; import { ICurrentUser } from '@modules/authentication'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; @@ -6,8 +8,6 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts index 9e666437748..7f56152f7da 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts @@ -1,4 +1,6 @@ import { createMock } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ICurrentUser } from '@modules/authentication'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; @@ -6,8 +8,6 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, fileRecordFactory, diff --git a/apps/server/src/modules/files-storage/controller/files-storage.consumer.ts b/apps/server/src/modules/files-storage/controller/files-storage.consumer.ts index fc500fedee1..3b6b5654c4c 100644 --- a/apps/server/src/modules/files-storage/controller/files-storage.consumer.ts +++ b/apps/server/src/modules/files-storage/controller/files-storage.consumer.ts @@ -1,10 +1,10 @@ import { RabbitPayload, RabbitRPC } from '@golevelup/nestjs-rabbitmq'; +import { CopyFileDO, FileDO, FilesStorageEvents, FilesStorageExchange } from '@infra/rabbitmq'; +import { RpcMessage } from '@infra/rabbitmq/rpc-message'; import { MikroORM, UseRequestContext } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; -import { RpcMessage } from '@infra/rabbitmq/rpc-message'; import { LegacyLogger } from '@src/core/logger'; -import { FilesStorageEvents, FilesStorageExchange, ICopyFileDO, IFileDO } from '@infra/rabbitmq'; import { FilesStorageMapper } from '../mapper'; import { FilesStorageService } from '../service/files-storage.service'; import { PreviewService } from '../service/preview.service'; @@ -30,7 +30,7 @@ export class FilesStorageConsumer { @UseRequestContext() public async copyFilesOfParent( @RabbitPayload() payload: CopyFilesOfParentPayload - ): Promise> { + ): Promise> { this.logger.debug({ action: 'copyFilesOfParent', payload }); const { userId, source, target } = payload; @@ -45,7 +45,7 @@ export class FilesStorageConsumer { queue: FilesStorageEvents.LIST_FILES_OF_PARENT, }) @UseRequestContext() - public async getFilesOfParent(@RabbitPayload() payload: FileRecordParams): Promise> { + public async getFilesOfParent(@RabbitPayload() payload: FileRecordParams): Promise> { this.logger.debug({ action: 'getFilesOfParent', payload }); const [fileRecords, total] = await this.filesStorageService.getFileRecordsOfParent(payload.parentId); @@ -60,7 +60,7 @@ export class FilesStorageConsumer { queue: FilesStorageEvents.DELETE_FILES_OF_PARENT, }) @UseRequestContext() - public async deleteFilesOfParent(@RabbitPayload() payload: EntityId): Promise> { + public async deleteFilesOfParent(@RabbitPayload() payload: EntityId): Promise> { this.logger.debug({ action: 'deleteFilesOfParent', payload }); const [fileRecords, total] = await this.filesStorageService.getFileRecordsOfParent(payload); diff --git a/apps/server/src/modules/files-storage/entity/filerecord.entity.spec.ts b/apps/server/src/modules/files-storage/entity/filerecord.entity.spec.ts index f497ff39a6c..a3d28c5bdec 100644 --- a/apps/server/src/modules/files-storage/entity/filerecord.entity.spec.ts +++ b/apps/server/src/modules/files-storage/entity/filerecord.entity.spec.ts @@ -6,8 +6,8 @@ import { PreviewInputMimeTypes } from '../interface'; import { FileRecord, FileRecordParentType, + FileRecordProperties, FileRecordSecurityCheck, - IFileRecordProperties, PreviewStatus, ScanStatus, } from './filerecord.entity'; @@ -18,7 +18,7 @@ describe('FileRecord Entity', () => { }); describe('when creating a new instance using the constructor', () => { - let props: IFileRecordProperties; + let props: FileRecordProperties; beforeEach(() => { props = { diff --git a/apps/server/src/modules/files-storage/entity/filerecord.entity.ts b/apps/server/src/modules/files-storage/entity/filerecord.entity.ts index 26f2807924c..5f7c01ac2d8 100644 --- a/apps/server/src/modules/files-storage/entity/filerecord.entity.ts +++ b/apps/server/src/modules/files-storage/entity/filerecord.entity.ts @@ -34,7 +34,7 @@ export enum PreviewStatus { PREVIEW_NOT_POSSIBLE_WRONG_MIME_TYPE = 'preview_not_possible_wrong_mime_type', } -export interface IFileRecordSecurityCheckProperties { +export interface FileRecordSecurityCheckProperties { status?: ScanStatus; reason?: string; requestToken?: string; @@ -56,7 +56,7 @@ export class FileRecordSecurityCheck { @Property() updatedAt = new Date(); - constructor(props: IFileRecordSecurityCheckProperties) { + constructor(props: FileRecordSecurityCheckProperties) { if (props.status !== undefined) { this.status = props.status; } @@ -69,7 +69,7 @@ export class FileRecordSecurityCheck { } } -export interface IFileRecordProperties { +export interface FileRecordProperties { size: number; name: string; mimeType: string; @@ -81,13 +81,13 @@ export interface IFileRecordProperties { isCopyFrom?: EntityId; } -interface IParentInfo { +interface ParentInfo { schoolId: EntityId; parentId: EntityId; parentType: FileRecordParentType; } -// TODO: IEntityWithSchool +// TODO: EntityWithSchool /** * Note: The file record entity will not manage any entity relations by itself. @@ -150,7 +150,7 @@ export class FileRecord extends BaseEntityWithTimestamps { return result; } - constructor(props: IFileRecordProperties) { + constructor(props: FileRecordProperties) { super(); this.size = props.size; this.name = props.name; @@ -177,7 +177,7 @@ export class FileRecord extends BaseEntityWithTimestamps { return this.securityCheck.requestToken; } - public copy(userId: EntityId, targetParentInfo: IParentInfo): FileRecord { + public copy(userId: EntityId, targetParentInfo: ParentInfo): FileRecord { const { size, name, mimeType, id } = this; const { parentType, parentId, schoolId } = targetParentInfo; @@ -261,7 +261,7 @@ export class FileRecord extends BaseEntityWithTimestamps { return isPreviewPossible; } - public getParentInfo(): IParentInfo { + public getParentInfo(): ParentInfo { const { parentId, parentType, schoolId } = this; return { parentId, parentType, schoolId }; diff --git a/apps/server/src/modules/files-storage/files-preview-amqp.module.ts b/apps/server/src/modules/files-storage/files-preview-amqp.module.ts index 78a1aec0129..c59dc720729 100644 --- a/apps/server/src/modules/files-storage/files-preview-amqp.module.ts +++ b/apps/server/src/modules/files-storage/files-preview-amqp.module.ts @@ -1,8 +1,12 @@ -import { Module } from '@nestjs/common'; import { PreviewGeneratorConsumerModule } from '@infra/preview-generator'; +import { Module } from '@nestjs/common'; +import { CoreModule } from '@src/core'; import { defaultConfig, s3Config } from './files-storage.config'; @Module({ - imports: [PreviewGeneratorConsumerModule.register({ storageConfig: s3Config, serverConfig: defaultConfig })], + imports: [ + PreviewGeneratorConsumerModule.register({ storageConfig: s3Config, serverConfig: defaultConfig }), + CoreModule, + ], }) export class PreviewGeneratorAMQPModule {} diff --git a/apps/server/src/modules/files-storage/files-storage.config.ts b/apps/server/src/modules/files-storage/files-storage.config.ts index 985b07f0ef1..5c02be598e6 100644 --- a/apps/server/src/modules/files-storage/files-storage.config.ts +++ b/apps/server/src/modules/files-storage/files-storage.config.ts @@ -1,9 +1,9 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { S3Config } from '@infra/s3-client'; -import { ICoreModuleConfig } from '@src/core'; +import { CoreModuleConfig } from '@src/core'; export const FILES_STORAGE_S3_CONNECTION = 'FILES_STORAGE_S3_CONNECTION'; -export interface IFileStorageConfig extends ICoreModuleConfig { +export interface FileStorageConfig extends CoreModuleConfig { MAX_FILE_SIZE: number; MAX_SECURITY_CHECK_FILE_SIZE: number; USE_STREAM_TO_ANTIVIRUS: boolean; @@ -14,7 +14,7 @@ export const defaultConfig = { INCOMING_REQUEST_TIMEOUT: Configuration.get('FILES_STORAGE__INCOMING_REQUEST_TIMEOUT') as number, }; -const fileStorageConfig: IFileStorageConfig = { +const fileStorageConfig: FileStorageConfig = { INCOMING_REQUEST_TIMEOUT_COPY_API: Configuration.get('INCOMING_REQUEST_TIMEOUT_COPY_API') as number, MAX_FILE_SIZE: Configuration.get('FILES_STORAGE__MAX_FILE_SIZE') as number, MAX_SECURITY_CHECK_FILE_SIZE: Configuration.get('FILES_STORAGE__MAX_FILE_SIZE') as number, diff --git a/apps/server/src/modules/files-storage/service/files-storage.service.ts b/apps/server/src/modules/files-storage/service/files-storage.service.ts index 6eb9e89ea96..aa8cdce73f1 100644 --- a/apps/server/src/modules/files-storage/service/files-storage.service.ts +++ b/apps/server/src/modules/files-storage/service/files-storage.service.ts @@ -25,7 +25,7 @@ import { import { FileDto } from '../dto'; import { FileRecord, ScanStatus } from '../entity'; import { ErrorType } from '../error'; -import { FILES_STORAGE_S3_CONNECTION, IFileStorageConfig } from '../files-storage.config'; +import { FileStorageConfig, FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createCopyFiles, createFileRecord, @@ -45,7 +45,7 @@ export class FilesStorageService { private readonly fileRecordRepo: FileRecordRepo, @Inject(FILES_STORAGE_S3_CONNECTION) private readonly storageClient: S3ClientAdapter, private readonly antivirusService: AntivirusService, - private readonly configService: ConfigService, + private readonly configService: ConfigService, private logger: LegacyLogger ) { this.logger.setContext(FilesStorageService.name); diff --git a/apps/server/src/modules/group/controller/group.controller.ts b/apps/server/src/modules/group/controller/group.controller.ts index c92ee337050..2ae47b54552 100644 --- a/apps/server/src/modules/group/controller/group.controller.ts +++ b/apps/server/src/modules/group/controller/group.controller.ts @@ -6,7 +6,7 @@ import { Page } from '@shared/domain'; import { ErrorResponse } from '@src/core/error/dto'; import { GroupUc } from '../uc'; import { ClassInfoDto, ResolvedGroupDto } from '../uc/dto'; -import { ClassInfoSearchListResponse, ClassSortParams, GroupIdParams, GroupResponse, ClassFilterParams } from './dto'; +import { ClassFilterParams, ClassInfoSearchListResponse, ClassSortParams, GroupIdParams, GroupResponse } from './dto'; import { GroupResponseMapper } from './mapper'; @ApiTags('Group') diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts index e2af08f3fd5..ff093b2c712 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts @@ -1,9 +1,9 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest/lib/mocks'; +import { createMock, DeepMocked } from '@golevelup/ts-jest/lib/mocks'; +import { S3ClientAdapter } from '@infra/s3-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts index 05132888f71..5ee9d7f15f7 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-files.api.spec.ts @@ -1,10 +1,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ILibraryName } from '@lumieducation/h5p-server'; import { ContentMetadata } from '@lumieducation/h5p-server/build/src/ContentMetadata'; import { EntityManager } from '@mikro-orm/mongodb'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { S3ClientAdapter } from '@infra/s3-client'; import { courseFactory, h5pContentFactory, @@ -14,7 +14,7 @@ import { } from '@shared/testing'; import { ObjectID } from 'bson'; import { Readable } from 'stream'; -import { H5PContent, H5PContentParentType, IH5PContentProperties, H5pEditorTempFile } from '../../entity'; +import { H5PContent, H5PContentParentType, H5PContentProperties, H5pEditorTempFile } from '../../entity'; import { H5PEditorTestModule } from '../../h5p-editor-test.module'; import { H5P_CONTENT_S3_CONNECTION, H5P_LIBRARIES_S3_CONNECTION } from '../../h5p-editor.config'; import { ContentStorage, LibraryStorage, TemporaryFileStorage } from '../../service'; @@ -45,7 +45,7 @@ const helpers = { const content = { data: `Data #${n}`, }; - const h5pContentProperties: IH5PContentProperties = { + const h5pContentProperties: H5PContentProperties = { creatorId: new ObjectID().toString(), parentId: new ObjectID().toString(), schoolId: new ObjectID().toString(), diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts index 3f738fd67c0..c22c7394fab 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts @@ -1,9 +1,9 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest/lib/mocks'; +import { createMock, DeepMocked } from '@golevelup/ts-jest/lib/mocks'; +import { S3ClientAdapter } from '@infra/s3-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts index 6e98bb6905a..cb5a3d487eb 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts @@ -1,10 +1,10 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest/lib/mocks'; +import { createMock, DeepMocked } from '@golevelup/ts-jest/lib/mocks'; +import { S3ClientAdapter } from '@infra/s3-client'; import { IPlayerModel } from '@lumieducation/h5p-server'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { S3ClientAdapter } from '@infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; diff --git a/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts b/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts index 8c80d6bc0e4..c46c782da2d 100644 --- a/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts +++ b/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts @@ -1,3 +1,5 @@ +import { CurrentUser, ICurrentUser } from '@modules/authentication'; +import { Authenticate } from '@modules/authentication/decorator/auth.decorator'; import { BadRequestException, Body, @@ -18,13 +20,10 @@ import { import { FileFieldsInterceptor } from '@nestjs/platform-express'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser, CurrentUser } from '@modules/authentication'; import { Request, Response } from 'express'; -import { Authenticate } from '@modules/authentication/decorator/auth.decorator'; import { H5PEditorUc } from '../uc/h5p.uc'; -import { AjaxPostBodyParamsTransformPipe } from './dto/ajax/post.body.params.transform-pipe'; import { AjaxGetQueryParams, AjaxPostBodyParams, @@ -37,6 +36,7 @@ import { PostH5PContentCreateParams, SaveH5PEditorParams, } from './dto'; +import { AjaxPostBodyParamsTransformPipe } from './dto/ajax/post.body.params.transform-pipe'; import { H5PEditorModelContentResponse, H5PEditorModelResponse, H5PSaveResponse } from './dto/h5p-editor.response'; @ApiTags('h5p-editor') diff --git a/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.spec.ts b/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.spec.ts index cea707c8ccf..04655d5036a 100644 --- a/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.spec.ts +++ b/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.spec.ts @@ -1,10 +1,10 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { ContentMetadata, H5PContent, H5PContentParentType, IH5PContentProperties } from './h5p-content.entity'; +import { ContentMetadata, H5PContent, H5PContentParentType, H5PContentProperties } from './h5p-content.entity'; describe('H5PContent class', () => { describe('when an H5PContent instance is created', () => { const setup = () => { - const dummyIH5PContentProperties: IH5PContentProperties = { + const dummyH5PContentProperties: H5PContentProperties = { creatorId: '507f1f77bcf86cd799439011', parentType: H5PContentParentType.Lesson, parentId: '507f1f77bcf86cd799439012', @@ -23,19 +23,19 @@ describe('H5PContent class', () => { content: {}, }; - const h5pContent = new H5PContent(dummyIH5PContentProperties); - return { h5pContent, dummyIH5PContentProperties }; + const h5pContent = new H5PContent(dummyH5PContentProperties); + return { h5pContent, dummyH5PContentProperties }; }; it('should correctly return the creatorId', () => { - const { h5pContent, dummyIH5PContentProperties } = setup(); - const expectedCreatorId = new ObjectId(dummyIH5PContentProperties.creatorId).toHexString(); + const { h5pContent, dummyH5PContentProperties } = setup(); + const expectedCreatorId = new ObjectId(dummyH5PContentProperties.creatorId).toHexString(); expect(h5pContent.creatorId).toBe(expectedCreatorId); }); it('should correctly return the schoolId', () => { - const { h5pContent, dummyIH5PContentProperties } = setup(); - const expectedSchoolId = new ObjectId(dummyIH5PContentProperties.schoolId).toHexString(); + const { h5pContent, dummyH5PContentProperties } = setup(); + const expectedSchoolId = new ObjectId(dummyH5PContentProperties.schoolId).toHexString(); expect(h5pContent.schoolId).toBe(expectedSchoolId); }); }); diff --git a/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.ts b/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.ts index 3f9e6113172..7b96978294f 100644 --- a/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.ts +++ b/apps/server/src/modules/h5p-editor/entity/h5p-content.entity.ts @@ -106,7 +106,7 @@ export enum H5PContentParentType { 'Lesson' = 'lessons', } -export interface IH5PContentProperties { +export interface H5PContentProperties { creatorId: EntityId; parentType: H5PContentParentType; parentId: EntityId; @@ -149,7 +149,7 @@ export class H5PContent extends BaseEntityWithTimestamps { @Property({ type: JsonType }) content: unknown; - constructor({ parentType, parentId, creatorId, schoolId, metadata, content }: IH5PContentProperties) { + constructor({ parentType, parentId, creatorId, schoolId, metadata, content }: H5PContentProperties) { super(); this.parentType = parentType; diff --git a/apps/server/src/modules/h5p-editor/entity/h5p-editor-tempfile.entity.ts b/apps/server/src/modules/h5p-editor/entity/h5p-editor-tempfile.entity.ts index a4ebeb30e8a..f7a6913eef7 100644 --- a/apps/server/src/modules/h5p-editor/entity/h5p-editor-tempfile.entity.ts +++ b/apps/server/src/modules/h5p-editor/entity/h5p-editor-tempfile.entity.ts @@ -1,8 +1,8 @@ +import { IFileStats, ITemporaryFile } from '@lumieducation/h5p-server'; import { Entity, Property } from '@mikro-orm/core'; -import { ITemporaryFile, IFileStats } from '@lumieducation/h5p-server'; import { BaseEntityWithTimestamps } from '@shared/domain'; -export interface ITemporaryFileProperties { +export interface TemporaryFileProperties { filename: string; ownedByUserId: string; expiresAt: Date; @@ -30,7 +30,7 @@ export class H5pEditorTempFile extends BaseEntityWithTimestamps implements ITemp @Property() size: number; - constructor({ filename, ownedByUserId, expiresAt, birthtime, size }: ITemporaryFileProperties) { + constructor({ filename, ownedByUserId, expiresAt, birthtime, size }: TemporaryFileProperties) { super(); this.filename = filename; this.ownedByUserId = ownedByUserId; diff --git a/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts b/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts index df19f05ae21..4fb094e8e5b 100644 --- a/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts +++ b/apps/server/src/modules/h5p-editor/service/contentStorage.service.spec.ts @@ -1,14 +1,14 @@ import { HeadObjectCommandOutput } from '@aws-sdk/client-s3'; -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { S3ClientAdapter } from '@infra/s3-client'; import { IContentMetadata, ILibraryName, IUser, LibraryName } from '@lumieducation/h5p-server'; import { HttpException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { IEntity } from '@shared/domain'; -import { S3ClientAdapter } from '@infra/s3-client'; import { ObjectID } from 'bson'; import { Readable } from 'stream'; import { GetH5PFileResponse } from '../controller/dto'; -import { H5PContent, H5PContentParentType, IH5PContentProperties } from '../entity'; +import { H5PContent, H5PContentParentType, H5PContentProperties } from '../entity'; import { H5P_CONTENT_S3_CONNECTION } from '../h5p-editor.config'; import { H5PContentRepo } from '../repo'; import { H5PContentParentParams, LumiUserWithContentData } from '../types/lumi-types'; @@ -40,7 +40,7 @@ const helpers = { const content = { data: `Data #${n}`, }; - const h5pContentProperties: IH5PContentProperties = { + const h5pContentProperties: H5PContentProperties = { creatorId: new ObjectID().toString(), parentId: new ObjectID().toString(), schoolId: new ObjectID().toString(), diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-delete.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-delete.uc.spec.ts index 174d5c0fd3a..040d5817f8a 100644 --- a/apps/server/src/modules/h5p-editor/uc/h5p-delete.uc.spec.ts +++ b/apps/server/src/modules/h5p-editor/uc/h5p-delete.uc.spec.ts @@ -1,4 +1,4 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { H5PEditor, H5PPlayer } from '@lumieducation/h5p-server'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -6,9 +6,9 @@ import { h5pContentFactory, setupEntities } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src/modules/authorization/domain'; import { UserService } from '@src/modules/user'; +import { H5PAjaxEndpointProvider } from '../provider'; import { H5PContentRepo } from '../repo'; import { LibraryStorage } from '../service'; -import { H5PAjaxEndpointProvider } from '../provider'; import { H5PEditorUc } from './h5p.uc'; const createParams = () => { diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-files.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-files.uc.spec.ts index ab38282cc56..c1c7987ace8 100644 --- a/apps/server/src/modules/h5p-editor/uc/h5p-files.uc.spec.ts +++ b/apps/server/src/modules/h5p-editor/uc/h5p-files.uc.spec.ts @@ -1,4 +1,4 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { H5PAjaxEndpoint, H5PEditor, IPlayerModel } from '@lumieducation/h5p-server'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -8,9 +8,9 @@ import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src import { UserService } from '@src/modules/user'; import { Request } from 'express'; import { Readable } from 'stream'; +import { H5PEditorProvider, H5PPlayerProvider } from '../provider'; import { H5PContentRepo } from '../repo'; import { ContentStorage, LibraryStorage } from '../service'; -import { H5PEditorProvider, H5PPlayerProvider } from '../provider'; import { TemporaryFileStorage } from '../service/temporary-file-storage.service'; import { H5PEditorUc } from './h5p.uc'; diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-get-editor.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-get-editor.uc.spec.ts index 4322dd06352..824b1f7de8a 100644 --- a/apps/server/src/modules/h5p-editor/uc/h5p-get-editor.uc.spec.ts +++ b/apps/server/src/modules/h5p-editor/uc/h5p-get-editor.uc.spec.ts @@ -1,4 +1,4 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { H5PEditor, H5PPlayer, IEditorModel } from '@lumieducation/h5p-server'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -8,9 +8,9 @@ import { h5pContentFactory, setupEntities } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src/modules/authorization/domain'; import { UserService } from '@src/modules/user'; +import { H5PAjaxEndpointProvider } from '../provider'; import { H5PContentRepo } from '../repo'; import { LibraryStorage } from '../service'; -import { H5PAjaxEndpointProvider } from '../provider'; import { H5PEditorUc } from './h5p.uc'; const createParams = () => { diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-get-player.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-get-player.uc.spec.ts index 6db9d27a905..c8648cb413c 100644 --- a/apps/server/src/modules/h5p-editor/uc/h5p-get-player.uc.spec.ts +++ b/apps/server/src/modules/h5p-editor/uc/h5p-get-player.uc.spec.ts @@ -1,14 +1,14 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { H5PEditor, H5PPlayer, IPlayerModel } from '@lumieducation/h5p-server'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { h5pContentFactory, setupEntities } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; -import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src/modules/authorization/domain'; import { UserService } from '@src/modules/user'; +import { H5PAjaxEndpointProvider } from '../provider'; import { H5PContentRepo } from '../repo'; import { LibraryStorage } from '../service'; -import { H5PAjaxEndpointProvider } from '../provider'; import { H5PEditorUc } from './h5p.uc'; const createParams = () => { diff --git a/apps/server/src/modules/h5p-editor/uc/h5p-save-create.uc.spec.ts b/apps/server/src/modules/h5p-editor/uc/h5p-save-create.uc.spec.ts index 2bd23edecdc..2fba57f5bc2 100644 --- a/apps/server/src/modules/h5p-editor/uc/h5p-save-create.uc.spec.ts +++ b/apps/server/src/modules/h5p-editor/uc/h5p-save-create.uc.spec.ts @@ -1,18 +1,18 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { H5PEditor, H5PPlayer } from '@lumieducation/h5p-server'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { h5pContentFactory, setupEntities } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { ForbiddenException } from '@nestjs/common'; import { AuthorizationContextBuilder, AuthorizationReferenceService } from '@src/modules/authorization/domain'; import { UserService } from '@src/modules/user'; -import { LibraryStorage } from '../service'; -import { H5PAjaxEndpointProvider } from '../provider'; -import { H5PEditorUc } from './h5p.uc'; import { H5PContentParentType } from '../entity'; +import { H5PAjaxEndpointProvider } from '../provider'; import { H5PContentRepo } from '../repo'; +import { LibraryStorage } from '../service'; import { LumiUserWithContentData } from '../types/lumi-types'; +import { H5PEditorUc } from './h5p.uc'; const createParams = () => { const { content: parameters, metadata } = h5pContentFactory.build(); diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-element.interface.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-element.interface.ts index 7f30cec6977..400b31dabb1 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-element.interface.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-element.interface.ts @@ -1,3 +1,3 @@ -export interface ICommonCartridgeElement { +export interface CommonCartridgeElement { transform(): Record; } diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.spec.ts index 9c44732c61e..7b5709f8a9e 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.spec.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.spec.ts @@ -1,7 +1,7 @@ import AdmZip from 'adm-zip'; import { parseStringPromise } from 'xml2js'; -import { CommonCartridgeFileBuilder, ICommonCartridgeFileBuilderOptions } from './common-cartridge-file-builder'; import { CommonCartridgeResourceType, CommonCartridgeVersion } from './common-cartridge-enums'; +import { CommonCartridgeFileBuilder, CommonCartridgeFileBuilderOptions } from './common-cartridge-file-builder'; import { ICommonCartridgeOrganizationProps } from './common-cartridge-organization-item-element'; import { ICommonCartridgeResourceProps } from './common-cartridge-resource-item-element'; @@ -9,7 +9,7 @@ describe('CommonCartridgeFileBuilder', () => { let archive: AdmZip; const getFileContentAsString = (path: string): string | undefined => archive.getEntry(path)?.getData().toString(); - const fileBuilderOptions: ICommonCartridgeFileBuilderOptions = { + const fileBuilderOptions: CommonCartridgeFileBuilderOptions = { identifier: 'file-identifier', copyrightOwners: 'Placeholder Copyright', creationYear: 'Placeholder Creation Year', diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.ts index e119aba85fe..5a40269c57b 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.ts @@ -1,18 +1,18 @@ import AdmZip from 'adm-zip'; import { Builder } from 'xml2js'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; import { CommonCartridgeVersion } from './common-cartridge-enums'; -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; import { CommonCartridgeManifestElement } from './common-cartridge-manifest-element'; -import { - CommonCartridgeResourceItemElement, - ICommonCartridgeResourceProps, -} from './common-cartridge-resource-item-element'; import { CommonCartridgeOrganizationItemElement, ICommonCartridgeOrganizationProps, } from './common-cartridge-organization-item-element'; +import { + CommonCartridgeResourceItemElement, + ICommonCartridgeResourceProps, +} from './common-cartridge-resource-item-element'; -export type ICommonCartridgeFileBuilderOptions = { +export type CommonCartridgeFileBuilderOptions = { identifier: string; title: string; copyrightOwners: string; @@ -39,11 +39,11 @@ class CommonCartridgeOrganizationBuilder implements ICommonCartridgeOrganization private readonly zipBuilder: AdmZip ) {} - get organization(): ICommonCartridgeElement { + get organization(): CommonCartridgeElement { return new CommonCartridgeOrganizationItemElement(this.props); } - get resources(): ICommonCartridgeElement[] { + get resources(): CommonCartridgeElement[] { return this.props.resources.map( (resourceProps) => new CommonCartridgeResourceItemElement(resourceProps, this.xmlBuilder) ); @@ -68,7 +68,7 @@ export class CommonCartridgeFileBuilder implements ICommonCartridgeFileBuilder { private readonly resources = new Array(); - constructor(private readonly options: ICommonCartridgeFileBuilderOptions) {} + constructor(private readonly options: CommonCartridgeFileBuilderOptions) {} addOrganization(props: ICommonCartridgeOrganizationProps): ICommonCartridgeOrganizationBuilder { const organizationBuilder = new CommonCartridgeOrganizationBuilder(props, this.xmlBuilder, this.zipBuilder); diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file.interface.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file.interface.ts index 3d936699c64..0969e712b05 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file.interface.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file.interface.ts @@ -1,4 +1,4 @@ -export interface ICommonCartridgeFile { +export interface CommonCartridgeFile { canInline(): boolean; content(): string; } diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.ts index 993ed71a6d6..a374b3687a6 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.ts @@ -1,7 +1,7 @@ import { Builder } from 'xml2js'; -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; -import { ICommonCartridgeFile } from './common-cartridge-file.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; import { CommonCartridgeResourceType, CommonCartridgeVersion } from './common-cartridge-enums'; +import { CommonCartridgeFile } from './common-cartridge-file.interface'; export type ICommonCartridgeLtiResourceProps = { type: CommonCartridgeResourceType.LTI; @@ -13,7 +13,7 @@ export type ICommonCartridgeLtiResourceProps = { url: string; }; -export class CommonCartridgeLtiResource implements ICommonCartridgeElement, ICommonCartridgeFile { +export class CommonCartridgeLtiResource implements CommonCartridgeElement, CommonCartridgeFile { constructor(private readonly props: ICommonCartridgeLtiResourceProps, private readonly xmlBuilder: Builder) {} canInline(): boolean { diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.ts index 6411359726a..8e71b9adee4 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.ts @@ -1,19 +1,19 @@ -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; +import { CommonCartridgeVersion } from './common-cartridge-enums'; import { CommonCartridgeMetadataElement, ICommonCartridgeMetadataProps } from './common-cartridge-metadata-element'; import { CommonCartridgeOrganizationWrapperElement } from './common-cartridge-organization-wrapper-element'; import { CommonCartridgeResourceWrapperElement } from './common-cartridge-resource-wrapper-element'; -import { CommonCartridgeVersion } from './common-cartridge-enums'; export type ICommonCartridgeManifestProps = { identifier: string; }; -export class CommonCartridgeManifestElement implements ICommonCartridgeElement { +export class CommonCartridgeManifestElement implements CommonCartridgeElement { constructor( private readonly props: ICommonCartridgeManifestProps, private readonly metadataProps: ICommonCartridgeMetadataProps, - private readonly organizations: ICommonCartridgeElement[], - private readonly resources: ICommonCartridgeElement[] + private readonly organizations: CommonCartridgeElement[], + private readonly resources: CommonCartridgeElement[] ) {} transform(): Record { diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.ts index b15cc01d8d1..17a0cf45faa 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.ts @@ -1,4 +1,4 @@ -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; import { CommonCartridgeVersion } from './common-cartridge-enums'; export type ICommonCartridgeMetadataProps = { @@ -8,7 +8,7 @@ export type ICommonCartridgeMetadataProps = { version: CommonCartridgeVersion; }; -export class CommonCartridgeMetadataElement implements ICommonCartridgeElement { +export class CommonCartridgeMetadataElement implements CommonCartridgeElement { constructor(private readonly props: ICommonCartridgeMetadataProps) {} transform(): Record { diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.ts index e77fcbc0905..5d237fc3f98 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.ts @@ -1,4 +1,4 @@ -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; import { ICommonCartridgeResourceProps } from './common-cartridge-resource-item-element'; import { createIdentifier } from './utils'; @@ -9,7 +9,7 @@ export type ICommonCartridgeOrganizationProps = { resources: ICommonCartridgeResourceProps[]; }; -export class CommonCartridgeOrganizationItemElement implements ICommonCartridgeElement { +export class CommonCartridgeOrganizationItemElement implements CommonCartridgeElement { constructor(private readonly props: ICommonCartridgeOrganizationProps) {} transform(): Record { diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.spec.ts index 6f3e08793af..a26e40bc37c 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.spec.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.spec.ts @@ -1,9 +1,9 @@ -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; import { CommonCartridgeOrganizationWrapperElement } from './common-cartridge-organization-wrapper-element'; describe('CommonCartridgeOrganizationWrapperElement', () => { it('should transform the organization elements into the expected structure', () => { - const organizationElementsMock: ICommonCartridgeElement[] = [ + const organizationElementsMock: CommonCartridgeElement[] = [ { transform: jest.fn().mockReturnValue({ identifier: 'element-1' }), }, diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.ts index 12def098fd3..34200b31e37 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.ts @@ -1,7 +1,7 @@ -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; -export class CommonCartridgeOrganizationWrapperElement implements ICommonCartridgeElement { - constructor(private readonly organizationElements: ICommonCartridgeElement[]) {} +export class CommonCartridgeOrganizationWrapperElement implements CommonCartridgeElement { + constructor(private readonly organizationElements: CommonCartridgeElement[]) {} transform(): Record { return { diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.ts index 1ea4c909cb8..219e7296075 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.ts @@ -1,4 +1,7 @@ import { Builder } from 'xml2js'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; +import { CommonCartridgeResourceType } from './common-cartridge-enums'; +import { CommonCartridgeFile } from './common-cartridge-file.interface'; import { CommonCartridgeLtiResource, ICommonCartridgeLtiResourceProps } from './common-cartridge-lti-resource'; import { CommonCartridgeWebContentResource, @@ -8,17 +11,14 @@ import { CommonCartridgeWebLinkResourceElement, ICommonCartridgeWebLinkResourceProps, } from './common-cartridge-web-link-resource'; -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; -import { ICommonCartridgeFile } from './common-cartridge-file.interface'; -import { CommonCartridgeResourceType } from './common-cartridge-enums'; export type ICommonCartridgeResourceProps = | ICommonCartridgeLtiResourceProps | ICommonCartridgeWebContentResourceProps | ICommonCartridgeWebLinkResourceProps; -export class CommonCartridgeResourceItemElement implements ICommonCartridgeElement, ICommonCartridgeFile { - private readonly inner: ICommonCartridgeElement & ICommonCartridgeFile; +export class CommonCartridgeResourceItemElement implements CommonCartridgeElement, CommonCartridgeFile { + private readonly inner: CommonCartridgeElement & CommonCartridgeFile; constructor(props: ICommonCartridgeResourceProps, xmlBuilder: Builder) { if (props.type === CommonCartridgeResourceType.LTI) { diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.spec.ts index 8e267b33b11..cd96c9d6784 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.spec.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.spec.ts @@ -1,9 +1,9 @@ -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; import { CommonCartridgeResourceWrapperElement } from './common-cartridge-resource-wrapper-element'; describe('CommonCartridgeResourceWrapperElement', () => { it('should transform the resource elements into an array of transformed objects', () => { - const resourceElementsMock: ICommonCartridgeElement[] = [ + const resourceElementsMock: CommonCartridgeElement[] = [ { transform: jest.fn().mockReturnValue({ identifier: 'resource-1' }), }, diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.ts index 613090ae8a8..c188651f3d4 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.ts @@ -1,7 +1,7 @@ -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; -export class CommonCartridgeResourceWrapperElement implements ICommonCartridgeElement { - constructor(private readonly resourceElements: ICommonCartridgeElement[]) {} +export class CommonCartridgeResourceWrapperElement implements CommonCartridgeElement { + constructor(private readonly resourceElements: CommonCartridgeElement[]) {} transform(): Record { return { diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.ts index 740f4779a17..c45b981184f 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.ts @@ -1,10 +1,10 @@ -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; -import { ICommonCartridgeFile } from './common-cartridge-file.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; import { CommonCartridgeIntendedUseType, CommonCartridgeResourceType, CommonCartridgeVersion, } from './common-cartridge-enums'; +import { CommonCartridgeFile } from './common-cartridge-file.interface'; export type ICommonCartridgeWebContentResourceProps = { type: CommonCartridgeResourceType.WEB_CONTENT; @@ -16,7 +16,7 @@ export type ICommonCartridgeWebContentResourceProps = { intendedUse?: CommonCartridgeIntendedUseType; }; -export class CommonCartridgeWebContentResource implements ICommonCartridgeElement, ICommonCartridgeFile { +export class CommonCartridgeWebContentResource implements CommonCartridgeElement, CommonCartridgeFile { constructor(private readonly props: ICommonCartridgeWebContentResourceProps) {} canInline(): boolean { diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.ts index f9d78af76f1..09184d5ef0f 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.ts @@ -1,7 +1,7 @@ import { Builder } from 'xml2js'; -import { ICommonCartridgeElement } from './common-cartridge-element.interface'; -import { ICommonCartridgeFile } from './common-cartridge-file.interface'; +import { CommonCartridgeElement } from './common-cartridge-element.interface'; import { CommonCartridgeResourceType, CommonCartridgeVersion } from './common-cartridge-enums'; +import { CommonCartridgeFile } from './common-cartridge-file.interface'; export type ICommonCartridgeWebLinkResourceProps = { type: CommonCartridgeResourceType.WEB_LINK_V1 | CommonCartridgeResourceType.WEB_LINK_V3; @@ -12,7 +12,7 @@ export type ICommonCartridgeWebLinkResourceProps = { url: string; }; -export class CommonCartridgeWebLinkResourceElement implements ICommonCartridgeElement, ICommonCartridgeFile { +export class CommonCartridgeWebLinkResourceElement implements CommonCartridgeElement, CommonCartridgeFile { constructor(private readonly props: ICommonCartridgeWebLinkResourceProps, private readonly xmlBuilder: Builder) {} canInline(): boolean { diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge.config.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge.config.ts index 2041383512b..afbff1098a5 100644 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge.config.ts +++ b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge.config.ts @@ -1,3 +1,3 @@ -export interface ICommonCartridgeConfig { +export interface CommonCartridgeConfig { FEATURE_IMSCC_COURSE_EXPORT_ENABLED: boolean; } diff --git a/apps/server/src/modules/learnroom/controller/api-test/dashboard.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/dashboard.api.spec.ts index b6c27736d93..be9b9fad583 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/dashboard.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/dashboard.api.spec.ts @@ -1,13 +1,13 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { DashboardEntity, GridElement, Permission, User, RoleName } from '@shared/domain'; import { ICurrentUser } from '@modules/authentication'; -import { IDashboardRepo } from '@shared/repo'; -import { courseFactory, mapUserToCurrentUser, roleFactory, userFactory } from '@shared/testing'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { DashboardResponse } from '@modules/learnroom/controller/dto'; import { ServerTestModule } from '@modules/server/server.module'; +import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DashboardEntity, GridElement, Permission, RoleName, User } from '@shared/domain'; +import { IDashboardRepo } from '@shared/repo'; +import { courseFactory, mapUserToCurrentUser, roleFactory, userFactory } from '@shared/testing'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/learnroom/controller/api-test/rooms-copy-timeout.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/rooms-copy-timeout.api.spec.ts index 2941e185ca1..c73b9861c1f 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/rooms-copy-timeout.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/rooms-copy-timeout.api.spec.ts @@ -1,9 +1,12 @@ +import { createMock } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseFactory, @@ -12,11 +15,8 @@ import { roleFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { Request } from 'express'; import request from 'supertest'; -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { createMock } from '@golevelup/ts-jest'; // config must be set outside before the server module is importat, otherwise the configuration is already set const configBefore = Configuration.toObject({ plainSecrets: true }); diff --git a/apps/server/src/modules/learnroom/controller/api-test/rooms.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/rooms.api.spec.ts index 6db5100405a..ec6114f8f60 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/rooms.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/rooms.api.spec.ts @@ -1,6 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { CopyApiResponse } from '@modules/copy-helper'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { SingleColumnBoardResponse } from '@modules/learnroom/controller/dto'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Board, Course, Permission, Task } from '@shared/domain'; @@ -14,12 +20,6 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { CopyApiResponse } from '@modules/copy-helper'; -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { SingleColumnBoardResponse } from '@modules/learnroom/controller/dto'; -import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index dfb4e920957..7f026002ea7 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -1,13 +1,13 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Controller, Get, NotFoundException, Param, Query, Res, StreamableFile } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { ApiTags } from '@nestjs/swagger'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { PaginationParams } from '@shared/controller/'; import { Response } from 'express'; -import { ConfigService } from '@nestjs/config'; -import { CourseUc } from '../uc/course.uc'; -import { CourseExportUc } from '../uc/course-export.uc'; -import { CourseMetadataListResponse, CourseUrlParams, CourseQueryParams } from './dto'; import { CourseMapper } from '../mapper/course.mapper'; +import { CourseExportUc } from '../uc/course-export.uc'; +import { CourseUc } from '../uc/course.uc'; +import { CourseMetadataListResponse, CourseQueryParams, CourseUrlParams } from './dto'; @ApiTags('Courses') @Authenticate('jwt') diff --git a/apps/server/src/modules/learnroom/controller/dashboard.controller.spec.ts b/apps/server/src/modules/learnroom/controller/dashboard.controller.spec.ts index 284f18ef862..76ebe449829 100644 --- a/apps/server/src/modules/learnroom/controller/dashboard.controller.spec.ts +++ b/apps/server/src/modules/learnroom/controller/dashboard.controller.spec.ts @@ -1,3 +1,4 @@ +import { ICurrentUser } from '@modules/authentication'; import { Test, TestingModule } from '@nestjs/testing'; import { DashboardEntity, @@ -7,7 +8,6 @@ import { LearnroomMetadata, LearnroomTypes, } from '@shared/domain'; -import { ICurrentUser } from '@modules/authentication'; import { DashboardUc } from '../uc/dashboard.uc'; import { DashboardController } from './dashboard.controller'; import { DashboardResponse } from './dto'; diff --git a/apps/server/src/modules/learnroom/controller/dashboard.controller.ts b/apps/server/src/modules/learnroom/controller/dashboard.controller.ts index 224f6c41ca7..c2c2ff6d9ee 100644 --- a/apps/server/src/modules/learnroom/controller/dashboard.controller.ts +++ b/apps/server/src/modules/learnroom/controller/dashboard.controller.ts @@ -1,6 +1,6 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, Get, Param, Patch, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { DashboardMapper } from '../mapper/dashboard.mapper'; import { DashboardUc } from '../uc/dashboard.uc'; import { DashboardResponse, DashboardUrlParams, MoveElementParams, PatchGroupParams } from './dto'; diff --git a/apps/server/src/modules/learnroom/controller/rooms.controller.spec.ts b/apps/server/src/modules/learnroom/controller/rooms.controller.spec.ts index c63e2e380ae..4e10bd706a3 100644 --- a/apps/server/src/modules/learnroom/controller/rooms.controller.spec.ts +++ b/apps/server/src/modules/learnroom/controller/rooms.controller.spec.ts @@ -1,8 +1,8 @@ import { createMock } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { EntityId } from '@shared/domain'; import { ICurrentUser } from '@modules/authentication'; import { CopyApiResponse, CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { Test, TestingModule } from '@nestjs/testing'; +import { EntityId } from '@shared/domain'; import { RoomBoardResponseMapper } from '../mapper/room-board-response.mapper'; import { RoomBoardDTO } from '../types'; import { CourseCopyUC } from '../uc/course-copy.uc'; diff --git a/apps/server/src/modules/learnroom/controller/rooms.controller.ts b/apps/server/src/modules/learnroom/controller/rooms.controller.ts index 0e0b2f7c7a0..c81b40d6bba 100644 --- a/apps/server/src/modules/learnroom/controller/rooms.controller.ts +++ b/apps/server/src/modules/learnroom/controller/rooms.controller.ts @@ -1,9 +1,9 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; +import { serverConfig } from '@modules/server/server.config'; import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { RequestTimeout } from '@shared/common'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; -import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; -import { serverConfig } from '@modules/server/server.config'; import { RoomBoardResponseMapper } from '../mapper/room-board-response.mapper'; import { CourseCopyUC } from '../uc/course-copy.uc'; import { LessonCopyUC } from '../uc/lesson-copy.uc'; diff --git a/apps/server/src/modules/learnroom/index.ts b/apps/server/src/modules/learnroom/index.ts index e4d907784d5..9fe9c100886 100644 --- a/apps/server/src/modules/learnroom/index.ts +++ b/apps/server/src/modules/learnroom/index.ts @@ -1,3 +1,2 @@ export * from './learnroom.module'; -export * from './service/course-copy.service'; -export { CourseService } from './service'; +export { CommonCartridgeExportService, CourseCopyService, CourseService, RoomsService } from './service'; diff --git a/apps/server/src/modules/learnroom/learnroom-api.module.ts b/apps/server/src/modules/learnroom/learnroom-api.module.ts index 5cfaada65b8..a2a407daf21 100644 --- a/apps/server/src/modules/learnroom/learnroom-api.module.ts +++ b/apps/server/src/modules/learnroom/learnroom-api.module.ts @@ -1,9 +1,9 @@ -import { Module } from '@nestjs/common'; -import { BoardRepo, CourseRepo, DashboardModelMapper, DashboardRepo, LessonRepo, UserRepo } from '@shared/repo'; import { AuthorizationModule } from '@modules/authorization'; import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; import { CopyHelperModule } from '@modules/copy-helper'; import { LessonModule } from '@modules/lesson'; +import { Module } from '@nestjs/common'; +import { BoardRepo, CourseRepo, DashboardModelMapper, DashboardRepo, UserRepo } from '@shared/repo'; import { CourseController } from './controller/course.controller'; import { DashboardController } from './controller/dashboard.controller'; import { RoomsController } from './controller/rooms.controller'; @@ -42,7 +42,6 @@ import { CourseRepo, UserRepo, BoardRepo, - LessonRepo, ], }) export class LearnroomApiModule {} diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index c84310ba05e..02071369766 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -1,10 +1,10 @@ -import { Module } from '@nestjs/common'; -import { BoardRepo, CourseRepo, DashboardModelMapper, DashboardRepo, LessonRepo, UserRepo } from '@shared/repo'; -import { LoggerModule } from '@src/core/logger'; import { BoardModule } from '@modules/board'; import { CopyHelperModule } from '@modules/copy-helper'; import { LessonModule } from '@modules/lesson'; import { TaskModule } from '@modules/task'; +import { Module } from '@nestjs/common'; +import { BoardRepo, CourseRepo, DashboardModelMapper, DashboardRepo, UserRepo } from '@shared/repo'; +import { LoggerModule } from '@src/core/logger'; import { BoardCopyService, ColumnBoardTargetService, @@ -23,7 +23,6 @@ import { }, DashboardModelMapper, CourseRepo, - LessonRepo, BoardRepo, UserRepo, BoardCopyService, diff --git a/apps/server/src/modules/learnroom/mapper/board-taskStatus.mapper.ts b/apps/server/src/modules/learnroom/mapper/board-taskStatus.mapper.ts index 623a2fe7627..b493f0de91e 100644 --- a/apps/server/src/modules/learnroom/mapper/board-taskStatus.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/board-taskStatus.mapper.ts @@ -1,8 +1,8 @@ -import { ITaskStatus } from '@shared/domain'; +import { TaskStatus } from '@shared/domain'; import { BoardTaskStatusResponse } from '../controller/dto'; export class BoardTaskStatusMapper { - static mapToResponse(status: ITaskStatus): BoardTaskStatusResponse { + static mapToResponse(status: TaskStatus): BoardTaskStatusResponse { const dto = new BoardTaskStatusResponse(status); return dto; diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts index af1bc727d6a..8cd788f7793 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts @@ -1,18 +1,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { CourseService } from '@modules/learnroom/service'; +import { CommonCartridgeExportService } from '@modules/learnroom/service/common-cartridge-export.service'; +import { LessonService } from '@modules/lesson/service'; +import { TaskService } from '@modules/task/service/task.service'; import { Test, TestingModule } from '@nestjs/testing'; import { + ComponentProperties, + ComponentTextProperties, ComponentType, Course, - IComponentProperties, - IComponentTextProperties, LessonEntity, Task, } from '@shared/domain'; import { courseFactory, lessonFactory, setupEntities, taskFactory } from '@shared/testing'; -import { CommonCartridgeExportService } from '@modules/learnroom/service/common-cartridge-export.service'; -import { CourseService } from '@modules/learnroom/service'; -import { LessonService } from '@modules/lesson/service'; -import { TaskService } from '@modules/task/service/task.service'; import AdmZip from 'adm-zip'; import { CommonCartridgeVersion } from '../common-cartridge'; @@ -59,22 +59,22 @@ describe('CommonCartridgeExportService', () => { content: { text: 'text', }, - } as IComponentProperties, + } as ComponentProperties, { component: ComponentType.ETHERPAD, title: 'Etherpad', content: { url: 'url', }, - } as IComponentProperties, + } as ComponentProperties, { component: ComponentType.GEOGEBRA, title: 'Geogebra', content: { materialId: 'materialId', }, - } as IComponentProperties, - {} as IComponentProperties, + } as ComponentProperties, + {} as ComponentProperties, ], }); tasks = taskFactory.buildListWithId(5); @@ -87,8 +87,8 @@ describe('CommonCartridgeExportService', () => { describe('exportCourse', () => { const setupExport = async (version: CommonCartridgeVersion) => { const [lesson] = lessons; - const textContent = { text: 'Some random text' } as IComponentTextProperties; - const lessonContent: IComponentProperties = { + const textContent = { text: 'Some random text' } as ComponentTextProperties; + const lessonContent: ComponentProperties = { _id: 'random_id', title: 'A random title', hidden: false, diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts index e25d2a62367..4da9bfeedd0 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts @@ -1,8 +1,8 @@ -import { Injectable } from '@nestjs/common'; -import { Course, EntityId, IComponentProperties, Task } from '@shared/domain'; import { LessonService } from '@modules/lesson/service'; -import { ComponentType } from '@src/shared/domain/entity/lesson.entity'; import { TaskService } from '@modules/task/service'; +import { Injectable } from '@nestjs/common'; +import { ComponentProperties, Course, EntityId, Task } from '@shared/domain'; +import { ComponentType } from '@src/shared/domain/entity/lesson.entity'; import { CommonCartridgeFileBuilder, CommonCartridgeIntendedUseType, @@ -11,8 +11,8 @@ import { ICommonCartridgeResourceProps, ICommonCartridgeWebContentResourceProps, } from '../common-cartridge'; -import { CourseService } from './course.service'; import { createIdentifier } from '../common-cartridge/utils'; +import { CourseService } from './course.service'; @Injectable() export class CommonCartridgeExportService { @@ -84,7 +84,7 @@ export class CommonCartridgeExportService { private mapContentToResource( lessonId: string, - content: IComponentProperties, + content: ComponentProperties, version: CommonCartridgeVersion ): ICommonCartridgeResourceProps | undefined { const commonProps = { diff --git a/apps/server/src/modules/learnroom/service/rooms.service.spec.ts b/apps/server/src/modules/learnroom/service/rooms.service.spec.ts index 2358e2b2067..ef161ce179a 100644 --- a/apps/server/src/modules/learnroom/service/rooms.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/rooms.service.spec.ts @@ -2,19 +2,20 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; import { ObjectId } from '@mikro-orm/mongodb'; +import { CardService, ColumnBoardService, ColumnService, ContentElementService } from '@modules/board'; +import { LessonService } from '@modules/lesson'; +import { TaskService } from '@modules/task'; import { Test, TestingModule } from '@nestjs/testing'; import { BoardExternalReference, BoardExternalReferenceType, EntityId } from '@shared/domain'; -import { BoardRepo, LessonRepo } from '@shared/repo'; +import { BoardRepo } from '@shared/repo'; import { boardFactory, courseFactory, lessonFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; -import { CardService, ColumnBoardService, ColumnService, ContentElementService } from '@modules/board'; -import { TaskService } from '@modules/task/service'; import { ColumnBoardTargetService } from './column-board-target.service'; import { RoomsService } from './rooms.service'; describe('rooms service', () => { let module: TestingModule; let roomsService: RoomsService; - let lessonRepo: DeepMocked; + let lessonService: DeepMocked; let taskService: DeepMocked; let boardRepo: DeepMocked; let columnBoardService: DeepMocked; @@ -32,8 +33,8 @@ describe('rooms service', () => { providers: [ RoomsService, { - provide: LessonRepo, - useValue: createMock(), + provide: LessonService, + useValue: createMock(), }, { provide: TaskService, @@ -66,7 +67,7 @@ describe('rooms service', () => { ], }).compile(); roomsService = module.get(RoomsService); - lessonRepo = module.get(LessonRepo); + lessonService = module.get(LessonService); taskService = module.get(TaskService); boardRepo = module.get(BoardRepo); columnBoardService = module.get(ColumnBoardService); @@ -90,7 +91,7 @@ describe('rooms service', () => { board.syncBoardElementReferences([...tasks, ...lessons]); const tasksSpy = taskService.findBySingleParent.mockResolvedValue([tasks, 3]); - const lessonsSpy = lessonRepo.findAllByCourseIds.mockResolvedValue([lessons, 3]); + const lessonsSpy = lessonService.findByCourseIds.mockResolvedValue([lessons, 3]); const syncBoardElementReferencesSpy = jest.spyOn(board, 'syncBoardElementReferences'); const saveSpy = boardRepo.save.mockResolvedValue(); @@ -134,7 +135,7 @@ describe('rooms service', () => { describe('for column boards', () => { const setup = () => { - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); taskService.findBySingleParent.mockResolvedValueOnce([[], 0]); const user = userFactory.buildWithId(); diff --git a/apps/server/src/modules/learnroom/service/rooms.service.ts b/apps/server/src/modules/learnroom/service/rooms.service.ts index cc8b95e09b0..19ddfd57be8 100644 --- a/apps/server/src/modules/learnroom/service/rooms.service.ts +++ b/apps/server/src/modules/learnroom/service/rooms.service.ts @@ -1,23 +1,24 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { ColumnBoardService } from '@modules/board'; +import { LessonService } from '@modules/lesson'; +import { TaskService } from '@modules/task'; import { Injectable } from '@nestjs/common'; import { Board, BoardExternalReferenceType, ColumnBoardTarget, EntityId } from '@shared/domain'; -import { BoardRepo, LessonRepo } from '@shared/repo'; -import { ColumnBoardService } from '@modules/board'; -import { TaskService } from '@modules/task/service'; +import { BoardRepo } from '@shared/repo'; import { ColumnBoardTargetService } from './column-board-target.service'; @Injectable() export class RoomsService { constructor( private readonly taskService: TaskService, - private readonly lessonRepo: LessonRepo, + private readonly lessonService: LessonService, private readonly boardRepo: BoardRepo, private readonly columnBoardService: ColumnBoardService, private readonly columnBoardTargetService: ColumnBoardTargetService ) {} async updateBoard(board: Board, roomId: EntityId, userId: EntityId): Promise { - const [courseLessons] = await this.lessonRepo.findAllByCourseIds([roomId]); + const [courseLessons] = await this.lessonService.findByCourseIds([roomId]); const [courseTasks] = await this.taskService.findBySingleParent(userId, roomId); const courseColumnBoardTargets = await this.handleColumnBoardIntegration(roomId); diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index 750c9b1a287..3ab86b82c65 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { SortOrder } from '@shared/domain'; -import { CourseRepo, LessonRepo } from '@shared/repo'; +import { CourseRepo } from '@shared/repo'; import { courseFactory, setupEntities } from '@shared/testing'; import { CourseUc } from './course.uc'; @@ -19,10 +19,6 @@ describe('CourseUc', () => { provide: CourseRepo, useValue: createMock(), }, - { - provide: LessonRepo, - useValue: createMock(), - }, ], }).compile(); diff --git a/apps/server/src/modules/learnroom/uc/dashboard.uc.ts b/apps/server/src/modules/learnroom/uc/dashboard.uc.ts index 9bade9a6d71..1e1df78fcba 100644 --- a/apps/server/src/modules/learnroom/uc/dashboard.uc.ts +++ b/apps/server/src/modules/learnroom/uc/dashboard.uc.ts @@ -1,6 +1,6 @@ -import { Injectable, Inject, NotFoundException } from '@nestjs/common'; -import { DashboardEntity, EntityId, GridPositionWithGroupIndex, GridPosition, SortOrder } from '@shared/domain'; -import { IDashboardRepo, CourseRepo } from '@shared/repo'; +import { Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { DashboardEntity, EntityId, GridPosition, GridPositionWithGroupIndex, SortOrder } from '@shared/domain'; +import { CourseRepo, IDashboardRepo } from '@shared/repo'; // import { NotFound } from '@feathersjs/errors'; // wrong import? see NotFoundException @Injectable() diff --git a/apps/server/src/modules/learnroom/uc/lesson-copy.uc.spec.ts b/apps/server/src/modules/learnroom/uc/lesson-copy.uc.spec.ts index 49c7c53435c..6f148abc94f 100644 --- a/apps/server/src/modules/learnroom/uc/lesson-copy.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/lesson-copy.uc.spec.ts @@ -1,21 +1,21 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; import { ObjectId } from '@mikro-orm/mongodb'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@modules/copy-helper'; +import { LessonCopyService, LessonService } from '@modules/lesson'; import { ForbiddenException, InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { CourseRepo, LessonRepo, UserRepo } from '@shared/repo'; +import { CourseRepo, UserRepo } from '@shared/repo'; import { courseFactory, lessonFactory, setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@modules/copy-helper'; -import { EtherpadService, LessonCopyService } from '@modules/lesson/service'; import { LessonCopyUC } from './lesson-copy.uc'; describe('lesson copy uc', () => { let module: TestingModule; let uc: LessonCopyUC; let userRepo: DeepMocked; - let lessonRepo: DeepMocked; + let lessonService: DeepMocked; let courseRepo: DeepMocked; let authorisation: DeepMocked; let lessonCopyService: DeepMocked; @@ -35,8 +35,8 @@ describe('lesson copy uc', () => { useValue: createMock(), }, { - provide: LessonRepo, - useValue: createMock(), + provide: LessonService, + useValue: createMock(), }, { provide: CourseRepo, @@ -54,16 +54,12 @@ describe('lesson copy uc', () => { provide: CopyHelperService, useValue: createMock(), }, - { - provide: EtherpadService, - useValue: createMock(), - }, ], }).compile(); uc = module.get(LessonCopyUC); userRepo = module.get(UserRepo); - lessonRepo = module.get(LessonRepo); + lessonService = module.get(LessonService); authorisation = module.get(AuthorizationService); courseRepo = module.get(CourseRepo); lessonCopyService = module.get(LessonCopyService); @@ -128,8 +124,8 @@ describe('lesson copy uc', () => { authorisation.getUserWithPermissions.mockResolvedValueOnce(user); authorisation.hasPermission.mockReturnValue(true); - lessonRepo.findById.mockResolvedValueOnce(lesson); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([allLessons, allLessons.length]); + lessonService.findById.mockResolvedValueOnce(lesson); + lessonService.findByCourseIds.mockResolvedValueOnce([allLessons, allLessons.length]); courseRepo.findById.mockResolvedValueOnce(course); lessonCopyService.copyLesson.mockResolvedValueOnce(status); @@ -185,8 +181,8 @@ describe('lesson copy uc', () => { authorisation.getUserWithPermissions.mockResolvedValueOnce(user); authorisation.hasPermission.mockReturnValue(true); - lessonRepo.findById.mockResolvedValueOnce(lesson); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([allLessons, allLessons.length]); + lessonService.findById.mockResolvedValueOnce(lesson); + lessonService.findByCourseIds.mockResolvedValueOnce([allLessons, allLessons.length]); courseRepo.findById.mockResolvedValueOnce(course); lessonCopyService.copyLesson.mockResolvedValueOnce(status); @@ -221,7 +217,7 @@ describe('lesson copy uc', () => { await uc.copyLesson(userId, lessonId, parentParams); - expect(lessonRepo.findById).toBeCalledWith(lessonId); + expect(lessonService.findById).toBeCalledWith(lessonId); }); it('should fetch destination course', async () => { @@ -285,7 +281,7 @@ describe('lesson copy uc', () => { await uc.copyLesson(userId, lessonId, parentParams); - expect(lessonRepo.findAllByCourseIds).toHaveBeenCalledWith([courseId]); + expect(lessonService.findByCourseIds).toHaveBeenCalledWith([courseId]); }); }); @@ -300,7 +296,7 @@ describe('lesson copy uc', () => { const parentParams = { courseId: course.id, userId: new ObjectId().toHexString() }; userRepo.findById.mockResolvedValueOnce(user); - lessonRepo.findById.mockResolvedValueOnce(lesson); + lessonService.findById.mockResolvedValueOnce(lesson); courseRepo.findById.mockResolvedValueOnce(course); authorisation.hasPermission.mockReturnValueOnce(false); @@ -331,7 +327,7 @@ describe('lesson copy uc', () => { const parentParams = { courseId: course.id, userId: new ObjectId().toHexString() }; userRepo.findById.mockResolvedValueOnce(user); - lessonRepo.findById.mockResolvedValueOnce(lesson); + lessonService.findById.mockResolvedValueOnce(lesson); courseRepo.findById.mockResolvedValueOnce(course); authorisation.checkPermission.mockImplementationOnce(() => { throw new ForbiddenException(); diff --git a/apps/server/src/modules/learnroom/uc/lesson-copy.uc.ts b/apps/server/src/modules/learnroom/uc/lesson-copy.uc.ts index ae41fb6881b..c5c75a6e011 100644 --- a/apps/server/src/modules/learnroom/uc/lesson-copy.uc.ts +++ b/apps/server/src/modules/learnroom/uc/lesson-copy.uc.ts @@ -1,19 +1,18 @@ import { Configuration } from '@hpi-schul-cloud/commons'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CopyHelperService, CopyStatus } from '@modules/copy-helper'; +import { LessonCopyParentParams, LessonCopyService, LessonService } from '@modules/lesson'; import { ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common'; import { Course, EntityId, LessonEntity, User } from '@shared/domain'; import { Permission } from '@shared/domain/interface/permission.enum'; -import { CourseRepo, LessonRepo } from '@shared/repo'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { CopyHelperService, CopyStatus } from '@modules/copy-helper'; -import { LessonCopyParentParams } from '@modules/lesson'; -import { LessonCopyService } from '@modules/lesson/service'; +import { CourseRepo } from '@shared/repo'; @Injectable() export class LessonCopyUC { constructor( private readonly authorisation: AuthorizationService, private readonly lessonCopyService: LessonCopyService, - private readonly lessonRepo: LessonRepo, + private readonly lessonService: LessonService, private readonly courseRepo: CourseRepo, private readonly copyHelperService: CopyHelperService ) {} @@ -23,7 +22,7 @@ export class LessonCopyUC { const [user, originalLesson]: [User, LessonEntity] = await Promise.all([ this.authorisation.getUserWithPermissions(userId), - this.lessonRepo.findById(lessonId), + this.lessonService.findById(lessonId), ]); this.checkOriginalLessonAuthorization(user, originalLesson); @@ -37,7 +36,7 @@ export class LessonCopyUC { this.checkDestinationCourseAuthorization(user, destinationCourse); // should be a seperate private method - const [existingLessons] = await this.lessonRepo.findAllByCourseIds([originalLesson.course.id]); + const [existingLessons] = await this.lessonService.findByCourseIds([originalLesson.course.id]); const existingNames = existingLessons.map((l) => l.name); const copyName = this.copyHelperService.deriveCopyName(originalLesson.name, existingNames); diff --git a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts index 91bd043d472..0a74a5a34e8 100644 --- a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts +++ b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts @@ -1,4 +1,5 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { Action, AuthorizationService } from '@modules/authorization'; import { Injectable } from '@nestjs/common'; import { Board, @@ -7,14 +8,13 @@ import { ColumnboardBoardElement, ColumnBoardTarget, Course, - ITaskStatus, LessonEntity, Permission, Task, + TaskStatus, TaskWithStatusVo, User, } from '@shared/domain'; -import { AuthorizationService, Action } from '@modules/authorization'; import { ColumnBoardMetaData, LessonMetaData, @@ -126,8 +126,8 @@ class DtoCreator { return { type: RoomBoardElementTypes.TASK, content }; } - private createTaskStatus(task: Task): ITaskStatus { - let status: ITaskStatus; + private createTaskStatus(task: Task): TaskStatus { + let status: TaskStatus; if (this.isTeacher()) { status = task.createTeacherStatusForUser(this.user); } else { diff --git a/apps/server/src/modules/learnroom/uc/rooms.uc.spec.ts b/apps/server/src/modules/learnroom/uc/rooms.uc.spec.ts index 7c6b82aad84..69faa5c8fd7 100644 --- a/apps/server/src/modules/learnroom/uc/rooms.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/rooms.uc.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardRepo, CourseRepo, LessonRepo, TaskRepo, UserRepo } from '@shared/repo'; +import { BoardRepo, CourseRepo, TaskRepo, UserRepo } from '@shared/repo'; import { boardFactory, courseFactory, lessonFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; import { RoomsService } from '../service/rooms.service'; import { RoomBoardDTOFactory } from './room-board-dto.factory'; @@ -11,7 +11,6 @@ import { RoomsUc } from './rooms.uc'; describe('rooms usecase', () => { let uc: RoomsUc; let courseRepo: DeepMocked; - let lessonRepo: DeepMocked; let taskRepo: DeepMocked; let userRepo: DeepMocked; let boardRepo: DeepMocked; @@ -33,10 +32,6 @@ describe('rooms usecase', () => { provide: CourseRepo, useValue: createMock(), }, - { - provide: LessonRepo, - useValue: createMock(), - }, { provide: TaskRepo, useValue: createMock(), @@ -66,7 +61,6 @@ describe('rooms usecase', () => { uc = module.get(RoomsUc); courseRepo = module.get(CourseRepo); - lessonRepo = module.get(LessonRepo); taskRepo = module.get(TaskRepo); userRepo = module.get(UserRepo); boardRepo = module.get(BoardRepo); @@ -97,7 +91,6 @@ describe('rooms usecase', () => { const roomSpy = courseRepo.findOne.mockResolvedValue(room); const boardSpy = boardRepo.findByCourseId.mockResolvedValue(board); const tasksSpy = taskRepo.findBySingleParent.mockResolvedValue([tasks, 3]); - const lessonsSpy = lessonRepo.findAllByCourseIds.mockResolvedValue([lessons, 3]); const syncBoardElementReferencesSpy = jest.spyOn(board, 'syncBoardElementReferences'); const mapperSpy = factory.createDTO.mockReturnValue(roomBoardDTO); const saveSpy = boardRepo.save.mockResolvedValue(); @@ -114,7 +107,6 @@ describe('rooms usecase', () => { roomSpy, boardSpy, tasksSpy, - lessonsSpy, syncBoardElementReferencesSpy, mapperSpy, saveSpy, diff --git a/apps/server/src/modules/lesson/controller/lesson.controller.ts b/apps/server/src/modules/lesson/controller/lesson.controller.ts index 14762a47d8c..66e52b06478 100644 --- a/apps/server/src/modules/lesson/controller/lesson.controller.ts +++ b/apps/server/src/modules/lesson/controller/lesson.controller.ts @@ -1,6 +1,6 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Controller, Delete, Param } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { LessonUC } from '../uc'; import { LessonUrlParams } from './dto'; diff --git a/apps/server/src/modules/lesson/index.ts b/apps/server/src/modules/lesson/index.ts index 61e512d84b7..b552bf9c988 100644 --- a/apps/server/src/modules/lesson/index.ts +++ b/apps/server/src/modules/lesson/index.ts @@ -1,3 +1,5 @@ export * from './lesson.module'; +export * from './service/lesson-copy.service'; +export * from './service/lesson.service'; export * from './types/lesson-copy-parent.params'; export * from './types/lesson-copy.params'; diff --git a/apps/server/src/modules/lesson/lesson.module.ts b/apps/server/src/modules/lesson/lesson.module.ts index dde1eb157ec..3a009550010 100644 --- a/apps/server/src/modules/lesson/lesson.module.ts +++ b/apps/server/src/modules/lesson/lesson.module.ts @@ -1,10 +1,10 @@ -import { Module } from '@nestjs/common'; import { FeathersServiceProvider } from '@infra/feathers'; -import { LessonRepo } from '@shared/repo'; -import { LoggerModule } from '@src/core/logger'; import { CopyHelperModule } from '@modules/copy-helper'; import { FilesStorageClientModule } from '@modules/files-storage-client'; import { TaskModule } from '@modules/task'; +import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; +import { LessonRepo } from './repository'; import { EtherpadService, LessonCopyService, LessonService, NexboardService } from './service'; @Module({ diff --git a/apps/server/src/shared/repo/lesson/index.ts b/apps/server/src/modules/lesson/repository/index.ts similarity index 100% rename from apps/server/src/shared/repo/lesson/index.ts rename to apps/server/src/modules/lesson/repository/index.ts diff --git a/apps/server/src/shared/repo/lesson/lesson-scope.ts b/apps/server/src/modules/lesson/repository/lesson-scope.ts similarity index 90% rename from apps/server/src/shared/repo/lesson/lesson-scope.ts rename to apps/server/src/modules/lesson/repository/lesson-scope.ts index ef99b7624c1..81529b03f88 100644 --- a/apps/server/src/shared/repo/lesson/lesson-scope.ts +++ b/apps/server/src/modules/lesson/repository/lesson-scope.ts @@ -1,5 +1,5 @@ import { EntityId, LessonEntity } from '@shared/domain'; -import { Scope } from '../scope'; +import { Scope } from '@shared/repo'; export class LessonScope extends Scope { byCourseIds(courseIds: EntityId[]): LessonScope { diff --git a/apps/server/src/shared/repo/lesson/lesson.repo.integration.spec.ts b/apps/server/src/modules/lesson/repository/lesson.repo.integration.spec.ts similarity index 97% rename from apps/server/src/shared/repo/lesson/lesson.repo.integration.spec.ts rename to apps/server/src/modules/lesson/repository/lesson.repo.integration.spec.ts index eae071d55ae..d6bdafd1971 100644 --- a/apps/server/src/shared/repo/lesson/lesson.repo.integration.spec.ts +++ b/apps/server/src/modules/lesson/repository/lesson.repo.integration.spec.ts @@ -1,6 +1,6 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { ComponentType, IComponentProperties, LessonEntity } from '@shared/domain'; +import { ComponentProperties, ComponentType, LessonEntity } from '@shared/domain'; import { cleanupCollections, courseFactory, lessonFactory, materialFactory, taskFactory } from '@shared/testing'; import { MongoMemoryDatabaseModule } from '@infra/database'; @@ -165,7 +165,7 @@ describe('LessonRepo', () => { it('should return lessons which contains a specific userId', async () => { // Arrange const userId = new ObjectId().toHexString(); - const contentExample: IComponentProperties = { + const contentExample: ComponentProperties = { title: 'title', hidden: false, user: userId, @@ -195,7 +195,7 @@ describe('LessonRepo', () => { it('should update Lessons without deleted user', async () => { // Arrange const userId = new ObjectId().toHexString(); - const contentExample: IComponentProperties = { + const contentExample: ComponentProperties = { title: 'title', hidden: false, user: userId, diff --git a/apps/server/src/shared/repo/lesson/lesson.repo.ts b/apps/server/src/modules/lesson/repository/lesson.repo.ts similarity index 97% rename from apps/server/src/shared/repo/lesson/lesson.repo.ts rename to apps/server/src/modules/lesson/repository/lesson.repo.ts index 26f66e9587d..c2fc2f0269e 100644 --- a/apps/server/src/shared/repo/lesson/lesson.repo.ts +++ b/apps/server/src/modules/lesson/repository/lesson.repo.ts @@ -1,7 +1,7 @@ +import { EntityDictionary } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { Counted, EntityId, LessonEntity, SortOrder } from '@shared/domain'; -import { EntityDictionary } from '@mikro-orm/core'; -import { BaseRepo } from '../base.repo'; +import { BaseRepo } from '@shared/repo'; import { LessonScope } from './lesson-scope'; @Injectable() diff --git a/apps/server/src/modules/lesson/service/lesson-copy.service.spec.ts b/apps/server/src/modules/lesson/service/lesson-copy.service.spec.ts index 34392c91c0c..05f9e452aa5 100644 --- a/apps/server/src/modules/lesson/service/lesson-copy.service.spec.ts +++ b/apps/server/src/modules/lesson/service/lesson-copy.service.spec.ts @@ -1,21 +1,23 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyFilesService } from '@modules/files-storage-client'; +import { TaskCopyService } from '@modules/task'; import { Test, TestingModule } from '@nestjs/testing'; import { BaseEntity, + ComponentEtherpadProperties, + ComponentGeogebraProperties, + ComponentInternalProperties, + ComponentNexboardProperties, + ComponentProperties, + ComponentTextProperties, ComponentType, EntityId, - IComponentEtherpadProperties, - IComponentGeogebraProperties, - IComponentInternalProperties, - IComponentNexboardProperties, - IComponentProperties, - IComponentTextProperties, LessonEntity, Material, } from '@shared/domain'; import { AuthorizableObject } from '@shared/domain/domain-object'; -import { LessonRepo } from '@shared/repo'; import { courseFactory, lessonFactory, @@ -24,9 +26,7 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; -import { CopyFilesService } from '@modules/files-storage-client'; -import { TaskCopyService } from '@modules/task/service'; +import { LessonRepo } from '../repository'; import { EtherpadService } from './etherpad.service'; import { LessonCopyService } from './lesson-copy.service'; import { NexboardService } from './nexboard.service'; @@ -304,7 +304,7 @@ describe('lesson copy service', () => { describe('when lesson contains at least one content element', () => { const setup = () => { - const contentOne: IComponentProperties = { + const contentOne: ComponentProperties = { title: 'title component 1', hidden: false, component: ComponentType.TEXT, @@ -312,7 +312,7 @@ describe('lesson copy service', () => { text: 'this is a text content', }, }; - const contentTwo: IComponentProperties = { + const contentTwo: ComponentProperties = { title: 'title component 2', hidden: false, component: ComponentType.LERNSTORE, @@ -405,7 +405,7 @@ describe('lesson copy service', () => { describe('when lesson contains text content element', () => { const setup = (text = 'this is a text content') => { - const textContent: IComponentProperties = { + const textContent: ComponentProperties = { title: 'text component 1', hidden: false, component: ComponentType.TEXT, @@ -467,13 +467,13 @@ describe('lesson copy service', () => { const lessonCopy = status.copyEntity as LessonEntity; const contentsStatus = status.elements?.find((el) => el.type === CopyElementType.LESSON_CONTENT_GROUP); expect(contentsStatus).toBeDefined(); - expect((lessonCopy.contents[0].content as IComponentTextProperties).text).not.toContain(FILE_ID_TO_BE_REPLACED); + expect((lessonCopy.contents[0].content as ComponentTextProperties).text).not.toContain(FILE_ID_TO_BE_REPLACED); }); }); describe('when lesson contains LernStore content element', () => { const setup = () => { - const lernStoreContent: IComponentProperties = { + const lernStoreContent: ComponentProperties = { title: 'text component 1', hidden: false, component: ComponentType.LERNSTORE, @@ -510,7 +510,7 @@ describe('lesson copy service', () => { user, }); - const copiedLessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; + const copiedLessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; expect(copiedLessonContents[0]).toEqual(lernStoreContent); }); @@ -533,7 +533,7 @@ describe('lesson copy service', () => { describe('when lesson contains LernStore content element without set resource', () => { const setup = () => { - const lernStoreContent: IComponentProperties = { + const lernStoreContent: ComponentProperties = { title: 'text component 1', hidden: false, component: ComponentType.LERNSTORE, @@ -559,7 +559,7 @@ describe('lesson copy service', () => { user, }); - const copiedLessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; + const copiedLessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; expect(copiedLessonContents[0]).toEqual(lernStoreContent); }); @@ -582,7 +582,7 @@ describe('lesson copy service', () => { describe('when lesson contains geoGebra content element', () => { const setup = () => { - const geoGebraContent: IComponentProperties = { + const geoGebraContent: ComponentProperties = { title: 'text component 1', hidden: false, component: ComponentType.GEOGEBRA, @@ -611,8 +611,8 @@ describe('lesson copy service', () => { user, }); - const lessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; - const geoGebraContent = lessonContents[0].content as IComponentGeogebraProperties; + const lessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; + const geoGebraContent = lessonContents[0].content as ComponentGeogebraProperties; expect(geoGebraContent.materialId).toEqual(''); }); @@ -626,7 +626,7 @@ describe('lesson copy service', () => { user, }); - const lessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; + const lessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; expect(lessonContents[0].hidden).toEqual(true); }); @@ -815,7 +815,7 @@ describe('lesson copy service', () => { describe('when lesson contains Etherpad content element', () => { const setup = () => { - const etherpadContent: IComponentProperties = { + const etherpadContent: ComponentProperties = { title: 'text', hidden: false, component: ComponentType.ETHERPAD, @@ -859,7 +859,7 @@ describe('lesson copy service', () => { user, }); - const lessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; + const lessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; expect(configurationSpy).toHaveBeenCalledWith('FEATURE_ETHERPAD_ENABLED'); expect(etherpadService.createEtherpad).not.toHaveBeenCalled(); expect(lessonContents).toEqual([]); @@ -897,7 +897,7 @@ describe('lesson copy service', () => { } expect(contentStatus).toEqual(CopyStatusEnum.FAIL); - const lessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; + const lessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; expect(lessonContents.length).toEqual(0); }); @@ -911,8 +911,8 @@ describe('lesson copy service', () => { destinationCourse, user, }); - const copiedLessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; - const copiedEtherpad = copiedLessonContents[0].content as IComponentEtherpadProperties; + const copiedLessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; + const copiedEtherpad = copiedLessonContents[0].content as ComponentEtherpadProperties; expect(copiedEtherpad.url).toEqual('http://pad.uri/abc'); }); @@ -939,7 +939,7 @@ describe('lesson copy service', () => { const user = userFactory.build(); const originalCourse = courseFactory.build({ school: user.school, teachers: [user] }); const destinationCourse = courseFactory.build({ school: user.school, teachers: [user] }); - const embeddedTaskContent: IComponentProperties = { + const embeddedTaskContent: ComponentProperties = { title: 'title', hidden: false, component: ComponentType.INTERNAL, @@ -1008,7 +1008,7 @@ describe('lesson copy service', () => { describe('when lesson contains neXboard content element', () => { const setup = () => { - const nexboardContent: IComponentProperties = { + const nexboardContent: ComponentProperties = { title: 'text', hidden: false, component: ComponentType.NEXBOARD, @@ -1050,7 +1050,7 @@ describe('lesson copy service', () => { user, }); - const lessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; + const lessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; expect(configurationSpy).toHaveBeenCalledWith('FEATURE_NEXBOARD_ENABLED'); expect(nexboardService.createNexboard).not.toHaveBeenCalled(); expect(lessonContents).toEqual([]); @@ -1088,7 +1088,7 @@ describe('lesson copy service', () => { } expect(contentStatus).toEqual(CopyStatusEnum.FAIL); - const lessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; + const lessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; expect(lessonContents.length).toEqual(0); }); @@ -1102,8 +1102,8 @@ describe('lesson copy service', () => { destinationCourse, user, }); - const copiedLessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; - const copiedNexboard = copiedLessonContents[0].content as IComponentNexboardProperties; + const copiedLessonContents = (status.copyEntity as LessonEntity).contents as ComponentProperties[]; + const copiedNexboard = copiedLessonContents[0].content as ComponentNexboardProperties; expect(copiedNexboard.url).toEqual('abc'); expect(copiedNexboard.board).toEqual('123'); }); @@ -1324,7 +1324,7 @@ describe('lesson copy service', () => { const copiedLesson = lessonFactory.buildWithId(); const originalTask = taskFactory.buildWithId({ lesson: originalLesson }); const copiedTask = taskFactory.buildWithId({ lesson: copiedLesson }); - const embeddedTaskContent: IComponentProperties = { + const embeddedTaskContent: ComponentProperties = { title: 'title', hidden: false, component: ComponentType.INTERNAL, @@ -1332,7 +1332,7 @@ describe('lesson copy service', () => { url: `http://somebasedomain.de/homeworks/${originalTask.id}`, }, }; - const textContent: IComponentProperties = { + const textContent: ComponentProperties = { title: 'title component', hidden: false, component: ComponentType.TEXT, @@ -1388,8 +1388,8 @@ describe('lesson copy service', () => { throw new Error('lesson should be part of the copy'); } const content = lesson.contents.find((el) => el.component === ComponentType.INTERNAL); - expect((content?.content as IComponentInternalProperties).url).not.toContain(originalTask.id); - expect((content?.content as IComponentInternalProperties).url).toContain(copiedTask.id); + expect((content?.content as ComponentInternalProperties).url).not.toContain(originalTask.id); + expect((content?.content as ComponentInternalProperties).url).toContain(copiedTask.id); }); it('should maintain order of content elements', () => { @@ -1413,7 +1413,7 @@ describe('lesson copy service', () => { throw new Error('lesson should be part of the copy'); } const content = lesson.contents.find((el) => el.component === ComponentType.INTERNAL); - expect((content?.content as IComponentInternalProperties).url).toEqual( + expect((content?.content as ComponentInternalProperties).url).toEqual( `http://somebasedomain.de/homeworks/${originalTask.id}` ); }); diff --git a/apps/server/src/modules/lesson/service/lesson-copy.service.ts b/apps/server/src/modules/lesson/service/lesson-copy.service.ts index b6d7e7849c7..00cf3cee8b9 100644 --- a/apps/server/src/modules/lesson/service/lesson-copy.service.ts +++ b/apps/server/src/modules/lesson/service/lesson-copy.service.ts @@ -1,22 +1,21 @@ import { Configuration } from '@hpi-schul-cloud/commons'; +import { CopyDictionary, CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyFilesService, FileUrlReplacement } from '@modules/files-storage-client'; +import { TaskCopyService } from '@modules/task/service/task-copy.service'; import { Injectable } from '@nestjs/common'; import { + ComponentEtherpadProperties, + ComponentGeogebraProperties, + ComponentLernstoreProperties, + ComponentNexboardProperties, + ComponentProperties, + ComponentTextProperties, ComponentType, - IComponentEtherpadProperties, - IComponentGeogebraProperties, - IComponentLernstoreProperties, - IComponentNexboardProperties, - IComponentProperties, - IComponentTextProperties, LessonEntity, Material, } from '@shared/domain'; -import { LessonRepo } from '@shared/repo'; -import { CopyDictionary, CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; -import { CopyFilesService } from '@modules/files-storage-client'; -import { FileUrlReplacement } from '@modules/files-storage-client/service/copy-files.service'; -import { TaskCopyService } from '@modules/task/service/task-copy.service'; import { randomBytes } from 'crypto'; +import { LessonRepo } from '../repository'; import { LessonCopyParams } from '../types'; import { EtherpadService } from './etherpad.service'; import { NexboardService } from './nexboard.service'; @@ -109,7 +108,7 @@ export class LessonCopyService { return lessonStatus; } - copiedLesson.contents = copiedLesson.contents.map((value: IComponentProperties) => + copiedLesson.contents = copiedLesson.contents.map((value: ComponentProperties) => this.updateCopiedEmbeddedTaskId(value, copyDict) ); @@ -118,10 +117,7 @@ export class LessonCopyService { return lessonStatus; } - private updateCopiedEmbeddedTaskId = ( - value: IComponentProperties, - copyDict: CopyDictionary - ): IComponentProperties => { + private updateCopiedEmbeddedTaskId = (value: ComponentProperties, copyDict: CopyDictionary): ComponentProperties => { if (value.component !== ComponentType.INTERNAL || value.content === undefined || value.content.url === undefined) { return value; } @@ -145,10 +141,10 @@ export class LessonCopyService { }; private replaceUrlsInContents( - contents: IComponentProperties[], + contents: ComponentProperties[], fileUrlReplacements: FileUrlReplacement[] - ): IComponentProperties[] { - contents = contents.map((item: IComponentProperties) => { + ): ComponentProperties[] { + contents = contents.map((item: ComponentProperties) => { if (item.component === 'text' && item.content && 'text' in item.content && item.content.text) { let { text } = item.content; fileUrlReplacements.forEach(({ regex, replacement }) => { @@ -163,15 +159,15 @@ export class LessonCopyService { } private async copyLessonContent( - content: IComponentProperties[], + content: ComponentProperties[], params: LessonCopyParams ): Promise<{ - copiedContent: IComponentProperties[]; + copiedContent: ComponentProperties[]; contentStatus: CopyStatus[]; }> { const etherpadEnabled = Configuration.get('FEATURE_ETHERPAD_ENABLED') as boolean; const nexboardEnabled = Configuration.get('FEATURE_NEXBOARD_ENABLED') as boolean; - const copiedContent: IComponentProperties[] = []; + const copiedContent: ComponentProperties[] = []; const copiedContentStatus: CopyStatus[] = []; for (let i = 0; i < content.length; i += 1) { const element = content[i]; @@ -247,20 +243,20 @@ export class LessonCopyService { return { copiedContent, contentStatus }; } - private copyTextContent(element: IComponentProperties): IComponentProperties { + private copyTextContent(element: ComponentProperties): ComponentProperties { return { title: element.title, hidden: element.hidden, component: ComponentType.TEXT, user: element.user, // TODO should be params.user - but that made the server crash, but property is normally undefined content: { - text: (element.content as IComponentTextProperties).text, + text: (element.content as ComponentTextProperties).text, }, }; } - private copyLernStore(element: IComponentProperties): IComponentProperties { - const lernstore: IComponentProperties = { + private copyLernStore(element: ComponentProperties): ComponentProperties { + const lernstore: ComponentProperties = { title: element.title, hidden: element.hidden, component: ComponentType.LERNSTORE, @@ -268,7 +264,7 @@ export class LessonCopyService { }; if (element.content) { - const resources = ((element.content as IComponentLernstoreProperties).resources ?? []).map( + const resources = ((element.content as ComponentLernstoreProperties).resources ?? []).map( ({ client, description, merlinReference, title, url }) => { const result = { client, @@ -281,27 +277,27 @@ export class LessonCopyService { } ); - const lernstoreContent: IComponentLernstoreProperties = { resources }; + const lernstoreContent: ComponentLernstoreProperties = { resources }; lernstore.content = lernstoreContent; } return lernstore; } - private static copyGeogebra(originalElement: IComponentProperties): IComponentProperties { - const copy = { ...originalElement, hidden: true } as IComponentProperties; - copy.content = { ...copy.content, materialId: '' } as IComponentGeogebraProperties; + private static copyGeogebra(originalElement: ComponentProperties): ComponentProperties { + const copy = { ...originalElement, hidden: true } as ComponentProperties; + copy.content = { ...copy.content, materialId: '' } as ComponentGeogebraProperties; delete copy._id; return copy; } private async copyEtherpad( - originalElement: IComponentProperties, + originalElement: ComponentProperties, params: LessonCopyParams - ): Promise { - const copy = { ...originalElement } as IComponentProperties; + ): Promise { + const copy = { ...originalElement } as ComponentProperties; delete copy._id; - const content = { ...copy.content, url: '' } as IComponentEtherpadProperties; + const content = { ...copy.content, url: '' } as ComponentEtherpadProperties; content.title = randomBytes(12).toString('hex'); const etherpadPadId = await this.etherpadService.createEtherpad( @@ -319,12 +315,12 @@ export class LessonCopyService { } private async copyNexboard( - originalElement: IComponentProperties, + originalElement: ComponentProperties, params: LessonCopyParams - ): Promise { - const copy = { ...originalElement } as IComponentProperties; + ): Promise { + const copy = { ...originalElement } as ComponentProperties; delete copy._id; - const content = { ...copy.content, url: '', board: '' } as IComponentNexboardProperties; + const content = { ...copy.content, url: '', board: '' } as ComponentNexboardProperties; const nexboard = await this.nexboardService.createNexboard(params.user.id, content.title, content.description); if (nexboard) { @@ -389,8 +385,8 @@ export class LessonCopyService { return { copiedMaterials, materialsStatus }; } - private copyEmbeddedTaskLink(originalElement: IComponentProperties) { - const copy = JSON.parse(JSON.stringify(originalElement)) as IComponentProperties; + private copyEmbeddedTaskLink(originalElement: ComponentProperties) { + const copy = JSON.parse(JSON.stringify(originalElement)) as ComponentProperties; delete copy._id; return copy; } diff --git a/apps/server/src/modules/lesson/service/lesson.service.spec.ts b/apps/server/src/modules/lesson/service/lesson.service.spec.ts index a94ecfe9c8b..958677a3e26 100644 --- a/apps/server/src/modules/lesson/service/lesson.service.spec.ts +++ b/apps/server/src/modules/lesson/service/lesson.service.spec.ts @@ -1,10 +1,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { Test, TestingModule } from '@nestjs/testing'; -import { LessonRepo } from '@shared/repo'; +import { ComponentProperties, ComponentType } from '@shared/domain'; import { lessonFactory, setupEntities } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { ComponentType, IComponentProperties } from '@shared/domain'; +import { LessonRepo } from '../repository'; import { LessonService } from './lesson.service'; describe('LessonService', () => { @@ -71,9 +71,19 @@ describe('LessonService', () => { const courseIds = ['course-1', 'course-2']; lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); - await expect(lessonService.findByCourseIds(courseIds)).resolves.not.toThrow(); - expect(lessonRepo.findAllByCourseIds).toBeCalledTimes(1); - expect(lessonRepo.findAllByCourseIds).toBeCalledWith(courseIds); + await lessonService.findByCourseIds(courseIds); + + expect(lessonRepo.findAllByCourseIds).toBeCalledWith(courseIds, undefined); + }); + + it('should pass filters', async () => { + const courseIds = ['course-1', 'course-2']; + lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); + const filters = { hidden: false }; + + await lessonService.findByCourseIds(courseIds, filters); + + expect(lessonRepo.findAllByCourseIds).toBeCalledWith(courseIds, filters); }); }); @@ -81,7 +91,7 @@ describe('LessonService', () => { describe('when finding by userId', () => { const setup = () => { const userId = new ObjectId().toHexString(); - const contentExample: IComponentProperties = { + const contentExample: ComponentProperties = { title: 'title', hidden: false, user: userId, @@ -122,7 +132,7 @@ describe('LessonService', () => { describe('when deleting by userId', () => { const setup = () => { const userId = new ObjectId().toHexString(); - const contentExample: IComponentProperties = { + const contentExample: ComponentProperties = { title: 'title', hidden: false, user: userId, diff --git a/apps/server/src/modules/lesson/service/lesson.service.ts b/apps/server/src/modules/lesson/service/lesson.service.ts index 2dee6f05563..3ef2f44d9bf 100644 --- a/apps/server/src/modules/lesson/service/lesson.service.ts +++ b/apps/server/src/modules/lesson/service/lesson.service.ts @@ -1,10 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { Counted, EntityId, IComponentProperties, LessonEntity } from '@shared/domain'; -import { LessonRepo } from '@shared/repo'; import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { Injectable } from '@nestjs/common'; +import { ComponentProperties, Counted, EntityId, LessonEntity } from '@shared/domain'; +import { AuthorizationLoaderService } from '@src/modules/authorization'; +import { LessonRepo } from '../repository'; @Injectable() -export class LessonService { +export class LessonService implements AuthorizationLoaderService { constructor( private readonly lessonRepo: LessonRepo, private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService @@ -20,8 +21,8 @@ export class LessonService { return this.lessonRepo.findById(lessonId); } - async findByCourseIds(courseIds: EntityId[]): Promise> { - return this.lessonRepo.findAllByCourseIds(courseIds); + async findByCourseIds(courseIds: EntityId[], filters?: { hidden?: boolean }): Promise> { + return this.lessonRepo.findAllByCourseIds(courseIds, filters); } async findAllLessonsByUserId(userId: EntityId): Promise { @@ -34,7 +35,7 @@ export class LessonService { const lessons = await this.lessonRepo.findByUserId(userId); const updatedLessons = lessons.map((lesson: LessonEntity) => { - lesson.contents.map((c: IComponentProperties) => { + lesson.contents.map((c: ComponentProperties) => { if (c.user === userId) { c.user = ''; } diff --git a/apps/server/src/modules/management/seed-data/federalstates.ts b/apps/server/src/modules/management/seed-data/federalstates.ts index c5f6447a4b7..2630aa8797a 100644 --- a/apps/server/src/modules/management/seed-data/federalstates.ts +++ b/apps/server/src/modules/management/seed-data/federalstates.ts @@ -1,8 +1,8 @@ -import { County, IFederalStateProperties } from '@shared/domain/entity/federal-state.entity'; +import { County, FederalStateProperties } from '@shared/domain/entity/federal-state.entity'; import { federalStateFactory } from '@shared/testing/factory/federal-state.factory'; import { DeepPartial } from 'fishery'; -type SeedFederalStateProperties = Omit & { +type SeedFederalStateProperties = Omit & { id: string; counties?: (County & { id: string })[]; createdAt?: string; @@ -219,7 +219,7 @@ export function generateFederalStates() { }) ) ?? []; - const params: DeepPartial = { + const params: DeepPartial = { counties, name: federalState.name, abbreviation: federalState.abbreviation, diff --git a/apps/server/src/modules/management/seed-data/roles.ts b/apps/server/src/modules/management/seed-data/roles.ts index f5a4ee0fbdc..3fba9d7d8ee 100644 --- a/apps/server/src/modules/management/seed-data/roles.ts +++ b/apps/server/src/modules/management/seed-data/roles.ts @@ -1,10 +1,10 @@ // All user accounts are organized by school in a single array -import { IRoleProperties, Permission, Role, RoleName } from '@shared/domain'; +import { Permission, Role, RoleName, RoleProperties } from '@shared/domain'; import { roleFactory } from '@shared/testing'; import { DeepPartial } from 'fishery'; -type SeedRoleProperties = Omit & { +type SeedRoleProperties = Omit & { id: string; createdAt: string; updatedAt: string; @@ -443,7 +443,7 @@ export function generateRole(localRoleSeedData?: { [key: string | RoleName]: See if (subRoles.some((r) => !r)) { throw new Error(`Role ${roleName} depends on non existing role`); } - const params: DeepPartial = { + const params: DeepPartial = { name: partial.name, permissions: partial.permissions, roles: subRoles, diff --git a/apps/server/src/modules/management/seed-data/schools.ts b/apps/server/src/modules/management/seed-data/schools.ts index ba09c0d6952..9c0cec78153 100644 --- a/apps/server/src/modules/management/seed-data/schools.ts +++ b/apps/server/src/modules/management/seed-data/schools.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/dot-notation */ import { FederalStateEntity, - ISchoolProperties, SchoolFeatures, + SchoolProperties, SchoolRoles, SchoolYearEntity, SystemEntity, @@ -12,7 +12,7 @@ import { DeepPartial } from 'fishery'; import { EFederalState } from './federalstates'; import { SeedSchoolYearEnum } from './schoolyears'; -type SeedSchoolProperties = Omit & { +type SeedSchoolProperties = Omit & { id: string; updatedAt?: string; createdAt?: string; @@ -288,7 +288,7 @@ export function generateSchools(entities: { entities.federalStates.find((fs) => partial.federalState && fs.name === partial.federalState) ?? federalStateFactory.build(); - const params: DeepPartial = { + const params: DeepPartial = { externalId: partial.externalId, features: partial.features, inMaintenanceSince: partial.inMaintenanceSince, diff --git a/apps/server/src/modules/management/seed-data/schoolyears.ts b/apps/server/src/modules/management/seed-data/schoolyears.ts index afd0a7a8d49..c52dee77642 100644 --- a/apps/server/src/modules/management/seed-data/schoolyears.ts +++ b/apps/server/src/modules/management/seed-data/schoolyears.ts @@ -1,8 +1,8 @@ -import { ISchoolYearProperties } from '@shared/domain'; +import { SchoolYearProperties } from '@shared/domain'; import { schoolYearFactory } from '@shared/testing/factory/schoolyear.factory'; import { DeepPartial } from 'fishery'; -type SeedSchoolYearProperties = Pick & { +type SeedSchoolYearProperties = Pick & { id: string; endDate: string; startDate: string; @@ -79,7 +79,7 @@ const seedSchoolYears: SeedSchoolYearProperties[] = [ export function generateSchoolYears() { return seedSchoolYears.map((year) => { - const params: DeepPartial = { + const params: DeepPartial = { endDate: new Date(year.endDate), name: year.name, startDate: new Date(year.startDate), diff --git a/apps/server/src/modules/management/seed-data/systems.ts b/apps/server/src/modules/management/seed-data/systems.ts index adbffbc4b35..a9140458f6d 100644 --- a/apps/server/src/modules/management/seed-data/systems.ts +++ b/apps/server/src/modules/management/seed-data/systems.ts @@ -1,10 +1,10 @@ /* eslint-disable no-template-curly-in-string */ -import { ISystemProperties } from '@shared/domain'; +import { SystemProperties } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { systemFactory } from '@shared/testing'; import { DeepPartial } from 'fishery'; -type SystemPartial = DeepPartial & { +type SystemPartial = DeepPartial & { id?: string; createdAt?: string; updatedAt?: string; @@ -66,7 +66,7 @@ const data: SystemPartial[] = [ export function generateSystems(injectEnvVars: (s: string) => string) { const systems = data.map((d) => { d = JSON.parse(injectEnvVars(JSON.stringify(d))) as typeof d; - const params: DeepPartial = { + const params: DeepPartial = { alias: d.alias, displayName: d.displayName, type: d.type, diff --git a/apps/server/src/modules/management/uc/database-management.uc.ts b/apps/server/src/modules/management/uc/database-management.uc.ts index 7b3d034c504..370c4652aeb 100644 --- a/apps/server/src/modules/management/uc/database-management.uc.ts +++ b/apps/server/src/modules/management/uc/database-management.uc.ts @@ -1,17 +1,17 @@ import { Configuration } from '@hpi-schul-cloud/commons'; +import { DatabaseManagementService } from '@infra/database'; +import { DefaultEncryptionService, EncryptionService, LdapEncryptionService } from '@infra/encryption'; +import { FileSystemAdapter } from '@infra/file-system'; import { EntityManager } from '@mikro-orm/mongodb'; import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { StorageProviderEntity, SystemEntity } from '@shared/domain'; -import { DatabaseManagementService } from '@infra/database'; -import { DefaultEncryptionService, IEncryptionService, LdapEncryptionService } from '@infra/encryption'; -import { FileSystemAdapter } from '@infra/file-system'; import { LegacyLogger } from '@src/core/logger'; import { orderBy } from 'lodash'; import { BsonConverter } from '../converter/bson.converter'; import { generateSeedData } from '../seed-data/generateSeedData'; -export interface ICollectionFilePath { +export interface CollectionFilePath { filePath: string; collectionName: string; } @@ -35,8 +35,8 @@ export class DatabaseManagementUc { private readonly configService: ConfigService, private readonly logger: LegacyLogger, private em: EntityManager, - @Inject(DefaultEncryptionService) private readonly defaultEncryptionService: IEncryptionService, - @Inject(LdapEncryptionService) private readonly ldapEncryptionService: IEncryptionService + @Inject(DefaultEncryptionService) private readonly defaultEncryptionService: EncryptionService, + @Inject(LdapEncryptionService) private readonly ldapEncryptionService: EncryptionService ) { this.logger.setContext(DatabaseManagementUc.name); } @@ -75,9 +75,9 @@ export class DatabaseManagementUc { /** * Loads all collection names from database and adds related file paths. - * @returns {ICollectionFilePath} + * @returns {CollectionFilePath} */ - private async loadAllCollectionsFromDatabase(targetFolder: string): Promise { + private async loadAllCollectionsFromDatabase(targetFolder: string): Promise { const collections = await this.databaseManagementService.getCollectionNames(); const collectionsWithFilePaths = collections.map((collectionName) => { return { @@ -90,9 +90,9 @@ export class DatabaseManagementUc { /** * Loads all collection names and file paths from backup files. - * @returns {ICollectionFilePath} + * @returns {CollectionFilePath} */ - private async loadAllCollectionsFromFilesystem(baseDir: string): Promise { + private async loadAllCollectionsFromFilesystem(baseDir: string): Promise { const filenames = await this.fileSystemAdapter.readDir(baseDir); const collectionsWithFilePaths = filenames.map((fileName) => { return { @@ -107,14 +107,14 @@ export class DatabaseManagementUc { * Scans for existing collections and optionally filters them based on * @param source * @param collectionNameFilter - * @returns {ICollectionFilePath} the filtered collection names and related file paths + * @returns {CollectionFilePath} the filtered collection names and related file paths */ private async loadCollectionsAvailableFromSourceAndFilterByCollectionNames( source: 'files' | 'database', folder: string, collectionNameFilter?: string[] ) { - let allCollectionsWithFilePaths: ICollectionFilePath[] = []; + let allCollectionsWithFilePaths: CollectionFilePath[] = []; // load all available collections from source if (source === 'files') { diff --git a/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts b/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts index c80d47df66d..2a6c2a93d90 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts @@ -5,7 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; import { MetaTagExtractorService } from '../../service'; -const URL = 'https://test.de'; +const URL = 'https://best-example.de/my-article'; const mockedResponse = { url: URL, @@ -13,7 +13,7 @@ const mockedResponse = { description: 'with great description', }; -describe(`get data (api)`, () => { +describe(`get meta tags (api)`, () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; @@ -24,7 +24,7 @@ describe(`get data (api)`, () => { }) .overrideProvider(MetaTagExtractorService) .useValue({ - fetchMetaData: () => mockedResponse, + getMetaData: () => mockedResponse, }) .compile(); @@ -63,7 +63,7 @@ describe(`get data (api)`, () => { const response = await loggedInClient.post(undefined, { url: URL }); - expect(response?.body).toEqual(mockedResponse); + expect(response?.body).toEqual(expect.objectContaining(mockedResponse)); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.spec.ts b/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.spec.ts index 29dfbd94c72..0f527d1139d 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.spec.ts @@ -8,13 +8,19 @@ describe(MetaTagExtractorResponse.name, () => { title: 'Testbild', description: 'Here we describe what this page is about.', imageUrl: 'https://www.abc.de/test.png', + type: 'unknown', + parentTitle: 'Math', + parentType: 'course', }; - const errorResponse = new MetaTagExtractorResponse(properties); - expect(errorResponse.url).toEqual(properties.url); - expect(errorResponse.title).toEqual(properties.title); - expect(errorResponse.description).toEqual(properties.description); - expect(errorResponse.imageUrl).toEqual(properties.imageUrl); + const response = new MetaTagExtractorResponse(properties); + expect(response.url).toEqual(properties.url); + expect(response.title).toEqual(properties.title); + expect(response.description).toEqual(properties.description); + expect(response.imageUrl).toEqual(properties.imageUrl); + expect(response.type).toEqual(properties.type); + expect(response.parentTitle).toEqual(properties.parentTitle); + expect(response.parentType).toEqual(properties.parentType); }); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.ts b/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.ts index a2f5acd8465..16863f0e16a 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.ts @@ -1,13 +1,17 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; import { IsString, IsUrl } from 'class-validator'; +import { MetaDataEntityType } from '../../types'; export class MetaTagExtractorResponse { - constructor({ url, title, description, imageUrl }: MetaTagExtractorResponse) { + constructor({ url, title, description, imageUrl, type, parentTitle, parentType }: MetaTagExtractorResponse) { this.url = url; this.title = title; this.description = description; this.imageUrl = imageUrl; + this.type = type; + this.parentTitle = parentTitle; + this.parentType = parentType; } @ApiProperty() @@ -25,4 +29,16 @@ export class MetaTagExtractorResponse { @ApiProperty() @IsString() imageUrl?: string; + + @ApiProperty() + @IsString() + type: MetaDataEntityType; + + @ApiProperty() + @DecodeHtmlEntities() + parentTitle?: string; + + @ApiProperty() + @IsString() + parentType?: MetaDataEntityType; } diff --git a/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts b/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts index 8133c4c0b83..79f798c9e29 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts @@ -1,7 +1,6 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, InternalServerErrorException, Post, UnauthorizedException } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; import { MetaTagExtractorUc } from '../uc'; import { MetaTagExtractorResponse } from './dto'; import { GetMetaTagDataBody } from './post-link-url.body.params'; @@ -17,11 +16,11 @@ export class MetaTagExtractorController { @ApiResponse({ status: 401, type: UnauthorizedException }) @ApiResponse({ status: 500, type: InternalServerErrorException }) @Post('') - async getData( + async getMetaTags( @CurrentUser() currentUser: ICurrentUser, @Body() bodyParams: GetMetaTagDataBody ): Promise { - const result = await this.metaTagExtractorUc.fetchMetaData(currentUser.userId, bodyParams.url); + const result = await this.metaTagExtractorUc.getMetaData(currentUser.userId, bodyParams.url); const imageUrl = result.image?.url; const response = new MetaTagExtractorResponse({ ...result, imageUrl }); return response; diff --git a/apps/server/src/modules/meta-tag-extractor/controller/post-link-url.body.params.ts b/apps/server/src/modules/meta-tag-extractor/controller/post-link-url.body.params.ts index 1e9cd1f7f34..ac6baeebbe8 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/post-link-url.body.params.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/post-link-url.body.params.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsUrl } from 'class-validator'; +import { IsString } from 'class-validator'; export class GetMetaTagDataBody { - @IsUrl() + @IsString() @ApiProperty({ required: true, nullable: false, diff --git a/apps/server/src/modules/meta-tag-extractor/interface/url-handler.ts b/apps/server/src/modules/meta-tag-extractor/interface/url-handler.ts new file mode 100644 index 00000000000..fc09d2cd40e --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/interface/url-handler.ts @@ -0,0 +1,6 @@ +import { MetaData } from '../types'; + +export interface UrlHandler { + doesUrlMatch(url: string): boolean; + getMetaData(url: string): Promise; +} diff --git a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor-api.module.ts b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor-api.module.ts index d9095315e87..acc5eb31776 100644 --- a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor-api.module.ts +++ b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor-api.module.ts @@ -1,6 +1,6 @@ +import { AuthorizationModule } from '@modules/authorization'; import { forwardRef, Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; import { MetaTagExtractorController } from './controller'; import { MetaTagExtractorModule } from './meta-tag-extractor.module'; import { MetaTagExtractorUc } from './uc'; diff --git a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts index a85e71c526f..d7a1e0faeff 100644 --- a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts +++ b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts @@ -5,20 +5,37 @@ import { ConsoleWriterModule } from '@infra/console'; import { createConfigModuleOptions } from '@src/config'; import { LoggerModule } from '@src/core/logger'; import { AuthenticationModule } from '../authentication/authentication.module'; +import { BoardModule } from '../board'; +import { LearnroomModule } from '../learnroom'; +import { LessonModule } from '../lesson'; +import { TaskModule } from '../task'; import { UserModule } from '../user'; import metaTagExtractorConfig from './meta-tag-extractor.config'; import { MetaTagExtractorService } from './service'; +import { MetaTagInternalUrlService } from './service/meta-tag-internal-url.service'; +import { BoardUrlHandler, CourseUrlHandler, LessonUrlHandler, TaskUrlHandler } from './service/url-handler'; @Module({ imports: [ AuthenticationModule, + BoardModule, ConsoleWriterModule, HttpModule, + LearnroomModule, + LessonModule, LoggerModule, + TaskModule, UserModule, ConfigModule.forRoot(createConfigModuleOptions(metaTagExtractorConfig)), ], - providers: [MetaTagExtractorService], + providers: [ + MetaTagExtractorService, + MetaTagInternalUrlService, + TaskUrlHandler, + LessonUrlHandler, + CourseUrlHandler, + BoardUrlHandler, + ], exports: [MetaTagExtractorService], }) export class MetaTagExtractorModule {} diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts index af1a256d121..06fa3b09170 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts @@ -1,35 +1,52 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; +import ogs from 'open-graph-scraper'; import { ImageObject } from 'open-graph-scraper/dist/lib/types'; import { MetaTagExtractorService } from './meta-tag-extractor.service'; +import { MetaTagInternalUrlService } from './meta-tag-internal-url.service'; -let ogsResponseMock = {}; -let ogsRejectMock: Error | undefined; - -jest.mock('open-graph-scraper', () => () => { - if (ogsRejectMock) { - return Promise.reject(ogsRejectMock); - } +jest.mock('open-graph-scraper', () => { + return { + __esModule: true, + default: jest.fn(), + }; +}); - return Promise.resolve({ +const mockOgsResolve = (result: Record) => { + const mockedOgs = ogs as jest.Mock; + mockedOgs.mockResolvedValueOnce({ error: false, html: '', response: {}, - result: ogsResponseMock, + result, }); -}); +}; + +const mockOgsReject = (error: Error) => { + const mockedOgs = ogs as jest.Mock; + mockedOgs.mockRejectedValueOnce(error); +}; describe(MetaTagExtractorService.name, () => { let module: TestingModule; + let metaTagInternalUrlService: DeepMocked; let service: MetaTagExtractorService; beforeAll(async () => { module = await Test.createTestingModule({ - providers: [MetaTagExtractorService], + providers: [ + MetaTagExtractorService, + { + provide: MetaTagInternalUrlService, + useValue: createMock(), + }, + ], }).compile(); + metaTagInternalUrlService = module.get(MetaTagInternalUrlService); service = module.get(MetaTagExtractorService); - await setupEntities(); }); @@ -38,8 +55,8 @@ describe(MetaTagExtractorService.name, () => { }); beforeEach(() => { - ogsResponseMock = {}; - ogsRejectMock = undefined; + Configuration.set('SC_DOMAIN', 'localhost'); + metaTagInternalUrlService.tryInternalLinkMetaTags.mockResolvedValue(undefined); }); afterEach(() => { @@ -48,26 +65,28 @@ describe(MetaTagExtractorService.name, () => { describe('create', () => { describe('when url points to webpage', () => { + it('should thrown an error if url is an empty string', async () => { + const url = ''; + + await expect(service.getMetaData(url)).rejects.toThrow(); + }); + it('should return also the original url', async () => { + const ogTitle = 'My Title'; const url = 'https://de.wikipedia.org'; + mockOgsResolve({ url, ogTitle }); - const result = await service.fetchMetaData(url); + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ url })); }); - it('should thrown an error if url is an empty string', async () => { - const url = ''; - - await expect(service.fetchMetaData(url)).rejects.toThrow(); - }); - it('should return ogTitle as title', async () => { const ogTitle = 'My Title'; const url = 'https://de.wikipedia.org'; - ogsResponseMock = { ogTitle }; + mockOgsResolve({ ogTitle }); - const result = await service.fetchMetaData(url); + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ title: ogTitle })); }); @@ -91,9 +110,9 @@ describe(MetaTagExtractorService.name, () => { }, ]; const url = 'https://de.wikipedia.org'; - ogsResponseMock = { ogImage }; + mockOgsResolve({ url, ogImage }); - const result = await service.fetchMetaData(url); + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ image: ogImage[1] })); }); @@ -102,9 +121,10 @@ describe(MetaTagExtractorService.name, () => { describe('when url points to a file', () => { it('should return filename as title', async () => { const url = 'https://de.wikipedia.org/abc.jpg'; - ogsRejectMock = new Error('no open graph data included... probably not a webpage'); - const result = await service.fetchMetaData(url); + mockOgsReject(new Error('no open graph data included... probably not a webpage')); + + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ title: 'abc.jpg' })); }); }); @@ -112,9 +132,10 @@ describe(MetaTagExtractorService.name, () => { describe('when url is invalid', () => { it('should return url as it is', async () => { const url = 'not-a-real-domain'; - ogsRejectMock = new Error('no open graph data included... probably not a webpage'); - const result = await service.fetchMetaData(url); + mockOgsReject(new Error('no open graph data included... probably not a webpage')); + + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ url, title: '', description: '' })); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts index 46c30c17702..d64c3a42a58 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts @@ -2,24 +2,29 @@ import { Injectable } from '@nestjs/common'; import ogs from 'open-graph-scraper'; import { ImageObject } from 'open-graph-scraper/dist/lib/types'; import { basename } from 'path'; - -export type MetaData = { - title: string; - description: string; - url: string; - image?: ImageObject; -}; +import type { MetaData } from '../types'; +import { MetaTagInternalUrlService } from './meta-tag-internal-url.service'; @Injectable() export class MetaTagExtractorService { - async fetchMetaData(url: string): Promise { + constructor(private readonly internalLinkMataTagService: MetaTagInternalUrlService) {} + + async getMetaData(url: string): Promise { if (url.length === 0) { throw new Error(`MetaTagExtractorService requires a valid URL. Given URL: ${url}`); } - const metaData = (await this.tryExtractMetaTags(url)) ?? this.tryFilenameAsFallback(url); + const metaData = + (await this.tryInternalLinkMetaTags(url)) ?? + (await this.tryExtractMetaTags(url)) ?? + this.tryFilenameAsFallback(url) ?? + this.getDefaultMetaData(url); + + return metaData; + } - return metaData ?? { url, title: '', description: '' }; + private async tryInternalLinkMetaTags(url: string): Promise { + return this.internalLinkMataTagService.tryInternalLinkMetaTags(url); } private async tryExtractMetaTags(url: string): Promise { @@ -35,6 +40,7 @@ export class MetaTagExtractorService { description, image, url, + type: 'external', }; } catch (error) { return undefined; @@ -49,12 +55,17 @@ export class MetaTagExtractorService { title, description: '', url, + type: 'unknown', }; } catch (error) { return undefined; } } + private getDefaultMetaData(url: string): MetaData { + return { url, title: '', description: '', type: 'unknown' }; + } + private pickImage(images: ImageObject[], minWidth = 400): ImageObject | undefined { const sortedImages = [...images]; sortedImages.sort((a, b) => (a.width && b.width ? Number(a.width) - Number(b.width) : 0)); diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts new file mode 100644 index 00000000000..04d2f8b0c77 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts @@ -0,0 +1,136 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { MetaData } from '../types'; +import { MetaTagExtractorService } from './meta-tag-extractor.service'; +import { MetaTagInternalUrlService } from './meta-tag-internal-url.service'; +import { BoardUrlHandler, CourseUrlHandler, LessonUrlHandler, TaskUrlHandler } from './url-handler'; + +const INTERNAL_DOMAIN = 'my-school-cloud.org'; +const INTERNAL_URL = `https://${INTERNAL_DOMAIN}/my-article`; +const UNKNOWN_INTERNAL_URL = `https://${INTERNAL_DOMAIN}/playground/23hafe23234`; +const EXTERNAL_URL = 'https://de.wikipedia.org/example-article'; + +describe(MetaTagExtractorService.name, () => { + let module: TestingModule; + let taskUrlHandler: DeepMocked; + let lessonUrlHandler: DeepMocked; + let courseUrlHandler: DeepMocked; + let boardUrlHandler: DeepMocked; + let service: MetaTagInternalUrlService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + MetaTagInternalUrlService, + { + provide: TaskUrlHandler, + useValue: createMock(), + }, + { + provide: LessonUrlHandler, + useValue: createMock(), + }, + { + provide: CourseUrlHandler, + useValue: createMock(), + }, + { + provide: BoardUrlHandler, + useValue: createMock(), + }, + ], + }).compile(); + + taskUrlHandler = module.get(TaskUrlHandler); + lessonUrlHandler = module.get(LessonUrlHandler); + courseUrlHandler = module.get(CourseUrlHandler); + boardUrlHandler = module.get(BoardUrlHandler); + service = module.get(MetaTagInternalUrlService); + await setupEntities(); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => {}); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('isInternalUrl', () => { + const setup = () => { + Configuration.set('SC_DOMAIN', INTERNAL_DOMAIN); + taskUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + lessonUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + courseUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + boardUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + }; + + it('should return true for internal urls', () => { + setup(); + + expect(service.isInternalUrl(INTERNAL_URL)).toBe(true); + }); + + it('should return false for external urls', () => { + setup(); + + expect(service.isInternalUrl(EXTERNAL_URL)).toBe(false); + }); + }); + + describe('tryInternalLinkMetaTags', () => { + const setup = () => { + Configuration.set('SC_DOMAIN', INTERNAL_DOMAIN); + taskUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + lessonUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + boardUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + const mockedMetaTags: MetaData = { + title: 'My Title', + url: INTERNAL_URL, + description: '', + type: 'course', + }; + + return { mockedMetaTags }; + }; + + describe('when url matches to a handler', () => { + it('should return the handlers meta tags', async () => { + const { mockedMetaTags } = setup(); + courseUrlHandler.doesUrlMatch.mockReturnValueOnce(true); + courseUrlHandler.getMetaData.mockResolvedValueOnce(mockedMetaTags); + + const result = await service.tryInternalLinkMetaTags(INTERNAL_URL); + + expect(result).toEqual(mockedMetaTags); + }); + }); + + describe('when url matches to none of the handlers', () => { + it('should return default meta tags', async () => { + setup(); + courseUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + + const result = await service.tryInternalLinkMetaTags(UNKNOWN_INTERNAL_URL); + + expect(result).toEqual(expect.objectContaining({ type: 'unknown' })); + }); + }); + + describe('when url is external', () => { + it('should return undefined', async () => { + setup(); + courseUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + + const result = await service.tryInternalLinkMetaTags(EXTERNAL_URL); + + expect(result).toBeUndefined(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts new file mode 100644 index 00000000000..5c0d5efca5c --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts @@ -0,0 +1,51 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { Injectable } from '@nestjs/common'; +import type { UrlHandler } from '../interface/url-handler'; +import { MetaData } from '../types'; +import { BoardUrlHandler, CourseUrlHandler, LessonUrlHandler, TaskUrlHandler } from './url-handler'; + +@Injectable() +export class MetaTagInternalUrlService { + private handlers: UrlHandler[] = []; + + constructor( + private readonly taskUrlHandler: TaskUrlHandler, + private readonly lessonUrlHandler: LessonUrlHandler, + private readonly courseUrlHandler: CourseUrlHandler, + private readonly boardUrlHandler: BoardUrlHandler + ) { + this.handlers = [this.taskUrlHandler, this.lessonUrlHandler, this.courseUrlHandler, this.boardUrlHandler]; + } + + async tryInternalLinkMetaTags(url: string): Promise { + if (this.isInternalUrl(url)) { + return this.composeMetaTags(url); + } + return Promise.resolve(undefined); + } + + isInternalUrl(url: string) { + let domain = Configuration.get('SC_DOMAIN') as string; + domain = domain === '' ? 'nothing-configured-for-internal-url.de' : domain; + const isInternal = url.toLowerCase().includes(domain.toLowerCase()); + return isInternal; + } + + private async composeMetaTags(url: string): Promise { + const urlObject = new URL(url); + + const handler = this.handlers.find((h) => h.doesUrlMatch(url)); + if (handler) { + const result = await handler.getMetaData(url); + return result; + } + + const title = urlObject.pathname; + return Promise.resolve({ + title, + description: '', + url, + type: 'unknown', + }); + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts new file mode 100644 index 00000000000..b6900cfd492 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts @@ -0,0 +1,59 @@ +import { AbstractUrlHandler } from './abstract-url-handler'; + +class DummyHandler extends AbstractUrlHandler { + patterns: RegExp[] = [/\/dummy\/([0-9a-z]+)$/i]; + + extractId(url: string): string | undefined { + return super.extractId(url); + } +} + +describe(AbstractUrlHandler.name, () => { + const setup = () => { + const id = 'af322312feae'; + const url = `https://localhost/dummy/${id}`; + const invalidUrl = `https://localhost/wrong/${id}`; + const handler = new DummyHandler(); + return { id, url, invalidUrl, handler }; + }; + + describe('extractId', () => { + describe('when no id was extracted', () => { + it('should return undefined', () => { + const { invalidUrl, handler } = setup(); + + const result = handler.extractId(invalidUrl); + + expect(result).toBeUndefined(); + }); + }); + }); + + describe('doesUrlMatch', () => { + it('should be true for valid urls', () => { + const { url, handler } = setup(); + + const result = handler.doesUrlMatch(url); + + expect(result).toBe(true); + }); + + it('should be false for invalid urls', () => { + const { invalidUrl, handler } = setup(); + + const result = handler.doesUrlMatch(invalidUrl); + + expect(result).toBe(false); + }); + }); + + describe('getDefaultMetaData', () => { + it('should return meta data of type unknown', () => { + const { url, handler } = setup(); + + const result = handler.getDefaultMetaData(url); + + expect(result).toEqual(expect.objectContaining({ type: 'unknown', url })); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts new file mode 100644 index 00000000000..fb618c3bf36 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts @@ -0,0 +1,35 @@ +import { basename } from 'node:path'; +import { MetaData } from '../../types'; + +export abstract class AbstractUrlHandler { + protected abstract patterns: RegExp[]; + + protected extractId(url: string): string | undefined { + const results: RegExpMatchArray = this.patterns + .map((pattern: RegExp) => pattern.exec(url)) + .filter((result) => result !== null) + .find((result) => (result?.length ?? 0) >= 2) as RegExpMatchArray; + + if (results && results[1]) { + return results[1]; + } + return undefined; + } + + doesUrlMatch(url: string): boolean { + const doesMatch = this.patterns.some((pattern) => pattern.test(url)); + return doesMatch; + } + + getDefaultMetaData(url: string, partial: Partial = {}): MetaData { + const urlObject = new URL(url); + const title = basename(urlObject.pathname); + return { + title, + description: '', + url, + type: 'unknown', + ...partial, + }; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts new file mode 100644 index 00000000000..f7775b58f1f --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts @@ -0,0 +1,70 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ColumnBoardService } from '@modules/board'; +import { CourseService } from '@modules/learnroom'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ColumnBoard } from '@shared/domain'; +import { setupEntities } from '@shared/testing'; +import { BoardUrlHandler } from './board-url-handler'; + +describe(BoardUrlHandler.name, () => { + let module: TestingModule; + let columnBoardService: DeepMocked; + let boardUrlHandler: BoardUrlHandler; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardUrlHandler, + { + provide: ColumnBoardService, + useValue: createMock(), + }, + { + provide: CourseService, + useValue: createMock(), + }, + ], + }).compile(); + + columnBoardService = module.get(ColumnBoardService); + boardUrlHandler = module.get(BoardUrlHandler); + await setupEntities(); + }); + + describe('getMetaData', () => { + describe('when url fits', () => { + it('should call courseService with the correct id', async () => { + const id = 'af322312feae'; + const url = `https://localhost/rooms/${id}/board`; + + await boardUrlHandler.getMetaData(url); + + expect(columnBoardService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the board name', async () => { + const id = 'af322312feae'; + const url = `https://localhost/rooms/${id}/board`; + const boardName = 'My Board'; + columnBoardService.findById.mockResolvedValue({ + title: boardName, + context: { type: 'course', id: 'a-board-id' }, + } as ColumnBoard); + + const result = await boardUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: boardName, type: 'board' })); + }); + }); + + describe('when url does not fit', () => { + it('should return undefined', async () => { + const url = `https://localhost/invalid/ef2345abe4e3b`; + + const result = await boardUrlHandler.getMetaData(url); + + expect(result).toBeUndefined(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts new file mode 100644 index 00000000000..013631244dd --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts @@ -0,0 +1,37 @@ +import { ColumnBoardService } from '@modules/board'; +import { CourseService } from '@modules/learnroom'; +import { Injectable } from '@nestjs/common'; +import { BoardExternalReferenceType } from '@shared/domain'; +import type { UrlHandler } from '../../interface/url-handler'; +import { MetaData } from '../../types'; +import { AbstractUrlHandler } from './abstract-url-handler'; + +@Injectable() +export class BoardUrlHandler extends AbstractUrlHandler implements UrlHandler { + patterns: RegExp[] = [/\/rooms\/(.*?)\/board\/?$/i]; + + constructor(private readonly columnBoardService: ColumnBoardService, private readonly courseService: CourseService) { + super(); + } + + async getMetaData(url: string): Promise { + const id = this.extractId(url); + if (id === undefined) { + return undefined; + } + + const metaData = this.getDefaultMetaData(url, { type: 'board' }); + + const columnBoard = await this.columnBoardService.findById(id); + if (columnBoard) { + metaData.title = columnBoard.title; + if (columnBoard.context.type === BoardExternalReferenceType.Course) { + const course = await this.courseService.findById(columnBoard.context.id); + metaData.parentType = 'course'; + metaData.parentTitle = course.name; + } + } + + return metaData; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts new file mode 100644 index 00000000000..75a43876de4 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts @@ -0,0 +1,62 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { CourseService } from '@modules/learnroom'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Course } from '@shared/domain'; +import { setupEntities } from '@shared/testing'; +import { CourseUrlHandler } from './course-url-handler'; + +describe(CourseUrlHandler.name, () => { + let module: TestingModule; + let courseService: DeepMocked; + let courseUrlHandler: CourseUrlHandler; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + CourseUrlHandler, + { + provide: CourseService, + useValue: createMock(), + }, + ], + }).compile(); + + courseService = module.get(CourseService); + courseUrlHandler = module.get(CourseUrlHandler); + await setupEntities(); + }); + + describe('getMetaData', () => { + describe('when url fits', () => { + it('should call courseService with the correct id', async () => { + const id = 'af322312feae'; + const url = `https://localhost/rooms/${id}`; + + await courseUrlHandler.getMetaData(url); + + expect(courseService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the course name', async () => { + const id = 'af322312feae'; + const url = `https://localhost/rooms/${id}`; + const courseName = 'My Course'; + courseService.findById.mockResolvedValue({ name: courseName } as Course); + + const result = await courseUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: courseName, type: 'course' })); + }); + }); + + describe('when url does not fit', () => { + it('should return undefined', async () => { + const url = `https://localhost/invalid/ef2345abe4e3b`; + + const result = await courseUrlHandler.getMetaData(url); + + expect(result).toBeUndefined(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts new file mode 100644 index 00000000000..def041886f1 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts @@ -0,0 +1,29 @@ +import { CourseService } from '@modules/learnroom'; +import { Injectable } from '@nestjs/common'; +import type { UrlHandler } from '../../interface/url-handler'; +import { MetaData } from '../../types'; +import { AbstractUrlHandler } from './abstract-url-handler'; + +@Injectable() +export class CourseUrlHandler extends AbstractUrlHandler implements UrlHandler { + patterns: RegExp[] = [/\/rooms\/([0-9a-z]+)$/i]; + + constructor(private readonly courseService: CourseService) { + super(); + } + + async getMetaData(url: string): Promise { + const id = this.extractId(url); + if (id === undefined) { + return undefined; + } + + const metaData = this.getDefaultMetaData(url, { type: 'course' }); + const course = await this.courseService.findById(id); + if (course) { + metaData.title = course.name; + } + + return metaData; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/index.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/index.ts new file mode 100644 index 00000000000..a29b8401da2 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/index.ts @@ -0,0 +1,4 @@ +export * from './board-url-handler'; +export * from './course-url-handler'; +export * from './lesson-url-handler'; +export * from './task-url-handler'; diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts new file mode 100644 index 00000000000..53b59d86ab7 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts @@ -0,0 +1,62 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { LessonService } from '@modules/lesson'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LessonEntity } from '@shared/domain'; +import { setupEntities } from '@shared/testing'; +import { LessonUrlHandler } from './lesson-url-handler'; + +describe(LessonUrlHandler.name, () => { + let module: TestingModule; + let lessonService: DeepMocked; + let lessonUrlHandler: LessonUrlHandler; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + LessonUrlHandler, + { + provide: LessonService, + useValue: createMock(), + }, + ], + }).compile(); + + lessonService = module.get(LessonService); + lessonUrlHandler = module.get(LessonUrlHandler); + await setupEntities(); + }); + + describe('getMetaData', () => { + describe('when url fits', () => { + it('should call lessonService with the correct id', async () => { + const id = 'af322312feae'; + const url = `https://localhost/topics/${id}`; + + await lessonUrlHandler.getMetaData(url); + + expect(lessonService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the lessons name', async () => { + const id = 'af322312feae'; + const url = `https://localhost/topics/${id}`; + const lessonName = 'My lesson'; + lessonService.findById.mockResolvedValue({ name: lessonName } as LessonEntity); + + const result = await lessonUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: lessonName, type: 'lesson' })); + }); + }); + + describe('when url does not fit', () => { + it('should return undefined', async () => { + const url = `https://localhost/invalid/ef2345abe4e3b`; + + const result = await lessonUrlHandler.getMetaData(url); + + expect(result).toBeUndefined(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts new file mode 100644 index 00000000000..c5264020a50 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts @@ -0,0 +1,29 @@ +import { LessonService } from '@modules/lesson'; +import { Injectable } from '@nestjs/common'; +import type { UrlHandler } from '../../interface/url-handler'; +import { MetaData } from '../../types'; +import { AbstractUrlHandler } from './abstract-url-handler'; + +@Injectable() +export class LessonUrlHandler extends AbstractUrlHandler implements UrlHandler { + patterns: RegExp[] = [/\/topics\/([0-9a-z]+)$/i]; + + constructor(private readonly lessonService: LessonService) { + super(); + } + + async getMetaData(url: string): Promise { + const id = this.extractId(url); + if (id === undefined) { + return undefined; + } + + const metaData = this.getDefaultMetaData(url, { type: 'lesson' }); + const lesson = await this.lessonService.findById(id); + if (lesson) { + metaData.title = lesson.name; + } + + return metaData; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts new file mode 100644 index 00000000000..0423382f2a8 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts @@ -0,0 +1,62 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { TaskService } from '@modules/task'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Task } from '@shared/domain'; +import { setupEntities } from '@shared/testing'; +import { TaskUrlHandler } from './task-url-handler'; + +describe(TaskUrlHandler.name, () => { + let module: TestingModule; + let taskService: DeepMocked; + let taskUrlHandler: TaskUrlHandler; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + TaskUrlHandler, + { + provide: TaskService, + useValue: createMock(), + }, + ], + }).compile(); + + taskService = module.get(TaskService); + taskUrlHandler = module.get(TaskUrlHandler); + await setupEntities(); + }); + + describe('getMetaData', () => { + describe('when url fits', () => { + it('should call taskService with the correct id', async () => { + const id = 'af322312feae'; + const url = `https://localhost/homework/${id}`; + + await taskUrlHandler.getMetaData(url); + + expect(taskService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the tasks name', async () => { + const id = 'af322312feae'; + const url = `https://localhost/homework/${id}`; + const taskName = 'My Task'; + taskService.findById.mockResolvedValue({ name: taskName } as Task); + + const result = await taskUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: taskName, type: 'task' })); + }); + }); + + describe('when url does not fit', () => { + it('should return undefined', async () => { + const url = `https://localhost/invalid/ef2345abe4e3b`; + + const result = await taskUrlHandler.getMetaData(url); + + expect(result).toBeUndefined(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts new file mode 100644 index 00000000000..cb1cec86048 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts @@ -0,0 +1,29 @@ +import { TaskService } from '@modules/task'; +import { Injectable } from '@nestjs/common'; +import type { UrlHandler } from '../../interface/url-handler'; +import { MetaData } from '../../types'; +import { AbstractUrlHandler } from './abstract-url-handler'; + +@Injectable() +export class TaskUrlHandler extends AbstractUrlHandler implements UrlHandler { + patterns: RegExp[] = [/\/homework\/([0-9a-z]+)$/i]; + + constructor(private readonly taskService: TaskService) { + super(); + } + + async getMetaData(url: string): Promise { + const id = this.extractId(url); + if (id === undefined) { + return undefined; + } + + const metaData = this.getDefaultMetaData(url, { type: 'task' }); + const task = await this.taskService.findById(id); + if (task) { + metaData.title = task.name; + } + + return metaData; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/types/index.ts b/apps/server/src/modules/meta-tag-extractor/types/index.ts new file mode 100644 index 00000000000..776e417867e --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/types/index.ts @@ -0,0 +1 @@ +export * from './meta-data.type'; diff --git a/apps/server/src/modules/meta-tag-extractor/types/meta-data.type.ts b/apps/server/src/modules/meta-tag-extractor/types/meta-data.type.ts new file mode 100644 index 00000000000..b4da460d6e9 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/types/meta-data.type.ts @@ -0,0 +1,13 @@ +import { ImageObject } from 'open-graph-scraper/dist/lib/types'; + +export type MetaDataEntityType = 'external' | 'course' | 'board' | 'task' | 'lesson' | 'unknown'; + +export type MetaData = { + title: string; + description: string; + url: string; + image?: ImageObject; + type: MetaDataEntityType; + parentTitle?: string; + parentType?: MetaDataEntityType; +}; diff --git a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts index 118b7d82633..f5aa0c6cd72 100644 --- a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AuthorizationService } from '@modules/authorization'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationService } from '@src/modules/authorization'; import { MetaTagExtractorService } from '../service'; import { MetaTagExtractorUc } from './meta-tag-extractor.uc'; @@ -42,7 +42,7 @@ describe(MetaTagExtractorUc.name, () => { jest.resetAllMocks(); }); - describe('fetchMetaData', () => { + describe('getMetaData', () => { describe('when user exists', () => { const setup = () => { const user = userFactory.build(); @@ -57,7 +57,7 @@ describe(MetaTagExtractorUc.name, () => { authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); const url = 'https://www.example.com/great-example'; - await uc.fetchMetaData(user.id, url); + await uc.getMetaData(user.id, url); expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(user.id); }); @@ -66,9 +66,9 @@ describe(MetaTagExtractorUc.name, () => { const { user } = setup(); const url = 'https://www.example.com/great-example'; - await uc.fetchMetaData(user.id, url); + await uc.getMetaData(user.id, url); - expect(metaTagExtractorService.fetchMetaData).toHaveBeenCalledWith(url); + expect(metaTagExtractorService.getMetaData).toHaveBeenCalledWith(url); }); }); @@ -84,7 +84,7 @@ describe(MetaTagExtractorUc.name, () => { const { user } = setup(); const url = 'https://www.example.com/great-example'; - await expect(uc.fetchMetaData(user.id, url)).rejects.toThrow(UnauthorizedException); + await expect(uc.getMetaData(user.id, url)).rejects.toThrow(UnauthorizedException); }); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts index 5daca6c962d..47ac53d88b0 100644 --- a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts +++ b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts @@ -1,7 +1,8 @@ +import { AuthorizationService } from '@modules/authorization'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { EntityId } from '@shared/domain'; -import { AuthorizationService } from '@src/modules/authorization'; -import { MetaData, MetaTagExtractorService } from '../service'; +import { MetaTagExtractorService } from '../service'; +import { MetaData } from '../types'; @Injectable() export class MetaTagExtractorUc { @@ -10,14 +11,14 @@ export class MetaTagExtractorUc { private readonly metaTagExtractorService: MetaTagExtractorService ) {} - async fetchMetaData(userId: EntityId, url: string): Promise { + async getMetaData(userId: EntityId, url: string): Promise { try { await this.authorizationService.getUserWithPermissions(userId); } catch (error) { throw new UnauthorizedException(); } - const result = await this.metaTagExtractorService.fetchMetaData(url); + const result = await this.metaTagExtractorService.getMetaData(url); return result; } } diff --git a/apps/server/src/modules/news/controller/news.controller.ts b/apps/server/src/modules/news/controller/news.controller.ts index 2f1c227401a..c2cf6ba4e98 100644 --- a/apps/server/src/modules/news/controller/news.controller.ts +++ b/apps/server/src/modules/news/controller/news.controller.ts @@ -1,7 +1,7 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { NewsMapper } from '../mapper/news.mapper'; import { NewsUc } from '../uc/news.uc'; import { diff --git a/apps/server/src/modules/news/controller/team-news.controller.ts b/apps/server/src/modules/news/controller/team-news.controller.ts index 2344d6f2ac9..ac70748e439 100644 --- a/apps/server/src/modules/news/controller/team-news.controller.ts +++ b/apps/server/src/modules/news/controller/team-news.controller.ts @@ -1,6 +1,6 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Controller, Get, Param, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { PaginationParams } from '@shared/controller'; import { NewsMapper } from '../mapper/news.mapper'; import { NewsUc } from '../uc'; diff --git a/apps/server/src/modules/news/mapper/news.mapper.spec.ts b/apps/server/src/modules/news/mapper/news.mapper.spec.ts index 1ca549bfe74..40bd66d8130 100644 --- a/apps/server/src/modules/news/mapper/news.mapper.spec.ts +++ b/apps/server/src/modules/news/mapper/news.mapper.spec.ts @@ -1,21 +1,20 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { CourseNews, - INewsProperties, + CreateNews, + INewsScope, + IUpdateNews, News, + NewsProperties, + NewsTarget, + NewsTargetModel, SchoolEntity, SchoolNews, TeamEntity, TeamNews, User, - NewsTargetModel, - INewsScope, - ICreateNews, - IUpdateNews, - NewsTarget, } from '@shared/domain'; -import { courseFactory, schoolFactory, userFactory, setupEntities } from '@shared/testing'; -import { NewsMapper } from './news.mapper'; +import { courseFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; import { CreateNewsParams, FilterNewsParams, @@ -25,6 +24,7 @@ import { UserInfoResponse, } from '../controller/dto'; import { TargetInfoResponse } from '../controller/dto/target-info.response'; +import { NewsMapper } from './news.mapper'; const getTargetModel = (news: News): NewsTargetModel => { if (news instanceof SchoolNews) { @@ -42,14 +42,14 @@ const date = new Date(2021, 1, 1, 0, 0, 0); const createNews = ( newsProps, - NewsType: { new (props: INewsProperties): T }, + NewsType: { new (props: NewsProperties): T }, school: SchoolEntity, creator: User, target: NewsTarget ): T => { const newsId = new ObjectId().toHexString(); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const props: INewsProperties = { + const props: NewsProperties = { id: newsId, displayAt: date, updatedAt: date, @@ -204,8 +204,8 @@ describe('NewsMapper', () => { targetModel, targetId, }; - const result: ICreateNews = NewsMapper.mapCreateNewsToDomain(params); - const expected: ICreateNews = { + const result: CreateNews = NewsMapper.mapCreateNewsToDomain(params); + const expected: CreateNews = { title: params.title, content: params.content, displayAt: date, diff --git a/apps/server/src/modules/news/mapper/news.mapper.ts b/apps/server/src/modules/news/mapper/news.mapper.ts index aa064a97b1a..36c17f57607 100644 --- a/apps/server/src/modules/news/mapper/news.mapper.ts +++ b/apps/server/src/modules/news/mapper/news.mapper.ts @@ -1,4 +1,4 @@ -import { News, ICreateNews, INewsScope, IUpdateNews, NewsTargetModel } from '@shared/domain'; +import { CreateNews, INewsScope, IUpdateNews, News, NewsTargetModel } from '@shared/domain'; import { LogMessageData } from '@src/core/logger'; import { CreateNewsParams, FilterNewsParams, NewsResponse, UpdateNewsParams } from '../controller/dto'; import { SchoolInfoMapper } from './school-info.mapper'; @@ -49,7 +49,7 @@ export class NewsMapper { return dto; } - static mapCreateNewsToDomain(params: CreateNewsParams): ICreateNews { + static mapCreateNewsToDomain(params: CreateNewsParams): CreateNews { const dto = { title: params.title, content: params.content, diff --git a/apps/server/src/modules/news/uc/news.uc.spec.ts b/apps/server/src/modules/news/uc/news.uc.spec.ts index d0671dd4259..91166132320 100644 --- a/apps/server/src/modules/news/uc/news.uc.spec.ts +++ b/apps/server/src/modules/news/uc/news.uc.spec.ts @@ -1,12 +1,12 @@ import { createMock } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; +import { FeathersAuthorizationService } from '@modules/authorization'; import { UnauthorizedException } from '@nestjs/common'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; import { Test, TestingModule } from '@nestjs/testing'; -import { ICreateNews, NewsTargetModel, Permission } from '@shared/domain'; +import { CreateNews, NewsTargetModel, Permission } from '@shared/domain'; import { NewsRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { FeathersAuthorizationService } from '@modules/authorization'; import { NewsUc } from './news.uc'; describe('NewsUc', () => { @@ -232,7 +232,7 @@ describe('NewsUc', () => { content: 'content', displayAt: new Date(), target: { targetModel: NewsTargetModel.School, targetId: schoolId }, - } as ICreateNews; + } as CreateNews; const createdNews = await service.create(userId, schoolId, params); expect(createdNews.school).toEqual(schoolId); expect(createdNews.creator).toEqual(userId); @@ -247,7 +247,7 @@ describe('NewsUc', () => { content: 'content', displayAt: new Date(), target: { targetModel: NewsTargetModel.School, targetId: schoolId }, - } as ICreateNews; + } as CreateNews; await service.create(userId, schoolId, params); expect(createSpy).toHaveBeenCalled(); }); diff --git a/apps/server/src/modules/news/uc/news.uc.ts b/apps/server/src/modules/news/uc/news.uc.ts index de0608b0234..002eaa45819 100644 --- a/apps/server/src/modules/news/uc/news.uc.ts +++ b/apps/server/src/modules/news/uc/news.uc.ts @@ -1,8 +1,9 @@ +import { FeathersAuthorizationService } from '@modules/authorization'; import { Injectable } from '@nestjs/common'; import { Counted, + CreateNews, EntityId, - ICreateNews, IFindOptions, INewsScope, IUpdateNews, @@ -14,7 +15,6 @@ import { import { NewsRepo, NewsTargetFilter } from '@shared/repo'; import { CrudOperation } from '@shared/types'; import { Logger } from '@src/core/logger'; -import { FeathersAuthorizationService } from '@modules/authorization'; import { NewsCrudOperationLoggable } from '../loggable/news-crud-operation.loggable'; type NewsPermission = Permission.NEWS_VIEW | Permission.NEWS_EDIT; @@ -36,7 +36,7 @@ export class NewsUc { * @param params * @returns */ - public async create(userId: EntityId, schoolId: EntityId, params: ICreateNews): Promise { + public async create(userId: EntityId, schoolId: EntityId, params: CreateNews): Promise { const { targetModel, targetId } = params.target; await this.authorizationService.checkEntityPermissions(userId, targetModel, targetId, [Permission.NEWS_CREATE]); diff --git a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts index bc993271066..515f54f0a39 100644 --- a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts +++ b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts @@ -1,8 +1,12 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { OauthProviderLogoutFlowUc } from '@modules/oauth-provider/uc/oauth-provider.logout-flow.uc'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { OauthProviderResponseMapper } from '@modules/oauth-provider/mapper/oauth-provider-response.mapper'; +import { + ProviderConsentResponse, + ProviderConsentSessionResponse, + ProviderLoginResponse, + ProviderRedirectResponse, +} from '@infra/oauth-provider/dto'; +import { ICurrentUser } from '@modules/authentication'; import { AcceptQuery, ChallengeParams, @@ -15,18 +19,14 @@ import { OauthClientResponse, RedirectResponse, } from '@modules/oauth-provider/controller/dto'; -import { - ProviderConsentResponse, - ProviderConsentSessionResponse, - ProviderLoginResponse, - ProviderRedirectResponse, -} from '@infra/oauth-provider/dto'; +import { OauthProviderResponseMapper } from '@modules/oauth-provider/mapper/oauth-provider-response.mapper'; import { OauthProviderConsentFlowUc } from '@modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; -import { ICurrentUser } from '@modules/authentication'; +import { OauthProviderLogoutFlowUc } from '@modules/oauth-provider/uc/oauth-provider.logout-flow.uc'; import { OauthProviderUc } from '@modules/oauth-provider/uc/oauth-provider.uc'; -import { OauthProviderController } from './oauth-provider.controller'; +import { Test, TestingModule } from '@nestjs/testing'; import { OauthProviderClientCrudUc } from '../uc/oauth-provider.client-crud.uc'; import { OauthProviderLoginFlowUc } from '../uc/oauth-provider.login-flow.uc'; +import { OauthProviderController } from './oauth-provider.controller'; describe('OauthProviderController', () => { let module: TestingModule; diff --git a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts index 97b16e8e49f..29644154a3a 100644 --- a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts +++ b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts @@ -1,22 +1,20 @@ import { Configuration } from '@hpi-schul-cloud/commons'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; -import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; // import should be @infra/oauth-provider import { ProviderConsentResponse, + ProviderConsentSessionResponse, ProviderLoginResponse, ProviderOauthClient, ProviderRedirectResponse, - ProviderConsentSessionResponse, } from '@infra/oauth-provider/dto'; import { ApiTags } from '@nestjs/swagger'; -import { OauthProviderLogoutFlowUc } from '../uc/oauth-provider.logout-flow.uc'; -import { OauthProviderLoginFlowUc } from '../uc/oauth-provider.login-flow.uc'; import { OauthProviderResponseMapper } from '../mapper/oauth-provider-response.mapper'; -import { OauthProviderConsentFlowUc } from '../uc/oauth-provider.consent-flow.uc'; -import { ConsentResponse } from './dto/response/consent.response'; import { OauthProviderClientCrudUc } from '../uc/oauth-provider.client-crud.uc'; -import { RedirectResponse } from './dto/response/redirect.response'; +import { OauthProviderConsentFlowUc } from '../uc/oauth-provider.consent-flow.uc'; +import { OauthProviderLoginFlowUc } from '../uc/oauth-provider.login-flow.uc'; +import { OauthProviderLogoutFlowUc } from '../uc/oauth-provider.logout-flow.uc'; import { OauthProviderUc } from '../uc/oauth-provider.uc'; import { AcceptQuery, @@ -31,6 +29,8 @@ import { OauthClientResponse, RevokeConsentParams, } from './dto'; +import { ConsentResponse } from './dto/response/consent.response'; +import { RedirectResponse } from './dto/response/redirect.response'; @Controller('oauth2') @ApiTags('Oauth2') diff --git a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts index 3c85dde6c62..052f3f1c6a2 100644 --- a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts +++ b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts @@ -1,12 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { NotFoundException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { LtiToolDO } from '@shared/domain'; -import { externalToolFactory, ltiToolDOFactory, setupEntities } from '@shared/testing'; import { LtiToolService } from '@modules/lti-tool'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { ExternalToolService } from '@modules/tool/external-tool/service'; import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LtiToolDO } from '@shared/domain'; +import { externalToolFactory, ltiToolDOFactory, setupEntities } from '@shared/testing'; import { OauthProviderLoginFlowService } from './oauth-provider.login-flow.service'; describe('OauthProviderLoginFlowService', () => { diff --git a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts index 0b1948baa28..adf363415fd 100644 --- a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts +++ b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts @@ -1,11 +1,11 @@ -import { Inject } from '@nestjs/common'; -import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; -import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; import { LtiToolService } from '@modules/lti-tool/service'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { ExternalToolService } from '@modules/tool/external-tool/service'; import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; +import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; +import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; @Injectable() export class OauthProviderLoginFlowService { diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts index d2eb1636e53..8e80da47969 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts @@ -1,12 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { OauthProviderService } from '@infra/oauth-provider'; +import { ProviderOauthClient } from '@infra/oauth-provider/dto'; +import { ICurrentUser } from '@modules/authentication'; +import { AuthorizationService } from '@modules/authorization'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, User } from '@shared/domain'; -import { OauthProviderService } from '@infra/oauth-provider'; -import { ProviderOauthClient } from '@infra/oauth-provider/dto'; import { setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationService } from '@modules/authorization'; -import { ICurrentUser } from '@modules/authentication'; import { OauthProviderClientCrudUc } from './oauth-provider.client-crud.uc'; import resetAllMocks = jest.resetAllMocks; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts index 18fd23ae788..84221847796 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts @@ -1,9 +1,9 @@ -import { Injectable } from '@nestjs/common'; -import { OauthProviderService } from '@infra/oauth-provider/index'; -import { Permission, User } from '@shared/domain/index'; -import { AuthorizationService } from '@modules/authorization'; import { ProviderOauthClient } from '@infra/oauth-provider/dto'; +import { OauthProviderService } from '@infra/oauth-provider/index'; import { ICurrentUser } from '@modules/authentication'; +import { AuthorizationService } from '@modules/authorization'; +import { Injectable } from '@nestjs/common'; +import { Permission, User } from '@shared/domain/index'; @Injectable() export class OauthProviderClientCrudUc { diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts index e56700477a8..e1a1663818e 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts @@ -1,13 +1,13 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { OauthProviderService } from '@infra/oauth-provider'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { AcceptQuery, ConsentRequestBody } from '@modules/oauth-provider/controller/dto'; +import { OauthProviderService } from '@infra/oauth-provider'; import { AcceptConsentRequestBody, ProviderConsentResponse, ProviderRedirectResponse } from '@infra/oauth-provider/dto'; -import { OauthProviderConsentFlowUc } from '@modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; import { ICurrentUser } from '@modules/authentication'; -import { ForbiddenException } from '@nestjs/common'; -import { IdTokenService } from '@modules/oauth-provider/service/id-token.service'; +import { AcceptQuery, ConsentRequestBody } from '@modules/oauth-provider/controller/dto'; import { IdToken } from '@modules/oauth-provider/interface/id-token'; +import { IdTokenService } from '@modules/oauth-provider/service/id-token.service'; +import { OauthProviderConsentFlowUc } from '@modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; +import { ForbiddenException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; describe('OauthProviderConsentFlowUc', () => { let module: TestingModule; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts index 126f68f1b80..a9c54b4f502 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts @@ -1,15 +1,15 @@ +import { OauthProviderService } from '@infra/oauth-provider'; import { AcceptConsentRequestBody, ProviderConsentResponse, ProviderRedirectResponse, RejectRequestBody, } from '@infra/oauth-provider/dto'; -import { AcceptQuery, ConsentRequestBody } from '@modules/oauth-provider/controller/dto'; import { ICurrentUser } from '@modules/authentication'; -import { ForbiddenException, Injectable } from '@nestjs/common'; -import { IdTokenService } from '@modules/oauth-provider/service/id-token.service'; -import { OauthProviderService } from '@infra/oauth-provider'; +import { AcceptQuery, ConsentRequestBody } from '@modules/oauth-provider/controller/dto'; import { IdToken } from '@modules/oauth-provider/interface/id-token'; +import { IdTokenService } from '@modules/oauth-provider/service/id-token.service'; +import { ForbiddenException, Injectable } from '@nestjs/common'; @Injectable() export class OauthProviderConsentFlowUc { diff --git a/apps/server/src/modules/oauth/service/hydra.service.ts b/apps/server/src/modules/oauth/service/hydra.service.ts index 360926d080a..9c02a537d42 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.ts @@ -1,26 +1,26 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; +import { AuthorizationParams } from '@modules/oauth/controller/dto/authorization.params'; +import { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; +import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; import { HttpService } from '@nestjs/axios'; import { Inject, InternalServerErrorException } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { OauthConfig } from '@shared/domain'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; import { LtiToolRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationParams } from '@modules/oauth/controller/dto/authorization.params'; -import { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; -import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { nanoid } from 'nanoid'; import QueryString from 'qs'; -import { Observable, firstValueFrom } from 'rxjs'; +import { firstValueFrom, Observable } from 'rxjs'; @Injectable() export class HydraSsoService { constructor( private readonly ltiRepo: LtiToolRepo, private readonly httpService: HttpService, - @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: IEncryptionService, + @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: EncryptionService, private readonly logger: LegacyLogger ) {} diff --git a/apps/server/src/modules/oauth/service/oauth.service.spec.ts b/apps/server/src/modules/oauth/service/oauth.service.spec.ts index adfa9603bc6..368586af7c4 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; -import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; +import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { LegacySchoolService } from '@modules/legacy-school'; import { ProvisioningService } from '@modules/provisioning'; @@ -70,7 +70,7 @@ describe('OAuthService', () => { }, { provide: DefaultEncryptionService, - useValue: createMock(), + useValue: createMock(), }, { provide: LegacyLogger, diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index 5e787d79082..299198ef33a 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -1,6 +1,6 @@ -import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { LegacySchoolService } from '@modules/legacy-school'; -import { ProvisioningService, OauthDataDto } from '@modules/provisioning'; +import { OauthDataDto, ProvisioningService } from '@modules/provisioning'; import { SystemService } from '@modules/system'; import { SystemDto } from '@modules/system/service'; import { UserService } from '@modules/user'; @@ -21,7 +21,7 @@ export class OAuthService { constructor( private readonly userService: UserService, private readonly oauthAdapterService: OauthAdapterService, - @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: IEncryptionService, + @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: EncryptionService, private readonly logger: LegacyLogger, private readonly provisioningService: ProvisioningService, private readonly systemService: SystemService, diff --git a/apps/server/src/modules/provisioning/dto/external-user.dto.ts b/apps/server/src/modules/provisioning/dto/external-user.dto.ts index eb973cf0fbe..d7291504b57 100644 --- a/apps/server/src/modules/provisioning/dto/external-user.dto.ts +++ b/apps/server/src/modules/provisioning/dto/external-user.dto.ts @@ -11,11 +11,14 @@ export class ExternalUserDto { roles?: RoleName[]; + birthday?: Date; + constructor(props: ExternalUserDto) { this.externalId = props.externalId; this.firstName = props.firstName; this.lastName = props.lastName; this.email = props.email; this.roles = props.roles; + this.birthday = props.birthday; } } diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts index 5fcd5fc37a5..c313be9973d 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts @@ -518,6 +518,7 @@ describe('OidcProvisioningService', () => { const setupUser = () => { const systemId = 'systemId'; const schoolId = 'schoolId'; + const birthday = new Date('2023-11-17'); const existingUser: UserDO = userDoFactory.withRoles([{ id: 'existingRoleId', name: RoleName.USER }]).buildWithId( { firstName: 'existingFirstName', @@ -525,6 +526,7 @@ describe('OidcProvisioningService', () => { email: 'existingEmail', schoolId: 'existingSchoolId', externalId: 'externalUserId', + birthday: new Date('2023-11-16'), }, 'userId' ); @@ -535,6 +537,7 @@ describe('OidcProvisioningService', () => { email: 'email', schoolId, externalId: 'externalUserId', + birthday, }, 'userId' ); @@ -544,6 +547,7 @@ describe('OidcProvisioningService', () => { lastName: 'lastName', email: 'email', roles: [RoleName.USER], + birthday, }); const userRole: RoleDto = new RoleDto({ id: 'roleId', diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts index 6d13537439b..52b0e7472e2 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts @@ -92,6 +92,7 @@ export class OidcProvisioningService { user.email = externalUser.email ?? existingUser.email; user.roles = roleRefs ?? existingUser.roles; user.schoolId = schoolId ?? existingUser.schoolId; + user.birthday = externalUser.birthday ?? existingUser.birthday; } else { createNewAccount = true; @@ -108,6 +109,7 @@ export class OidcProvisioningService { email: externalUser.email ?? '', roles: roleRefs ?? [], schoolId, + birthday: externalUser.birthday, }); } diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-geburt-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-geburt-response.ts new file mode 100644 index 00000000000..618be9dd9e6 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-geburt-response.ts @@ -0,0 +1,3 @@ +export interface SanisGeburtResponse { + datum?: string; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-name-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-name-response.ts index fad5c23319a..1da6a4ad8f9 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-name-response.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-name-response.ts @@ -1,5 +1,5 @@ export interface SanisNameResponse { - familienname: string; + familienname?: string; - vorname: string; + vorname?: string; } diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-person-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-person-response.ts index e34d324b2e7..6b225b58032 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-person-response.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-person-response.ts @@ -1,11 +1,14 @@ +import { SanisGeburtResponse } from './sanis-geburt-response'; import { SanisNameResponse } from './sanis-name-response'; export interface SanisPersonResponse { - name: SanisNameResponse; + name?: SanisNameResponse; - geschlecht: string; + geburt?: SanisGeburtResponse; - lokalisierung: string; + geschlecht?: string; - vertrauensstufe: string; + lokalisierung?: string; + + vertrauensstufe?: string; } diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts index 9829e3cb930..c560d4f7c3a 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts @@ -36,6 +36,7 @@ describe('SanisResponseMapper', () => { const setupSanisResponse = () => { const externalUserId = 'aef1f4fd-c323-466e-962b-a84354c0e713'; const externalSchoolId = 'df66c8e6-cfac-40f7-b35b-0da5d8ee680e'; + const sanisResponse: SanisResponse = { pid: externalUserId, person: { @@ -43,6 +44,9 @@ describe('SanisResponseMapper', () => { vorname: 'firstName', familienname: 'lastName', }, + geburt: { + datum: '2023-11-17', + }, geschlecht: 'x', lokalisierung: 'de-de', vertrauensstufe: '', @@ -124,6 +128,7 @@ describe('SanisResponseMapper', () => { firstName: 'firstName', lastName: 'lastName', roles: [RoleName.STUDENT], + birthday: new Date('2023-11-17'), }); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts index ee912dd67b5..3ca0d06806e 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts @@ -53,10 +53,11 @@ export class SanisResponseMapper { mapToExternalUserDto(source: SanisResponse): ExternalUserDto { const mapped = new ExternalUserDto({ - firstName: source.person.name.vorname, - lastName: source.person.name.familienname, + firstName: source.person.name?.vorname, + lastName: source.person.name?.familienname, roles: [this.mapSanisRoleToRoleName(source)], externalId: source.pid, + birthday: source.person.geburt?.datum ? new Date(source.person.geburt?.datum) : undefined, }); return mapped; diff --git a/apps/server/src/modules/pseudonym/controller/pseudonym.controller.ts b/apps/server/src/modules/pseudonym/controller/pseudonym.controller.ts index f7e378e050a..993f3ba9ded 100644 --- a/apps/server/src/modules/pseudonym/controller/pseudonym.controller.ts +++ b/apps/server/src/modules/pseudonym/controller/pseudonym.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Controller, Get, Param } from '@nestjs/common'; import { ApiForbiddenResponse, @@ -7,7 +8,6 @@ import { ApiUnauthorizedResponse, } from '@nestjs/swagger'; import { Pseudonym } from '@shared/domain'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { PseudonymMapper } from '../mapper/pseudonym.mapper'; import { PseudonymUc } from '../uc'; import { PseudonymResponse } from './dto'; diff --git a/apps/server/src/modules/pseudonym/entity/external-tool-pseudonym.entity.ts b/apps/server/src/modules/pseudonym/entity/external-tool-pseudonym.entity.ts index 3505f0bc7df..9fabd37f91c 100644 --- a/apps/server/src/modules/pseudonym/entity/external-tool-pseudonym.entity.ts +++ b/apps/server/src/modules/pseudonym/entity/external-tool-pseudonym.entity.ts @@ -1,9 +1,9 @@ import { Entity, Property, Unique } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { EntityId } from '@shared/domain'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; -export interface IExternalToolPseudonymEntityProps { +export interface ExternalToolPseudonymEntityProps { id?: EntityId; pseudonym: string; toolId: ObjectId; @@ -23,7 +23,7 @@ export class ExternalToolPseudonymEntity extends BaseEntityWithTimestamps { @Property() userId: ObjectId; - constructor(props: IExternalToolPseudonymEntityProps) { + constructor(props: ExternalToolPseudonymEntityProps) { super(); if (props.id != null) { this.id = props.id; diff --git a/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.ts b/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.ts index 79a17a80541..65ef03cd539 100644 --- a/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.ts +++ b/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.ts @@ -1,9 +1,9 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; -import { EntityId, IFindOptions, IPagination, Page, Pseudonym } from '@shared/domain'; +import { EntityId, IFindOptions, Page, Pagination, Pseudonym } from '@shared/domain'; import { Scope } from '@shared/repo'; import { PseudonymSearchQuery } from '../domain'; -import { ExternalToolPseudonymEntity, IExternalToolPseudonymEntityProps } from '../entity'; +import { ExternalToolPseudonymEntity, ExternalToolPseudonymEntityProps } from '../entity'; import { PseudonymScope } from '../entity/pseudonym.scope'; @Injectable() @@ -50,7 +50,7 @@ export class ExternalToolPseudonymRepo { .getUnitOfWork() .getById(ExternalToolPseudonymEntity.name, domainObject.id); - const entityProps: IExternalToolPseudonymEntityProps = this.mapDomainObjectToEntityProperties(domainObject); + const entityProps: ExternalToolPseudonymEntityProps = this.mapDomainObjectToEntityProperties(domainObject); let entity: ExternalToolPseudonymEntity = new ExternalToolPseudonymEntity(entityProps); if (existing) { @@ -101,7 +101,7 @@ export class ExternalToolPseudonymRepo { return pseudonym; } - protected mapDomainObjectToEntityProperties(entityDO: Pseudonym): IExternalToolPseudonymEntityProps { + protected mapDomainObjectToEntityProperties(entityDO: Pseudonym): ExternalToolPseudonymEntityProps { return { pseudonym: entityDO.pseudonym, toolId: new ObjectId(entityDO.toolId), @@ -110,7 +110,7 @@ export class ExternalToolPseudonymRepo { } async findPseudonym(query: PseudonymSearchQuery, options?: IFindOptions): Promise> { - const pagination: IPagination = options?.pagination ?? {}; + const pagination: Pagination = options?.pagination ?? {}; const scope: Scope = new PseudonymScope() .byPseudonym(query.pseudonym) .byToolId(query.toolId) diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 5d1ea95cc3b..c119d0fa25a 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -1,11 +1,11 @@ import { Configuration } from '@hpi-schul-cloud/commons'; -import type { IIdentityManagementConfig } from '@infra/identity-management'; -import type { ICoreModuleConfig } from '@src/core'; -import type { IAccountConfig } from '@modules/account'; -import type { IFilesStorageClientConfig } from '@modules/files-storage-client'; -import type { IUserConfig } from '@modules/user'; -import type { ICommonCartridgeConfig } from '@modules/learnroom/common-cartridge'; -import { IMailConfig } from '@src/infra/mail/interfaces/mail-config'; +import type { IdentityManagementConfig } from '@infra/identity-management'; +import type { AccountConfig } from '@modules/account'; +import type { FilesStorageClientConfig } from '@modules/files-storage-client'; +import type { CommonCartridgeConfig } from '@modules/learnroom/common-cartridge'; +import type { UserConfig } from '@modules/user'; +import type { CoreModuleConfig } from '@src/core'; +import { MailConfig } from '@src/infra/mail/interfaces/mail-config'; export enum NodeEnvType { TEST = 'test', @@ -14,19 +14,19 @@ export enum NodeEnvType { MIGRATION = 'migration', } -export interface IServerConfig - extends ICoreModuleConfig, - IUserConfig, - IFilesStorageClientConfig, - IAccountConfig, - IIdentityManagementConfig, - ICommonCartridgeConfig, - IMailConfig { +export interface ServerConfig + extends CoreModuleConfig, + UserConfig, + FilesStorageClientConfig, + AccountConfig, + IdentityManagementConfig, + CommonCartridgeConfig, + MailConfig { NODE_ENV: string; SC_DOMAIN: string; } -const config: IServerConfig = { +const config: ServerConfig = { SC_DOMAIN: Configuration.get('SC_DOMAIN') as string, INCOMING_REQUEST_TIMEOUT: Configuration.get('INCOMING_REQUEST_TIMEOUT_API') as number, INCOMING_REQUEST_TIMEOUT_COPY_API: Configuration.get('INCOMING_REQUEST_TIMEOUT_COPY_API') as number, diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts index 7890f778f86..a79b02046ac 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts @@ -1,12 +1,12 @@ -import { Request } from 'express'; -import request from 'supertest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseFactory, @@ -15,10 +15,10 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; -import { ShareTokenBodyParams, ShareTokenResponse } from '../dto'; +import { Request } from 'express'; +import request from 'supertest'; import { ShareTokenParentType } from '../../domainobject/share-token.do'; +import { ShareTokenBodyParams, ShareTokenResponse } from '../dto'; const baseRouteName = '/sharetoken'; diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts index f19dea681f2..9d8df39e195 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts @@ -1,10 +1,13 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; +import { ServerTestModule } from '@modules/server'; import { ExecutionContext, HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseFactory, @@ -13,9 +16,6 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; -import { ServerTestModule } from '@modules/server'; import { Request } from 'express'; import request from 'supertest'; import { ShareTokenContext, ShareTokenContextType, ShareTokenParentType } from '../../domainobject/share-token.do'; diff --git a/apps/server/src/modules/sharing/controller/share-token.controller.spec.ts b/apps/server/src/modules/sharing/controller/share-token.controller.spec.ts index 7d6f51fa471..d37b8b435ff 100644 --- a/apps/server/src/modules/sharing/controller/share-token.controller.spec.ts +++ b/apps/server/src/modules/sharing/controller/share-token.controller.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { courseFactory, setupEntities, shareTokenFactory } from '@shared/testing'; import { ICurrentUser } from '@modules/authentication'; import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { Test, TestingModule } from '@nestjs/testing'; +import { courseFactory, setupEntities, shareTokenFactory } from '@shared/testing'; import { ShareTokenParentType } from '../domainobject/share-token.do'; import { ShareTokenUC } from '../uc'; import { ShareTokenInfoDto } from '../uc/dto'; diff --git a/apps/server/src/modules/sharing/controller/share-token.controller.ts b/apps/server/src/modules/sharing/controller/share-token.controller.ts index 373e1169600..5d990e1d99f 100644 --- a/apps/server/src/modules/sharing/controller/share-token.controller.ts +++ b/apps/server/src/modules/sharing/controller/share-token.controller.ts @@ -1,3 +1,5 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; import { Body, Controller, @@ -11,8 +13,6 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError, RequestTimeout } from '@shared/common'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; -import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; // invalid import can produce dependency cycles import { serverConfig } from '@modules/server/server.config'; import { ShareTokenInfoResponseMapper, ShareTokenResponseMapper } from '../mapper'; diff --git a/apps/server/src/modules/sharing/entity/share-token.entity.ts b/apps/server/src/modules/sharing/entity/share-token.entity.ts index fd83e054029..c5e2b41e6d0 100644 --- a/apps/server/src/modules/sharing/entity/share-token.entity.ts +++ b/apps/server/src/modules/sharing/entity/share-token.entity.ts @@ -4,7 +4,7 @@ import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { EntityId } from '@shared/domain/types/entity-id'; import { ShareTokenContextType, ShareTokenParentType, ShareTokenString } from '../domainobject/share-token.do'; -export interface IShareTokenProperties { +export interface ShareTokenProperties { token: ShareTokenString; parentType: ShareTokenParentType; parentId: EntityId | ObjectId; @@ -42,7 +42,7 @@ export class ShareToken extends BaseEntityWithTimestamps { @Property({ nullable: true }) expiresAt?: Date; - constructor(props: IShareTokenProperties) { + constructor(props: ShareTokenProperties) { super(); this.token = props.token; this.parentType = props.parentType; diff --git a/apps/server/src/modules/sharing/repo/share-token.repo.ts b/apps/server/src/modules/sharing/repo/share-token.repo.ts index a04808df2de..ea5fee7194f 100644 --- a/apps/server/src/modules/sharing/repo/share-token.repo.ts +++ b/apps/server/src/modules/sharing/repo/share-token.repo.ts @@ -2,15 +2,15 @@ import { EntityName } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { BaseDORepo } from '@shared/repo/base.do.repo'; import { ShareTokenContext, ShareTokenDO, ShareTokenPayload, ShareTokenString } from '../domainobject/share-token.do'; -import { IShareTokenProperties, ShareToken } from '../entity/share-token.entity'; +import { ShareToken, ShareTokenProperties } from '../entity/share-token.entity'; @Injectable() -export class ShareTokenRepo extends BaseDORepo { +export class ShareTokenRepo extends BaseDORepo { get entityName(): EntityName { return ShareToken; } - entityFactory(props: IShareTokenProperties): ShareToken { + entityFactory(props: ShareTokenProperties): ShareToken { return new ShareToken(props); } @@ -43,8 +43,8 @@ export class ShareTokenRepo extends BaseDORepo { let authorization: DeepMocked; let authorizationReferenceService: DeepMocked; let courseService: DeepMocked; - let lessonRepo: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -62,10 +63,6 @@ describe('ShareTokenUC', () => { provide: LessonCopyService, useValue: createMock(), }, - { - provide: LessonRepo, - useValue: createMock(), - }, { provide: CourseService, useValue: createMock(), @@ -89,7 +86,6 @@ describe('ShareTokenUC', () => { authorization = module.get(AuthorizationService); authorizationReferenceService = module.get(AuthorizationReferenceService); courseService = module.get(CourseService); - lessonRepo = module.get(LessonRepo); await setupEntities(); }); @@ -729,7 +725,6 @@ describe('ShareTokenUC', () => { const course = courseFactory.buildWithId(); courseService.findById.mockResolvedValue(course); const lesson = lessonFactory.buildWithId({ course }); - lessonRepo.findById.mockResolvedValue(lesson); const status: CopyStatus = { type: CopyElementType.LESSON, diff --git a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts index d067abf1889..3167558461d 100644 --- a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts +++ b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts @@ -1,11 +1,11 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { OauthConfig, SystemEntity } from '@shared/domain'; import { cleanupCollections, systemFactory } from '@shared/testing'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server'; import { Request } from 'express'; import request, { Response } from 'supertest'; import { PublicSystemListResponse } from '../dto/public-system-list.response'; diff --git a/apps/server/src/modules/task/controller/api-test/submission.api.spec.ts b/apps/server/src/modules/task/controller/api-test/submission.api.spec.ts index 6aea7ac2521..aed731a86e8 100644 --- a/apps/server/src/modules/task/controller/api-test/submission.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/submission.api.spec.ts @@ -1,10 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { ServerTestModule } from '@modules/server/server.module'; +import { SubmissionStatusListResponse } from '@modules/task/controller/dto/submission.response'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission, Submission } from '@shared/domain'; -import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseGroupFactory, @@ -14,10 +18,6 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; -import { SubmissionStatusListResponse } from '@modules/task/controller/dto/submission.response'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/task/controller/api-test/task-finished.api.spec.ts b/apps/server/src/modules/task/controller/api-test/task-finished.api.spec.ts index 93c79e4015d..f8261f9649b 100644 --- a/apps/server/src/modules/task/controller/api-test/task-finished.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/task-finished.api.spec.ts @@ -1,8 +1,11 @@ import { EntityManager } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; +import { TaskListResponse } from '@modules/task/controller/dto'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseFactory, @@ -12,9 +15,6 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; -import { TaskListResponse } from '@modules/task/controller/dto'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/task/controller/dto/task-create.params.ts b/apps/server/src/modules/task/controller/dto/task-create.params.ts index 0fa020ad779..1489f208a37 100644 --- a/apps/server/src/modules/task/controller/dto/task-create.params.ts +++ b/apps/server/src/modules/task/controller/dto/task-create.params.ts @@ -1,9 +1,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsDate, IsMongoId, IsOptional, IsString } from 'class-validator'; -import { InputFormat, ITaskCreate } from '@shared/domain'; import { SanitizeHtml } from '@shared/controller'; +import { InputFormat, TaskCreate } from '@shared/domain'; +import { IsDate, IsMongoId, IsOptional, IsString } from 'class-validator'; -export class TaskCreateParams implements ITaskCreate { +export class TaskCreateParams implements TaskCreate { @IsString() @IsMongoId() @IsOptional() diff --git a/apps/server/src/modules/task/controller/dto/task-update.params.ts b/apps/server/src/modules/task/controller/dto/task-update.params.ts index 5d906ca078c..7cefef35311 100644 --- a/apps/server/src/modules/task/controller/dto/task-update.params.ts +++ b/apps/server/src/modules/task/controller/dto/task-update.params.ts @@ -1,9 +1,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsDate, IsMongoId, IsOptional, IsString } from 'class-validator'; -import { InputFormat, ITaskUpdate } from '@shared/domain'; import { SanitizeHtml } from '@shared/controller'; +import { InputFormat, TaskUpdate } from '@shared/domain'; +import { IsDate, IsMongoId, IsOptional, IsString } from 'class-validator'; -export class TaskUpdateParams implements ITaskUpdate { +export class TaskUpdateParams implements TaskUpdate { @IsString() @IsMongoId() @IsOptional() diff --git a/apps/server/src/modules/task/controller/submission.controller.ts b/apps/server/src/modules/task/controller/submission.controller.ts index 64d3a23296f..48f0899b07e 100644 --- a/apps/server/src/modules/task/controller/submission.controller.ts +++ b/apps/server/src/modules/task/controller/submission.controller.ts @@ -1,6 +1,6 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Controller, Delete, Get, Param } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { SubmissionMapper } from '../mapper'; import { SubmissionUc } from '../uc'; import { SubmissionStatusListResponse, SubmissionUrlParams, TaskUrlParams } from './dto'; diff --git a/apps/server/src/modules/task/controller/task.controller.spec.ts b/apps/server/src/modules/task/controller/task.controller.spec.ts index e44deee06c3..b76bb66d4da 100644 --- a/apps/server/src/modules/task/controller/task.controller.spec.ts +++ b/apps/server/src/modules/task/controller/task.controller.spec.ts @@ -1,8 +1,8 @@ import { createMock } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; import { ICurrentUser } from '@modules/authentication'; import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { CopyApiResponse } from '@modules/copy-helper/dto/copy.response'; +import { Test, TestingModule } from '@nestjs/testing'; import { TaskCopyUC, TaskUC } from '../uc'; import { TaskController } from './task.controller'; diff --git a/apps/server/src/modules/task/controller/task.controller.ts b/apps/server/src/modules/task/controller/task.controller.ts index 44911973ffd..b8e7231a1b6 100644 --- a/apps/server/src/modules/task/controller/task.controller.ts +++ b/apps/server/src/modules/task/controller/task.controller.ts @@ -1,9 +1,9 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { RequestTimeout } from '@shared/common'; import { PaginationParams } from '@shared/controller/'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; -import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; // invalid import can produce dependency cycles import { serverConfig } from '@modules/server/server.config'; import { TaskMapper } from '../mapper'; diff --git a/apps/server/src/modules/task/index.ts b/apps/server/src/modules/task/index.ts index 62ab8a69e94..8734b0eb623 100644 --- a/apps/server/src/modules/task/index.ts +++ b/apps/server/src/modules/task/index.ts @@ -1 +1,4 @@ +export * from './service/submission.service'; +export * from './service/task-copy.service'; +export * from './service/task.service'; export * from './task.module'; diff --git a/apps/server/src/modules/task/mapper/task-status.mapper.ts b/apps/server/src/modules/task/mapper/task-status.mapper.ts index 56eda114a11..291ed270ba6 100644 --- a/apps/server/src/modules/task/mapper/task-status.mapper.ts +++ b/apps/server/src/modules/task/mapper/task-status.mapper.ts @@ -1,8 +1,8 @@ -import { ITaskStatus } from '@shared/domain'; +import { TaskStatus } from '@shared/domain'; import { TaskStatusResponse } from '../controller/dto/task-status.response'; export class TaskStatusMapper { - static mapToResponse(status: ITaskStatus): TaskStatusResponse { + static mapToResponse(status: TaskStatus): TaskStatusResponse { const dto = new TaskStatusResponse(status); return dto; diff --git a/apps/server/src/modules/task/mapper/task.mapper.spec.ts b/apps/server/src/modules/task/mapper/task.mapper.spec.ts index f53d383e07e..2513101eca3 100644 --- a/apps/server/src/modules/task/mapper/task.mapper.spec.ts +++ b/apps/server/src/modules/task/mapper/task.mapper.spec.ts @@ -1,14 +1,10 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { InputFormat, ITaskStatus, ITaskUpdate, Task, TaskParentDescriptions } from '@shared/domain'; +import { InputFormat, Task, TaskParentDescriptions, TaskStatus, TaskUpdate } from '@shared/domain'; import { setupEntities, taskFactory } from '@shared/testing'; import { TaskCreateParams, TaskResponse, TaskStatusResponse, TaskUpdateParams } from '../controller/dto'; import { TaskMapper } from './task.mapper'; -const createExpectedResponse = ( - task: Task, - status: ITaskStatus, - descriptions: TaskParentDescriptions -): TaskResponse => { +const createExpectedResponse = (task: Task, status: TaskStatus, descriptions: TaskParentDescriptions): TaskResponse => { const expectedStatus = Object.create(TaskStatusResponse.prototype) as TaskStatusResponse; expectedStatus.graded = status.graded; expectedStatus.maxSubmissions = status.maxSubmissions; @@ -88,7 +84,7 @@ describe('task.mapper', () => { }; const result = TaskMapper.mapTaskUpdateToDomain(params); - const expected: ITaskUpdate = { + const expected: TaskUpdate = { name: params.name, courseId: params.courseId, lessonId: params.lessonId, @@ -112,7 +108,7 @@ describe('task.mapper', () => { }; const result = TaskMapper.mapTaskCreateToDomain(params); - const expected: ITaskUpdate = { + const expected: TaskUpdate = { name: params.name, courseId: params.courseId, lessonId: params.lessonId, diff --git a/apps/server/src/modules/task/mapper/task.mapper.ts b/apps/server/src/modules/task/mapper/task.mapper.ts index a8db8e59af4..2e8d02ae977 100644 --- a/apps/server/src/modules/task/mapper/task.mapper.ts +++ b/apps/server/src/modules/task/mapper/task.mapper.ts @@ -1,4 +1,4 @@ -import { InputFormat, ITaskCreate, ITaskUpdate, RichText, TaskWithStatusVo } from '@shared/domain'; +import { InputFormat, RichText, TaskCreate, TaskUpdate, TaskWithStatusVo } from '@shared/domain'; import { TaskCreateParams, TaskResponse, TaskUpdateParams } from '../controller/dto'; import { TaskStatusMapper } from './task-status.mapper'; @@ -36,8 +36,8 @@ export class TaskMapper { return dto; } - static mapTaskUpdateToDomain(params: TaskUpdateParams): ITaskUpdate { - const dto: ITaskUpdate = { + static mapTaskUpdateToDomain(params: TaskUpdateParams): TaskUpdate { + const dto: TaskUpdate = { name: params.name, courseId: params.courseId, lessonId: params.lessonId, @@ -51,8 +51,8 @@ export class TaskMapper { return dto; } - static mapTaskCreateToDomain(params: TaskCreateParams): ITaskCreate { - const dto: ITaskCreate = { + static mapTaskCreateToDomain(params: TaskCreateParams): TaskCreate { + const dto: TaskCreate = { name: params.name || 'Draft', courseId: params.courseId, lessonId: params.lessonId, diff --git a/apps/server/src/modules/task/task-api.module.ts b/apps/server/src/modules/task/task-api.module.ts index cdb998eab4a..6085e426f6c 100644 --- a/apps/server/src/modules/task/task-api.module.ts +++ b/apps/server/src/modules/task/task-api.module.ts @@ -1,14 +1,15 @@ -import { Module } from '@nestjs/common'; -import { CourseRepo, LessonRepo, TaskRepo } from '@shared/repo'; import { AuthorizationModule } from '@modules/authorization'; import { CopyHelperModule } from '@modules/copy-helper/copy-helper.module'; +import { Module } from '@nestjs/common'; +import { CourseRepo, TaskRepo } from '@shared/repo'; +import { LessonModule } from '@modules/lesson'; import { SubmissionController, TaskController } from './controller'; import { TaskModule } from './task.module'; import { SubmissionUc, TaskCopyUC, TaskUC } from './uc'; @Module({ - imports: [AuthorizationModule, CopyHelperModule, TaskModule], + imports: [AuthorizationModule, CopyHelperModule, TaskModule, LessonModule], controllers: [TaskController, SubmissionController], - providers: [TaskUC, TaskRepo, LessonRepo, CourseRepo, TaskCopyUC, SubmissionUc], + providers: [TaskUC, TaskRepo, CourseRepo, TaskCopyUC, SubmissionUc], }) export class TaskApiModule {} diff --git a/apps/server/src/modules/task/task.module.ts b/apps/server/src/modules/task/task.module.ts index 45a0fdb720a..87ecf144798 100644 --- a/apps/server/src/modules/task/task.module.ts +++ b/apps/server/src/modules/task/task.module.ts @@ -1,12 +1,12 @@ import { CopyHelperModule } from '@modules/copy-helper'; import { FilesStorageClientModule } from '@modules/files-storage-client'; import { Module } from '@nestjs/common'; -import { CourseRepo, LessonRepo, SubmissionRepo, TaskRepo } from '@shared/repo'; +import { CourseRepo, SubmissionRepo, TaskRepo } from '@shared/repo'; import { SubmissionService, TaskCopyService, TaskService } from './service'; @Module({ imports: [FilesStorageClientModule, CopyHelperModule], - providers: [TaskService, TaskCopyService, SubmissionService, TaskRepo, LessonRepo, CourseRepo, SubmissionRepo], + providers: [TaskService, TaskCopyService, SubmissionService, TaskRepo, CourseRepo, SubmissionRepo], exports: [TaskService, TaskCopyService, SubmissionService], }) export class TaskModule {} diff --git a/apps/server/src/modules/task/uc/task-copy.uc.spec.ts b/apps/server/src/modules/task/uc/task-copy.uc.spec.ts index dc381cda22e..182294cfd91 100644 --- a/apps/server/src/modules/task/uc/task-copy.uc.spec.ts +++ b/apps/server/src/modules/task/uc/task-copy.uc.spec.ts @@ -1,23 +1,24 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; import { ObjectId } from '@mikro-orm/mongodb'; -import { ForbiddenException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { CourseRepo, LessonRepo, TaskRepo, UserRepo } from '@shared/repo'; -import { courseFactory, lessonFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; import { Action, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@modules/copy-helper'; import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { LessonService } from '@modules/lesson'; +import { ForbiddenException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { CourseRepo, TaskRepo, UserRepo } from '@shared/repo'; +import { courseFactory, lessonFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; import { TaskCopyService } from '../service'; -import { TaskCopyUC } from './task-copy.uc'; import { TaskCopyParentParams } from '../types'; +import { TaskCopyUC } from './task-copy.uc'; describe('task copy uc', () => { let uc: TaskCopyUC; let userRepo: DeepMocked; let taskRepo: DeepMocked; let courseRepo: DeepMocked; - let lessonRepo: DeepMocked; + let lessonService: DeepMocked; let authorisation: DeepMocked; let taskCopyService: DeepMocked; let copyHelperService: DeepMocked; @@ -42,8 +43,8 @@ describe('task copy uc', () => { useValue: createMock(), }, { - provide: LessonRepo, - useValue: createMock(), + provide: LessonService, + useValue: createMock(), }, { provide: AuthorizationService, @@ -69,7 +70,7 @@ describe('task copy uc', () => { taskRepo = module.get(TaskRepo); authorisation = module.get(AuthorizationService); courseRepo = module.get(CourseRepo); - lessonRepo = module.get(LessonRepo); + lessonService = module.get(LessonService); taskCopyService = module.get(TaskCopyService); copyHelperService = module.get(CopyHelperService); }); @@ -104,7 +105,7 @@ describe('task copy uc', () => { authorisation.getUserWithPermissions.mockResolvedValueOnce(user); taskRepo.findById.mockResolvedValueOnce(task); - lessonRepo.findById.mockResolvedValueOnce(lesson); + lessonService.findById.mockResolvedValueOnce(lesson); taskRepo.findBySingleParent.mockResolvedValueOnce([allTasks, allTasks.length]); courseRepo.findById.mockResolvedValueOnce(course); authorisation.hasPermission.mockReturnValueOnce(true).mockReturnValueOnce(true); @@ -187,7 +188,7 @@ describe('task copy uc', () => { await uc.copyTask(user.id, task.id, { lessonId: lesson.id, userId }); - expect(lessonRepo.findById).toBeCalledWith(lesson.id); + expect(lessonService.findById).toBeCalledWith(lesson.id); }); it('should pass without destination lesson', async () => { @@ -195,7 +196,7 @@ describe('task copy uc', () => { await uc.copyTask(user.id, task.id, { userId }); - expect(lessonRepo.findById).not.toHaveBeenCalled(); + expect(lessonService.findById).not.toHaveBeenCalled(); }); }); @@ -365,7 +366,7 @@ describe('task copy uc', () => { userRepo.findById.mockResolvedValueOnce(user); taskRepo.findById.mockResolvedValueOnce(task); courseRepo.findById.mockResolvedValueOnce(course); - lessonRepo.findById.mockResolvedValueOnce(lesson); + lessonService.findById.mockResolvedValueOnce(lesson); // first canReadTask > second canWriteLesson authorisation.hasPermission.mockReturnValueOnce(true).mockReturnValueOnce(false); diff --git a/apps/server/src/modules/task/uc/task-copy.uc.ts b/apps/server/src/modules/task/uc/task-copy.uc.ts index 69fd99e224f..58316aa1972 100644 --- a/apps/server/src/modules/task/uc/task-copy.uc.ts +++ b/apps/server/src/modules/task/uc/task-copy.uc.ts @@ -1,9 +1,10 @@ import { Configuration } from '@hpi-schul-cloud/commons'; -import { ForbiddenException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; -import { Course, EntityId, Task, LessonEntity, User } from '@shared/domain'; -import { CourseRepo, LessonRepo, TaskRepo } from '@shared/repo'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { CopyHelperService, CopyStatus } from '@modules/copy-helper'; +import { ForbiddenException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { Course, EntityId, LessonEntity, Task, User } from '@shared/domain'; +import { CourseRepo, TaskRepo } from '@shared/repo'; +import { LessonService } from '@modules/lesson'; import { TaskCopyService } from '../service'; import { TaskCopyParentParams } from '../types'; @@ -11,7 +12,7 @@ import { TaskCopyParentParams } from '../types'; export class TaskCopyUC { constructor( private readonly courseRepo: CourseRepo, - private readonly lessonRepo: LessonRepo, + private readonly lessonService: LessonService, private readonly authorisation: AuthorizationService, private readonly taskCopyService: TaskCopyService, private readonly taskRepo: TaskRepo, @@ -104,7 +105,7 @@ export class TaskCopyUC { return undefined; } - const destinationLesson = await this.lessonRepo.findById(lessonId); + const destinationLesson = await this.lessonService.findById(lessonId); return destinationLesson; } diff --git a/apps/server/src/modules/task/uc/task.uc.spec.ts b/apps/server/src/modules/task/uc/task.uc.spec.ts index 90bb29db444..cc11f214bcf 100644 --- a/apps/server/src/modules/task/uc/task.uc.spec.ts +++ b/apps/server/src/modules/task/uc/task.uc.spec.ts @@ -1,9 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Action, AuthorizationService } from '@modules/authorization'; +import { LessonService } from '@modules/lesson'; import { ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { PaginationParams } from '@shared/controller'; -import { ITaskStatus, Permission, SortOrder } from '@shared/domain'; -import { CourseRepo, LessonRepo, TaskRepo } from '@shared/repo'; +import { Permission, SortOrder, TaskStatus } from '@shared/domain'; +import { CourseRepo, TaskRepo } from '@shared/repo'; import { courseFactory, lessonFactory, @@ -13,7 +15,6 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { Action, AuthorizationService } from '@modules/authorization'; import { TaskService } from '../service'; import { TaskUC } from './task.uc'; @@ -22,7 +23,7 @@ describe('TaskUC', () => { let service: TaskUC; let taskRepo: DeepMocked; let courseRepo: DeepMocked; - let lessonRepo: DeepMocked; + let lessonService: DeepMocked; let authorizationService: DeepMocked; let taskService: DeepMocked; @@ -48,8 +49,8 @@ describe('TaskUC', () => { useValue: createMock(), }, { - provide: LessonRepo, - useValue: createMock(), + provide: LessonService, + useValue: createMock(), }, { provide: AuthorizationService, @@ -69,7 +70,7 @@ describe('TaskUC', () => { service = module.get(TaskUC); taskRepo = module.get(TaskRepo); courseRepo = module.get(CourseRepo); - lessonRepo = module.get(LessonRepo); + lessonService = module.get(LessonService); authorizationService = module.get(AuthorizationService); taskService = module.get(TaskService); }); @@ -90,8 +91,8 @@ describe('TaskUC', () => { const finishedTask = taskFactory.finished(user).build(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); courseRepo.findAllByUserId.mockResolvedValueOnce([[], 0]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); authorizationService.hasPermission.mockReturnValueOnce(false); taskRepo.findAllFinishedByParentIds.mockResolvedValueOnce([[finishedTask], 1]); @@ -204,8 +205,8 @@ describe('TaskUC', () => { authorizationService.getUserWithPermissions.mockResolvedValue(user); courseRepo.findAllByUserId.mockResolvedValue([[], 0]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[lesson], 1]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[lesson], 1]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); authorizationService.hasPermission.mockReturnValueOnce(false); taskRepo.findAllFinishedByParentIds.mockResolvedValue([[task], 1]); @@ -240,8 +241,8 @@ describe('TaskUC', () => { authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); courseRepo.findAllByUserId.mockResolvedValueOnce([[course], 1]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); authorizationService.hasPermission.mockReturnValueOnce(false); taskRepo.findAllFinishedByParentIds.mockResolvedValueOnce([[task], 1]); @@ -276,8 +277,8 @@ describe('TaskUC', () => { authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); courseRepo.findAllByUserId.mockResolvedValueOnce([[], 0]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); authorizationService.hasPermission.mockReturnValueOnce(true); taskRepo.findAllFinishedByParentIds.mockResolvedValueOnce([[task], 1]); @@ -303,8 +304,8 @@ describe('TaskUC', () => { authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); courseRepo.findAllByUserId.mockResolvedValueOnce([[course], 1]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); authorizationService.hasPermission.mockReturnValueOnce(true); authorizationService.hasPermission.mockReturnValueOnce(true); taskRepo.findAllFinishedByParentIds.mockResolvedValueOnce([[task], 1]); @@ -389,8 +390,8 @@ describe('TaskUC', () => { authorizationService.hasAllPermissions.mockReturnValueOnce(true); courseRepo.findAllByUserId.mockResolvedValueOnce([[course], 1]); authorizationService.hasPermission.mockReturnValueOnce(false); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[lesson], 1]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[lesson], 1]); taskRepo.findAllByParentIds.mockResolvedValueOnce([[task1, task2, task3], 3]); return { user, course, lesson, task1, paginationParams }; @@ -503,8 +504,8 @@ describe('TaskUC', () => { authorizationService.hasAllPermissions.mockReturnValueOnce(true); courseRepo.findAllForTeacherOrSubstituteTeacher.mockResolvedValueOnce([[course], 1]); authorizationService.hasPermission.mockReturnValueOnce(true); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[lesson], 1]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[lesson], 1]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); taskRepo.findAllByParentIds.mockResolvedValueOnce([[task], 1]); return { user, paginationParams }; @@ -538,8 +539,8 @@ describe('TaskUC', () => { authorizationService.hasAllPermissions.mockReturnValueOnce(true); courseRepo.findAllForTeacherOrSubstituteTeacher.mockResolvedValueOnce([[course], 1]); authorizationService.hasPermission.mockReturnValueOnce(true); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[lesson], 1]); - lessonRepo.findAllByCourseIds.mockResolvedValueOnce([[], 0]); + lessonService.findByCourseIds.mockResolvedValueOnce([[lesson], 1]); + lessonService.findByCourseIds.mockResolvedValueOnce([[], 0]); taskRepo.findAllByParentIds.mockResolvedValueOnce([[task1, task2, task3], 3]); return { user, course, lesson, task1, paginationParams }; @@ -615,7 +616,7 @@ describe('TaskUC', () => { }); describe('[method] changeFinishedForUser', () => { - const mockStatus: ITaskStatus = { + const mockStatus: TaskStatus = { submitted: 1, graded: 1, maxSubmissions: 1, diff --git a/apps/server/src/modules/task/uc/task.uc.ts b/apps/server/src/modules/task/uc/task.uc.ts index a6e40dd3b6d..e385a7f7309 100644 --- a/apps/server/src/modules/task/uc/task.uc.ts +++ b/apps/server/src/modules/task/uc/task.uc.ts @@ -1,18 +1,19 @@ +import { Action, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LessonService } from '@modules/lesson'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Counted, Course, EntityId, - IPagination, - ITaskStatus, LessonEntity, + Pagination, Permission, SortOrder, + TaskStatus, TaskWithStatusVo, User, } from '@shared/domain'; -import { CourseRepo, LessonRepo, TaskRepo } from '@shared/repo'; -import { Action, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CourseRepo, TaskRepo } from '@shared/repo'; import { TaskService } from '../service'; @Injectable() @@ -21,11 +22,11 @@ export class TaskUC { private readonly taskRepo: TaskRepo, private readonly authorizationService: AuthorizationService, private readonly courseRepo: CourseRepo, - private readonly lessonRepo: LessonRepo, + private readonly lessonService: LessonService, private readonly taskService: TaskService ) {} - async findAllFinished(userId: EntityId, pagination?: IPagination): Promise> { + async findAllFinished(userId: EntityId, pagination?: Pagination): Promise> { const user = await this.authorizationService.getUserWithPermissions(userId); this.authorizationService.checkOneOfPermissions(user, [ @@ -53,7 +54,7 @@ export class TaskUC { ); const taskWithStatusVos = tasks.map((task) => { - let status: ITaskStatus; + let status: TaskStatus; if (this.authorizationService.hasPermission(user, task, AuthorizationContextBuilder.write([]))) { status = task.createTeacherStatusForUser(user); } else { @@ -66,7 +67,7 @@ export class TaskUC { return [taskWithStatusVos, total]; } - async findAll(userId: EntityId, pagination: IPagination): Promise> { + async findAll(userId: EntityId, pagination: Pagination): Promise> { let response: Counted; const user = await this.authorizationService.getUserWithPermissions(userId); @@ -123,7 +124,7 @@ export class TaskUC { return result; } - private async findAllForStudent(user: User, pagination: IPagination): Promise> { + private async findAllForStudent(user: User, pagination: Pagination): Promise> { const courses = await this.getPermittedCourses(user, Action.read); const openCourses = courses.filter((c) => !c.isFinished()); const lessons = await this.getPermittedLessons(user, openCourses); @@ -152,7 +153,7 @@ export class TaskUC { return [taskWithStatusVos, total]; } - private async findAllForTeacher(user: User, pagination: IPagination): Promise> { + private async findAllForTeacher(user: User, pagination: Pagination): Promise> { const courses = await this.getPermittedCourses(user, Action.write); const openCourses = courses.filter((c) => !c.isFinished()); const lessons = await this.getPermittedLessons(user, openCourses); @@ -206,8 +207,8 @@ export class TaskUC { // idea as combined query: // [{courseIds: onlyWriteCoursesIds}, { courseIds: onlyReadCourses, filter: { hidden: false }}] const [[writeLessons], [readLessons]] = await Promise.all([ - this.lessonRepo.findAllByCourseIds(writeCourseIds), - this.lessonRepo.findAllByCourseIds(readCourseIds, { hidden: false }), + this.lessonService.findByCourseIds(writeCourseIds), + this.lessonService.findByCourseIds(readCourseIds, { hidden: false }), ]); const permittedLessons = [...writeLessons, ...readLessons]; diff --git a/apps/server/src/modules/tool/common/mapper/tool-context.mapper.ts b/apps/server/src/modules/tool/common/mapper/tool-context.mapper.ts new file mode 100644 index 00000000000..183da9cdbbd --- /dev/null +++ b/apps/server/src/modules/tool/common/mapper/tool-context.mapper.ts @@ -0,0 +1,9 @@ +import { ToolContextType } from '../enum'; +import { ContextExternalToolType } from '../../context-external-tool/entity'; + +export class ToolContextMapper { + static contextMapping: Record = { + [ToolContextType.COURSE]: ContextExternalToolType.COURSE, + [ToolContextType.BOARD_ELEMENT]: ContextExternalToolType.BOARD_ELEMENT, + }; +} diff --git a/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts b/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts index d3ca547a854..1f76ec01a14 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-validation.service.spec.ts @@ -109,104 +109,93 @@ describe('CommonToolValidationService', () => { }); }); - describe('checkForDuplicateParameters', () => { - describe('when given parameters has a case sensitive duplicate', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - parameters: [ - { name: 'nameDuplicate', value: 'value' }, - { name: 'nameDuplicate', value: 'value' }, - ], - }); + describe('checkCustomParameterEntries', () => { + const createTools = ( + externalToolMock?: Partial, + schoolExternalToolMock?: Partial, + contextExternalToolMock?: Partial + ) => { + const externalTool: ExternalTool = new ExternalTool({ + ...externalToolFactory.buildWithId(), + ...externalToolMock, + }); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ + ...schoolExternalToolFactory.buildWithId(), + ...schoolExternalToolMock, + }); + const schoolExternalToolId = schoolExternalTool.id as string; + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + ...contextExternalToolFactory.buildWithId(), + ...contextExternalToolMock, + }); - return { - schoolExternalTool, - }; + return { + externalTool, + schoolExternalTool, + schoolExternalToolId, + contextExternalTool, }; + }; - it('should throw error', () => { - const { schoolExternalTool } = setup(); - - const func = () => service.checkForDuplicateParameters(schoolExternalTool); - - expect(func).toThrow('tool_param_duplicate'); - }); - }); - - describe('when given parameters has case insensitive duplicate', () => { + describe('when a parameter is a duplicate', () => { const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ + const externalTool: ExternalTool = externalToolFactory.buildWithId({ parameters: [ - { name: 'nameDuplicate', value: 'value' }, - { name: 'nameduplicate', value: 'value' }, + customParameterFactory.build({ + name: 'duplicate', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.STRING, + isOptional: true, + }), + ], + }); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id, + parameters: [ + { name: 'duplicate', value: undefined }, + { name: 'duplicate', value: undefined }, ], }); return { + externalTool, schoolExternalTool, }; }; - it('should throw error when given parameters has case insensitive duplicate', () => { - const { schoolExternalTool } = setup(); + it('should throw error', () => { + const { externalTool, schoolExternalTool } = setup(); - const func = () => service.checkForDuplicateParameters(schoolExternalTool); + const func = () => service.checkCustomParameterEntries(externalTool, schoolExternalTool); expect(func).toThrowError('tool_param_duplicate'); }); }); - describe('when given parameters has no duplicates', () => { + describe('when a parameter is unknown', () => { const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - parameters: [ - { name: 'nameNoDuplicate1', value: 'value' }, - { name: 'nameNoDuplicate2', value: 'value' }, - ], + const externalTool: ExternalTool = externalToolFactory.buildWithId({ + parameters: [], + }); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id, + parameters: [{ name: 'unknownParameter', value: undefined }], }); return { + externalTool, schoolExternalTool, }; }; - it('when given parameters has no duplicates should return without error', () => { - const { schoolExternalTool } = setup(); + it('should throw error', () => { + const { externalTool, schoolExternalTool } = setup(); - const func = () => service.checkForDuplicateParameters(schoolExternalTool); + const func = () => service.checkCustomParameterEntries(externalTool, schoolExternalTool); - expect(func).not.toThrowError('tool_param_duplicate'); + expect(func).toThrowError('tool_param_unknown'); }); }); - }); - - describe('checkCustomParameterEntries', () => { - const createTools = ( - externalToolMock?: Partial, - schoolExternalToolMock?: Partial, - contextExternalToolMock?: Partial - ) => { - const externalTool: ExternalTool = new ExternalTool({ - ...externalToolFactory.buildWithId(), - ...externalToolMock, - }); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - ...schoolExternalToolFactory.buildWithId(), - ...schoolExternalToolMock, - }); - const schoolExternalToolId = schoolExternalTool.id as string; - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - ...contextExternalToolFactory.buildWithId(), - ...contextExternalToolMock, - }); - - return { - externalTool, - schoolExternalTool, - schoolExternalToolId, - contextExternalTool, - }; - }; describe('when checking parameter is required', () => { describe('and given parameter is not optional and parameter value is empty', () => { @@ -280,16 +269,23 @@ describe('CommonToolValidationService', () => { describe('when parameter is not school or context', () => { const setup = () => { - const notSchoolParam: CustomParameter = customParameterFactory.build({ - name: 'notSchoolParam', - scope: CustomParameterScope.GLOBAL, - type: CustomParameterType.BOOLEAN, - }); - const { externalTool, schoolExternalTool } = createTools( - { parameters: [notSchoolParam] }, { - parameters: [{ name: 'name', value: 'true' }], + parameters: [ + customParameterFactory.build({ + name: 'notSchoolParam', + scope: CustomParameterScope.GLOBAL, + type: CustomParameterType.BOOLEAN, + }), + customParameterFactory.build({ + name: 'schoolParam', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.BOOLEAN, + }), + ], + }, + { + parameters: [{ name: 'schoolParam', value: 'true' }], } ); @@ -320,7 +316,7 @@ describe('CommonToolValidationService', () => { const { externalTool, schoolExternalTool } = createTools( { parameters: [missingParam] }, { - parameters: [{ name: 'anotherParam', value: 'value' }], + parameters: [], } ); @@ -339,18 +335,26 @@ describe('CommonToolValidationService', () => { }); }); - describe('when parameter is optional but is missing on params', () => { + describe('when parameter is optional and was not defined', () => { const setup = () => { - const param: CustomParameter = customParameterFactory.build({ - name: 'notChecked', - scope: CustomParameterScope.SCHOOL, - isOptional: true, - }); - const { externalTool, schoolExternalTool } = createTools( - { parameters: [param] }, { - parameters: [{ name: 'anotherParam', value: 'value' }], + parameters: [ + customParameterFactory.build({ + name: 'optionalParameter', + scope: CustomParameterScope.SCHOOL, + isOptional: true, + }), + customParameterFactory.build({ + name: 'requiredParameter', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.STRING, + isOptional: false, + }), + ], + }, + { + parameters: [{ name: 'requiredParameter', value: 'value' }], } ); @@ -360,7 +364,7 @@ describe('CommonToolValidationService', () => { }; }; - it('should return without error ', () => { + it('should return without error', () => { const { externalTool, schoolExternalTool } = setup(); const func = () => service.checkCustomParameterEntries(externalTool, schoolExternalTool); @@ -385,7 +389,7 @@ describe('CommonToolValidationService', () => { }, undefined, { - parameters: [{ name: 'anotherParam', value: 'value' }], + parameters: [], } ); diff --git a/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts b/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts index 3ee9ee7d465..e6c3a31b288 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-validation.service.ts @@ -29,12 +29,25 @@ export class CommonToolValidationService { return isValid; } - public checkForDuplicateParameters(validatableTool: ValidatableTool): void { - const caseInsensitiveNames: string[] = validatableTool.parameters.map(({ name }: CustomParameterEntry) => - name.toLowerCase() + public checkCustomParameterEntries(loadedExternalTool: ExternalTool, validatableTool: ValidatableTool): void { + this.checkForDuplicateParameters(validatableTool); + + const parametersForScope: CustomParameter[] = (loadedExternalTool.parameters ?? []).filter( + (param: CustomParameter) => + (validatableTool instanceof SchoolExternalTool && param.scope === CustomParameterScope.SCHOOL) || + (validatableTool instanceof ContextExternalTool && param.scope === CustomParameterScope.CONTEXT) ); + this.checkForUnknownParameters(validatableTool, parametersForScope); + + this.checkValidityOfParameters(validatableTool, parametersForScope); + } + + private checkForDuplicateParameters(validatableTool: ValidatableTool): void { + const caseInsensitiveNames: string[] = validatableTool.parameters.map(({ name }: CustomParameterEntry) => name); + const uniqueNames: Set = new Set(caseInsensitiveNames); + if (uniqueNames.size !== validatableTool.parameters.length) { throw new ValidationError( `tool_param_duplicate: The tool ${validatableTool.id ?? ''} contains multiple of the same custom parameters.` @@ -42,28 +55,33 @@ export class CommonToolValidationService { } } - public checkCustomParameterEntries(loadedExternalTool: ExternalTool, validatableTool: ValidatableTool): void { - if (loadedExternalTool.parameters) { - for (const param of loadedExternalTool.parameters) { - this.checkScopeAndValidateParameter(validatableTool, param); + private checkForUnknownParameters(validatableTool: ValidatableTool, parametersForScope: CustomParameter[]): void { + for (const entry of validatableTool.parameters) { + const foundParameter: CustomParameter | undefined = parametersForScope.find( + ({ name }: CustomParameter): boolean => name === entry.name + ); + + if (!foundParameter) { + throw new ValidationError( + `tool_param_unknown: The parameter with name ${entry.name} is not part of this tool.` + ); } } } - private checkScopeAndValidateParameter(validatableTool: ValidatableTool, param: CustomParameter): void { - const foundEntry: CustomParameterEntry | undefined = validatableTool.parameters.find( - ({ name }: CustomParameterEntry): boolean => name.toLowerCase() === param.name.toLowerCase() - ); + private checkValidityOfParameters(validatableTool: ValidatableTool, parametersForScope: CustomParameter[]): void { + for (const param of parametersForScope) { + const foundEntry: CustomParameterEntry | undefined = validatableTool.parameters.find( + ({ name }: CustomParameterEntry): boolean => name === param.name + ); - if (param.scope === CustomParameterScope.SCHOOL && validatableTool instanceof SchoolExternalTool) { - this.validateParameter(param, foundEntry); - } else if (param.scope === CustomParameterScope.CONTEXT && validatableTool instanceof ContextExternalTool) { this.validateParameter(param, foundEntry); } } private validateParameter(param: CustomParameter, foundEntry: CustomParameterEntry | undefined): void { this.checkOptionalParameter(param, foundEntry); + if (foundEntry) { this.checkParameterType(foundEntry, param); this.checkParameterRegex(foundEntry, param); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts index eb570130c4e..af14eb379f3 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts @@ -17,9 +17,8 @@ import { userFactory, } from '@shared/testing'; import { ObjectId } from 'bson'; -import { CustomParameterScope, ToolContextType } from '../../../common/enum'; +import { CustomParameterScope, CustomParameterType, ToolContextType } from '../../../common/enum'; import { ExternalToolEntity } from '../../../external-tool/entity'; -import { CustomParameterEntryResponse } from '../../../school-external-tool/controller/dto'; import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; import { ContextExternalToolEntity, ContextExternalToolType } from '../../entity'; import { @@ -66,10 +65,27 @@ describe('ToolContextController (API)', () => { const course: Course = courseFactory.buildWithId({ teachers: [teacherUser], school }); - const paramEntry: CustomParameterEntryResponse = { name: 'name', value: 'value' }; + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ + parameters: [ + customParameterEntityFactory.build({ + name: 'param1', + scope: CustomParameterScope.CONTEXT, + type: CustomParameterType.STRING, + isOptional: false, + }), + customParameterEntityFactory.build({ + name: 'param2', + scope: CustomParameterScope.CONTEXT, + type: CustomParameterType.BOOLEAN, + isOptional: true, + }), + ], + version: 1, + }); const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + tool: externalToolEntity, school, - schoolParameters: [paramEntry], + schoolParameters: [], toolVersion: 1, }); @@ -78,7 +94,10 @@ describe('ToolContextController (API)', () => { contextId: course.id, displayName: course.name, contextType: ToolContextType.COURSE, - parameters: [paramEntry], + parameters: [ + { name: 'param1', value: 'value' }, + { name: 'param2', value: '' }, + ], toolVersion: 1, }; @@ -87,30 +106,30 @@ describe('ToolContextController (API)', () => { const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount); - const expected: ContextExternalToolResponse = { - id: expect.any(String), - schoolToolId: postParams.schoolToolId, - contextId: postParams.contextId, - displayName: postParams.displayName, - contextType: postParams.contextType, - parameters: [paramEntry], - toolVersion: postParams.toolVersion, - }; - return { loggedInClient, postParams, - expected, }; }; it('should create a contextExternalTool', async () => { - const { postParams, loggedInClient, expected } = await setup(); + const { postParams, loggedInClient } = await setup(); const response = await loggedInClient.post().send(postParams); expect(response.statusCode).toEqual(HttpStatus.CREATED); - expect(response.body).toEqual(expect.objectContaining(expected)); + expect(response.body).toEqual({ + id: expect.any(String), + schoolToolId: postParams.schoolToolId, + contextId: postParams.contextId, + displayName: postParams.displayName, + contextType: postParams.contextType, + parameters: [ + { name: 'param1', value: 'value' }, + { name: 'param2', value: undefined }, + ], + toolVersion: postParams.toolVersion, + }); }); }); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts b/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts index 3c00d30e6e2..4f7b7ed2703 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts @@ -1,6 +1,6 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Controller, Get, Param } from '@nestjs/common'; import { ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { ToolReference } from '../domain'; import { ContextExternalToolResponseMapper } from '../mapper'; import { ToolReferenceUc } from '../uc'; diff --git a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts index aebab1a8d5d..b82475a64ca 100644 --- a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts +++ b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts @@ -4,7 +4,7 @@ import { CustomParameterEntryEntity } from '../../common/entity'; import { SchoolExternalToolEntity } from '../../school-external-tool/entity'; import { ContextExternalToolType } from './context-external-tool-type.enum'; -export interface IContextExternalToolProperties { +export interface ContextExternalToolProperties { schoolTool: SchoolExternalToolEntity; contextId: string; @@ -38,7 +38,7 @@ export class ContextExternalToolEntity extends BaseEntityWithTimestamps { @Property() toolVersion: number; - constructor(props: IContextExternalToolProperties) { + constructor(props: ContextExternalToolProperties) { super(); this.schoolTool = props.schoolTool; this.contextId = props.contextId; diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts index 951559afbcc..8e72f5f540b 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts @@ -25,7 +25,7 @@ export class ContextExternalToolRequestMapper { return customParameterParams.map((customParameterParam: CustomParameterEntryParam) => { return { name: customParameterParam.name, - value: customParameterParam.value, + value: customParameterParam.value || undefined, }; }); } diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts index 2cce83a08b2..b12193eadd5 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts @@ -18,7 +18,7 @@ export class ContextExternalToolValidationService { ) {} async validate(contextExternalTool: ContextExternalTool): Promise { - await this.checkDuplicateInContext(contextExternalTool); + await this.checkDuplicateUsesInContext(contextExternalTool); const loadedSchoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( contextExternalTool.schoolToolRef.schoolToolId @@ -29,7 +29,7 @@ export class ContextExternalToolValidationService { this.commonToolValidationService.checkCustomParameterEntries(loadedExternalTool, contextExternalTool); } - private async checkDuplicateInContext(contextExternalTool: ContextExternalTool) { + private async checkDuplicateUsesInContext(contextExternalTool: ContextExternalTool) { let duplicate: ContextExternalTool[] = await this.contextExternalToolService.findContextExternalTools({ schoolToolRef: contextExternalTool.schoolToolRef, context: contextExternalTool.contextRef, diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.spec.ts index 33c77ecb7ea..87f96a096d2 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.spec.ts @@ -1,13 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; import { ApiValidationError } from '@shared/common'; -import { SchoolExternalToolValidationService } from '../../school-external-tool/service'; -import { ToolVersionService } from './tool-version-service'; -import { ContextExternalToolValidationService } from './context-external-tool-validation.service'; +import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; +import { ToolConfigurationStatus } from '../../common/enum'; import { CommonToolService } from '../../common/service'; +import { SchoolExternalToolValidationService } from '../../school-external-tool/service'; import { IToolFeatures, ToolFeatures } from '../../tool-config'; -import { ToolConfigurationStatus } from '../../common/enum'; +import { ContextExternalToolValidationService } from './context-external-tool-validation.service'; +import { ToolVersionService } from './tool-version-service'; describe('ToolVersionService', () => { let module: TestingModule; diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.ts index 2ba0709372a..91c77a1aa98 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-version-service.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { Inject } from '@nestjs/common'; -import { ContextExternalToolValidationService } from './context-external-tool-validation.service'; -import { SchoolExternalToolValidationService } from '../../school-external-tool/service'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; +import { ToolConfigurationStatus } from '../../common/enum'; +import { CommonToolService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { SchoolExternalToolValidationService } from '../../school-external-tool/service'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ContextExternalTool } from '../domain'; -import { ToolConfigurationStatus } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; +import { ContextExternalToolValidationService } from './context-external-tool-validation.service'; @Injectable() export class ToolVersionService { diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts index ebff659529d..75dc1229231 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts @@ -1,16 +1,19 @@ import { Loaded } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission } from '@shared/domain'; +import { Permission, SchoolEntity } from '@shared/domain'; import { cleanupCollections, + contextExternalToolEntityFactory, externalToolEntityFactory, externalToolFactory, + schoolExternalToolEntityFactory, + schoolFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@modules/server'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { Response } from 'supertest'; @@ -20,8 +23,16 @@ import { CustomParameterTypeParams, ToolConfigType, } from '../../../common/enum'; +import { ContextExternalToolEntity, ContextExternalToolType } from '../../../context-external-tool/entity'; +import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; +import { ExternalToolMetadata } from '../../domain'; import { ExternalToolEntity } from '../../entity'; -import { ExternalToolCreateParams, ExternalToolResponse, ExternalToolSearchListResponse } from '../dto'; +import { + ExternalToolCreateParams, + ExternalToolResponse, + ExternalToolSearchListResponse, + ExternalToolMetadataResponse, +} from '../dto'; describe('ToolController (API)', () => { let app: INestApplication; @@ -617,4 +628,82 @@ describe('ToolController (API)', () => { }); }); }); + + describe('[GET] tools/external-tools/:externalToolId/metadata', () => { + describe('when user is not authenticated', () => { + const setup = () => { + const toolId: string = new ObjectId().toHexString(); + + return { toolId }; + }; + + it('should return unauthorized', async () => { + const { toolId } = setup(); + + const response: Response = await testApiClient.get(`${toolId}/metadata`); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when externalToolId is given ', () => { + const setup = async () => { + const toolId: string = new ObjectId().toHexString(); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(undefined, toolId); + + const school: SchoolEntity = schoolFactory.buildWithId(); + const schoolExternalToolEntitys: SchoolExternalToolEntity[] = schoolExternalToolEntityFactory.buildList(2, { + tool: externalToolEntity, + school, + }); + + const courseTools: ContextExternalToolEntity[] = contextExternalToolEntityFactory.buildList(3, { + schoolTool: schoolExternalToolEntitys[0], + contextType: ContextExternalToolType.COURSE, + }); + + const boardTools: ContextExternalToolEntity[] = contextExternalToolEntityFactory.buildList(2, { + schoolTool: schoolExternalToolEntitys[1], + contextType: ContextExternalToolType.BOARD_ELEMENT, + contextId: new ObjectId().toHexString(), + }); + + const externalToolMetadata: ExternalToolMetadata = new ExternalToolMetadata({ + schoolExternalToolCount: 2, + contextExternalToolCountPerContext: { course: 3, boardElement: 2 }, + }); + + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({}, [Permission.TOOL_ADMIN]); + await em.persistAndFlush([ + adminAccount, + adminUser, + school, + externalToolEntity, + ...schoolExternalToolEntitys, + ...courseTools, + ...boardTools, + ]); + em.clear(); + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { loggedInClient, toolId, externalToolEntity, externalToolMetadata }; + }; + + it('should return the metadata of externalTool', async () => { + const { loggedInClient, externalToolEntity } = await setup(); + + const response: Response = await loggedInClient.get(`${externalToolEntity.id}/metadata`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ + schoolExternalToolCount: 2, + contextExternalToolCountPerContext: { + course: 3, + boardElement: 2, + }, + }); + }); + }); + }); }); diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-metadata.response.ts b/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-metadata.response.ts new file mode 100644 index 00000000000..d38b48cc503 --- /dev/null +++ b/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-metadata.response.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ContextExternalToolType } from '../../../../context-external-tool/entity'; + +export class ExternalToolMetadataResponse { + @ApiProperty() + schoolExternalToolCount: number; + + @ApiProperty({ + type: 'object', + properties: Object.fromEntries( + Object.values(ContextExternalToolType).map((key: ContextExternalToolType) => [key, { type: 'number' }]) + ), + }) + contextExternalToolCountPerContext: Record; + + constructor(externalToolMetadataResponse: ExternalToolMetadataResponse) { + this.schoolExternalToolCount = externalToolMetadataResponse.schoolExternalToolCount; + this.contextExternalToolCountPerContext = externalToolMetadataResponse.contextExternalToolCountPerContext; + } +} diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts b/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts index e9e5fafa376..4f532238863 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts @@ -6,3 +6,4 @@ export * from './context-external-tool-configuration-template.response'; export * from './context-external-tool-configuration-template-list.response'; export * from './school-external-tool-configuration-template.response'; export * from './school-external-tool-configuration-template-list.response'; +export * from './external-tool-metadata.response'; diff --git a/apps/server/src/modules/tool/external-tool/controller/tool-configuration.controller.ts b/apps/server/src/modules/tool/external-tool/controller/tool-configuration.controller.ts index 589a1e38e7c..5dbe2db8e9b 100644 --- a/apps/server/src/modules/tool/external-tool/controller/tool-configuration.controller.ts +++ b/apps/server/src/modules/tool/external-tool/controller/tool-configuration.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Controller, Get, Param } from '@nestjs/common'; import { ApiForbiddenResponse, @@ -7,7 +8,6 @@ import { ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { ExternalTool } from '../domain'; import { ToolConfigurationMapper } from '../mapper/tool-configuration.mapper'; import { ContextExternalToolTemplateInfo, ExternalToolConfigurationUc } from '../uc'; diff --git a/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts b/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts index 80139a586cb..b534b01b83b 100644 --- a/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts +++ b/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, Res } from '@nestjs/common'; import { ApiCreatedResponse, @@ -14,12 +15,12 @@ import { ValidationError } from '@shared/common'; import { PaginationParams } from '@shared/controller'; import { IFindOptions, Page } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Response } from 'express'; import { ExternalToolSearchQuery } from '../../common/interface'; -import { ExternalTool } from '../domain'; +import { ExternalTool, ExternalToolMetadata } from '../domain'; import { ExternalToolLogo } from '../domain/external-tool-logo'; -import { ExternalToolRequestMapper, ExternalToolResponseMapper } from '../mapper'; + +import { ExternalToolRequestMapper, ExternalToolResponseMapper, ExternalToolMetadataMapper } from '../mapper'; import { ExternalToolLogoService } from '../service'; import { ExternalToolCreate, ExternalToolUc, ExternalToolUpdate } from '../uc'; import { @@ -30,6 +31,7 @@ import { ExternalToolSearchParams, ExternalToolUpdateParams, SortExternalToolParams, + ExternalToolMetadataResponse, } from './dto'; @ApiTags('Tool') @@ -165,4 +167,26 @@ export class ToolController { res.setHeader('Cache-Control', 'must-revalidate'); res.send(externalToolLogo.logo); } + + @Get('/:externalToolId/metadata') + @ApiOperation({ summary: 'Gets the metadata of an external tool.' }) + @ApiOkResponse({ + description: 'Metadata of external tool fetched successfully.', + type: ExternalToolMetadataResponse, + }) + @ApiUnauthorizedResponse({ description: 'User is not logged in.' }) + async getMetaDataForExternalTool( + @CurrentUser() currentUser: ICurrentUser, + @Param() params: ExternalToolIdParams + ): Promise { + const externalToolMetadata: ExternalToolMetadata = await this.externalToolUc.getMetadataForExternalTool( + currentUser.userId, + params.externalToolId + ); + + const mapped: ExternalToolMetadataResponse = + ExternalToolMetadataMapper.mapToExternalToolMetadataResponse(externalToolMetadata); + + return mapped; + } } diff --git a/apps/server/src/modules/tool/external-tool/domain/external-tool-metadata.ts b/apps/server/src/modules/tool/external-tool/domain/external-tool-metadata.ts new file mode 100644 index 00000000000..04492680ff7 --- /dev/null +++ b/apps/server/src/modules/tool/external-tool/domain/external-tool-metadata.ts @@ -0,0 +1,10 @@ +export class ExternalToolMetadata { + schoolExternalToolCount: number; + + contextExternalToolCountPerContext: Record; + + constructor(externalToolMetadata: ExternalToolMetadata) { + this.schoolExternalToolCount = externalToolMetadata.schoolExternalToolCount; + this.contextExternalToolCountPerContext = externalToolMetadata.contextExternalToolCountPerContext; + } +} diff --git a/apps/server/src/modules/tool/external-tool/domain/index.ts b/apps/server/src/modules/tool/external-tool/domain/index.ts index e5a1dab735d..61fca0d2bfe 100644 --- a/apps/server/src/modules/tool/external-tool/domain/index.ts +++ b/apps/server/src/modules/tool/external-tool/domain/index.ts @@ -1,2 +1,3 @@ export * from './external-tool.do'; export * from './config'; +export * from './external-tool-metadata'; diff --git a/apps/server/src/modules/tool/external-tool/external-tool.module.ts b/apps/server/src/modules/tool/external-tool/external-tool.module.ts index 9aa0a11e448..a84734fecb2 100644 --- a/apps/server/src/modules/tool/external-tool/external-tool.module.ts +++ b/apps/server/src/modules/tool/external-tool/external-tool.module.ts @@ -5,6 +5,8 @@ import { OauthProviderServiceModule } from '@infra/oauth-provider'; import { EncryptionModule } from '@infra/encryption'; import { ExternalToolRepo } from '@shared/repo'; import { ToolConfigModule } from '../tool-config.module'; +import { ExternalToolMetadataMapper } from './mapper'; +import { ToolContextMapper } from '../common/mapper/tool-context.mapper'; import { ExternalToolConfigurationService, ExternalToolLogoService, @@ -13,6 +15,7 @@ import { ExternalToolServiceMapper, ExternalToolValidationService, ExternalToolVersionIncrementService, + ExternalToolMetadataService, } from './service'; import { CommonToolModule } from '../common'; @@ -27,6 +30,9 @@ import { CommonToolModule } from '../common'; ExternalToolConfigurationService, ExternalToolLogoService, ExternalToolRepo, + ExternalToolMetadataService, + ExternalToolMetadataMapper, + ToolContextMapper, ], exports: [ ExternalToolService, @@ -34,6 +40,7 @@ import { CommonToolModule } from '../common'; ExternalToolVersionIncrementService, ExternalToolConfigurationService, ExternalToolLogoService, + ExternalToolMetadataService, ], }) export class ExternalToolModule {} diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-metadata.mapper.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-metadata.mapper.ts new file mode 100644 index 00000000000..b3d6555f898 --- /dev/null +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-metadata.mapper.ts @@ -0,0 +1,13 @@ +import { ExternalToolMetadataResponse } from '../controller/dto'; +import { ExternalToolMetadata } from '../domain'; + +export class ExternalToolMetadataMapper { + static mapToExternalToolMetadataResponse(externalToolMetadata: ExternalToolMetadata): ExternalToolMetadataResponse { + const externalToolMetadataResponse: ExternalToolMetadataResponse = new ExternalToolMetadataResponse({ + schoolExternalToolCount: externalToolMetadata.schoolExternalToolCount, + contextExternalToolCountPerContext: externalToolMetadata.contextExternalToolCountPerContext, + }); + + return externalToolMetadataResponse; + } +} diff --git a/apps/server/src/modules/tool/external-tool/mapper/index.ts b/apps/server/src/modules/tool/external-tool/mapper/index.ts index 4149a17a519..73fdb05cf5a 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/index.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/index.ts @@ -1,2 +1,3 @@ export * from './external-tool-request.mapper'; export * from './external-tool-response.mapper'; +export * from './external-tool-metadata.mapper'; diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.ts index 3b5a186d1ba..3d021c592fc 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.ts @@ -1,11 +1,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { EntityId, Page } from '@shared/domain'; +import { CustomParameter } from '../../common/domain'; +import { CustomParameterScope } from '../../common/enum'; +import { ContextExternalTool } from '../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { CustomParameterScope } from '../../common/enum'; -import { CustomParameter } from '../../common/domain'; import { ContextExternalToolTemplateInfo } from '../uc/dto'; @Injectable() diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.spec.ts new file mode 100644 index 00000000000..4753cc805f4 --- /dev/null +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.spec.ts @@ -0,0 +1,145 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ContextExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; +import { externalToolFactory, legacySchoolDoFactory, schoolExternalToolFactory } from '@shared/testing'; +import { ContextExternalToolType } from '../../context-external-tool/entity'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { ExternalTool, ExternalToolMetadata } from '../domain'; +import { ExternalToolMetadataService } from './external-tool-metadata.service'; + +describe('ExternalToolMetadataService', () => { + let module: TestingModule; + let service: ExternalToolMetadataService; + + let schoolExternalToolRepo: DeepMocked; + let contextExternalToolRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ExternalToolMetadataService, + { + provide: SchoolExternalToolRepo, + useValue: createMock(), + }, + { + provide: ContextExternalToolRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ExternalToolMetadataService); + schoolExternalToolRepo = module.get(SchoolExternalToolRepo); + contextExternalToolRepo = module.get(ContextExternalToolRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getMetadata', () => { + describe('when externalToolId is given', () => { + const setup = () => { + const toolId: string = new ObjectId().toHexString(); + + const school = legacySchoolDoFactory.buildWithId(); + const school1 = legacySchoolDoFactory.buildWithId(); + + const schoolToolId: string = new ObjectId().toHexString(); + const schoolToolId1: string = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId, + schoolId: school.id, + id: schoolToolId, + }); + const schoolExternalTool1: SchoolExternalTool = schoolExternalToolFactory.buildWithId( + { toolId, schoolId: school1.id, id: schoolToolId1 }, + schoolToolId1 + ); + + const externalToolMetadata: ExternalToolMetadata = new ExternalToolMetadata({ + schoolExternalToolCount: 2, + contextExternalToolCountPerContext: { course: 3, boardElement: 3 }, + }); + + schoolExternalToolRepo.findByExternalToolId.mockResolvedValue([schoolExternalTool, schoolExternalTool1]); + contextExternalToolRepo.countBySchoolToolIdsAndContextType.mockResolvedValue(3); + + return { + toolId, + externalToolMetadata, + schoolExternalTool, + schoolExternalTool1, + }; + }; + + it('should call the repo to get schoolExternalTools by externalToolId', async () => { + const { toolId } = setup(); + + await service.getMetadata(toolId); + + expect(schoolExternalToolRepo.findByExternalToolId).toHaveBeenCalledWith(toolId); + }); + + it('should call the repo to count contextExternalTools by schoolExternalToolId and context', async () => { + const { toolId, schoolExternalTool, schoolExternalTool1 } = setup(); + + await service.getMetadata(toolId); + + expect(contextExternalToolRepo.countBySchoolToolIdsAndContextType).toHaveBeenCalledWith( + ContextExternalToolType.COURSE, + [schoolExternalTool.id, schoolExternalTool1.id] + ); + expect(contextExternalToolRepo.countBySchoolToolIdsAndContextType).toHaveBeenCalledWith( + ContextExternalToolType.BOARD_ELEMENT, + [schoolExternalTool.id, schoolExternalTool1.id] + ); + expect(contextExternalToolRepo.countBySchoolToolIdsAndContextType).toHaveBeenCalledTimes(2); + }); + + it('should return externalToolMetadata', async () => { + const { toolId, externalToolMetadata } = setup(); + + const result: ExternalToolMetadata = await service.getMetadata(toolId); + + expect(result).toEqual(externalToolMetadata); + }); + }); + + describe('when no related school external tool was found', () => { + const setup = () => { + const toolId: string = new ObjectId().toHexString(); + const externalToolEntity: ExternalTool = externalToolFactory.buildWithId(undefined, toolId); + + const externalToolMetadata: ExternalToolMetadata = new ExternalToolMetadata({ + schoolExternalToolCount: 0, + contextExternalToolCountPerContext: { course: 0, boardElement: 0 }, + }); + + schoolExternalToolRepo.findByExternalToolId.mockResolvedValue([]); + contextExternalToolRepo.countBySchoolToolIdsAndContextType.mockResolvedValue(0); + + return { + toolId, + externalToolEntity, + externalToolMetadata, + }; + }; + + it('should return empty externalToolMetadata', async () => { + const { toolId, externalToolMetadata } = setup(); + + const result: ExternalToolMetadata = await service.getMetadata(toolId); + + expect(result).toEqual(externalToolMetadata); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.ts new file mode 100644 index 00000000000..9476e738a87 --- /dev/null +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ContextExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; +import { ToolContextType } from '../../common/enum'; +import { ContextExternalToolType } from '../../context-external-tool/entity'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { ExternalToolMetadata } from '../domain'; +import { ToolContextMapper } from '../../common/mapper/tool-context.mapper'; + +@Injectable() +export class ExternalToolMetadataService { + constructor( + private readonly schoolToolRepo: SchoolExternalToolRepo, + private readonly contextToolRepo: ContextExternalToolRepo + ) {} + + async getMetadata(toolId: EntityId): Promise { + const schoolExternalTools: SchoolExternalTool[] = await this.schoolToolRepo.findByExternalToolId(toolId); + + const schoolExternalToolIds: string[] = schoolExternalTools.map( + (schoolExternalTool: SchoolExternalTool): string => + // We can be sure that the repo returns the id + schoolExternalTool.id as string + ); + const contextExternalToolCount: Record = { + [ContextExternalToolType.BOARD_ELEMENT]: 0, + [ContextExternalToolType.COURSE]: 0, + }; + if (schoolExternalTools.length >= 1) { + await Promise.all( + Object.values(ToolContextType).map(async (contextType: ToolContextType): Promise => { + const type: ContextExternalToolType = ToolContextMapper.contextMapping[contextType]; + + const countPerContext: number = await this.contextToolRepo.countBySchoolToolIdsAndContextType( + type, + schoolExternalToolIds + ); + contextExternalToolCount[type] = countPerContext; + }) + ); + } + + const externaltoolMetadata: ExternalToolMetadata = new ExternalToolMetadata({ + schoolExternalToolCount: schoolExternalTools.length, + contextExternalToolCountPerContext: contextExternalToolCount, + }); + + return externaltoolMetadata; + } +} diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts index be146f30941..05416f30fb6 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts @@ -1,10 +1,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; +import { OauthProviderService } from '@infra/oauth-provider'; +import { ProviderOauthClient } from '@infra/oauth-provider/dto'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { IFindOptions, Page, SortOrder } from '@shared/domain'; -import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; -import { OauthProviderService } from '@infra/oauth-provider'; -import { ProviderOauthClient } from '@infra/oauth-provider/dto'; import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { externalToolFactory, @@ -28,7 +28,7 @@ describe('ExternalToolService', () => { let courseToolRepo: DeepMocked; let oauthProviderService: DeepMocked; let mapper: DeepMocked; - let encryptionService: DeepMocked; + let encryptionService: DeepMocked; let versionService: DeepMocked; beforeAll(async () => { @@ -49,7 +49,7 @@ describe('ExternalToolService', () => { }, { provide: DefaultEncryptionService, - useValue: createMock(), + useValue: createMock(), }, { provide: SchoolExternalToolRepo, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts index d8481c28c71..8fb5d8e4387 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts @@ -1,8 +1,8 @@ -import { Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; -import { EntityId, IFindOptions, Page } from '@shared/domain'; -import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { OauthProviderService } from '@infra/oauth-provider'; import { ProviderOauthClient } from '@infra/oauth-provider/dto'; +import { Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { EntityId, IFindOptions, Page } from '@shared/domain'; import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; import { TokenEndpointAuthMethod } from '../../common/enum'; @@ -20,7 +20,7 @@ export class ExternalToolService { private readonly mapper: ExternalToolServiceMapper, private readonly schoolExternalToolRepo: SchoolExternalToolRepo, private readonly contextExternalToolRepo: ContextExternalToolRepo, - @Inject(DefaultEncryptionService) private readonly encryptionService: IEncryptionService, + @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService, private readonly legacyLogger: LegacyLogger, private readonly externalToolVersionService: ExternalToolVersionIncrementService ) {} diff --git a/apps/server/src/modules/tool/external-tool/service/index.ts b/apps/server/src/modules/tool/external-tool/service/index.ts index f2290ca8969..e2a936d158b 100644 --- a/apps/server/src/modules/tool/external-tool/service/index.ts +++ b/apps/server/src/modules/tool/external-tool/service/index.ts @@ -5,3 +5,4 @@ export * from './external-tool-validation.service'; export * from './external-tool-parameter-validation.service'; export * from './external-tool-configuration.service'; export * from './external-tool-logo.service'; +export * from './external-tool-metadata.service'; diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts index 2d371f47c9e..ff209ac301d 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts @@ -1,18 +1,24 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { Action, AuthorizationService } from '@modules/authorization'; import { UnauthorizedException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { IFindOptions, Permission, SortOrder, User } from '@shared/domain'; +import { IFindOptions, Permission, Role, SortOrder, User } from '@shared/domain'; import { Page } from '@shared/domain/domainobject/page'; -import { setupEntities, userFactory } from '@shared/testing'; +import { roleFactory, setupEntities, userFactory } from '@shared/testing'; import { externalToolFactory, oauth2ToolConfigFactory, } from '@shared/testing/factory/domainobject/tool/external-tool.factory'; -import { ICurrentUser } from '@modules/authentication'; -import { AuthorizationService } from '@modules/authorization'; import { ExternalToolSearchQuery } from '../../common/interface'; -import { ExternalTool, Oauth2ToolConfig } from '../domain'; -import { ExternalToolLogoService, ExternalToolService, ExternalToolValidationService } from '../service'; +import { ExternalTool, ExternalToolMetadata, Oauth2ToolConfig } from '../domain'; +import { + ExternalToolLogoService, + ExternalToolMetadataService, + ExternalToolService, + ExternalToolValidationService, +} from '../service'; import { ExternalToolUpdate } from './dto'; import { ExternalToolUc } from './external-tool.uc'; @@ -25,6 +31,7 @@ describe('ExternalToolUc', () => { let authorizationService: DeepMocked; let toolValidationService: DeepMocked; let logoService: DeepMocked; + let externalToolMetadataService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -48,6 +55,10 @@ describe('ExternalToolUc', () => { provide: ExternalToolLogoService, useValue: createMock(), }, + { + provide: ExternalToolMetadataService, + useValue: createMock(), + }, ], }).compile(); @@ -56,6 +67,7 @@ describe('ExternalToolUc', () => { authorizationService = module.get(AuthorizationService); toolValidationService = module.get(ExternalToolValidationService); logoService = module.get(ExternalToolLogoService); + externalToolMetadataService = module.get(ExternalToolMetadataService); }); afterAll(async () => { @@ -468,4 +480,109 @@ describe('ExternalToolUc', () => { expect(externalToolService.deleteExternalTool).toHaveBeenCalledWith(toolId); }); }); + + describe('getMetadataForExternalTool', () => { + describe('when authorize user', () => { + const setupMetadata = () => { + const toolId: string = new ObjectId().toHexString(); + const tool: ExternalTool = externalToolFactory.buildWithId({ id: toolId }, toolId); + + const role: Role = roleFactory.buildWithId({ permissions: [Permission.TOOL_ADMIN] }); + const user: User = userFactory.buildWithId({ roles: [role] }); + const currentUser: ICurrentUser = { userId: user.id } as ICurrentUser; + const context = { action: Action.read, requiredPermissions: [Permission.TOOL_ADMIN] }; + + externalToolService.findById.mockResolvedValue(tool); + authorizationService.getUserWithPermissions.mockResolvedValue(user); + + return { + user, + currentUser, + toolId, + tool, + context, + }; + }; + + it('get user with permissions', async () => { + const { toolId, user } = setupMetadata(); + + await uc.getMetadataForExternalTool(user.id, toolId); + + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(user.id); + }); + + it('should check that the user has TOOL_ADMIN permission', async () => { + const { user, tool } = setupMetadata(); + + await uc.getMetadataForExternalTool(user.id, tool.id!); + + expect(authorizationService.checkAllPermissions).toHaveBeenCalledWith(user, [Permission.TOOL_ADMIN]); + }); + }); + + describe('when user has insufficient permission to get an metadata for external tool ', () => { + const setupMetadata = () => { + const toolId: string = new ObjectId().toHexString(); + + const user: User = userFactory.buildWithId(); + + authorizationService.getUserWithPermissions.mockRejectedValue(new UnauthorizedException()); + + return { + user, + toolId, + }; + }; + + it('should throw UnauthorizedException ', async () => { + const { toolId, user } = setupMetadata(); + + const result: Promise = uc.getMetadataForExternalTool(user.id, toolId); + + await expect(result).rejects.toThrow(UnauthorizedException); + }); + }); + + describe('when externalToolId is given', () => { + const setupMetadata = () => { + const toolId: string = new ObjectId().toHexString(); + + const externalToolMetadata: ExternalToolMetadata = new ExternalToolMetadata({ + schoolExternalToolCount: 2, + contextExternalToolCountPerContext: { course: 3, 'board-element': 3 }, + }); + + externalToolMetadataService.getMetadata.mockResolvedValue(externalToolMetadata); + + const user: User = userFactory.buildWithId(); + const currentUser: ICurrentUser = { userId: user.id } as ICurrentUser; + + authorizationService.getUserWithPermissions.mockResolvedValue(user); + + return { + user, + currentUser, + toolId, + externalToolMetadata, + }; + }; + + it('get metadata for external tool', async () => { + const { toolId, currentUser } = setupMetadata(); + + await uc.getMetadataForExternalTool(currentUser.userId, toolId); + + expect(externalToolMetadataService.getMetadata).toHaveBeenCalledWith(toolId); + }); + + it('return metadata of external tool', async () => { + const { toolId, currentUser, externalToolMetadata } = setupMetadata(); + + const result = await uc.getMetadataForExternalTool(currentUser.userId, toolId); + + expect(result).toEqual(externalToolMetadata); + }); + }); + }); }); diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts index 2cf49867103..9880994272a 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts @@ -2,8 +2,13 @@ import { Injectable } from '@nestjs/common'; import { EntityId, IFindOptions, Page, Permission, User } from '@shared/domain'; import { AuthorizationService } from '@modules/authorization'; import { ExternalToolSearchQuery } from '../../common/interface'; -import { ExternalTool, ExternalToolConfig } from '../domain'; -import { ExternalToolLogoService, ExternalToolService, ExternalToolValidationService } from '../service'; +import { ExternalTool, ExternalToolConfig, ExternalToolMetadata } from '../domain'; +import { + ExternalToolLogoService, + ExternalToolService, + ExternalToolValidationService, + ExternalToolMetadataService, +} from '../service'; import { ExternalToolCreate, ExternalToolUpdate } from './dto'; @Injectable() @@ -12,7 +17,8 @@ export class ExternalToolUc { private readonly externalToolService: ExternalToolService, private readonly authorizationService: AuthorizationService, private readonly toolValidationService: ExternalToolValidationService, - private readonly externalToolLogoService: ExternalToolLogoService + private readonly externalToolLogoService: ExternalToolLogoService, + private readonly externalToolMetadataService: ExternalToolMetadataService ) {} async createExternalTool(userId: EntityId, externalToolCreate: ExternalToolCreate): Promise { @@ -74,6 +80,15 @@ export class ExternalToolUc { return promise; } + async getMetadataForExternalTool(userId: EntityId, toolId: EntityId): Promise { + // TODO N21-1496: Change External Tools to use authorizationService.checkPermission + await this.ensurePermission(userId, Permission.TOOL_ADMIN); + + const metadata: ExternalToolMetadata = await this.externalToolMetadataService.getMetadata(toolId); + + return metadata; + } + private async ensurePermission(userId: EntityId, permission: Permission) { const user: User = await this.authorizationService.getUserWithPermissions(userId); this.authorizationService.checkAllPermissions(user, [permission]); diff --git a/apps/server/src/modules/tool/index.ts b/apps/server/src/modules/tool/index.ts index 5af54483f5b..a7a6c23ddab 100644 --- a/apps/server/src/modules/tool/index.ts +++ b/apps/server/src/modules/tool/index.ts @@ -1,6 +1,7 @@ -export * from './school-external-tool/entity/school-external-tool.entity'; export * from './common/entity/custom-parameter-entry.entity'; +export * from './common/interface'; export * from './context-external-tool/entity'; +export * from './context-external-tool/service/context-external-tool-authorizable.service'; export * from './external-tool'; +export * from './school-external-tool/entity/school-external-tool.entity'; export * from './tool.module'; -export * from './common/interface'; diff --git a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts index 3512e66038e..274bd289edf 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts @@ -1,9 +1,13 @@ import { EntityManager, MikroORM } from '@mikro-orm/core'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, Permission, SchoolEntity, User } from '@shared/domain'; import { accountFactory, + contextExternalToolEntityFactory, + customParameterEntityFactory, externalToolEntityFactory, schoolExternalToolEntityFactory, schoolFactory, @@ -11,9 +15,9 @@ import { UserAndAccountTestFactory, userFactory, } from '@shared/testing'; -import { ServerTestModule } from '@modules/server'; -import { ToolConfigurationStatusResponse } from '../../../context-external-tool/controller/dto/tool-configuration-status.response'; -import { ExternalToolEntity } from '../../../external-tool/entity'; +import { ToolConfigurationStatusResponse } from '../../../context-external-tool/controller/dto'; +import { ContextExternalToolEntity, ContextExternalToolType } from '../../../context-external-tool/entity'; +import { CustomParameterScope, CustomParameterType, ExternalToolEntity } from '../../../external-tool/entity'; import { SchoolExternalToolEntity } from '../../entity'; import { CustomParameterEntryParam, @@ -21,6 +25,7 @@ import { SchoolExternalToolResponse, SchoolExternalToolSearchListResponse, SchoolExternalToolSearchParams, + SchoolExternalToolMetadataResponse, } from '../dto'; describe('ToolSchoolController (API)', () => { @@ -66,31 +71,31 @@ describe('ToolSchoolController (API)', () => { const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ version: 1, - parameters: [], + parameters: [ + customParameterEntityFactory.build({ + name: 'param1', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.STRING, + isOptional: false, + }), + customParameterEntityFactory.build({ + name: 'param2', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.BOOLEAN, + isOptional: true, + }), + ], }); - const paramEntry: CustomParameterEntryParam = { name: 'name', value: 'value' }; const postParams: SchoolExternalToolPostParams = { toolId: externalToolEntity.id, schoolId: school.id, version: 1, - parameters: [paramEntry], - }; - - const schoolExternalToolResponse: SchoolExternalToolResponse = new SchoolExternalToolResponse({ - id: expect.any(String), - name: externalToolEntity.name, - schoolId: postParams.schoolId, - toolId: postParams.toolId, - status: ToolConfigurationStatusResponse.LATEST, - toolVersion: postParams.version, parameters: [ - { - name: paramEntry.name, - value: paramEntry.value, - }, + { name: 'param1', value: 'value' }, + { name: 'param2', value: '' }, ], - }); + }; em.persist([ school, @@ -112,7 +117,7 @@ describe('ToolSchoolController (API)', () => { loggedInClientWithMissingPermission, loggedInClient, postParams, - schoolExternalToolResponse, + externalToolEntity, }; }; @@ -125,12 +130,23 @@ describe('ToolSchoolController (API)', () => { }); it('should create an school external tool', async () => { - const { loggedInClient, postParams, schoolExternalToolResponse } = await setup(); + const { loggedInClient, postParams, externalToolEntity } = await setup(); const response = await loggedInClient.post().send(postParams); expect(response.statusCode).toEqual(HttpStatus.CREATED); - expect(response.body).toEqual(schoolExternalToolResponse); + expect(response.body).toEqual({ + id: expect.any(String), + name: externalToolEntity.name, + schoolId: postParams.schoolId, + toolId: postParams.toolId, + status: ToolConfigurationStatusResponse.LATEST, + toolVersion: postParams.version, + parameters: [ + { name: 'param1', value: 'value' }, + { name: 'param2', value: undefined }, + ], + }); const createdSchoolExternalTool: SchoolExternalToolEntity | null = await em.findOne(SchoolExternalToolEntity, { school: postParams.schoolId, @@ -391,7 +407,14 @@ describe('ToolSchoolController (API)', () => { const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ version: 1, - parameters: [], + parameters: [ + customParameterEntityFactory.build({ + name: 'param1', + scope: CustomParameterScope.SCHOOL, + type: CustomParameterType.STRING, + isOptional: false, + }), + ], }); const externalToolEntity2: ExternalToolEntity = externalToolEntityFactory.buildWithId({ version: 1, @@ -422,7 +445,7 @@ describe('ToolSchoolController (API)', () => { accountWithMissingPermission ); - const paramEntry: CustomParameterEntryParam = { name: 'name', value: 'value' }; + const paramEntry: CustomParameterEntryParam = { name: 'param1', value: 'value' }; const postParams: SchoolExternalToolPostParams = { toolId: externalToolEntity.id, schoolId: school.id, @@ -430,7 +453,7 @@ describe('ToolSchoolController (API)', () => { parameters: [paramEntry], }; - const updatedParamEntry: CustomParameterEntryParam = { name: 'name', value: 'updatedValue' }; + const updatedParamEntry: CustomParameterEntryParam = { name: 'param1', value: 'updatedValue' }; const postParamsUpdate: SchoolExternalToolPostParams = { toolId: externalToolEntity.id, schoolId: school.id, @@ -487,4 +510,81 @@ describe('ToolSchoolController (API)', () => { expect(updatedSchoolExternalTool).toBeDefined(); }); }); + + describe('[GET] tools/school-external-tools/:schoolExternalToolId/metadata', () => { + describe('when user is not authenticated', () => { + const setup = () => { + const toolId: string = new ObjectId().toHexString(); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(undefined, toolId); + + return { toolId, externalToolEntity }; + }; + + it('should return unauthorized', async () => { + const { externalToolEntity } = setup(); + + const response = await testApiClient.get(`${externalToolEntity.id}/metadata`); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when schoolExternalToolId is given ', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + const schoolToolId: string = new ObjectId().toHexString(); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId( + { school }, + schoolToolId + ); + + const courseExternalToolEntitys: ContextExternalToolEntity[] = contextExternalToolEntityFactory.buildList(3, { + schoolTool: schoolExternalToolEntity, + contextType: ContextExternalToolType.COURSE, + contextId: new ObjectId().toHexString(), + }); + + const boardExternalToolEntitys: ContextExternalToolEntity[] = contextExternalToolEntityFactory.buildList(2, { + schoolTool: schoolExternalToolEntity, + contextType: ContextExternalToolType.BOARD_ELEMENT, + contextId: new ObjectId().toHexString(), + }); + + const schoolExternalToolMetadata: SchoolExternalToolMetadataResponse = new SchoolExternalToolMetadataResponse({ + contextExternalToolCountPerContext: { course: 3, boardElement: 2 }, + }); + + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ + Permission.SCHOOL_TOOL_ADMIN, + ]); + + await em.persistAndFlush([ + adminAccount, + adminUser, + schoolExternalToolEntity, + ...courseExternalToolEntitys, + ...boardExternalToolEntitys, + ]); + em.clear(); + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { loggedInClient, schoolExternalToolEntity, schoolExternalToolMetadata }; + }; + + it('should return the metadata of schoolExternalTool', async () => { + const { loggedInClient, schoolExternalToolEntity } = await setup(); + + const response = await loggedInClient.get(`${schoolExternalToolEntity.id}/metadata`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ + contextExternalToolCountPerContext: { + course: 3, + boardElement: 2, + }, + }); + }); + }); + }); }); diff --git a/apps/server/src/modules/tool/school-external-tool/controller/dto/index.ts b/apps/server/src/modules/tool/school-external-tool/controller/dto/index.ts index 5c3075e1cd2..595b16d7f9c 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/dto/index.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/dto/index.ts @@ -5,3 +5,4 @@ export * from './custom-parameter-entry.response'; export * from './school-external-tool-post.params'; export * from './school-external-tool-search.params'; export * from './school-external-tool-search-list.response'; +export * from './school-external-tool-metadata.response'; diff --git a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-metadata.response.ts b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-metadata.response.ts new file mode 100644 index 00000000000..db4806d23ec --- /dev/null +++ b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-metadata.response.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ContextExternalToolType } from '../../../context-external-tool/entity'; + +export class SchoolExternalToolMetadataResponse { + @ApiProperty({ + type: 'object', + properties: Object.fromEntries( + Object.values(ContextExternalToolType).map((key: ContextExternalToolType) => [key, { type: 'number' }]) + ), + }) + contextExternalToolCountPerContext: Record; + + constructor(schoolExternalToolMetadataResponse: SchoolExternalToolMetadataResponse) { + this.contextExternalToolCountPerContext = schoolExternalToolMetadataResponse.contextExternalToolCountPerContext; + } +} diff --git a/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts b/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts index 79d6f789443..44b1754ea9a 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts @@ -1,31 +1,36 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { + ApiBadRequestResponse, ApiCreatedResponse, ApiForbiddenResponse, ApiFoundResponse, - ApiResponse, ApiOkResponse, - ApiBadRequestResponse, + ApiOperation, + ApiResponse, ApiTags, ApiUnauthorizedResponse, ApiUnprocessableEntityResponse, - ApiOperation, } from '@nestjs/swagger'; -import { Body, Controller, Delete, Get, Param, Post, Query, Put, HttpCode, HttpStatus } from '@nestjs/common'; import { ValidationError } from '@shared/common'; import { LegacyLogger } from '@src/core/logger'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; -import { SchoolExternalToolRequestMapper, SchoolExternalToolResponseMapper } from '../mapper'; import { ExternalToolSearchListResponse } from '../../external-tool/controller/dto'; +import { SchoolExternalTool, SchoolExternalToolMetadata } from '../domain'; +import { + SchoolExternalToolMetadataMapper, + SchoolExternalToolRequestMapper, + SchoolExternalToolResponseMapper, +} from '../mapper'; +import { SchoolExternalToolUc } from '../uc'; +import { SchoolExternalToolDto } from '../uc/dto/school-external-tool.types'; import { SchoolExternalToolIdParams, + SchoolExternalToolMetadataResponse, SchoolExternalToolPostParams, SchoolExternalToolResponse, SchoolExternalToolSearchListResponse, SchoolExternalToolSearchParams, } from './dto'; -import { SchoolExternalToolDto } from '../uc/dto/school-external-tool.types'; -import { SchoolExternalToolUc } from '../uc'; -import { SchoolExternalTool } from '../domain'; @ApiTags('Tool') @Authenticate('jwt') @@ -136,4 +141,24 @@ export class ToolSchoolController { return response; } + + @Get('/:schoolExternalToolId/metadata') + @ApiOperation({ summary: 'Gets the metadata of an school external tool.' }) + @ApiOkResponse({ + description: 'Metadata of school external tool fetched successfully.', + type: SchoolExternalToolMetadataResponse, + }) + @ApiUnauthorizedResponse({ description: 'User is not logged in.' }) + async getMetaDataForExternalTool( + @CurrentUser() currentUser: ICurrentUser, + @Param() params: SchoolExternalToolIdParams + ): Promise { + const schoolExternalToolMetadata: SchoolExternalToolMetadata = + await this.schoolExternalToolUc.getMetadataForSchoolExternalTool(currentUser.userId, params.schoolExternalToolId); + + const mapped: SchoolExternalToolMetadataResponse = + SchoolExternalToolMetadataMapper.mapToSchoolExternalToolMetadataResponse(schoolExternalToolMetadata); + + return mapped; + } } diff --git a/apps/server/src/modules/tool/school-external-tool/domain/index.ts b/apps/server/src/modules/tool/school-external-tool/domain/index.ts index 1d734ed8376..d089fb2e908 100644 --- a/apps/server/src/modules/tool/school-external-tool/domain/index.ts +++ b/apps/server/src/modules/tool/school-external-tool/domain/index.ts @@ -1,2 +1,3 @@ export * from './school-external-tool.do'; export * from './school-external-tool-ref.do'; +export * from './school-external-tool-metadata'; diff --git a/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-metadata.ts b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-metadata.ts new file mode 100644 index 00000000000..4cccdfe11a1 --- /dev/null +++ b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-metadata.ts @@ -0,0 +1,7 @@ +export class SchoolExternalToolMetadata { + contextExternalToolCountPerContext: Record; + + constructor(schoolExternalToolMetadata: SchoolExternalToolMetadata) { + this.contextExternalToolCountPerContext = schoolExternalToolMetadata.contextExternalToolCountPerContext; + } +} diff --git a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.ts b/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.ts index fc7f6703d05..b5545239042 100644 --- a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.ts +++ b/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.ts @@ -4,7 +4,7 @@ import { SchoolEntity } from '@shared/domain/entity/school.entity'; import { CustomParameterEntryEntity } from '../../common/entity'; import { ExternalToolEntity } from '../../external-tool/entity'; -export interface ISchoolExternalToolProperties { +export interface SchoolExternalToolProperties { tool: ExternalToolEntity; school: SchoolEntity; schoolParameters?: CustomParameterEntryEntity[]; @@ -25,7 +25,7 @@ export class SchoolExternalToolEntity extends BaseEntityWithTimestamps { @Property() toolVersion: number; - constructor(props: ISchoolExternalToolProperties) { + constructor(props: SchoolExternalToolProperties) { super(); this.tool = props.tool; this.school = props.school; diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/index.ts b/apps/server/src/modules/tool/school-external-tool/mapper/index.ts index 961644805f1..48cd0b13a20 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/index.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/index.ts @@ -1,2 +1,3 @@ export * from './school-external-tool-request.mapper'; export * from './school-external-tool-response.mapper'; +export * from './school-external-tool-metadata.mapper'; diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-metadata.mapper.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-metadata.mapper.ts new file mode 100644 index 00000000000..1e42f22ebf2 --- /dev/null +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-metadata.mapper.ts @@ -0,0 +1,14 @@ +import { SchoolExternalToolMetadataResponse } from '../controller/dto'; +import { SchoolExternalToolMetadata } from '../domain'; + +export class SchoolExternalToolMetadataMapper { + static mapToSchoolExternalToolMetadataResponse( + schoolExternalToolMetadata: SchoolExternalToolMetadata + ): SchoolExternalToolMetadataResponse { + const externalToolMetadataResponse: SchoolExternalToolMetadataResponse = new SchoolExternalToolMetadataResponse({ + contextExternalToolCountPerContext: schoolExternalToolMetadata.contextExternalToolCountPerContext, + }); + + return externalToolMetadataResponse; + } +} diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.ts index 6617e4ec7fd..eff05c092cb 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-request.mapper.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { CustomParameterEntry } from '../../common/domain'; import { CustomParameterEntryParam, SchoolExternalToolPostParams } from '../controller/dto'; import { SchoolExternalToolDto } from '../uc/dto/school-external-tool.types'; -import { CustomParameterEntry } from '../../common/domain'; @Injectable() export class SchoolExternalToolRequestMapper { @@ -20,7 +20,7 @@ export class SchoolExternalToolRequestMapper { return customParameterParams.map((customParameterParam: CustomParameterEntryParam) => { return { name: customParameterParam.name, - value: customParameterParam.value, + value: customParameterParam.value || undefined, }; }); } diff --git a/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts b/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts index 898e159348a..93d4c4f6705 100644 --- a/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts +++ b/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts @@ -1,12 +1,16 @@ import { Module } from '@nestjs/common'; import { CommonToolModule } from '../common'; -import { SchoolExternalToolService, SchoolExternalToolValidationService } from './service'; +import { + SchoolExternalToolService, + SchoolExternalToolValidationService, + SchoolExternalToolMetadataService, +} from './service'; import { ExternalToolModule } from '../external-tool'; import { ToolConfigModule } from '../tool-config.module'; @Module({ imports: [CommonToolModule, ExternalToolModule, ToolConfigModule], - providers: [SchoolExternalToolService, SchoolExternalToolValidationService], - exports: [SchoolExternalToolService, SchoolExternalToolValidationService], + providers: [SchoolExternalToolService, SchoolExternalToolValidationService, SchoolExternalToolMetadataService], + exports: [SchoolExternalToolService, SchoolExternalToolValidationService, SchoolExternalToolMetadataService], }) export class SchoolExternalToolModule {} diff --git a/apps/server/src/modules/tool/school-external-tool/service/index.ts b/apps/server/src/modules/tool/school-external-tool/service/index.ts index 1ceab5f3da5..ea949d8b70a 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/index.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/index.ts @@ -1,2 +1,3 @@ export * from './school-external-tool.service'; export * from './school-external-tool-validation.service'; +export * from './school-external-tool-metadata.service'; diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.spec.ts new file mode 100644 index 00000000000..8aa29737550 --- /dev/null +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.spec.ts @@ -0,0 +1,93 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ContextExternalToolRepo } from '@shared/repo'; +import { Logger } from '@src/core/logger'; +import { SchoolExternalToolMetadata } from '../domain'; +import { SchoolExternalToolMetadataService } from './school-external-tool-metadata.service'; + +describe('SchoolExternalToolMetadataService', () => { + let module: TestingModule; + let service: SchoolExternalToolMetadataService; + + let contextExternalToolRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + SchoolExternalToolMetadataService, + { + provide: ContextExternalToolRepo, + useValue: createMock(), + }, + { + provide: Logger, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(SchoolExternalToolMetadataService); + contextExternalToolRepo = module.get(ContextExternalToolRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getMetadata', () => { + describe('when schoolExternalToolId is given', () => { + const setup = () => { + const schoolToolId: string = new ObjectId().toHexString(); + + const schoolExternalToolMetadata: SchoolExternalToolMetadata = new SchoolExternalToolMetadata({ + contextExternalToolCountPerContext: { course: 3, boardElement: 3 }, + }); + + contextExternalToolRepo.countBySchoolToolIdsAndContextType.mockResolvedValue(3); + + return { + schoolToolId, + schoolExternalToolMetadata, + }; + }; + + it('should return externalToolMetadata', async () => { + const { schoolToolId, schoolExternalToolMetadata } = setup(); + + const result: SchoolExternalToolMetadata = await service.getMetadata(schoolToolId); + + expect(result).toEqual(schoolExternalToolMetadata); + }); + }); + + describe('when no related context external tool was found', () => { + const setup = () => { + const schoolToolId: string = new ObjectId().toHexString(); + + const schoolExternalToolMetadata: SchoolExternalToolMetadata = new SchoolExternalToolMetadata({ + contextExternalToolCountPerContext: { course: 0, boardElement: 0 }, + }); + + contextExternalToolRepo.countBySchoolToolIdsAndContextType.mockResolvedValue(0); + + return { + schoolToolId, + schoolExternalToolMetadata, + }; + }; + + it('should return empty schoolExternalToolMetadata', async () => { + const { schoolToolId, schoolExternalToolMetadata } = setup(); + + const result: SchoolExternalToolMetadata = await service.getMetadata(schoolToolId); + + expect(result).toEqual(schoolExternalToolMetadata); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.ts new file mode 100644 index 00000000000..fa8fd4fc926 --- /dev/null +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ContextExternalToolRepo } from '@shared/repo'; +import { ToolContextType } from '../../common/enum'; +import { ToolContextMapper } from '../../common/mapper/tool-context.mapper'; +import { ContextExternalToolType } from '../../context-external-tool/entity'; +import { SchoolExternalToolMetadata } from '../domain'; + +@Injectable() +export class SchoolExternalToolMetadataService { + constructor(private readonly contextToolRepo: ContextExternalToolRepo) {} + + async getMetadata(schoolExternalToolId: EntityId) { + const contextExternalToolCount: Record = { + [ContextExternalToolType.BOARD_ELEMENT]: 0, + [ContextExternalToolType.COURSE]: 0, + }; + + await Promise.all( + Object.values(ToolContextType).map(async (contextType: ToolContextType): Promise => { + const type: ContextExternalToolType = ToolContextMapper.contextMapping[contextType]; + + const countPerContext: number = await this.contextToolRepo.countBySchoolToolIdsAndContextType(type, [ + schoolExternalToolId, + ]); + + contextExternalToolCount[type] = countPerContext; + }) + ); + + const schoolExternaltoolMetadata: SchoolExternalToolMetadata = new SchoolExternalToolMetadata({ + contextExternalToolCountPerContext: contextExternalToolCount, + }); + + return schoolExternaltoolMetadata; + } +} diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts index 6b4f69d6686..1f2ba7f5eb9 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts @@ -4,9 +4,9 @@ import { externalToolFactory, schoolExternalToolFactory } from '@shared/testing/ import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolValidationService } from './school-external-tool-validation.service'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; describe('SchoolExternalToolValidationService', () => { let module: TestingModule; @@ -82,14 +82,6 @@ describe('SchoolExternalToolValidationService', () => { expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); }); - it('should call commonToolValidationService.checkForDuplicateParameters', async () => { - const { schoolExternalTool } = setup(); - - await service.validate(schoolExternalTool); - - expect(commonToolValidationService.checkForDuplicateParameters).toHaveBeenCalledWith(schoolExternalTool); - }); - it('should call commonToolValidationService.checkCustomParameterEntries', async () => { const { schoolExternalTool } = setup(); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts index 3474aa90f5e..899055e321f 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts @@ -3,8 +3,8 @@ import { ValidationError } from '@shared/common'; import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; -import { SchoolExternalTool } from '../domain'; import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { SchoolExternalTool } from '../domain'; @Injectable() export class SchoolExternalToolValidationService { @@ -15,13 +15,12 @@ export class SchoolExternalToolValidationService { ) {} async validate(schoolExternalTool: SchoolExternalTool): Promise { - this.commonToolValidationService.checkForDuplicateParameters(schoolExternalTool); - const loadedExternalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); if (!this.toolFeatures.toolStatusWithoutVersions) { this.checkVersionMatch(schoolExternalTool.toolVersion, loadedExternalTool.version); } + this.commonToolValidationService.checkCustomParameterEntries(loadedExternalTool, schoolExternalTool); } diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts index b8c2789322d..1def9935806 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts @@ -1,16 +1,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { ApiValidationError } from '@shared/common'; import { SchoolExternalToolRepo } from '@shared/repo'; import { externalToolFactory } from '@shared/testing/factory/domainobject/tool/external-tool.factory'; import { schoolExternalToolFactory } from '@shared/testing/factory/domainobject/tool/school-external-tool.factory'; -import { ApiValidationError } from '@shared/common'; import { ToolConfigurationStatus } from '../../common/enum'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; -import { SchoolExternalTool } from '../domain'; -import { SchoolExternalToolService } from './school-external-tool.service'; import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolValidationService } from './school-external-tool-validation.service'; +import { SchoolExternalToolService } from './school-external-tool.service'; describe('SchoolExternalToolService', () => { let module: TestingModule; diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts index e654f82f96a..2ead0933582 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts @@ -4,10 +4,10 @@ import { SchoolExternalToolRepo } from '@shared/repo'; import { ToolConfigurationStatus } from '../../common/enum'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolQuery } from '../uc/dto/school-external-tool.types'; import { SchoolExternalToolValidationService } from './school-external-tool-validation.service'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; @Injectable() export class SchoolExternalToolService { diff --git a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts index 377a5d24a38..6f3512f6a0d 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, Permission, User } from '@shared/domain'; import { schoolExternalToolFactory, setupEntities, userFactory } from '@shared/testing'; @@ -6,7 +7,11 @@ import { AuthorizationContextBuilder } from '@modules/authorization'; import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalToolService } from '../../context-external-tool/service'; import { SchoolExternalTool } from '../domain'; -import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { + SchoolExternalToolMetadataService, + SchoolExternalToolService, + SchoolExternalToolValidationService, +} from '../service'; import { SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; import { SchoolExternalToolUc } from './school-external-tool.uc'; @@ -18,6 +23,7 @@ describe('SchoolExternalToolUc', () => { let contextExternalToolService: DeepMocked; let schoolExternalToolValidationService: DeepMocked; let toolPermissionHelper: DeepMocked; + let schoolExternalToolMetadataService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -40,6 +46,10 @@ describe('SchoolExternalToolUc', () => { provide: ToolPermissionHelper, useValue: createMock(), }, + { + provide: SchoolExternalToolMetadataService, + useValue: createMock(), + }, ], }).compile(); @@ -48,6 +58,7 @@ describe('SchoolExternalToolUc', () => { contextExternalToolService = module.get(ContextExternalToolService); schoolExternalToolValidationService = module.get(SchoolExternalToolValidationService); toolPermissionHelper = module.get(ToolPermissionHelper); + schoolExternalToolMetadataService = module.get(SchoolExternalToolMetadataService); }); afterAll(async () => { @@ -358,4 +369,55 @@ describe('SchoolExternalToolUc', () => { expect(result).toEqual(updatedTool); }); }); + + describe('getMetadataForSchoolExternalTool', () => { + describe('when authorize user', () => { + const setupMetadata = () => { + const toolId = new ObjectId().toHexString(); + const tool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ id: toolId }, toolId); + const userId: string = new ObjectId().toHexString(); + const user: User = userFactory.buildWithId({}, userId); + + schoolExternalToolService.findById.mockResolvedValue(tool); + + return { + user, + tool, + }; + }; + + it('should check the permissions of the user', async () => { + const { user, tool } = setupMetadata(); + + await uc.getMetadataForSchoolExternalTool(user.id, tool.id!); + + expect(toolPermissionHelper.ensureSchoolPermissions).toHaveBeenCalledWith( + user.id, + tool, + AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]) + ); + }); + }); + + describe('when externalToolId is given', () => { + const setupMetadata = () => { + const user: User = userFactory.buildWithId(); + + const toolId: string = new ObjectId().toHexString(); + + return { + toolId, + user, + }; + }; + + it('should call the service to get metadata', async () => { + const { toolId, user } = setupMetadata(); + + await uc.getMetadataForSchoolExternalTool(user.id, toolId); + + expect(schoolExternalToolMetadataService.getMetadata).toHaveBeenCalledWith(toolId); + }); + }); + }); }); diff --git a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts index d7adf3f4937..0d098c8c657 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts @@ -3,8 +3,12 @@ import { EntityId, Permission } from '@shared/domain'; import { AuthorizationContext, AuthorizationContextBuilder } from '@modules/authorization'; import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalTool } from '../domain'; -import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { SchoolExternalTool, SchoolExternalToolMetadata } from '../domain'; +import { + SchoolExternalToolService, + SchoolExternalToolValidationService, + SchoolExternalToolMetadataService, +} from '../service'; import { SchoolExternalToolDto, SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; @Injectable() @@ -13,6 +17,7 @@ export class SchoolExternalToolUc { private readonly schoolExternalToolService: SchoolExternalToolService, private readonly contextExternalToolService: ContextExternalToolService, private readonly schoolExternalToolValidationService: SchoolExternalToolValidationService, + private readonly schoolExternalToolMetadataService: SchoolExternalToolMetadataService, private readonly toolPermissionHelper: ToolPermissionHelper ) {} @@ -95,4 +100,20 @@ export class SchoolExternalToolUc { const saved = await this.schoolExternalToolService.saveSchoolExternalTool(updated); return saved; } + + async getMetadataForSchoolExternalTool( + userId: EntityId, + schoolExternalToolId: EntityId + ): Promise { + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); + + const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); + await this.toolPermissionHelper.ensureSchoolPermissions(userId, schoolExternalTool, context); + + const metadata: SchoolExternalToolMetadata = await this.schoolExternalToolMetadataService.getMetadata( + schoolExternalToolId + ); + + return metadata; + } } diff --git a/apps/server/src/modules/tool/tool-launch/controller/tool-launch.controller.ts b/apps/server/src/modules/tool/tool-launch/controller/tool-launch.controller.ts index 12424b294ed..3ac323b1000 100644 --- a/apps/server/src/modules/tool/tool-launch/controller/tool-launch.controller.ts +++ b/apps/server/src/modules/tool/tool-launch/controller/tool-launch.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Controller, Get, Param } from '@nestjs/common'; import { ApiBadRequestResponse, @@ -7,11 +8,10 @@ import { ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; -import { ToolLaunchUc } from '../uc'; -import { ToolLaunchParams, ToolLaunchRequestResponse } from './dto'; import { ToolLaunchMapper } from '../mapper'; import { ToolLaunchRequest } from '../types'; +import { ToolLaunchUc } from '../uc'; +import { ToolLaunchParams, ToolLaunchRequestResponse } from './dto'; @ApiTags('Tool') @Authenticate('jwt') diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts index 04868663671..7e170eb412b 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts @@ -30,7 +30,7 @@ import { AutoSchoolNumberStrategy, } from '../auto-parameter-strategy'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; -import { IToolLaunchParams } from './tool-launch-params.interface'; +import { ToolLaunchParams } from './tool-launch-params.interface'; const concreteConfigParameter: PropertyData = { location: PropertyLocation.QUERY, @@ -48,7 +48,7 @@ class TestLaunchStrategy extends AbstractLaunchStrategy { // eslint-disable-next-line @typescript-eslint/no-unused-vars userId: EntityId, // eslint-disable-next-line @typescript-eslint/no-unused-vars - config: IToolLaunchParams + config: ToolLaunchParams ): Promise { // Implement this method with your own logic for the mock launch strategy return Promise.resolve([concreteConfigParameter]); diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts index ce0f07c5731..d5e920bacf4 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts @@ -16,11 +16,11 @@ import { AutoSchoolIdStrategy, AutoSchoolNumberStrategy, } from '../auto-parameter-strategy'; -import { IToolLaunchParams } from './tool-launch-params.interface'; -import { IToolLaunchStrategy } from './tool-launch-strategy.interface'; +import { ToolLaunchParams } from './tool-launch-params.interface'; +import { ToolLaunchStrategy } from './tool-launch-strategy.interface'; @Injectable() -export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { +export abstract class AbstractLaunchStrategy implements ToolLaunchStrategy { private readonly autoParameterStrategyMap: Map; constructor( @@ -37,7 +37,7 @@ export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { ]); } - public async createLaunchData(userId: EntityId, data: IToolLaunchParams): Promise { + public async createLaunchData(userId: EntityId, data: ToolLaunchParams): Promise { const launchData: ToolLaunchData = this.buildToolLaunchDataFromExternalTool(data.externalTool); const launchDataProperties: PropertyData[] = await this.buildToolLaunchDataFromTools(data); @@ -54,7 +54,7 @@ export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { public abstract buildToolLaunchDataFromConcreteConfig( userId: EntityId, - config: IToolLaunchParams + config: ToolLaunchParams ): Promise; public abstract buildToolLaunchRequestPayload(url: string, properties: PropertyData[]): string | null; @@ -136,7 +136,7 @@ export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { return launchData; } - private async buildToolLaunchDataFromTools(data: IToolLaunchParams): Promise { + private async buildToolLaunchDataFromTools(data: ToolLaunchParams): Promise { const propertyData: PropertyData[] = []; const { externalTool, schoolExternalTool, contextExternalTool } = data; const customParameters = externalTool.parameters || []; diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts index db80f78498f..32ca68a74f0 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts @@ -12,7 +12,7 @@ import { AutoSchoolNumberStrategy, } from '../auto-parameter-strategy'; import { BasicToolLaunchStrategy } from './basic-tool-launch.strategy'; -import { IToolLaunchParams } from './tool-launch-params.interface'; +import { ToolLaunchParams } from './tool-launch-params.interface'; describe('BasicToolLaunchStrategy', () => { let module: TestingModule; @@ -118,7 +118,7 @@ describe('BasicToolLaunchStrategy', () => { const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); - const data: IToolLaunchParams = { + const data: ToolLaunchParams = { contextExternalTool, schoolExternalTool, externalTool, diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts index abe5810820d..399b428a710 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { LaunchRequestMethod, PropertyData, PropertyLocation } from '../../types'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; -import { IToolLaunchParams } from './tool-launch-params.interface'; +import { ToolLaunchParams } from './tool-launch-params.interface'; @Injectable() export class BasicToolLaunchStrategy extends AbstractLaunchStrategy { @@ -10,7 +10,7 @@ export class BasicToolLaunchStrategy extends AbstractLaunchStrategy { // eslint-disable-next-line @typescript-eslint/no-unused-vars userId: EntityId, // eslint-disable-next-line @typescript-eslint/no-unused-vars - data: IToolLaunchParams + data: ToolLaunchParams ): Promise { return Promise.resolve([]); } diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts index ee2932cd535..69ebd3e4d26 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts @@ -26,7 +26,7 @@ import { } from '../auto-parameter-strategy'; import { Lti11EncryptionService } from '../lti11-encryption.service'; import { Lti11ToolLaunchStrategy } from './lti11-tool-launch.strategy'; -import { IToolLaunchParams } from './tool-launch-params.interface'; +import { ToolLaunchParams } from './tool-launch-params.interface'; describe('Lti11ToolLaunchStrategy', () => { let module: TestingModule; @@ -108,7 +108,7 @@ describe('Lti11ToolLaunchStrategy', () => { const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); - const data: IToolLaunchParams = { + const data: ToolLaunchParams = { contextExternalTool, schoolExternalTool, externalTool, @@ -205,7 +205,7 @@ describe('Lti11ToolLaunchStrategy', () => { contextRef: { id: contextId, type: ToolContextType.COURSE }, }); - const data: IToolLaunchParams = { + const data: ToolLaunchParams = { contextExternalTool, schoolExternalTool, externalTool, @@ -252,7 +252,7 @@ describe('Lti11ToolLaunchStrategy', () => { const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); - const data: IToolLaunchParams = { + const data: ToolLaunchParams = { contextExternalTool, schoolExternalTool, externalTool, @@ -301,7 +301,7 @@ describe('Lti11ToolLaunchStrategy', () => { const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); - const data: IToolLaunchParams = { + const data: ToolLaunchParams = { contextExternalTool, schoolExternalTool, externalTool, @@ -352,7 +352,7 @@ describe('Lti11ToolLaunchStrategy', () => { const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); - const data: IToolLaunchParams = { + const data: ToolLaunchParams = { contextExternalTool, schoolExternalTool, externalTool, @@ -390,7 +390,7 @@ describe('Lti11ToolLaunchStrategy', () => { const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); - const data: IToolLaunchParams = { + const data: ToolLaunchParams = { contextExternalTool, schoolExternalTool, externalTool, diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts index 09516395234..fca44aaf90b 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts @@ -16,7 +16,7 @@ import { } from '../auto-parameter-strategy'; import { Lti11EncryptionService } from '../lti11-encryption.service'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; -import { IToolLaunchParams } from './tool-launch-params.interface'; +import { ToolLaunchParams } from './tool-launch-params.interface'; @Injectable() export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { @@ -35,7 +35,7 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { // eslint-disable-next-line @typescript-eslint/no-unused-vars public override async buildToolLaunchDataFromConcreteConfig( userId: EntityId, - data: IToolLaunchParams + data: ToolLaunchParams ): Promise { const { config } = data.externalTool; const contextId: EntityId = data.contextExternalTool.contextRef.id; diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts index 8f81a1d0eb2..4e80d439a97 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts @@ -12,7 +12,7 @@ import { AutoSchoolNumberStrategy, } from '../auto-parameter-strategy'; import { OAuth2ToolLaunchStrategy } from './oauth2-tool-launch.strategy'; -import { IToolLaunchParams } from './tool-launch-params.interface'; +import { ToolLaunchParams } from './tool-launch-params.interface'; describe('OAuth2ToolLaunchStrategy', () => { let module: TestingModule; @@ -61,7 +61,7 @@ describe('OAuth2ToolLaunchStrategy', () => { const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); - const data: IToolLaunchParams = { + const data: ToolLaunchParams = { contextExternalTool, schoolExternalTool, externalTool, diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts index 5052310a77d..833b0742a7b 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { LaunchRequestMethod, PropertyData } from '../../types'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; -import { IToolLaunchParams } from './tool-launch-params.interface'; +import { ToolLaunchParams } from './tool-launch-params.interface'; @Injectable() export class OAuth2ToolLaunchStrategy extends AbstractLaunchStrategy { @@ -10,7 +10,7 @@ export class OAuth2ToolLaunchStrategy extends AbstractLaunchStrategy { // eslint-disable-next-line @typescript-eslint/no-unused-vars userId: EntityId, // eslint-disable-next-line @typescript-eslint/no-unused-vars - data: IToolLaunchParams + data: ToolLaunchParams ): Promise { return Promise.resolve([]); } diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-params.interface.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-params.interface.ts index 24e368476f5..73fdf1d78b7 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-params.interface.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-params.interface.ts @@ -2,7 +2,7 @@ import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; -export interface IToolLaunchParams { +export interface ToolLaunchParams { externalTool: ExternalTool; schoolExternalTool: SchoolExternalTool; diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-strategy.interface.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-strategy.interface.ts index 09ea919b0c7..a0ba86c5648 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-strategy.interface.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-strategy.interface.ts @@ -1,9 +1,9 @@ import { EntityId } from '@shared/domain'; import { ToolLaunchData, ToolLaunchRequest } from '../../types'; -import { IToolLaunchParams } from './tool-launch-params.interface'; +import { ToolLaunchParams } from './tool-launch-params.interface'; -export interface IToolLaunchStrategy { - createLaunchData(userId: EntityId, params: IToolLaunchParams): Promise; +export interface ToolLaunchStrategy { + createLaunchData(userId: EntityId, params: ToolLaunchParams): Promise; createLaunchRequest(toolLaunchDataDO: ToolLaunchData): ToolLaunchRequest; } diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts index 16e8e093e9e..c16dc16f365 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts @@ -17,9 +17,9 @@ import { ToolStatusOutdatedLoggableException } from '../error'; import { LaunchRequestMethod, ToolLaunchData, ToolLaunchDataType, ToolLaunchRequest } from '../types'; import { BasicToolLaunchStrategy, - IToolLaunchParams, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy, + ToolLaunchParams, } from './launch-strategy'; import { ToolLaunchService } from './tool-launch.service'; import { ToolVersionService } from '../../context-external-tool/service/tool-version-service'; @@ -98,7 +98,7 @@ describe('ToolLaunchService', () => { openNewTab: false, }); - const launchParams: IToolLaunchParams = { + const launchParams: ToolLaunchParams = { externalTool, schoolExternalTool, contextExternalTool, @@ -165,7 +165,7 @@ describe('ToolLaunchService', () => { const externalTool: ExternalTool = externalToolFactory.build(); externalTool.config.type = 'unknown' as ToolConfigType; - const launchParams: IToolLaunchParams = { + const launchParams: ToolLaunchParams = { externalTool, schoolExternalTool, contextExternalTool, @@ -207,7 +207,7 @@ describe('ToolLaunchService', () => { openNewTab: false, }); - const launchParams: IToolLaunchParams = { + const launchParams: ToolLaunchParams = { externalTool, schoolExternalTool, contextExternalTool, diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts index 5b090f9778a..4ddbd55c3aa 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts @@ -11,15 +11,15 @@ import { ToolLaunchMapper } from '../mapper'; import { ToolLaunchData, ToolLaunchRequest } from '../types'; import { BasicToolLaunchStrategy, - IToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy, + ToolLaunchStrategy, } from './launch-strategy'; import { ToolVersionService } from '../../context-external-tool/service/tool-version-service'; @Injectable() export class ToolLaunchService { - private strategies: Map; + private strategies: Map; constructor( private readonly schoolExternalToolService: SchoolExternalToolService, @@ -37,7 +37,7 @@ export class ToolLaunchService { generateLaunchRequest(toolLaunchData: ToolLaunchData): ToolLaunchRequest { const toolConfigType: ToolConfigType = ToolLaunchMapper.mapToToolConfigType(toolLaunchData.type); - const strategy: IToolLaunchStrategy | undefined = this.strategies.get(toolConfigType); + const strategy: ToolLaunchStrategy | undefined = this.strategies.get(toolConfigType); if (!strategy) { throw new InternalServerErrorException('Unknown tool launch data type'); @@ -55,7 +55,7 @@ export class ToolLaunchService { await this.isToolStatusLatestOrThrow(userId, externalTool, schoolExternalTool, contextExternalTool); - const strategy: IToolLaunchStrategy | undefined = this.strategies.get(externalTool.config.type); + const strategy: ToolLaunchStrategy | undefined = this.strategies.get(externalTool.config.type); if (!strategy) { throw new InternalServerErrorException('Unknown tool config type'); diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts index 29b7796b40d..51bea6b1224 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts @@ -1,5 +1,24 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; +import { + FilterImportUserParams, + FilterMatchType, + FilterRoleType, + FilterUserParams, + ImportUserListResponse, + ImportUserResponse, + ImportUserSortOrder, + MatchType, + SortImportUserParams, + UpdateFlagParams, + UpdateMatchParams, + UserMatchListResponse, + UserMatchResponse, + UserRole, +} from '@modules/user-import/controller/dto'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { PaginationParams } from '@shared/controller'; @@ -14,7 +33,6 @@ import { SystemEntity, User, } from '@shared/domain'; -import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, importUserFactory, @@ -24,24 +42,6 @@ import { systemFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@modules/server/server.module'; -import { - FilterImportUserParams, - FilterMatchType, - FilterRoleType, - FilterUserParams, - ImportUserListResponse, - ImportUserResponse, - ImportUserSortOrder, - MatchType, - SortImportUserParams, - UpdateFlagParams, - UpdateMatchParams, - UserMatchListResponse, - UserMatchResponse, - UserRole, -} from '@modules/user-import/controller/dto'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/user-import/controller/import-user.controller.ts b/apps/server/src/modules/user-import/controller/import-user.controller.ts index c4368b5ea3f..bc29f09d6c9 100644 --- a/apps/server/src/modules/user-import/controller/import-user.controller.ts +++ b/apps/server/src/modules/user-import/controller/import-user.controller.ts @@ -1,8 +1,8 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller'; import { IFindOptions, ImportUser, User } from '@shared/domain'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { ImportUserMapper } from '../mapper/import-user.mapper'; import { UserMatchMapper } from '../mapper/user-match.mapper'; import { UserImportUc } from '../uc/user-import.uc'; diff --git a/apps/server/src/modules/user-import/mapper/import-user.mapper.ts b/apps/server/src/modules/user-import/mapper/import-user.mapper.ts index 6b19653f4de..e15b3f36b60 100644 --- a/apps/server/src/modules/user-import/mapper/import-user.mapper.ts +++ b/apps/server/src/modules/user-import/mapper/import-user.mapper.ts @@ -1,9 +1,9 @@ import { BadRequestException } from '@nestjs/common'; import { StringValidator } from '@shared/common'; -import { ImportUser, IImportUserScope, SortOrderMap } from '@shared/domain'; +import { IImportUserScope, ImportUser, SortOrderMap } from '@shared/domain'; import { - ImportUserResponse, FilterImportUserParams, + ImportUserResponse, ImportUserSortOrder, SortImportUserParams, } from '../controller/dto'; diff --git a/apps/server/src/modules/user-import/mapper/user-match.mapper.ts b/apps/server/src/modules/user-import/mapper/user-match.mapper.ts index 5d99bc1d860..8a826a613af 100644 --- a/apps/server/src/modules/user-import/mapper/user-match.mapper.ts +++ b/apps/server/src/modules/user-import/mapper/user-match.mapper.ts @@ -1,12 +1,12 @@ import { StringValidator } from '@shared/common'; -import { INameMatch, MatchCreator, User } from '@shared/domain'; -import { UserRole, UserMatchResponse } from '../controller/dto'; +import { MatchCreator, NameMatch, User } from '@shared/domain'; +import { UserMatchResponse, UserRole } from '../controller/dto'; import { FilterUserParams } from '../controller/dto/filter-user.params'; import { ImportUserMatchMapper } from './match.mapper'; export class UserMatchMapper { - static mapToDomain(query: FilterUserParams): INameMatch { - const scope: INameMatch = {}; + static mapToDomain(query: FilterUserParams): NameMatch { + const scope: NameMatch = {}; if (query.name) { if (StringValidator.isNotEmptyString(query.name, true)) { scope.name = query.name; diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.ts b/apps/server/src/modules/user-import/uc/user-import.uc.ts index 2dda936b1fb..a9290bb5e42 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.ts @@ -1,4 +1,8 @@ import { Configuration } from '@hpi-schul-cloud/commons'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto/account.dto'; +import { AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { BadRequestException, ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { @@ -8,10 +12,10 @@ import { IFindOptions, IImportUserScope, ImportUser, - INameMatch, LegacySchoolDo, MatchCreator, MatchCreatorScope, + NameMatch, Permission, SchoolFeatures, SystemEntity, @@ -19,10 +23,6 @@ import { } from '@shared/domain'; import { ImportUserRepo, SystemRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { AccountService } from '@modules/account/services/account.service'; -import { AccountDto } from '@modules/account/services/dto/account.dto'; -import { AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; import { AccountSaveDto } from '../../account/services/dto'; import { MigrationMayBeCompleted, @@ -164,7 +164,7 @@ export class UserImportUc { */ async findAllUnmatchedUsers( currentUserId: EntityId, - query: INameMatch, + query: NameMatch, options?: IFindOptions ): Promise> { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_VIEW); diff --git a/apps/server/src/modules/user/controller/api-test/user-language.api.spec.ts b/apps/server/src/modules/user/controller/api-test/user-language.api.spec.ts index 28bc7c5d14e..50b1c08f8d9 100644 --- a/apps/server/src/modules/user/controller/api-test/user-language.api.spec.ts +++ b/apps/server/src/modules/user/controller/api-test/user-language.api.spec.ts @@ -2,12 +2,12 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; -import { LanguageType, User } from '@shared/domain'; -import { cleanupCollections, mapUserToCurrentUser, roleFactory, userFactory } from '@shared/testing'; import { ICurrentUser } from '@modules/authentication'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server/server.module'; +import { ApiValidationError } from '@shared/common'; +import { LanguageType, User } from '@shared/domain'; +import { cleanupCollections, mapUserToCurrentUser, roleFactory, userFactory } from '@shared/testing'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/user/controller/api-test/user-me.api.spec.ts b/apps/server/src/modules/user/controller/api-test/user-me.api.spec.ts index 370de7e4fa6..c77f23f2e59 100644 --- a/apps/server/src/modules/user/controller/api-test/user-me.api.spec.ts +++ b/apps/server/src/modules/user/controller/api-test/user-me.api.spec.ts @@ -5,13 +5,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Request } from 'express'; import request from 'supertest'; -import { ApiValidationError } from '@shared/common'; -import { LanguageType } from '@shared/domain'; import { ICurrentUser } from '@modules/authentication'; -import { cleanupCollections, mapUserToCurrentUser, roleFactory, userFactory } from '@shared/testing'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server/server.module'; import { ResolvedUserResponse } from '@modules/user/controller/dto'; +import { ApiValidationError } from '@shared/common'; +import { LanguageType } from '@shared/domain'; +import { cleanupCollections, mapUserToCurrentUser, roleFactory, userFactory } from '@shared/testing'; const baseRouteName = '/user/me'; diff --git a/apps/server/src/modules/user/controller/user.controller.ts b/apps/server/src/modules/user/controller/user.controller.ts index 16729c2667b..1d3739b3144 100644 --- a/apps/server/src/modules/user/controller/user.controller.ts +++ b/apps/server/src/modules/user/controller/user.controller.ts @@ -1,6 +1,6 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, Get, Patch } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { ResolvedUserMapper } from '../mapper'; import { UserUc } from '../uc'; import { ChangeLanguageParams, ResolvedUserResponse, SuccessfulResponse } from './dto'; diff --git a/apps/server/src/modules/user/interfaces/user-config.ts b/apps/server/src/modules/user/interfaces/user-config.ts index cd8576522e1..95935d4866c 100644 --- a/apps/server/src/modules/user/interfaces/user-config.ts +++ b/apps/server/src/modules/user/interfaces/user-config.ts @@ -1,3 +1,3 @@ -export interface IUserConfig { +export interface UserConfig { AVAILABLE_LANGUAGES: string[]; } diff --git a/apps/server/src/modules/user/service/user.service.ts b/apps/server/src/modules/user/service/user.service.ts index 2cc95991f96..6ef014f8696 100644 --- a/apps/server/src/modules/user/service/user.service.ts +++ b/apps/server/src/modules/user/service/user.service.ts @@ -1,6 +1,7 @@ import { AccountService } from '@modules/account'; import { AccountDto } from '@modules/account/services/dto'; // invalid import +import { OauthCurrentUser } from '@modules/authentication/interface'; import { CurrentUserMapper } from '@modules/authentication/mapper'; import { RoleDto } from '@modules/role/service/dto/role.dto'; import { RoleService } from '@modules/role/service/role.service'; @@ -10,8 +11,7 @@ import { EntityId, IFindOptions, LanguageType, User } from '@shared/domain'; import { Page, RoleReference, UserDO } from '@shared/domain/domainobject'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; -import { OauthCurrentUser } from '@modules/authentication/interface'; -import { IUserConfig } from '../interfaces'; +import { UserConfig } from '../interfaces'; import { UserMapper } from '../mapper/user.mapper'; import { UserDto } from '../uc/dto/user.dto'; import { UserQuery } from './user-query.type'; @@ -21,7 +21,7 @@ export class UserService { constructor( private readonly userRepo: UserRepo, private readonly userDORepo: UserDORepo, - private readonly configService: ConfigService, + private readonly configService: ConfigService, private readonly roleService: RoleService, private readonly accountService: AccountService ) {} diff --git a/apps/server/src/modules/user/uc/user.uc.ts b/apps/server/src/modules/user/uc/user.uc.ts index 0996fa6bd9c..2f2661eb0a2 100644 --- a/apps/server/src/modules/user/uc/user.uc.ts +++ b/apps/server/src/modules/user/uc/user.uc.ts @@ -3,11 +3,11 @@ import { ConfigService } from '@nestjs/config'; import { EntityId, LanguageType, User } from '@shared/domain'; import { UserRepo } from '@shared/repo'; import { ChangeLanguageParams } from '../controller/dto'; -import { IUserConfig } from '../interfaces'; +import { UserConfig } from '../interfaces'; @Injectable() export class UserUc { - constructor(private readonly userRepo: UserRepo, private readonly configService: ConfigService) {} + constructor(private readonly userRepo: UserRepo, private readonly configService: ConfigService) {} async me(userId: EntityId): Promise<[User, string[]]> { const user = await this.userRepo.findById(userId, true); diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.ts index 54b9ef4549b..2fcc3db9981 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.ts @@ -4,7 +4,7 @@ import { ConverterUtil } from '@shared/common/utils'; import { ErrorUtils } from '@src/core/error/utils'; import { AxiosResponse } from 'axios'; import crypto from 'crypto'; -import { Observable, firstValueFrom } from 'rxjs'; +import { firstValueFrom, Observable } from 'rxjs'; import { URL, URLSearchParams } from 'url'; import { BbbSettings, IBbbSettings } from './bbb-settings.interface'; import { BBBBaseMeetingConfig, BBBCreateConfig, BBBJoinConfig } from './request'; diff --git a/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.spec.ts b/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.spec.ts index 6d78b513d75..01a62786582 100644 --- a/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.spec.ts +++ b/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; +import { ICurrentUser } from '@modules/authentication'; import { Test, TestingModule } from '@nestjs/testing'; import { VideoConferenceScope } from '@shared/domain/interface'; -import { ICurrentUser } from '@modules/authentication'; import { BBBBaseResponse, BBBCreateResponse } from '../bbb'; import { defaultVideoConferenceOptions } from '../interface'; import { VideoConferenceDeprecatedUc } from '../uc'; diff --git a/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.ts b/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.ts index 8dbec66972e..d32273b98f7 100644 --- a/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.ts +++ b/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { BadRequestException, Body, @@ -11,7 +12,6 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { VideoConferenceScope } from '@shared/domain/interface'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { BBBBaseResponse } from '../bbb'; import { defaultVideoConferenceOptions } from '../interface'; import { VideoConferenceResponseDeprecatedMapper } from '../mapper/vc-deprecated-response.mapper'; diff --git a/apps/server/src/modules/video-conference/controller/video-conference.controller.ts b/apps/server/src/modules/video-conference/controller/video-conference.controller.ts index 442e9132758..b55eac17f64 100644 --- a/apps/server/src/modules/video-conference/controller/video-conference.controller.ts +++ b/apps/server/src/modules/video-conference/controller/video-conference.controller.ts @@ -1,6 +1,6 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, Get, HttpStatus, Param, Put, Req } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Request } from 'express'; import { InvalidOriginForLogoutUrlLoggableException } from '../error'; import { VideoConferenceOptions } from '../interface'; diff --git a/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts b/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts index 28711a06c1a..2a5d223b553 100644 --- a/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts +++ b/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts @@ -1,6 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; +import { CalendarEventDto, CalendarService } from '@infra/calendar'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CourseService } from '@modules/learnroom/service'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; +import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; +import { Test, TestingModule } from '@nestjs/testing'; import { Course, EntityId, @@ -12,23 +18,17 @@ import { VideoConferenceDO, VideoConferenceScope, } from '@shared/domain'; -import { CalendarEventDto, CalendarService } from '@infra/calendar'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { UserService } from '@modules/user'; import { courseFactory, roleFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; -import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory'; -import { ObjectId } from 'bson'; import { teamFactory } from '@shared/testing/factory/team.factory'; -import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; import { teamUserFactory } from '@shared/testing/factory/teamuser.factory'; -import { CourseService } from '@modules/learnroom/service'; -import { VideoConferenceService } from './video-conference.service'; -import { ErrorStatus } from '../error'; +import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory'; +import { ObjectId } from 'bson'; import { BBBRole } from '../bbb'; -import { IScopeInfo, ScopeRef, VideoConferenceState } from '../uc/dto'; +import { ErrorStatus } from '../error'; import { IVideoConferenceSettings, VideoConferenceOptions, VideoConferenceSettings } from '../interface'; +import { ScopeInfo, ScopeRef, VideoConferenceState } from '../uc/dto'; +import { VideoConferenceService } from './video-conference.service'; describe('VideoConferenceService', () => { let service: DeepMocked; @@ -561,7 +561,7 @@ describe('VideoConferenceService', () => { course.id = scopeId; courseService.findById.mockResolvedValue(course); - const result: IScopeInfo = await service.getScopeInfo(userId, scopeId, conferenceScope); + const result: ScopeInfo = await service.getScopeInfo(userId, scopeId, conferenceScope); expect(result).toEqual({ scopeId, @@ -580,7 +580,7 @@ describe('VideoConferenceService', () => { const event: CalendarEventDto = { title: 'Event', teamId }; calendarService.findEvent.mockResolvedValue(event); - const result: IScopeInfo = await service.getScopeInfo(userId, scopeId, VideoConferenceScope.EVENT); + const result: ScopeInfo = await service.getScopeInfo(userId, scopeId, VideoConferenceScope.EVENT); expect(result).toEqual({ scopeId: teamId, diff --git a/apps/server/src/modules/video-conference/service/video-conference.service.ts b/apps/server/src/modules/video-conference/service/video-conference.service.ts index 401cc0a0015..706a00e6e3b 100644 --- a/apps/server/src/modules/video-conference/service/video-conference.service.ts +++ b/apps/server/src/modules/video-conference/service/video-conference.service.ts @@ -1,3 +1,8 @@ +import { CalendarEventDto, CalendarService } from '@infra/calendar'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CourseService } from '@modules/learnroom'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; import { Course, @@ -14,16 +19,11 @@ import { VideoConferenceOptionsDO, VideoConferenceScope, } from '@shared/domain'; -import { CalendarEventDto, CalendarService } from '@infra/calendar'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { CourseService } from '@modules/learnroom'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { UserService } from '@modules/user'; import { BBBRole } from '../bbb'; import { ErrorStatus } from '../error'; import { IVideoConferenceSettings, VideoConferenceOptions, VideoConferenceSettings } from '../interface'; -import { IScopeInfo, VideoConferenceState } from '../uc/dto'; +import { ScopeInfo, VideoConferenceState } from '../uc/dto'; @Injectable() export class VideoConferenceService { @@ -165,7 +165,7 @@ export class VideoConferenceService { return text.replace(/[^\dA-Za-zÀ-ÖØ-öø-ÿ.\-=_`´ ]/g, ''); } - async getScopeInfo(userId: EntityId, scopeId: string, scope: VideoConferenceScope): Promise { + async getScopeInfo(userId: EntityId, scopeId: string, scope: VideoConferenceScope): Promise { switch (scope) { case VideoConferenceScope.COURSE: { const course: Course = await this.courseService.findById(scopeId); @@ -197,7 +197,7 @@ export class VideoConferenceService { scopeId: EntityId, scope: VideoConferenceScope ): Promise<{ role: BBBRole; isGuest: boolean }> { - const scopeInfo: IScopeInfo = await this.getScopeInfo(userId, scopeId, scope); + const scopeInfo: ScopeInfo = await this.getScopeInfo(userId, scopeId, scope); const role: BBBRole = await this.determineBbbRole(userId, scopeInfo.scopeId, scope); diff --git a/apps/server/src/modules/video-conference/uc/dto/scope-info.interface.ts b/apps/server/src/modules/video-conference/uc/dto/scope-info.interface.ts index 1f304e3a235..2469f49ad29 100644 --- a/apps/server/src/modules/video-conference/uc/dto/scope-info.interface.ts +++ b/apps/server/src/modules/video-conference/uc/dto/scope-info.interface.ts @@ -1,6 +1,6 @@ import { EntityId } from '@shared/domain'; -export interface IScopeInfo { +export interface ScopeInfo { scopeId: EntityId; scopeName: string; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts index 4c38bf18467..1361cb053ad 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts @@ -1,16 +1,16 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UserService } from '@modules/user'; -import { userDoFactory } from '@shared/testing'; +import { ForbiddenException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; import { UserDO, VideoConferenceScope } from '@shared/domain'; +import { userDoFactory } from '@shared/testing'; import { ObjectId } from 'bson'; -import { ForbiddenException } from '@nestjs/common'; -import { VideoConferenceCreateUc } from './video-conference-create.uc'; -import { BBBService, VideoConferenceService } from '../service'; -import { VideoConferenceOptions } from '../interface'; import { BBBCreateResponse, BBBMeetingInfoResponse, BBBResponse, BBBRole, BBBStatus } from '../bbb'; -import { IScopeInfo, ScopeRef } from './dto'; import { ErrorStatus } from '../error/error-status.enum'; +import { VideoConferenceOptions } from '../interface'; +import { BBBService, VideoConferenceService } from '../service'; +import { ScopeInfo, ScopeRef } from './dto'; +import { VideoConferenceCreateUc } from './video-conference-create.uc'; describe('VideoConferenceCreateUc', () => { let module: TestingModule; @@ -87,7 +87,7 @@ describe('VideoConferenceCreateUc', () => { moderatorMustApproveJoinRequests: true, }; - const scopeInfo: IScopeInfo = { + const scopeInfo: ScopeInfo = { scopeId: scope.id, scopeName: 'scopeName', title: 'title', @@ -162,7 +162,7 @@ describe('VideoConferenceCreateUc', () => { moderatorMustApproveJoinRequests: true, }; - const scopeInfo: IScopeInfo = { + const scopeInfo: ScopeInfo = { scopeId: scope.id, scopeName: 'scopeName', title: 'title', diff --git a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts index 940113276a2..d9ba4e0d61c 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts @@ -1,6 +1,6 @@ +import { UserService } from '@modules/user'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { EntityId, UserDO } from '@shared/domain'; -import { UserService } from '@modules/user'; import { BBBBaseMeetingConfig, BBBCreateConfigBuilder, @@ -13,7 +13,7 @@ import { import { ErrorStatus } from '../error/error-status.enum'; import { VideoConferenceOptions } from '../interface'; import { VideoConferenceService } from '../service'; -import { IScopeInfo, ScopeRef } from './dto'; +import { ScopeInfo, ScopeRef } from './dto'; @Injectable() export class VideoConferenceCreateUc { @@ -48,7 +48,7 @@ export class VideoConferenceCreateUc { await this.verifyFeaturesEnabled(user.schoolId); - const scopeInfo: IScopeInfo = await this.videoConferenceService.getScopeInfo(currentUserId, scope.id, scope.scope); + const scopeInfo: ScopeInfo = await this.videoConferenceService.getScopeInfo(currentUserId, scope.id, scope.scope); const bbbRole: BBBRole = await this.videoConferenceService.determineBbbRole( currentUserId, @@ -67,7 +67,7 @@ export class VideoConferenceCreateUc { private prepareBBBCreateConfigBuilder( scope: ScopeRef, options: VideoConferenceOptions, - scopeInfo: IScopeInfo + scopeInfo: ScopeInfo ): BBBCreateConfigBuilder { const configBuilder: BBBCreateConfigBuilder = new BBBCreateConfigBuilder({ name: this.videoConferenceService.sanitizeString(scopeInfo.title), diff --git a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts index bf1fd3e1394..a8caf8f2dc9 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts @@ -1,5 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { CalendarService } from '@infra/calendar'; +import { CalendarEventDto } from '@infra/calendar/dto/calendar-event.dto'; +import { ICurrentUser } from '@modules/authentication'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; +import { CourseService } from '@modules/learnroom/service'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { BadRequestException, ForbiddenException, InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { @@ -14,18 +21,9 @@ import { VideoConferenceDO, } from '@shared/domain'; import { VideoConferenceScope } from '@shared/domain/interface'; -import { CalendarService } from '@infra/calendar'; -import { CalendarEventDto } from '@infra/calendar/dto/calendar-event.dto'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; import { roleFactory, setupEntities, userDoFactory } from '@shared/testing'; import { teamFactory } from '@shared/testing/factory/team.factory'; -import { AuthorizationReferenceService } from '@modules/authorization/domain'; -import { ICurrentUser } from '@modules/authentication'; -import { CourseService } from '@modules/learnroom/service'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { UserService } from '@modules/user'; -import { IScopeInfo, VideoConference, VideoConferenceJoin, VideoConferenceState } from './dto'; -import { VideoConferenceDeprecatedUc } from './video-conference-deprecated.uc'; import { BBBBaseMeetingConfig, BBBBaseResponse, @@ -39,11 +37,13 @@ import { BBBStatus, GuestPolicy, } from '../bbb'; -import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interface'; import { ErrorStatus } from '../error/error-status.enum'; +import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interface'; +import { ScopeInfo, VideoConference, VideoConferenceJoin, VideoConferenceState } from './dto'; +import { VideoConferenceDeprecatedUc } from './video-conference-deprecated.uc'; class VideoConferenceDeprecatedUcSpec extends VideoConferenceDeprecatedUc { - async getScopeInfoSpec(userId: EntityId, conferenceScope: VideoConferenceScope, refId: string): Promise { + async getScopeInfoSpec(userId: EntityId, conferenceScope: VideoConferenceScope, refId: string): Promise { return this.getScopeInfo(userId, conferenceScope, refId); } @@ -198,7 +198,7 @@ describe('VideoConferenceUc', () => { describe('getScopeInfo', () => { it('should return scope info for courses', async () => { // Act - const scopeInfo: IScopeInfo = await useCase.getScopeInfoSpec('userId', VideoConferenceScope.COURSE, course.id); + const scopeInfo: ScopeInfo = await useCase.getScopeInfoSpec('userId', VideoConferenceScope.COURSE, course.id); // Assert expect(scopeInfo.scopeId).toEqual(course.id); @@ -209,7 +209,7 @@ describe('VideoConferenceUc', () => { it('should return scope info for teams', async () => { // Act - const scopeInfo: IScopeInfo = await useCase.getScopeInfoSpec('userId', VideoConferenceScope.EVENT, eventId); + const scopeInfo: ScopeInfo = await useCase.getScopeInfoSpec('userId', VideoConferenceScope.EVENT, eventId); // Assert expect(scopeInfo.scopeId).toEqual(event.teamId); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts index 95470053e6d..14d8b594fb9 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts @@ -1,4 +1,12 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { CalendarService } from '@infra/calendar'; +import { CalendarEventDto } from '@infra/calendar/dto/calendar-event.dto'; +import { ICurrentUser } from '@modules/authentication'; +import { Action, AuthorizationContextBuilder } from '@modules/authorization'; +import { AuthorizableReferenceType, AuthorizationReferenceService } from '@modules/authorization/domain'; +import { CourseService } from '@modules/learnroom'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { Course, @@ -13,16 +21,8 @@ import { VideoConferenceOptionsDO, } from '@shared/domain'; import { VideoConferenceScope } from '@shared/domain/interface'; -import { CalendarService } from '@infra/calendar'; -import { CalendarEventDto } from '@infra/calendar/dto/calendar-event.dto'; import { TeamsRepo } from '@shared/repo'; import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo'; -import { ICurrentUser } from '@modules/authentication'; -import { Action, AuthorizationContextBuilder } from '@modules/authorization'; -import { AuthorizationReferenceService, AuthorizableReferenceType } from '@modules/authorization/domain'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { CourseService } from '@modules/learnroom'; -import { UserService } from '@modules/user'; import { BBBBaseMeetingConfig, BBBBaseResponse, @@ -37,7 +37,7 @@ import { } from '../bbb'; import { ErrorStatus } from '../error/error-status.enum'; import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interface'; -import { IScopeInfo, VideoConference, VideoConferenceInfo, VideoConferenceJoin, VideoConferenceState } from './dto'; +import { ScopeInfo, VideoConference, VideoConferenceInfo, VideoConferenceJoin, VideoConferenceState } from './dto'; const PermissionMapping = { [BBBRole.MODERATOR]: Permission.START_MEETING, @@ -86,7 +86,7 @@ export class VideoConferenceDeprecatedUc { const { userId, schoolId } = currentUser; await this.throwOnFeaturesDisabled(schoolId); - const scopeInfo: IScopeInfo = await this.getScopeInfo(userId, conferenceScope, refId); + const scopeInfo: ScopeInfo = await this.getScopeInfo(userId, conferenceScope, refId); const bbbRole: BBBRole = await this.checkPermission(userId, conferenceScope, scopeInfo.scopeId); @@ -150,7 +150,7 @@ export class VideoConferenceDeprecatedUc { await this.throwOnFeaturesDisabled(schoolId); - const scopeInfo: IScopeInfo = await this.getScopeInfo(userId, conferenceScope, refId); + const scopeInfo: ScopeInfo = await this.getScopeInfo(userId, conferenceScope, refId); const bbbRole: BBBRole = await this.checkPermission(userId, conferenceScope, scopeInfo.scopeId); @@ -205,7 +205,7 @@ export class VideoConferenceDeprecatedUc { await this.throwOnFeaturesDisabled(schoolId); - const scopeInfo: IScopeInfo = await this.getScopeInfo(userId, conferenceScope, refId); + const scopeInfo: ScopeInfo = await this.getScopeInfo(userId, conferenceScope, refId); const bbbRole: BBBRole = await this.checkPermission(userId, conferenceScope, scopeInfo.scopeId); @@ -337,13 +337,13 @@ export class VideoConferenceDeprecatedUc { * @param {EntityId} userId * @param {VideoConferenceScope} conferenceScope * @param {EntityId} refId eventId or courseId, depending on scope. - * @returns {Promise} + * @returns {Promise} */ protected async getScopeInfo( userId: EntityId, conferenceScope: VideoConferenceScope, refId: string - ): Promise { + ): Promise { switch (conferenceScope) { case VideoConferenceScope.COURSE: { const course: Course = await this.courseService.findById(refId); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts index d1dbfc18b9d..e18c7d7dc65 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts @@ -1,15 +1,15 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UserService } from '@modules/user'; -import { userDoFactory } from '@shared/testing'; +import { ForbiddenException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; import { UserDO, VideoConferenceScope } from '@shared/domain'; +import { userDoFactory } from '@shared/testing'; import { ObjectId } from 'bson'; -import { ForbiddenException } from '@nestjs/common'; -import { BBBService, VideoConferenceService } from '../service'; import { BBBBaseResponse, BBBResponse, BBBRole, BBBStatus } from '../bbb'; -import { IScopeInfo, VideoConference, VideoConferenceState } from './dto'; -import { VideoConferenceEndUc } from './video-conference-end.uc'; import { ErrorStatus } from '../error/error-status.enum'; +import { BBBService, VideoConferenceService } from '../service'; +import { ScopeInfo, VideoConference, VideoConferenceState } from './dto'; +import { VideoConferenceEndUc } from './video-conference-end.uc'; describe('VideoConferenceEndUc', () => { let module: TestingModule; @@ -57,7 +57,7 @@ describe('VideoConferenceEndUc', () => { const user: UserDO = userDoFactory.buildWithId(); const currentUserId: string = user.id as string; const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; - const scopeInfo: IScopeInfo = { + const scopeInfo: ScopeInfo = { scopeId: scope.id, scopeName: 'scopeName', title: 'title', @@ -93,7 +93,7 @@ describe('VideoConferenceEndUc', () => { const user: UserDO = userDoFactory.buildWithId(); const currentUserId: string = user.id as string; const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; - const scopeInfo: IScopeInfo = { + const scopeInfo: ScopeInfo = { scopeId: scope.id, scopeName: 'scopeName', title: 'title', diff --git a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts index 6cb70edc176..a1648a4cba1 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts @@ -1,11 +1,11 @@ +import { UserService } from '@modules/user'; +import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { EntityId, UserDO } from '@shared/domain'; -import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; -import { UserService } from '@modules/user'; import { BBBBaseMeetingConfig, BBBBaseResponse, BBBResponse, BBBRole, BBBService } from '../bbb'; -import { IScopeInfo, ScopeRef, VideoConference, VideoConferenceState } from './dto'; -import { VideoConferenceService } from '../service'; import { PermissionMapping } from '../mapper/video-conference.mapper'; +import { VideoConferenceService } from '../service'; +import { ScopeInfo, ScopeRef, VideoConference, VideoConferenceState } from './dto'; @Injectable() export class VideoConferenceEndUc { @@ -27,7 +27,7 @@ export class VideoConferenceEndUc { await this.videoConferenceService.throwOnFeaturesDisabled(user.schoolId); - const scopeInfo: IScopeInfo = await this.videoConferenceService.getScopeInfo(userId, scope.id, scope.scope); + const scopeInfo: ScopeInfo = await this.videoConferenceService.getScopeInfo(userId, scope.id, scope.scope); const bbbRole: BBBRole = await this.videoConferenceService.determineBbbRole(userId, scopeInfo.scopeId, scope.scope); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts index ebc8daa4084..3aac5d6e8ff 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts @@ -1,17 +1,17 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UserService } from '@modules/user'; -import { userDoFactory } from '@shared/testing'; +import { ForbiddenException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; import { Permission, UserDO, VideoConferenceDO, VideoConferenceScope } from '@shared/domain'; -import { ObjectId } from 'bson'; +import { userDoFactory } from '@shared/testing'; import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory'; -import { ForbiddenException } from '@nestjs/common'; -import { BBBService, VideoConferenceService } from '../service'; +import { ObjectId } from 'bson'; import { BBBMeetingInfoResponse, BBBResponse, BBBRole, BBBStatus } from '../bbb'; -import { IScopeInfo, VideoConferenceInfo, VideoConferenceState } from './dto'; -import { VideoConferenceInfoUc } from './video-conference-info.uc'; -import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interface'; import { ErrorStatus } from '../error/error-status.enum'; +import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interface'; +import { BBBService, VideoConferenceService } from '../service'; +import { ScopeInfo, VideoConferenceInfo, VideoConferenceState } from './dto'; +import { VideoConferenceInfoUc } from './video-conference-info.uc'; describe('VideoConferenceInfoUc', () => { let module: TestingModule; @@ -76,7 +76,7 @@ describe('VideoConferenceInfoUc', () => { const user: UserDO = userDoFactory.buildWithId(); const currentUserId: string = user.id as string; const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; - const scopeInfo: IScopeInfo = { + const scopeInfo: ScopeInfo = { scopeId: scope.id, scopeName: 'scopeName', title: 'title', @@ -117,7 +117,7 @@ describe('VideoConferenceInfoUc', () => { const user: UserDO = userDoFactory.buildWithId(); const currentUserId: string = user.id as string; const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; - const scopeInfo: IScopeInfo = { + const scopeInfo: ScopeInfo = { scopeId: scope.id, scopeName: 'scopeName', title: 'title', @@ -235,7 +235,7 @@ describe('VideoConferenceInfoUc', () => { const user: UserDO = userDoFactory.buildWithId(); const currentUserId: string = user.id as string; const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; - const scopeInfo: IScopeInfo = { + const scopeInfo: ScopeInfo = { scopeId: scope.id, scopeName: 'scopeName', title: 'title', @@ -283,7 +283,7 @@ describe('VideoConferenceInfoUc', () => { const user: UserDO = userDoFactory.buildWithId(); const currentUserId: string = user.id as string; const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; - const scopeInfo: IScopeInfo = { + const scopeInfo: ScopeInfo = { scopeId: scope.id, scopeName: 'scopeName', title: 'title', @@ -327,7 +327,7 @@ describe('VideoConferenceInfoUc', () => { const user: UserDO = userDoFactory.buildWithId(); const currentUserId: string = user.id as string; const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; - const scopeInfo: IScopeInfo = { + const scopeInfo: ScopeInfo = { scopeId: scope.id, scopeName: 'scopeName', title: 'title', diff --git a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts index 1c3736d1d01..7e39db7bb7b 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts @@ -1,12 +1,12 @@ +import { UserService } from '@modules/user'; +import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { EntityId, UserDO, VideoConferenceDO, VideoConferenceOptionsDO } from '@shared/domain'; -import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; -import { UserService } from '@modules/user'; import { BBBBaseMeetingConfig, BBBMeetingInfoResponse, BBBResponse, BBBRole, BBBService } from '../bbb'; -import { IScopeInfo, ScopeRef, VideoConferenceInfo, VideoConferenceState } from './dto'; -import { VideoConferenceService } from '../service'; -import { PermissionMapping } from '../mapper/video-conference.mapper'; import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interface'; +import { PermissionMapping } from '../mapper/video-conference.mapper'; +import { VideoConferenceService } from '../service'; +import { ScopeInfo, ScopeRef, VideoConferenceInfo, VideoConferenceState } from './dto'; @Injectable() export class VideoConferenceInfoUc { @@ -27,7 +27,7 @@ export class VideoConferenceInfoUc { await this.videoConferenceService.throwOnFeaturesDisabled(user.schoolId); - const scopeInfo: IScopeInfo = await this.videoConferenceService.getScopeInfo(currentUserId, scope.id, scope.scope); + const scopeInfo: ScopeInfo = await this.videoConferenceService.getScopeInfo(currentUserId, scope.id, scope.scope); const bbbRole: BBBRole = await this.videoConferenceService.determineBbbRole( currentUserId, diff --git a/apps/server/src/shared/common/error/business.error.ts b/apps/server/src/shared/common/error/business.error.ts index b65a43d0fa9..cb90422071c 100644 --- a/apps/server/src/shared/common/error/business.error.ts +++ b/apps/server/src/shared/common/error/business.error.ts @@ -1,7 +1,7 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ErrorResponse } from '@src/core/error/dto/error.response'; -import { IErrorType } from '@src/core/error/interface'; +import { ErrorType } from '@src/core/error/interface'; /** * Abstract base class for business errors, errors that are handled @@ -25,7 +25,7 @@ export abstract class BusinessError extends HttpException { readonly details?: Record; protected constructor( - { type, title, defaultMessage }: IErrorType, + { type, title, defaultMessage }: ErrorType, code: HttpStatus = HttpStatus.CONFLICT, details?: Record, cause?: unknown diff --git a/apps/server/src/shared/common/interceptor/interfaces/interceptor-config.ts b/apps/server/src/shared/common/interceptor/interfaces/interceptor-config.ts index 7bcbe5fad3f..510ada13458 100644 --- a/apps/server/src/shared/common/interceptor/interfaces/interceptor-config.ts +++ b/apps/server/src/shared/common/interceptor/interfaces/interceptor-config.ts @@ -1,4 +1,4 @@ -export interface IInterceptorConfig { +export interface InterceptorConfig { INCOMING_REQUEST_TIMEOUT: number; INCOMING_REQUEST_TIMEOUT_COPY_API: number; } diff --git a/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts b/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts index 838ae020039..4e3c256733f 100644 --- a/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts +++ b/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts @@ -1,9 +1,9 @@ +import { ICurrentUser } from '@modules/authentication/interface/user'; import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { LegacyLogger, RequestLoggingBody } from '@src/core/logger'; import { Request } from 'express'; import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; -import { ICurrentUser } from '@modules/authentication/interface/user'; @Injectable() export class RequestLoggingInterceptor implements NestInterceptor { diff --git a/apps/server/src/shared/domain/domainobject/user.do.ts b/apps/server/src/shared/domain/domainobject/user.do.ts index fa8e4f77b25..5c0deab549b 100644 --- a/apps/server/src/shared/domain/domainobject/user.do.ts +++ b/apps/server/src/shared/domain/domainobject/user.do.ts @@ -42,6 +42,8 @@ export class UserDO extends BaseDO { previousExternalId?: string; + birthday?: Date; + constructor(domainObject: UserDO) { super(domainObject.id); @@ -64,5 +66,6 @@ export class UserDO extends BaseDO { this.lastLoginSystemChange = domainObject.lastLoginSystemChange; this.outdatedSince = domainObject.outdatedSince; this.previousExternalId = domainObject.previousExternalId; + this.birthday = domainObject.birthday; } } diff --git a/apps/server/src/shared/domain/entity/course.entity.ts b/apps/server/src/shared/domain/entity/course.entity.ts index 81fdcb741d9..e0fb4d4bcc3 100644 --- a/apps/server/src/shared/domain/entity/course.entity.ts +++ b/apps/server/src/shared/domain/entity/course.entity.ts @@ -1,17 +1,17 @@ import { Collection, Entity, Enum, Index, ManyToMany, ManyToOne, OneToMany, Property, Unique } from '@mikro-orm/core'; -import { InternalServerErrorException } from '@nestjs/common/exceptions/internal-server-error.exception'; -import { IEntityWithSchool, ILearnroom } from '@shared/domain/interface'; import { ClassEntity } from '@modules/class/entity/class.entity'; import { GroupEntity } from '@modules/group/entity/group.entity'; +import { InternalServerErrorException } from '@nestjs/common/exceptions/internal-server-error.exception'; +import { EntityWithSchool, Learnroom } from '@shared/domain/interface'; import { EntityId, LearnroomMetadata, LearnroomTypes } from '../types'; import { BaseEntityWithTimestamps } from './base.entity'; import { CourseGroup } from './coursegroup.entity'; -import type { ILessonParent } from './lesson.entity'; +import type { LessonParent } from './lesson.entity'; import { SchoolEntity } from './school.entity'; -import type { ITaskParent } from './task.entity'; +import type { TaskParent } from './task.entity'; import type { User } from './user.entity'; -export interface ICourseProperties { +export interface CourseProperties { name?: string; description?: string; school: SchoolEntity; @@ -49,10 +49,7 @@ export class UsersList { } @Entity({ tableName: 'courses' }) -export class Course - extends BaseEntityWithTimestamps - implements ILearnroom, IEntityWithSchool, ITaskParent, ILessonParent -{ +export class Course extends BaseEntityWithTimestamps implements Learnroom, EntityWithSchool, TaskParent, LessonParent { @Property() name: string = DEFAULT.name; @@ -105,7 +102,7 @@ export class Course @ManyToMany(() => GroupEntity, undefined, { fieldName: 'groupIds' }) groups = new Collection(this); - constructor(props: ICourseProperties) { + constructor(props: CourseProperties) { super(); if (props.name) this.name = props.name; if (props.description) this.description = props.description; diff --git a/apps/server/src/shared/domain/entity/coursegroup.entity.ts b/apps/server/src/shared/domain/entity/coursegroup.entity.ts index c883e6adcce..8a2cade7aca 100644 --- a/apps/server/src/shared/domain/entity/coursegroup.entity.ts +++ b/apps/server/src/shared/domain/entity/coursegroup.entity.ts @@ -1,14 +1,14 @@ import { Collection, Entity, Index, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; -import { IEntityWithSchool } from '../interface'; +import { EntityWithSchool } from '../interface'; import { EntityId } from '../types'; import { BaseEntityWithTimestamps } from './base.entity'; import type { Course } from './course.entity'; -import type { ILessonParent } from './lesson.entity'; +import type { LessonParent } from './lesson.entity'; import { SchoolEntity } from './school.entity'; -import type { ITaskParent } from './task.entity'; +import type { TaskParent } from './task.entity'; import type { User } from './user.entity'; -export interface ICourseGroupProperties { +export interface CourseGroupProperties { name: string; course: Course; students?: User[]; @@ -16,7 +16,7 @@ export interface ICourseGroupProperties { @Entity({ tableName: 'coursegroups' }) @Index({ properties: ['school', 'course'] }) -export class CourseGroup extends BaseEntityWithTimestamps implements IEntityWithSchool, ITaskParent, ILessonParent { +export class CourseGroup extends BaseEntityWithTimestamps implements EntityWithSchool, TaskParent, LessonParent { @Property() name: string; @@ -32,7 +32,7 @@ export class CourseGroup extends BaseEntityWithTimestamps implements IEntityWith @Index() school: SchoolEntity; - constructor(props: ICourseGroupProperties) { + constructor(props: CourseGroupProperties) { super(); this.name = props.name; this.course = props.course; diff --git a/apps/server/src/shared/domain/entity/dashboard.entity.spec.ts b/apps/server/src/shared/domain/entity/dashboard.entity.spec.ts index b16eb7c0a16..5086255e6cf 100644 --- a/apps/server/src/shared/domain/entity/dashboard.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/dashboard.entity.spec.ts @@ -1,8 +1,8 @@ -import { NotFoundException, BadRequestException } from '@nestjs/common'; -import { ILearnroom, LearnroomTypes } from '@shared/domain'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Learnroom, LearnroomTypes } from '@shared/domain'; import { DashboardEntity, GridElement } from './dashboard.entity'; -const getLearnroomMock = (id: string): ILearnroom => { +const getLearnroomMock = (id: string): Learnroom => { return { getMetadata: () => { return { diff --git a/apps/server/src/shared/domain/entity/dashboard.entity.ts b/apps/server/src/shared/domain/entity/dashboard.entity.ts index b3165dedf8a..cb49032e9fc 100644 --- a/apps/server/src/shared/domain/entity/dashboard.entity.ts +++ b/apps/server/src/shared/domain/entity/dashboard.entity.ts @@ -1,6 +1,6 @@ -import { NotFoundException, BadRequestException } from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Learnroom } from '@shared/domain/interface'; import { EntityId, LearnroomMetadata } from '@shared/domain/types'; -import { ILearnroom } from '@shared/domain/interface'; const defaultColumns = 4; @@ -15,11 +15,11 @@ export interface IGridElement { removeReferenceByIndex(index: number): void; - removeReference(reference: ILearnroom): void; + removeReference(reference: Learnroom): void; - getReferences(): ILearnroom[]; + getReferences(): Learnroom[]; - addReferences(anotherReference: ILearnroom[]): void; + addReferences(anotherReference: Learnroom[]): void; setGroupName(newGroupName: string): void; } @@ -39,7 +39,7 @@ export class GridElement implements IGridElement { title?: string; - private sortReferences = (a: ILearnroom, b: ILearnroom) => { + private sortReferences = (a: Learnroom, b: Learnroom) => { const titleA = a.getMetadata().title; const titleB = b.getMetadata().title; if (titleA < titleB) { @@ -51,29 +51,29 @@ export class GridElement implements IGridElement { return 0; }; - private constructor(props: { id?: EntityId; title?: string; references: ILearnroom[] }) { + private constructor(props: { id?: EntityId; title?: string; references: Learnroom[] }) { if (props.id) this.id = props.id; if (props.title) this.title = props.title; this.references = props.references.sort(this.sortReferences); } - static FromPersistedReference(id: EntityId, reference: ILearnroom): GridElement { + static FromPersistedReference(id: EntityId, reference: Learnroom): GridElement { return new GridElement({ id, references: [reference] }); } - static FromPersistedGroup(id: EntityId, title: string | undefined, group: ILearnroom[]): GridElement { + static FromPersistedGroup(id: EntityId, title: string | undefined, group: Learnroom[]): GridElement { return new GridElement({ id, title, references: group }); } - static FromSingleReference(reference: ILearnroom): GridElement { + static FromSingleReference(reference: Learnroom): GridElement { return new GridElement({ references: [reference] }); } - static FromGroup(title: string, references: ILearnroom[]): GridElement { + static FromGroup(title: string, references: Learnroom[]): GridElement { return new GridElement({ title, references }); } - references: ILearnroom[]; + references: Learnroom[]; hasId(): boolean { return !!this.id; @@ -83,7 +83,7 @@ export class GridElement implements IGridElement { return this.id; } - getReferences(): ILearnroom[] { + getReferences(): Learnroom[] { return this.references; } @@ -97,7 +97,7 @@ export class GridElement implements IGridElement { this.references.splice(index, 1); } - removeReference(reference: ILearnroom): void { + removeReference(reference: Learnroom): void { const index = this.references.indexOf(reference); if (index === -1) { throw new BadRequestException('reference not found.'); @@ -105,7 +105,7 @@ export class GridElement implements IGridElement { this.references.splice(index, 1); } - addReferences(anotherReference: ILearnroom[]): void { + addReferences(anotherReference: Learnroom[]): void { if (!this.isGroup()) { this.references = this.references.concat(anotherReference).sort(this.sortReferences); this.setGroupName(''); @@ -228,7 +228,7 @@ export class DashboardEntity { }; } - setLearnRooms(rooms: ILearnroom[]): void { + setLearnRooms(rooms: Learnroom[]): void { this.removeRoomsNotInList(rooms); const newRooms = this.determineNewRoomsIn(rooms); @@ -237,7 +237,7 @@ export class DashboardEntity { }); } - private removeRoomsNotInList(roomList: ILearnroom[]): void { + private removeRoomsNotInList(roomList: Learnroom[]): void { [...this.grid.keys()].forEach((key) => { const element = this.grid.get(key) as IGridElement; const currentRooms = element.getReferences(); @@ -252,8 +252,8 @@ export class DashboardEntity { }); } - private determineNewRoomsIn(rooms: ILearnroom[]): ILearnroom[] { - const result: ILearnroom[] = []; + private determineNewRoomsIn(rooms: Learnroom[]): Learnroom[] { + const result: Learnroom[] = []; const existingRooms = this.allRooms(); rooms.forEach((room) => { if (!existingRooms.includes(room)) { @@ -263,13 +263,13 @@ export class DashboardEntity { return result; } - private allRooms(): ILearnroom[] { + private allRooms(): Learnroom[] { const elements = [...this.grid.values()]; const references = elements.map((el) => el.getReferences()).flat(); return references; } - private addRoom(room: ILearnroom): void { + private addRoom(room: Learnroom): void { const index = this.getFirstOpenIndex(); const newElement = GridElement.FromSingleReference(room); this.grid.set(index, newElement); diff --git a/apps/server/src/shared/domain/entity/dashboard.model.entity.ts b/apps/server/src/shared/domain/entity/dashboard.model.entity.ts index b7d6acbe8d6..b0463ff5355 100644 --- a/apps/server/src/shared/domain/entity/dashboard.model.entity.ts +++ b/apps/server/src/shared/domain/entity/dashboard.model.entity.ts @@ -56,7 +56,7 @@ export class DashboardGridElementModel extends BaseEntityWithTimestamps { dashboard: IdentifiedReference; } -export interface IDashboardModelProperties { +export interface DashboardModelProperties { id: string; user: User; gridElements?: DashboardGridElementModel[]; @@ -64,7 +64,7 @@ export interface IDashboardModelProperties { @Entity({ tableName: 'dashboard' }) export class DashboardModelEntity extends BaseEntityWithTimestamps { - constructor(props: IDashboardModelProperties) { + constructor(props: DashboardModelProperties) { super(); this._id = ObjectId.createFromHexString(props.id); this.id = props.id; diff --git a/apps/server/src/shared/domain/entity/federal-state.entity.ts b/apps/server/src/shared/domain/entity/federal-state.entity.ts index d65b36c9826..d7c84567fec 100644 --- a/apps/server/src/shared/domain/entity/federal-state.entity.ts +++ b/apps/server/src/shared/domain/entity/federal-state.entity.ts @@ -1,7 +1,7 @@ import { Embeddable, Embedded, Entity, Property } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from './base.entity'; -export interface IFederalStateProperties { +export interface FederalStateProperties { name: string; abbreviation: string; logoUrl: string; @@ -39,7 +39,7 @@ export class FederalStateEntity extends BaseEntityWithTimestamps { @Embedded(() => County, { array: true, nullable: true }) counties?: County[]; - constructor(props: IFederalStateProperties) { + constructor(props: FederalStateProperties) { super(); this.name = props.name; this.abbreviation = props.abbreviation; diff --git a/apps/server/src/shared/domain/entity/import-user.entity.ts b/apps/server/src/shared/domain/entity/import-user.entity.ts index 09a10179e3d..34d1f8b64e8 100644 --- a/apps/server/src/shared/domain/entity/import-user.entity.ts +++ b/apps/server/src/shared/domain/entity/import-user.entity.ts @@ -1,5 +1,5 @@ import { Entity, Enum, IdentifiedReference, ManyToOne, Property, Unique, wrap } from '@mikro-orm/core'; -import { IEntityWithSchool, RoleName } from '../interface'; +import { EntityWithSchool, RoleName } from '../interface'; import { BaseEntityReference, BaseEntityWithTimestamps } from './base.entity'; import { SchoolEntity } from './school.entity'; import { SystemEntity } from './system.entity'; @@ -7,7 +7,7 @@ import type { User } from './user.entity'; export type IImportUserRoleName = RoleName.ADMINISTRATOR | RoleName.TEACHER | RoleName.STUDENT; -export interface IImportUserProperties { +export interface ImportUserProperties { // references school: SchoolEntity; system: SystemEntity; @@ -34,8 +34,8 @@ export enum MatchCreator { @Unique({ properties: ['school', 'externalId'] }) @Unique({ properties: ['school', 'ldapDn'] }) @Unique({ properties: ['school', 'email'] }) -export class ImportUser extends BaseEntityWithTimestamps implements IEntityWithSchool { - constructor(props: IImportUserProperties) { +export class ImportUser extends BaseEntityWithTimestamps implements EntityWithSchool { + constructor(props: ImportUserProperties) { super(); this.school = wrap(props.school).toReference(); this.system = wrap(props.system).toReference(); diff --git a/apps/server/src/shared/domain/entity/legacy-board/board.entity.ts b/apps/server/src/shared/domain/entity/legacy-board/board.entity.ts index 073c30e8d94..18f6e7d864f 100644 --- a/apps/server/src/shared/domain/entity/legacy-board/board.entity.ts +++ b/apps/server/src/shared/domain/entity/legacy-board/board.entity.ts @@ -1,6 +1,6 @@ import { Collection, Entity, IdentifiedReference, ManyToMany, OneToOne, wrap } from '@mikro-orm/core'; import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { ILearnroomElement } from '../../interface'; +import { LearnroomElement } from '../../interface'; import { EntityId } from '../../types'; import { BaseEntityWithTimestamps } from '../base.entity'; import type { Course } from '../course.entity'; @@ -31,7 +31,7 @@ export class Board extends BaseEntityWithTimestamps { @ManyToMany('BoardElement', undefined, { fieldName: 'referenceIds' }) references = new Collection(this); - getByTargetId(id: EntityId): ILearnroomElement { + getByTargetId(id: EntityId): LearnroomElement { const element = this.getElementByTargetId(id); return element.target; } diff --git a/apps/server/src/shared/domain/entity/legacy-board/column-board-target.entity.ts b/apps/server/src/shared/domain/entity/legacy-board/column-board-target.entity.ts index 33545699362..6008c97c550 100644 --- a/apps/server/src/shared/domain/entity/legacy-board/column-board-target.entity.ts +++ b/apps/server/src/shared/domain/entity/legacy-board/column-board-target.entity.ts @@ -1,5 +1,5 @@ import { Entity, Property } from '@mikro-orm/core'; -import { ILearnroomElement } from '@shared/domain/interface'; +import { LearnroomElement } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { ObjectId } from 'bson'; import { BaseEntityWithTimestamps } from '../base.entity'; @@ -10,7 +10,7 @@ type ColumnBoardTargetProps = { }; @Entity() -export class ColumnBoardTarget extends BaseEntityWithTimestamps implements ILearnroomElement { +export class ColumnBoardTarget extends BaseEntityWithTimestamps implements LearnroomElement { constructor(props: ColumnBoardTargetProps) { super(); this._columnBoardId = new ObjectId(props.columnBoardId); diff --git a/apps/server/src/shared/domain/entity/lesson.entity.spec.ts b/apps/server/src/shared/domain/entity/lesson.entity.spec.ts index 631d04f0380..daca93d86e5 100644 --- a/apps/server/src/shared/domain/entity/lesson.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/lesson.entity.spec.ts @@ -8,7 +8,7 @@ import { setupEntities, taskFactory, } from '../../testing'; -import { ComponentType, IComponentProperties } from './lesson.entity'; +import { ComponentProperties, ComponentType } from './lesson.entity'; import { Material } from './materials.entity'; import { Task } from './task.entity'; @@ -152,7 +152,7 @@ describe('Lesson Entity', () => { describe('getLessonComponents', () => { it('should return the content components', () => { - const expectedTextContent: IComponentProperties = { + const expectedTextContent: ComponentProperties = { title: 'test component', hidden: false, component: ComponentType.TEXT, diff --git a/apps/server/src/shared/domain/entity/lesson.entity.ts b/apps/server/src/shared/domain/entity/lesson.entity.ts index a47aab2a8b3..eae1cb6169b 100644 --- a/apps/server/src/shared/domain/entity/lesson.entity.ts +++ b/apps/server/src/shared/domain/entity/lesson.entity.ts @@ -1,21 +1,21 @@ import { Collection, Entity, Index, ManyToMany, ManyToOne, OneToMany, Property } from '@mikro-orm/core'; import { InternalServerErrorException } from '@nestjs/common'; -import { ILearnroomElement } from '@shared/domain/interface'; +import { LearnroomElement } from '@shared/domain/interface'; import { EntityId } from '../types'; import { BaseEntityWithTimestamps } from './base.entity'; import type { Course } from './course.entity'; import { CourseGroup } from './coursegroup.entity'; import { Material } from './materials.entity'; +import type { TaskParent } from './task.entity'; import { Task } from './task.entity'; -import type { ITaskParent } from './task.entity'; -export interface ILessonProperties { +export interface LessonProperties { name: string; hidden: boolean; course: Course; courseGroup?: CourseGroup; position?: number; - contents: IComponentProperties[] | []; + contents: ComponentProperties[] | []; materials?: Material[]; } @@ -28,15 +28,15 @@ export enum ComponentType { NEXBOARD = 'neXboard', } -export interface IComponentTextProperties { +export interface ComponentTextProperties { text: string; } -export interface IComponentGeogebraProperties { +export interface ComponentGeogebraProperties { materialId: string; } -export interface IComponentLernstoreProperties { +export interface ComponentLernstoreProperties { resources: { client: string; description: string; @@ -46,43 +46,43 @@ export interface IComponentLernstoreProperties { }[]; } -export interface IComponentEtherpadProperties { +export interface ComponentEtherpadProperties { description: string; title: string; url: string; } -export interface IComponentNexboardProperties { +export interface ComponentNexboardProperties { board: string; description: string; title: string; url: string; } -export interface IComponentInternalProperties { +export interface ComponentInternalProperties { url: string; } -export type IComponentProperties = { +export type ComponentProperties = { _id?: string; title: string; hidden: boolean; user?: EntityId; } & ( - | { component: ComponentType.TEXT; content: IComponentTextProperties } - | { component: ComponentType.ETHERPAD; content: IComponentEtherpadProperties } - | { component: ComponentType.GEOGEBRA; content: IComponentGeogebraProperties } - | { component: ComponentType.INTERNAL; content: IComponentInternalProperties } - | { component: ComponentType.LERNSTORE; content?: IComponentLernstoreProperties } - | { component: ComponentType.NEXBOARD; content: IComponentNexboardProperties } + | { component: ComponentType.TEXT; content: ComponentTextProperties } + | { component: ComponentType.ETHERPAD; content: ComponentEtherpadProperties } + | { component: ComponentType.GEOGEBRA; content: ComponentGeogebraProperties } + | { component: ComponentType.INTERNAL; content: ComponentInternalProperties } + | { component: ComponentType.LERNSTORE; content?: ComponentLernstoreProperties } + | { component: ComponentType.NEXBOARD; content: ComponentNexboardProperties } ); -export interface ILessonParent { +export interface LessonParent { getStudentIds(): EntityId[]; } @Entity({ tableName: 'lessons' }) -export class LessonEntity extends BaseEntityWithTimestamps implements ILearnroomElement, ITaskParent { +export class LessonEntity extends BaseEntityWithTimestamps implements LearnroomElement, TaskParent { @Property() name: string; @@ -101,7 +101,7 @@ export class LessonEntity extends BaseEntityWithTimestamps implements ILearnroom position: number; @Property() - contents: IComponentProperties[] | []; + contents: ComponentProperties[] | []; @ManyToMany('Material', undefined, { fieldName: 'materialIds' }) materials = new Collection(this); @@ -109,7 +109,7 @@ export class LessonEntity extends BaseEntityWithTimestamps implements ILearnroom @OneToMany('Task', 'lesson', { orphanRemoval: true }) tasks = new Collection(this); - constructor(props: ILessonProperties) { + constructor(props: LessonProperties) { super(); this.name = props.name; if (props.hidden !== undefined) this.hidden = props.hidden; @@ -120,7 +120,7 @@ export class LessonEntity extends BaseEntityWithTimestamps implements ILearnroom if (props.materials) this.materials.set(props.materials); } - private getParent(): ILessonParent { + private getParent(): LessonParent { const parent = this.courseGroup || this.course; return parent; @@ -152,7 +152,7 @@ export class LessonEntity extends BaseEntityWithTimestamps implements ILearnroom return filtered.length; } - getLessonComponents(): IComponentProperties[] | [] { + getLessonComponents(): ComponentProperties[] | [] { return this.contents; } diff --git a/apps/server/src/shared/domain/entity/materials.entity.ts b/apps/server/src/shared/domain/entity/materials.entity.ts index 9bd73edcfd8..273174f9782 100644 --- a/apps/server/src/shared/domain/entity/materials.entity.ts +++ b/apps/server/src/shared/domain/entity/materials.entity.ts @@ -1,26 +1,26 @@ import { Entity, Property } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from './base.entity'; -export interface ITargetGroupProperties { +export interface TargetGroupProperties { state?: string; schoolType?: string; grade?: string; } -export interface IRelatedResourceProperties { +export interface RelatedResourceProperties { originId?: string; relationType?: string; } -export interface IMaterialProperties { +export interface MaterialProperties { client: string; description?: string; license: string[]; merlinReference?: string; - relatedResources: IRelatedResourceProperties[]; + relatedResources: RelatedResourceProperties[]; subjects: string[]; tags: string[]; - targetGroups: ITargetGroupProperties[]; + targetGroups: TargetGroupProperties[]; title: string; url: string; } @@ -40,7 +40,7 @@ export class Material extends BaseEntityWithTimestamps { merlinReference?: string; @Property() - relatedResources: IRelatedResourceProperties[] | []; + relatedResources: RelatedResourceProperties[] | []; @Property() subjects: string[] | []; @@ -49,7 +49,7 @@ export class Material extends BaseEntityWithTimestamps { tags: string[] | []; @Property() - targetGroups: ITargetGroupProperties[] | []; + targetGroups: TargetGroupProperties[] | []; @Property() title: string; @@ -57,7 +57,7 @@ export class Material extends BaseEntityWithTimestamps { @Property() url: string; - constructor(props: IMaterialProperties) { + constructor(props: MaterialProperties) { super(); this.client = props.client; this.description = props.description || ''; diff --git a/apps/server/src/shared/domain/entity/news.entity.ts b/apps/server/src/shared/domain/entity/news.entity.ts index 7b22c38f364..682192770a1 100644 --- a/apps/server/src/shared/domain/entity/news.entity.ts +++ b/apps/server/src/shared/domain/entity/news.entity.ts @@ -1,13 +1,13 @@ import { Entity, Enum, Index, ManyToOne, Property } from '@mikro-orm/core'; +import { EntityId } from '../types'; +import { NewsTarget, NewsTargetModel } from '../types/news.types'; import { BaseEntityWithTimestamps } from './base.entity'; import type { Course } from './course.entity'; import { SchoolEntity } from './school.entity'; import type { TeamEntity } from './team.entity'; import type { User } from './user.entity'; -import { NewsTarget, NewsTargetModel } from '../types/news.types'; -import { EntityId } from '../types'; -export interface INewsProperties { +export interface NewsProperties { title: string; content: string; displayAt: Date; @@ -69,7 +69,7 @@ export abstract class News extends BaseEntityWithTimestamps { permissions: string[] = []; - constructor(props: INewsProperties) { + constructor(props: NewsProperties) { super(); this.title = props.title; this.content = props.content; @@ -80,7 +80,7 @@ export abstract class News extends BaseEntityWithTimestamps { this.sourceDescription = props.sourceDescription; } - static createInstance(targetModel: NewsTargetModel, props: INewsProperties): News { + static createInstance(targetModel: NewsTargetModel, props: NewsProperties): News { let news: News; if (targetModel === NewsTargetModel.Course) { // eslint-disable-next-line @typescript-eslint/no-use-before-define @@ -101,7 +101,7 @@ export class SchoolNews extends News { @ManyToOne(() => SchoolEntity) target!: SchoolEntity; - constructor(props: INewsProperties) { + constructor(props: NewsProperties) { super(props); this.targetModel = NewsTargetModel.School; } @@ -115,7 +115,7 @@ export class CourseNews extends News { @ManyToOne('Course', { nullable: true }) target!: Course; - constructor(props: INewsProperties) { + constructor(props: NewsProperties) { super(props); this.targetModel = NewsTargetModel.Course; } @@ -126,7 +126,7 @@ export class TeamNews extends News { @ManyToOne('TeamEntity') target!: TeamEntity; - constructor(props: INewsProperties) { + constructor(props: NewsProperties) { super(props); this.targetModel = NewsTargetModel.Team; } diff --git a/apps/server/src/shared/domain/entity/role.entity.ts b/apps/server/src/shared/domain/entity/role.entity.ts index 66dc6e24191..923b75c8873 100644 --- a/apps/server/src/shared/domain/entity/role.entity.ts +++ b/apps/server/src/shared/domain/entity/role.entity.ts @@ -2,7 +2,7 @@ import { Collection, Entity, ManyToMany, Property, Unique } from '@mikro-orm/cor import { Permission, RoleName } from '../interface'; import { BaseEntityWithTimestamps } from './base.entity'; -export interface IRoleProperties { +export interface RoleProperties { permissions?: Permission[]; roles?: Role[]; name: RoleName; @@ -20,7 +20,7 @@ export class Role extends BaseEntityWithTimestamps { @ManyToMany({ entity: 'Role' }) roles = new Collection(this); - constructor(props: IRoleProperties) { + constructor(props: RoleProperties) { super(); this.name = props.name; if (props.permissions) this.permissions = props.permissions; diff --git a/apps/server/src/shared/domain/entity/school.entity.ts b/apps/server/src/shared/domain/entity/school.entity.ts index 98502a127eb..81802f595ed 100644 --- a/apps/server/src/shared/domain/entity/school.entity.ts +++ b/apps/server/src/shared/domain/entity/school.entity.ts @@ -11,9 +11,9 @@ import { } from '@mikro-orm/core'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { BaseEntity } from './base.entity'; +import { FederalStateEntity } from './federal-state.entity'; import { SchoolYearEntity } from './schoolyear.entity'; import { SystemEntity } from './system.entity'; -import { FederalStateEntity } from './federal-state.entity'; export enum SchoolFeatures { ROCKET_CHAT = 'rocketChat', @@ -26,7 +26,7 @@ export enum SchoolFeatures { ENABLE_LDAP_SYNC_DURING_MIGRATION = 'enableLdapSyncDuringMigration', } -export interface ISchoolProperties { +export interface SchoolProperties { _id?: string; externalId?: string; inMaintenanceSince?: Date; @@ -106,7 +106,7 @@ export class SchoolEntity extends BaseEntity { @ManyToOne(() => FederalStateEntity, { fieldName: 'federalState', nullable: false }) federalState: FederalStateEntity; - constructor(props: ISchoolProperties) { + constructor(props: SchoolProperties) { super(); if (props.externalId) { this.externalId = props.externalId; diff --git a/apps/server/src/shared/domain/entity/schoolyear.entity.ts b/apps/server/src/shared/domain/entity/schoolyear.entity.ts index e1d2c1c9895..f538119652c 100644 --- a/apps/server/src/shared/domain/entity/schoolyear.entity.ts +++ b/apps/server/src/shared/domain/entity/schoolyear.entity.ts @@ -1,14 +1,14 @@ import { Entity, Property } from '@mikro-orm/core'; import { BaseEntity } from './base.entity'; -export interface ISchoolYearProperties { +export interface SchoolYearProperties { name: string; startDate: Date; endDate: Date; } @Entity({ tableName: 'years' }) -export class SchoolYearEntity extends BaseEntity implements ISchoolYearProperties { +export class SchoolYearEntity extends BaseEntity implements SchoolYearProperties { @Property() name: string; @@ -18,7 +18,7 @@ export class SchoolYearEntity extends BaseEntity implements ISchoolYearPropertie @Property() endDate: Date; - constructor(props: ISchoolYearProperties) { + constructor(props: SchoolYearProperties) { super(); this.name = props.name; this.startDate = props.startDate; diff --git a/apps/server/src/shared/domain/entity/storageprovider.entity.ts b/apps/server/src/shared/domain/entity/storageprovider.entity.ts index 7bac9b3380a..e2c0fe885ef 100644 --- a/apps/server/src/shared/domain/entity/storageprovider.entity.ts +++ b/apps/server/src/shared/domain/entity/storageprovider.entity.ts @@ -2,7 +2,7 @@ import { Entity, Property } from '@mikro-orm/core'; import { StorageProviderEncryptedStringType } from '@shared/repo/types/StorageProviderEncryptedString.type'; import { BaseEntityWithTimestamps } from './base.entity'; -export interface IStorageProviderProperties { +export interface StorageProviderProperties { endpointUrl: string; accessKeyId: string; secretAccessKey: string; @@ -23,7 +23,7 @@ export class StorageProviderEntity extends BaseEntityWithTimestamps { @Property({ nullable: true }) region?: string; - constructor(props: IStorageProviderProperties) { + constructor(props: StorageProviderProperties) { super(); this.endpointUrl = props.endpointUrl; this.accessKeyId = props.accessKeyId; diff --git a/apps/server/src/shared/domain/entity/submission.entity.ts b/apps/server/src/shared/domain/entity/submission.entity.ts index de86ba9f814..12893ed3bd5 100644 --- a/apps/server/src/shared/domain/entity/submission.entity.ts +++ b/apps/server/src/shared/domain/entity/submission.entity.ts @@ -8,7 +8,7 @@ import { SchoolEntity } from './school.entity'; import type { Task } from './task.entity'; import type { User } from './user.entity'; -export interface ISubmissionProperties { +export interface SubmissionProperties { school: SchoolEntity; task: Task; student: User; @@ -57,7 +57,7 @@ export class Submission extends BaseEntityWithTimestamps { @Property({ nullable: true }) gradeComment?: string; - constructor(props: ISubmissionProperties) { + constructor(props: SubmissionProperties) { super(); this.school = props.school; this.student = props.student; diff --git a/apps/server/src/shared/domain/entity/system.entity.ts b/apps/server/src/shared/domain/entity/system.entity.ts index 0633e515589..07cfea5bb59 100644 --- a/apps/server/src/shared/domain/entity/system.entity.ts +++ b/apps/server/src/shared/domain/entity/system.entity.ts @@ -3,7 +3,7 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-prov import { EntityId } from '../types'; import { BaseEntityWithTimestamps } from './base.entity'; -export interface ISystemProperties { +export interface SystemProperties { type: string; url?: string; alias?: string; @@ -189,7 +189,7 @@ export class OidcConfig { @Entity({ tableName: 'systems' }) export class SystemEntity extends BaseEntityWithTimestamps { - constructor(props: ISystemProperties) { + constructor(props: SystemProperties) { super(); this.type = props.type; this.url = props.url; diff --git a/apps/server/src/shared/domain/entity/task.entity.ts b/apps/server/src/shared/domain/entity/task.entity.ts index c63156e6aa4..88af11a0a7d 100644 --- a/apps/server/src/shared/domain/entity/task.entity.ts +++ b/apps/server/src/shared/domain/entity/task.entity.ts @@ -2,10 +2,10 @@ import { Collection, Entity, Index, ManyToMany, ManyToOne, OneToMany, Property } import { InternalServerErrorException } from '@nestjs/common'; import { SchoolEntity } from '@shared/domain/entity/school.entity'; import { InputFormat } from '@shared/domain/types/input-format.types'; -import type { IEntityWithSchool } from '../interface'; -import type { ILearnroomElement } from '../interface/learnroom'; +import type { EntityWithSchool } from '../interface'; +import type { LearnroomElement } from '../interface/learnroom'; import type { EntityId } from '../types/entity-id'; -import type { ITaskProperties, ITaskStatus } from '../types/task.types'; +import type { TaskProperties, TaskStatus } from '../types/task.types'; import { BaseEntityWithTimestamps } from './base.entity'; import type { Course } from './course.entity'; import type { LessonEntity } from './lesson.entity'; @@ -15,9 +15,9 @@ import { User } from './user.entity'; export class TaskWithStatusVo { task!: Task; - status!: ITaskStatus; + status!: TaskStatus; - constructor(task: Task, status: ITaskStatus) { + constructor(task: Task, status: TaskStatus) { this.task = task; this.status = status; } @@ -31,7 +31,7 @@ export type TaskParentDescriptions = { color: string; }; -export interface ITaskParent { +export interface TaskParent { getStudentIds(): EntityId[]; } @@ -40,7 +40,7 @@ export interface ITaskParent { @Index({ properties: ['id', 'private'] }) @Index({ properties: ['finished', 'course'] }) @Index({ properties: ['finished', 'course'] }) -export class Task extends BaseEntityWithTimestamps implements ILearnroomElement, IEntityWithSchool { +export class Task extends BaseEntityWithTimestamps implements LearnroomElement, EntityWithSchool { @Property() name: string; @@ -89,7 +89,7 @@ export class Task extends BaseEntityWithTimestamps implements ILearnroomElement, @ManyToMany('User', undefined, { fieldName: 'archived' }) finished = new Collection(this); - constructor(props: ITaskProperties) { + constructor(props: TaskProperties) { super(); this.name = props.name; this.description = props.description || ''; @@ -128,7 +128,7 @@ export class Task extends BaseEntityWithTimestamps implements ILearnroomElement, return finishedIds; } - private getParent(): ITaskParent | User { + private getParent(): TaskParent | User { const parent = this.lesson || this.course || this.creator; return parent; @@ -234,7 +234,7 @@ export class Task extends BaseEntityWithTimestamps implements ILearnroomElement, return isSubstitutionTeacher; } - public createTeacherStatusForUser(user: User): ITaskStatus { + public createTeacherStatusForUser(user: User): TaskStatus { const submittedSubmissions = this.getSubmittedSubmissions(); const gradedSubmissions = this.getGradedSubmissions(); @@ -257,7 +257,7 @@ export class Task extends BaseEntityWithTimestamps implements ILearnroomElement, return status; } - public createStudentStatusForUser(user: User): ITaskStatus { + public createStudentStatusForUser(user: User): TaskStatus { const isSubmitted = this.isSubmittedForUser(user); const isGraded = this.isGradedForUser(user); const maxSubmissions = 1; diff --git a/apps/server/src/shared/domain/entity/team.entity.ts b/apps/server/src/shared/domain/entity/team.entity.ts index 70e7d142885..dd39e8c9ff6 100644 --- a/apps/server/src/shared/domain/entity/team.entity.ts +++ b/apps/server/src/shared/domain/entity/team.entity.ts @@ -4,12 +4,12 @@ import { Role } from './role.entity'; import { SchoolEntity } from './school.entity'; import { User } from './user.entity'; -export interface ITeamProperties { +export interface TeamProperties { name: string; teamUsers?: TeamUserEntity[]; } -export interface ITeamUserProperties { +export interface TeamUserProperties { user: User; role: Role; school: SchoolEntity; @@ -17,7 +17,7 @@ export interface ITeamUserProperties { @Embeddable() export class TeamUserEntity { - constructor(props: ITeamUserProperties) { + constructor(props: TeamUserProperties) { this.userId = props.user; this.role = props.role; this.schoolId = props.school; @@ -66,7 +66,7 @@ export class TeamEntity extends BaseEntityWithTimestamps { this.userIds = value; } - constructor(props: ITeamProperties) { + constructor(props: TeamProperties) { super(); this.name = props.name; this.userIds = props.teamUsers ? props.teamUsers.map((teamUser) => new TeamUserEntity(teamUser)) : []; diff --git a/apps/server/src/shared/domain/entity/user.entity.ts b/apps/server/src/shared/domain/entity/user.entity.ts index 2d1918e1d61..c9a982c3854 100644 --- a/apps/server/src/shared/domain/entity/user.entity.ts +++ b/apps/server/src/shared/domain/entity/user.entity.ts @@ -1,5 +1,5 @@ import { Collection, Entity, Index, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; -import { IEntityWithSchool } from '../interface'; +import { EntityWithSchool } from '../interface'; import { BaseEntityWithTimestamps } from './base.entity'; import { Role } from './role.entity'; import { SchoolEntity } from './school.entity'; @@ -11,7 +11,7 @@ export enum LanguageType { UK = 'uk', } -export interface IUserProperties { +export interface UserProperties { email: string; firstName: string; lastName: string; @@ -26,6 +26,7 @@ export interface IUserProperties { lastLoginSystemChange?: Date; outdatedSince?: Date; previousExternalId?: string; + birthday?: Date; } @Entity({ tableName: 'users' }) @@ -34,7 +35,7 @@ export interface IUserProperties { @Index({ properties: ['externalId', 'school'] }) @Index({ properties: ['school', 'ldapDn'] }) @Index({ properties: ['school', 'roles'] }) -export class User extends BaseEntityWithTimestamps implements IEntityWithSchool { +export class User extends BaseEntityWithTimestamps implements EntityWithSchool { @Property() @Index() // @Unique() @@ -96,7 +97,10 @@ export class User extends BaseEntityWithTimestamps implements IEntityWithSchool @Property({ nullable: true }) outdatedSince?: Date; - constructor(props: IUserProperties) { + @Property({ nullable: true }) + birthday?: Date; + + constructor(props: UserProperties) { super(); this.firstName = props.firstName; this.lastName = props.lastName; @@ -112,6 +116,7 @@ export class User extends BaseEntityWithTimestamps implements IEntityWithSchool this.lastLoginSystemChange = props.lastLoginSystemChange; this.outdatedSince = props.outdatedSince; this.previousExternalId = props.previousExternalId; + this.birthday = props.birthday; } public resolvePermissions(): string[] { diff --git a/apps/server/src/shared/domain/interface/entity.ts b/apps/server/src/shared/domain/interface/entity.ts index dc28d8469d3..97618b0734d 100644 --- a/apps/server/src/shared/domain/interface/entity.ts +++ b/apps/server/src/shared/domain/interface/entity.ts @@ -11,6 +11,6 @@ export interface IEntityWithTimestamps extends IEntity { updatedAt: Date; } -export interface IEntityWithSchool extends IEntity { +export interface EntityWithSchool extends IEntity { school: SchoolEntity; } diff --git a/apps/server/src/shared/domain/interface/find-options.ts b/apps/server/src/shared/domain/interface/find-options.ts index 2b02990d436..37fc49a5909 100644 --- a/apps/server/src/shared/domain/interface/find-options.ts +++ b/apps/server/src/shared/domain/interface/find-options.ts @@ -1,4 +1,4 @@ -export interface IPagination { +export interface Pagination { skip?: number; limit?: number; } @@ -11,6 +11,6 @@ export enum SortOrder { export type SortOrderMap = Partial>; export interface IFindOptions { - pagination?: IPagination; + pagination?: Pagination; order?: SortOrderMap; } diff --git a/apps/server/src/shared/domain/interface/learnroom.ts b/apps/server/src/shared/domain/interface/learnroom.ts index 1bbfaac1298..a0c8e794d68 100644 --- a/apps/server/src/shared/domain/interface/learnroom.ts +++ b/apps/server/src/shared/domain/interface/learnroom.ts @@ -1,10 +1,10 @@ import { LearnroomMetadata } from '@shared/domain/types'; -export interface ILearnroom { +export interface Learnroom { getMetadata: () => LearnroomMetadata; } -export interface ILearnroomElement { +export interface LearnroomElement { publish: () => void; unpublish: () => void; } diff --git a/apps/server/src/shared/domain/types/importuser.types.ts b/apps/server/src/shared/domain/types/importuser.types.ts index 52edf935fb9..de189aed43e 100644 --- a/apps/server/src/shared/domain/types/importuser.types.ts +++ b/apps/server/src/shared/domain/types/importuser.types.ts @@ -16,7 +16,7 @@ export interface IImportUserScope { classes?: string; } -export interface INameMatch { +export interface NameMatch { /** * Match filter for lastName or firstName */ diff --git a/apps/server/src/shared/domain/types/news.types.ts b/apps/server/src/shared/domain/types/news.types.ts index 241b4d90384..671545c8ceb 100644 --- a/apps/server/src/shared/domain/types/news.types.ts +++ b/apps/server/src/shared/domain/types/news.types.ts @@ -1,7 +1,7 @@ -import { EntityId } from './entity-id'; import type { Course } from '../entity/course.entity'; import type { SchoolEntity } from '../entity/school.entity'; import type { TeamEntity } from '../entity/team.entity'; +import { EntityId } from './entity-id'; export enum NewsTargetModel { 'School' = 'schools', @@ -10,7 +10,7 @@ export enum NewsTargetModel { } /** news interface for ceating news */ -export interface ICreateNews { +export interface CreateNews { title: string; content: string; displayAt?: Date; @@ -18,7 +18,7 @@ export interface ICreateNews { } /** news interface for updating news */ -export type IUpdateNews = Partial; +export type IUpdateNews = Partial; /** interface for finding news with optional targetId */ export interface INewsScope { diff --git a/apps/server/src/shared/domain/types/task.types.ts b/apps/server/src/shared/domain/types/task.types.ts index 3282fd24738..8f77e209eed 100644 --- a/apps/server/src/shared/domain/types/task.types.ts +++ b/apps/server/src/shared/domain/types/task.types.ts @@ -8,17 +8,17 @@ interface ITask { dueDate?: Date; } -export interface ITaskUpdate extends ITask { +export interface TaskUpdate extends ITask { courseId?: string; lessonId?: string; } -export interface ITaskCreate extends ITask { +export interface TaskCreate extends ITask { courseId?: string; lessonId?: string; } -export interface ITaskProperties extends ITask { +export interface TaskProperties extends ITask { course?: Course; lesson?: LessonEntity; creator: User; @@ -30,7 +30,7 @@ export interface ITaskProperties extends ITask { teamSubmissions?: boolean; } -export interface ITaskStatus { +export interface TaskStatus { submitted: number; maxSubmissions: number; graded: number; diff --git a/apps/server/src/shared/repo/base.do.repo.integration.spec.ts b/apps/server/src/shared/repo/base.do.repo.integration.spec.ts index 1ca91bd184e..b9683fbf1ea 100644 --- a/apps/server/src/shared/repo/base.do.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/base.do.repo.integration.spec.ts @@ -1,12 +1,12 @@ +import { createMock } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { Entity, EntityName, Property } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Entity, EntityName, Property } from '@mikro-orm/core'; import { BaseDO, BaseEntityWithTimestamps } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { Injectable } from '@nestjs/common'; import { BaseDORepo } from '@shared/repo/base.do.repo'; import { LegacyLogger } from '@src/core/logger'; -import { createMock } from '@golevelup/ts-jest'; describe('BaseDORepo', () => { @Entity() @@ -14,7 +14,7 @@ describe('BaseDORepo', () => { @Property() name: string; - constructor(props: ITestEntityProperties = { name: 'test' }) { + constructor(props: TestEntityProperties = { name: 'test' }) { super(); this.name = props.name; } @@ -30,17 +30,17 @@ describe('BaseDORepo', () => { } } - interface ITestEntityProperties { + interface TestEntityProperties { name: string; } @Injectable() - class TestRepo extends BaseDORepo { + class TestRepo extends BaseDORepo { get entityName(): EntityName { return TestEntity; } - entityFactory(props: ITestEntityProperties): TestEntity { + entityFactory(props: TestEntityProperties): TestEntity { return new TestEntity(props); } @@ -48,7 +48,7 @@ describe('BaseDORepo', () => { return new TestDO({ id: entity.id, name: entity.name }); } - mapDOToEntityProperties(entityDO: TestDO): ITestEntityProperties { + mapDOToEntityProperties(entityDO: TestDO): TestEntityProperties { return { name: entityDO.name, }; @@ -95,7 +95,7 @@ describe('BaseDORepo', () => { }); describe('entityFactory', () => { - const props: ITestEntityProperties = { + const props: TestEntityProperties = { name: 'name', }; diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts index 854c3958135..c6a684a377d 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts @@ -1,8 +1,14 @@ import { createMock } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { CustomParameterEntry } from '@modules/tool/common/domain'; +import { ToolContextType } from '@modules/tool/common/enum'; +import { ContextExternalTool, ContextExternalToolProps } from '@modules/tool/context-external-tool/domain'; +import { ContextExternalToolEntity, ContextExternalToolType } from '@modules/tool/context-external-tool/entity'; +import { ContextExternalToolQuery } from '@modules/tool/context-external-tool/uc/dto/context-external-tool.types'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@infra/database'; import { ExternalToolRepoMapper } from '@shared/repo/externaltool/external-tool.repo.mapper'; import { cleanupCollections, @@ -12,12 +18,6 @@ import { schoolFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { CustomParameterEntry } from '@modules/tool/common/domain'; -import { ToolContextType } from '@modules/tool/common/enum'; -import { ContextExternalTool, ContextExternalToolProps } from '@modules/tool/context-external-tool/domain'; -import { ContextExternalToolEntity, ContextExternalToolType } from '@modules/tool/context-external-tool/entity'; -import { ContextExternalToolQuery } from '@modules/tool/context-external-tool/uc/dto/context-external-tool.types'; -import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { ContextExternalToolRepo } from './context-external-tool.repo'; describe('ContextExternalToolRepo', () => { @@ -394,4 +394,86 @@ describe('ContextExternalToolRepo', () => { }); }); }); + + describe('countBySchoolToolIdsAndContextType', () => { + describe('when a ContextExternalTool is found for course context', () => { + const setup = async () => { + const schoolExternalTool = schoolExternalToolEntityFactory.buildWithId(); + const schoolExternalTool1 = schoolExternalToolEntityFactory.buildWithId(); + + const contextExternalTool = contextExternalToolEntityFactory.buildList(4, { + contextType: ContextExternalToolType.COURSE, + schoolTool: schoolExternalTool, + }); + + const contextExternalTool3 = contextExternalToolEntityFactory.buildList(2, { + contextType: ContextExternalToolType.COURSE, + schoolTool: schoolExternalTool1, + }); + + await em.persistAndFlush([ + schoolExternalTool, + schoolExternalTool1, + ...contextExternalTool, + ...contextExternalTool3, + ]); + + return { + schoolExternalTool, + schoolExternalTool1, + }; + }; + + it('should return correct results', async () => { + const { schoolExternalTool, schoolExternalTool1 } = await setup(); + + const result = await repo.countBySchoolToolIdsAndContextType(ContextExternalToolType.COURSE, [ + schoolExternalTool.id, + schoolExternalTool1.id, + ]); + + expect(result).toEqual(6); + }); + }); + + describe('when a ContextExternalTool is found for board context', () => { + const setup = async () => { + const schoolExternalTool = schoolExternalToolEntityFactory.buildWithId(); + const schoolExternalTool1 = schoolExternalToolEntityFactory.buildWithId(); + + const contextExternalTool1 = contextExternalToolEntityFactory.buildList(3, { + contextType: ContextExternalToolType.BOARD_ELEMENT, + schoolTool: schoolExternalTool, + }); + + const contextExternalTool2 = contextExternalToolEntityFactory.buildList(2, { + contextType: ContextExternalToolType.BOARD_ELEMENT, + schoolTool: schoolExternalTool1, + }); + + await em.persistAndFlush([ + schoolExternalTool, + schoolExternalTool1, + ...contextExternalTool1, + ...contextExternalTool2, + ]); + + return { + schoolExternalTool, + schoolExternalTool1, + }; + }; + + it('should return correct results', async () => { + const { schoolExternalTool, schoolExternalTool1 } = await setup(); + + const result = await repo.countBySchoolToolIdsAndContextType(ContextExternalToolType.BOARD_ELEMENT, [ + schoolExternalTool.id, + schoolExternalTool1.id, + ]); + + expect(result).toEqual(5); + }); + }); + }); }); diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts index 5ad1629f0c2..e51dac73b1c 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts @@ -1,18 +1,18 @@ import { EntityName } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; -import { Injectable } from '@nestjs/common'; -import { BaseDORepo } from '@shared/repo'; -import { LegacyLogger } from '@src/core/logger'; import { ToolContextType } from '@modules/tool/common/enum/tool-context-type.enum'; import { ContextExternalTool, ContextRef } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolEntity, + ContextExternalToolProperties, ContextExternalToolType, - IContextExternalToolProperties, } from '@modules/tool/context-external-tool/entity'; import { ContextExternalToolQuery } from '@modules/tool/context-external-tool/uc/dto/context-external-tool.types'; import { SchoolExternalToolRefDO } from '@modules/tool/school-external-tool/domain'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import { Injectable } from '@nestjs/common'; +import { BaseDORepo } from '@shared/repo'; +import { LegacyLogger } from '@src/core/logger'; import { EntityId } from '../../domain'; import { ExternalToolRepoMapper } from '../externaltool'; import { ContextExternalToolScope } from './context-external-tool.scope'; @@ -21,7 +21,7 @@ import { ContextExternalToolScope } from './context-external-tool.scope'; export class ContextExternalToolRepo extends BaseDORepo< ContextExternalTool, ContextExternalToolEntity, - IContextExternalToolProperties + ContextExternalToolProperties > { constructor(protected readonly _em: EntityManager, protected readonly logger: LegacyLogger) { super(_em, logger); @@ -31,7 +31,7 @@ export class ContextExternalToolRepo extends BaseDORepo< return ContextExternalToolEntity; } - entityFactory(props: IContextExternalToolProperties): ContextExternalToolEntity { + entityFactory(props: ContextExternalToolProperties): ContextExternalToolEntity { return new ContextExternalToolEntity(props); } @@ -53,6 +53,14 @@ export class ContextExternalToolRepo extends BaseDORepo< return dos; } + async countBySchoolToolIdsAndContextType(contextType: ContextExternalToolType, schoolExternalToolIds: string[]) { + const contextExternalToolCount = await this._em.count(this.entityName, { + $and: [{ schoolTool: { $in: schoolExternalToolIds }, contextType }], + }); + + return contextExternalToolCount; + } + public override async findById(id: EntityId): Promise { const entity: ContextExternalToolEntity = await this._em.findOneOrFail( this.entityName, @@ -100,7 +108,7 @@ export class ContextExternalToolRepo extends BaseDORepo< }); } - mapDOToEntityProperties(entityDO: ContextExternalTool): IContextExternalToolProperties { + mapDOToEntityProperties(entityDO: ContextExternalTool): ContextExternalToolProperties { return { contextId: entityDO.contextRef.id, contextType: this.mapContextTypeToEntityType(entityDO.contextRef.type), diff --git a/apps/server/src/shared/repo/dashboard/dashboard.model.mapper.ts b/apps/server/src/shared/repo/dashboard/dashboard.model.mapper.ts index 4cac8dc0b10..e43f08d147d 100644 --- a/apps/server/src/shared/repo/dashboard/dashboard.model.mapper.ts +++ b/apps/server/src/shared/repo/dashboard/dashboard.model.mapper.ts @@ -1,14 +1,14 @@ -import { wrap, EntityManager } from '@mikro-orm/core'; +import { EntityManager, wrap } from '@mikro-orm/core'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { + Course, DashboardEntity, + DashboardGridElementModel, + DashboardModelEntity, GridElement, GridElementWithPosition, - ILearnroom, + Learnroom, LearnroomTypes, - DashboardGridElementModel, - DashboardModelEntity, - Course, User, } from '@shared/domain'; @@ -39,7 +39,7 @@ export class DashboardModelMapper { return new DashboardEntity(modelEntity.id, { grid, userId: modelEntity.user.id }); } - mapReferenceToModel(reference: ILearnroom): Course { + mapReferenceToModel(reference: Learnroom): Course { const metadata = reference.getMetadata(); if (metadata.type === LearnroomTypes.Course) { const course = reference as Course; diff --git a/apps/server/src/shared/repo/dashboard/dashboard.repo.ts b/apps/server/src/shared/repo/dashboard/dashboard.repo.ts index c0353719696..30b463e9344 100644 --- a/apps/server/src/shared/repo/dashboard/dashboard.repo.ts +++ b/apps/server/src/shared/repo/dashboard/dashboard.repo.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@nestjs/common'; -import { EntityId, DashboardEntity, GridElementWithPosition, DashboardModelEntity } from '@shared/domain'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { DashboardEntity, DashboardModelEntity, EntityId, GridElementWithPosition } from '@shared/domain'; import { DashboardModelMapper } from './dashboard.model.mapper'; const generateEmptyDashboard = (userId: EntityId) => { diff --git a/apps/server/src/shared/repo/externaltool/external-tool.repo.ts b/apps/server/src/shared/repo/externaltool/external-tool.repo.ts index 4ea69a54855..3d071311d82 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.repo.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.repo.ts @@ -1,13 +1,13 @@ import { EntityName, QueryOrderMap } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; -import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { IFindOptions, IPagination, Page, SortOrder } from '@shared/domain'; -import { BaseDORepo, ExternalToolRepoMapper, ExternalToolSortingMapper, Scope } from '@shared/repo'; -import { LegacyLogger } from '@src/core/logger'; import { ToolConfigType } from '@modules/tool/common/enum'; import { ExternalToolSearchQuery } from '@modules/tool/common/interface'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { ExternalToolEntity, IExternalToolProperties } from '@modules/tool/external-tool/entity'; +import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; +import { IFindOptions, Page, Pagination, SortOrder } from '@shared/domain'; +import { BaseDORepo, ExternalToolRepoMapper, ExternalToolSortingMapper, Scope } from '@shared/repo'; +import { LegacyLogger } from '@src/core/logger'; import { ExternalToolScope } from './external-tool.scope'; @Injectable() @@ -52,7 +52,7 @@ export class ExternalToolRepo extends BaseDORepo): Promise> { - const pagination: IPagination = options?.pagination || {}; + const pagination: Pagination = options?.pagination || {}; const order: QueryOrderMap = ExternalToolSortingMapper.mapDOSortOrderToQueryOrder( options?.order || {} ); diff --git a/apps/server/src/shared/repo/index.ts b/apps/server/src/shared/repo/index.ts index 715a78ee420..ce9304ec7ef 100644 --- a/apps/server/src/shared/repo/index.ts +++ b/apps/server/src/shared/repo/index.ts @@ -12,7 +12,6 @@ export * from './coursegroup'; export * from './dashboard'; export * from './federalstate'; export * from './importuser'; -export * from './lesson'; export * from './ltitool'; export * from './materials'; export * from './mongo.patterns'; diff --git a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts b/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts index 775c193675d..097f5498685 100644 --- a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts @@ -1,19 +1,19 @@ import { createMock } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { - ISchoolProperties, LegacySchoolDo, SchoolEntity, + SchoolProperties, SchoolRolePermission, SchoolRoles, SchoolYearEntity, SystemEntity, UserLoginMigrationEntity, } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@infra/database'; import { legacySchoolDoFactory, schoolFactory, @@ -241,10 +241,10 @@ describe('LegacySchoolRepo', () => { }; }; - it('should map SchoolDO properties to ISchoolProperties', async () => { + it('should map SchoolDO properties to SchoolProperties', async () => { const { entityDO, emGetReferenceSpy, system1, system2, userLoginMigration } = await setup(); - const result: ISchoolProperties = repo.mapDOToEntityProperties(entityDO); + const result: SchoolProperties = repo.mapDOToEntityProperties(entityDO); expect(result.externalId).toEqual(entityDO.externalId); expect(result.features).toEqual(entityDO.features); diff --git a/apps/server/src/shared/repo/school/legacy-school.repo.ts b/apps/server/src/shared/repo/school/legacy-school.repo.ts index 711eaea27d9..a8b5265a5c4 100644 --- a/apps/server/src/shared/repo/school/legacy-school.repo.ts +++ b/apps/server/src/shared/repo/school/legacy-school.repo.ts @@ -3,9 +3,9 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId, - ISchoolProperties, LegacySchoolDo, SchoolEntity, + SchoolProperties, SystemEntity, UserLoginMigrationEntity, } from '@shared/domain'; @@ -16,7 +16,7 @@ import { BaseDORepo } from '../base.do.repo'; * @deprecated because it uses the deprecated LegacySchoolDo. */ @Injectable() -export class LegacySchoolRepo extends BaseDORepo { +export class LegacySchoolRepo extends BaseDORepo { constructor(protected readonly _em: EntityManager, protected readonly logger: LegacyLogger) { super(_em, logger); } @@ -42,7 +42,7 @@ export class LegacySchoolRepo extends BaseDORepo { constructor(protected readonly _em: EntityManager, protected readonly logger: LegacyLogger) { super(_em, logger); @@ -25,7 +25,7 @@ export class SchoolExternalToolRepo extends BaseDORepo< return SchoolExternalToolEntity; } - entityFactory(props: ISchoolExternalToolProperties): SchoolExternalToolEntity { + entityFactory(props: SchoolExternalToolProperties): SchoolExternalToolEntity { return new SchoolExternalToolEntity(props); } @@ -44,6 +44,7 @@ export class SchoolExternalToolRepo extends BaseDORepo< const domainObject: SchoolExternalTool = this.mapEntityToDO(entity); return domainObject; }); + return domainObjects; } @@ -81,7 +82,7 @@ export class SchoolExternalToolRepo extends BaseDORepo< }); } - mapDOToEntityProperties(entityDO: SchoolExternalTool): ISchoolExternalToolProperties { + mapDOToEntityProperties(entityDO: SchoolExternalTool): SchoolExternalToolProperties { return { school: this._em.getReference(SchoolEntity, entityDO.schoolId), tool: this._em.getReference(ExternalToolEntity, entityDO.toolId), diff --git a/apps/server/src/shared/repo/scope.spec.ts b/apps/server/src/shared/repo/scope.spec.ts index cf0e88b4d3b..894f7c9fe65 100644 --- a/apps/server/src/shared/repo/scope.spec.ts +++ b/apps/server/src/shared/repo/scope.spec.ts @@ -2,7 +2,7 @@ import { Entity, Property } from '@mikro-orm/core'; import { EmptyResultQuery } from './query/empty-result.query'; import { Scope } from './scope'; -export interface ITestEntityProperties { +export interface TestEntityProperties { name: string; numbers?: number[]; } @@ -15,7 +15,7 @@ class TestEntity { @Property() numbers: number[]; - constructor(props: ITestEntityProperties) { + constructor(props: TestEntityProperties) { this.name = props.name; this.numbers = props.numbers || []; } diff --git a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts index 43d46a95383..451285eebca 100644 --- a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts @@ -1,11 +1,12 @@ import { createMock } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { FindOptions, NotFoundError, QueryOrderMap } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { UserQuery } from '@modules/user/service/user-query.type'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; import { IFindOptions, - IUserProperties, LanguageType, Role, RoleName, @@ -13,10 +14,10 @@ import { SortOrder, SystemEntity, User, + UserProperties, } from '@shared/domain'; import { Page } from '@shared/domain/domainobject/page'; import { UserDO } from '@shared/domain/domainobject/user.do'; -import { MongoMemoryDatabaseModule } from '@infra/database'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { cleanupCollections, @@ -27,7 +28,6 @@ import { userFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { UserQuery } from '@modules/user/service/user-query.type'; describe('UserRepo', () => { let module: TestingModule; @@ -67,7 +67,7 @@ describe('UserRepo', () => { }); describe('entityFactory', () => { - const props: IUserProperties = { + const props: UserProperties = { email: 'email@email.email', firstName: 'firstName', lastName: 'lastName', @@ -234,6 +234,7 @@ describe('UserRepo', () => { language: LanguageType.DE, forcePasswordChange: false, preferences: { firstLogin: true }, + birthday: new Date(), }, id.toHexString() ); @@ -276,6 +277,7 @@ describe('UserRepo', () => { outdatedSince: testEntity.outdatedSince, lastLoginSystemChange: testEntity.lastLoginSystemChange, previousExternalId: testEntity.previousExternalId, + birthday: testEntity.birthday, }) ); }); @@ -299,13 +301,14 @@ describe('UserRepo', () => { outdatedSince: new Date(), lastLoginSystemChange: new Date(), previousExternalId: 'someId', + birthday: new Date(), }, 'testId' ); - const result: IUserProperties = repo.mapDOToEntityProperties(testDO); + const result: UserProperties = repo.mapDOToEntityProperties(testDO); - expect(result).toEqual({ + expect(result).toEqual({ email: testDO.email, firstName: testDO.firstName, lastName: testDO.lastName, @@ -321,6 +324,7 @@ describe('UserRepo', () => { outdatedSince: testDO.outdatedSince, lastLoginSystemChange: testDO.lastLoginSystemChange, previousExternalId: testDO.previousExternalId, + birthday: testDO.birthday, }); }); }); diff --git a/apps/server/src/shared/repo/user/user-do.repo.ts b/apps/server/src/shared/repo/user/user-do.repo.ts index e9eae128a1f..c28a1915eda 100644 --- a/apps/server/src/shared/repo/user/user-do.repo.ts +++ b/apps/server/src/shared/repo/user/user-do.repo.ts @@ -1,37 +1,37 @@ import { EntityName, FilterQuery, QueryOrderMap } from '@mikro-orm/core'; +import { UserQuery } from '@modules/user/service/user-query.type'; import { Injectable } from '@nestjs/common'; import { EntityNotFoundError } from '@shared/common'; import { EntityId, IFindOptions, - IPagination, - IUserProperties, + Pagination, Role, SchoolEntity, SortOrder, SortOrderMap, SystemEntity, User, + UserProperties, } from '@shared/domain'; import { RoleReference } from '@shared/domain/domainobject'; import { Page } from '@shared/domain/domainobject/page'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { BaseDORepo, Scope } from '@shared/repo'; -import { UserQuery } from '@modules/user/service/user-query.type'; import { UserScope } from './user.scope'; @Injectable() -export class UserDORepo extends BaseDORepo { +export class UserDORepo extends BaseDORepo { get entityName(): EntityName { return User; } - entityFactory(props: IUserProperties): User { + entityFactory(props: UserProperties): User { return new User(props); } async find(query: UserQuery, options?: IFindOptions) { - const pagination: IPagination = options?.pagination || {}; + const pagination: Pagination = options?.pagination || {}; const order: QueryOrderMap = this.createQueryOrderMap(options?.order || {}); const scope: Scope = new UserScope() .bySchoolId(query.schoolId) @@ -109,6 +109,7 @@ export class UserDORepo extends BaseDORepo { lastLoginSystemChange: entity.lastLoginSystemChange, outdatedSince: entity.outdatedSince, previousExternalId: entity.previousExternalId, + birthday: entity.birthday, }); if (entity.roles.isInitialized()) { @@ -120,7 +121,7 @@ export class UserDORepo extends BaseDORepo { return user; } - mapDOToEntityProperties(entityDO: UserDO): IUserProperties { + mapDOToEntityProperties(entityDO: UserDO): UserProperties { return { email: entityDO.email, firstName: entityDO.firstName, @@ -135,6 +136,7 @@ export class UserDORepo extends BaseDORepo { lastLoginSystemChange: entityDO.lastLoginSystemChange, outdatedSince: entityDO.outdatedSince, previousExternalId: entityDO.previousExternalId, + birthday: entityDO.birthday, }; } diff --git a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts index ea10e6e5b3e..3d44d0edfb8 100644 --- a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts @@ -1,8 +1,8 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { MatchCreator, SortOrder, SystemEntity, User } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@infra/database'; import { cleanupCollections, importUserFactory, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { systemFactory } from '@shared/testing/factory/system.factory'; import { UserRepo } from './user.repo'; @@ -70,6 +70,7 @@ describe('user repo', () => { 'lastLoginSystemChange', 'outdatedSince', 'previousExternalId', + 'birthday', ].sort() ); }); @@ -133,6 +134,7 @@ describe('user repo', () => { await em.persistAndFlush([userA, userB]); em.clear(); }); + it('should return right keys', async () => { const result = await repo.findByExternalIdOrFail(userA.externalId as string, sys.id); expect(Object.keys(result).sort()).toEqual( @@ -158,6 +160,7 @@ describe('user repo', () => { 'lastLoginSystemChange', 'outdatedSince', 'previousExternalId', + 'birthday', ].sort() ); }); diff --git a/apps/server/src/shared/repo/user/user.repo.ts b/apps/server/src/shared/repo/user/user.repo.ts index a067953faba..44acafe6a80 100644 --- a/apps/server/src/shared/repo/user/user.repo.ts +++ b/apps/server/src/shared/repo/user/user.repo.ts @@ -7,7 +7,7 @@ import { EntityId, IFindOptions, ImportUser, - INameMatch, + NameMatch, Role, SchoolEntity, SortOrder, @@ -47,7 +47,7 @@ export class UserRepo extends BaseRepo { */ async findWithoutImportUser( school: SchoolEntity, - filters?: INameMatch, + filters?: NameMatch, options?: IFindOptions ): Promise> { const { _id: schoolId } = school; diff --git a/apps/server/src/shared/testing/factory/base.factory.spec.ts b/apps/server/src/shared/testing/factory/base.factory.spec.ts index 8e681aff656..6c07cfceca8 100644 --- a/apps/server/src/shared/testing/factory/base.factory.spec.ts +++ b/apps/server/src/shared/testing/factory/base.factory.spec.ts @@ -1,7 +1,7 @@ import { BaseFactory } from './base.factory'; describe('BaseFactory', () => { - interface IUserProperties { + interface UserProperties { email: string; roles: string[]; nickName?: string; @@ -14,7 +14,7 @@ describe('BaseFactory', () => { nickName?: string; - constructor(props: IUserProperties) { + constructor(props: UserProperties) { this.email = props.email; this.roles = props.roles; this.nickName = props.nickName; @@ -29,7 +29,7 @@ describe('BaseFactory', () => { describe('when defining the factory', () => { it('should call the constructor', () => { const Constructor = jest.fn(); - const factory = BaseFactory.define(Constructor, () => { + const factory = BaseFactory.define(Constructor, () => { return { email: 'joe@example.com', roles: ['member'], @@ -41,7 +41,7 @@ describe('BaseFactory', () => { }); it('should create an instance of the class', () => { - const factory = BaseFactory.define(User, () => { + const factory = BaseFactory.define(User, () => { return { email: 'joe@example.com', roles: ['member'], @@ -52,7 +52,7 @@ describe('BaseFactory', () => { }); it('should override default properties', () => { - const factory = BaseFactory.define(User, () => { + const factory = BaseFactory.define(User, () => { return { email: 'joe@example.com', roles: ['member'], @@ -63,7 +63,7 @@ describe('BaseFactory', () => { }); it('should call afterBuild hook', () => { - const factory = BaseFactory.define(User, () => { + const factory = BaseFactory.define(User, () => { return { email: 'joe@example.com', roles: ['member'], @@ -79,7 +79,7 @@ describe('BaseFactory', () => { }); it('should delegate transient params as a trait', () => { - const factory = BaseFactory.define(User, ({ transientParams }) => { + const factory = BaseFactory.define(User, ({ transientParams }) => { const { registered, numTasks } = transientParams; return { email: `joe-${registered ? 'r' : 'u'}-${numTasks || '0'}@example.com`, roles: ['member'] }; }); @@ -88,7 +88,7 @@ describe('BaseFactory', () => { }); it('should delegate transientParams as a build option', () => { - const factory = BaseFactory.define(User, ({ transientParams }) => { + const factory = BaseFactory.define(User, ({ transientParams }) => { const { registered, numTasks } = transientParams; return { email: `joe-${registered ? 'r' : 'u'}-${numTasks || '0'}@example.com`, roles: ['member'] }; }); @@ -97,7 +97,7 @@ describe('BaseFactory', () => { }); it('should delegate associations as a trait', () => { - const factory = BaseFactory.define(User, ({ associations }) => { + const factory = BaseFactory.define(User, ({ associations }) => { return { email: 'joe@example.com', roles: associations.roles || ['member'], @@ -108,7 +108,7 @@ describe('BaseFactory', () => { }); it('should delegate associations as build option', () => { - const factory = BaseFactory.define(User, ({ associations }) => { + const factory = BaseFactory.define(User, ({ associations }) => { return { email: 'joe@example.com', roles: associations.roles || ['member'], @@ -120,7 +120,7 @@ describe('BaseFactory', () => { }); describe('when subclassing the factory', () => { - class UserFactory extends BaseFactory { + class UserFactory extends BaseFactory { admin() { return this.params({ roles: ['admin'] }); } @@ -168,7 +168,7 @@ describe('BaseFactory', () => { describe('when builing a list of objects', () => { it('should call the constructor for each item', () => { const Constructor = jest.fn(); - const factory = BaseFactory.define(Constructor, ({ sequence }) => { + const factory = BaseFactory.define(Constructor, ({ sequence }) => { return { email: `joe-${sequence}@example.com`, roles: ['member'], @@ -179,7 +179,7 @@ describe('BaseFactory', () => { }); it('should create an instance of the class for each item', () => { - const factory = BaseFactory.define(User, ({ sequence }) => { + const factory = BaseFactory.define(User, ({ sequence }) => { return { email: `joe-${sequence}@example.com`, roles: ['member'], @@ -192,7 +192,7 @@ describe('BaseFactory', () => { }); it('should override properties', () => { - const factory = BaseFactory.define(User, ({ sequence }) => { + const factory = BaseFactory.define(User, ({ sequence }) => { return { email: `joe-${sequence}@example.com`, roles: ['member'], diff --git a/apps/server/src/shared/testing/factory/context-external-tool-entity.factory.ts b/apps/server/src/shared/testing/factory/context-external-tool-entity.factory.ts index fb545c1e2e7..3e7f4e4b1cb 100644 --- a/apps/server/src/shared/testing/factory/context-external-tool-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/context-external-tool-entity.factory.ts @@ -1,16 +1,16 @@ -import { BaseFactory } from '@shared/testing/factory/base.factory'; import { CustomParameterEntryEntity } from '@modules/tool/common/entity'; import { ContextExternalToolEntity, + ContextExternalToolProperties, ContextExternalToolType, - IContextExternalToolProperties, } from '@modules/tool/context-external-tool/entity'; +import { BaseFactory } from '@shared/testing/factory/base.factory'; import { courseFactory } from './course.factory'; import { schoolExternalToolEntityFactory } from './school-external-tool-entity.factory'; export const contextExternalToolEntityFactory = BaseFactory.define< ContextExternalToolEntity, - IContextExternalToolProperties + ContextExternalToolProperties >(ContextExternalToolEntity, () => { return { contextId: courseFactory.buildWithId().id, diff --git a/apps/server/src/shared/testing/factory/course.factory.ts b/apps/server/src/shared/testing/factory/course.factory.ts index 150b7b2270b..a0418771486 100644 --- a/apps/server/src/shared/testing/factory/course.factory.ts +++ b/apps/server/src/shared/testing/factory/course.factory.ts @@ -1,38 +1,38 @@ import { DeepPartial } from 'fishery'; -import { Course, ICourseProperties } from '@shared/domain'; +import { Course, CourseProperties } from '@shared/domain'; -import { schoolFactory } from './school.factory'; import { BaseFactory } from './base.factory'; +import { schoolFactory } from './school.factory'; import { userFactory } from './user.factory'; const oneDay = 24 * 60 * 60 * 1000; -class CourseFactory extends BaseFactory { +class CourseFactory extends BaseFactory { isFinished(): this { const untilDate = new Date(Date.now() - oneDay); - const params: DeepPartial = { untilDate }; + const params: DeepPartial = { untilDate }; return this.params(params); } isOpen(): this { const untilDate = new Date(Date.now() + oneDay); - const params: DeepPartial = { untilDate }; + const params: DeepPartial = { untilDate }; return this.params(params); } studentsWithId(numberOfStudents: number): this { const students = userFactory.buildListWithId(numberOfStudents); - const params: DeepPartial = { students }; + const params: DeepPartial = { students }; return this.params(params); } teachersWithId(numberOfTeachers: number): this { const teachers = userFactory.buildListWithId(numberOfTeachers); - const params: DeepPartial = { teachers }; + const params: DeepPartial = { teachers }; return this.params(params); } diff --git a/apps/server/src/shared/testing/factory/coursegroup.factory.ts b/apps/server/src/shared/testing/factory/coursegroup.factory.ts index f1408fd664d..3b98de4a17c 100644 --- a/apps/server/src/shared/testing/factory/coursegroup.factory.ts +++ b/apps/server/src/shared/testing/factory/coursegroup.factory.ts @@ -1,13 +1,13 @@ -import { CourseGroup, ICourseGroupProperties } from '@shared/domain'; +import { CourseGroup, CourseGroupProperties } from '@shared/domain'; import { DeepPartial } from 'fishery'; -import { courseFactory } from './course.factory'; import { BaseFactory } from './base.factory'; +import { courseFactory } from './course.factory'; import { userFactory } from './user.factory'; -class CourseGroupFactory extends BaseFactory { +class CourseGroupFactory extends BaseFactory { studentsWithId(numberOfStudents: number): this { const students = userFactory.buildListWithId(numberOfStudents); - const params: DeepPartial = { students }; + const params: DeepPartial = { students }; return this.params(params); } diff --git a/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts b/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts index 32c077e2529..39ff2662cf7 100644 --- a/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts @@ -82,8 +82,6 @@ export const customParameterEntityFactory = BaseFactory.define(ExternalToolPseudonymEntity, ({ sequence }) => { return { pseudonym: `pseudonym-${sequence}`, diff --git a/apps/server/src/shared/testing/factory/federal-state.factory.ts b/apps/server/src/shared/testing/factory/federal-state.factory.ts index 71290151668..11a685140a5 100644 --- a/apps/server/src/shared/testing/factory/federal-state.factory.ts +++ b/apps/server/src/shared/testing/factory/federal-state.factory.ts @@ -1,7 +1,7 @@ -import { County, FederalStateEntity, IFederalStateProperties } from '@shared/domain'; +import { County, FederalStateEntity, FederalStateProperties } from '@shared/domain'; import { BaseFactory } from './base.factory'; -export const federalStateFactory = BaseFactory.define( +export const federalStateFactory = BaseFactory.define( FederalStateEntity, () => { return { diff --git a/apps/server/src/shared/testing/factory/filerecord.factory.ts b/apps/server/src/shared/testing/factory/filerecord.factory.ts index 4e12787f661..c05c1679ed2 100644 --- a/apps/server/src/shared/testing/factory/filerecord.factory.ts +++ b/apps/server/src/shared/testing/factory/filerecord.factory.ts @@ -1,14 +1,14 @@ import { FileRecordParentType } from '@infra/rabbitmq'; -import { FileRecord, FileRecordSecurityCheck, IFileRecordProperties } from '@modules/files-storage/entity'; +import { FileRecord, FileRecordProperties, FileRecordSecurityCheck } from '@modules/files-storage/entity'; import { ObjectId } from 'bson'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; const yesterday = new Date(Date.now() - 86400000); -class FileRecordFactory extends BaseFactory { +class FileRecordFactory extends BaseFactory { markedForDelete(): this { - const params: DeepPartial = { deletedSince: yesterday }; + const params: DeepPartial = { deletedSince: yesterday }; return this.params(params); } } diff --git a/apps/server/src/shared/testing/factory/h5p-content.factory.ts b/apps/server/src/shared/testing/factory/h5p-content.factory.ts index 4d07c369cd5..7931f9b1529 100644 --- a/apps/server/src/shared/testing/factory/h5p-content.factory.ts +++ b/apps/server/src/shared/testing/factory/h5p-content.factory.ts @@ -2,12 +2,12 @@ import { ContentMetadata, H5PContent, H5PContentParentType, - IH5PContentProperties, + H5PContentProperties, } from '@src/modules/h5p-editor/entity'; import { ObjectID } from 'bson'; import { BaseFactory } from './base.factory'; -class H5PContentFactory extends BaseFactory {} +class H5PContentFactory extends BaseFactory {} export const h5pContentFactory = H5PContentFactory.define(H5PContent, ({ sequence }) => { return { diff --git a/apps/server/src/shared/testing/factory/h5p-temporary-file.factory.ts b/apps/server/src/shared/testing/factory/h5p-temporary-file.factory.ts index 4c9fbea5b11..4f2205b0490 100644 --- a/apps/server/src/shared/testing/factory/h5p-temporary-file.factory.ts +++ b/apps/server/src/shared/testing/factory/h5p-temporary-file.factory.ts @@ -1,14 +1,14 @@ -import { ITemporaryFileProperties, H5pEditorTempFile } from '@src/modules/h5p-editor/entity'; +import { H5pEditorTempFile, TemporaryFileProperties } from '@src/modules/h5p-editor/entity'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; const oneDay = 24 * 60 * 60 * 1000; -class H5PTemporaryFileFactory extends BaseFactory { +class H5PTemporaryFileFactory extends BaseFactory { isExpired(): this { const birthtime = new Date(Date.now() - oneDay * 2); // Created two days ago const expiresAt = new Date(Date.now() - oneDay); // Expired yesterday - const params: DeepPartial = { expiresAt, birthtime }; + const params: DeepPartial = { expiresAt, birthtime }; return this.params(params); } diff --git a/apps/server/src/shared/testing/factory/import-user.factory.ts b/apps/server/src/shared/testing/factory/import-user.factory.ts index 1dd8fd3c024..d2cc011b995 100644 --- a/apps/server/src/shared/testing/factory/import-user.factory.ts +++ b/apps/server/src/shared/testing/factory/import-user.factory.ts @@ -1,14 +1,14 @@ import { v4 as uuidv4 } from 'uuid'; -import { IImportUserProperties, IImportUserRoleName, ImportUser, MatchCreator, RoleName, User } from '@shared/domain'; +import { IImportUserRoleName, ImportUser, ImportUserProperties, MatchCreator, RoleName, User } from '@shared/domain'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; import { schoolFactory } from './school.factory'; import { systemFactory } from './system.factory'; -class ImportUserFactory extends BaseFactory { +class ImportUserFactory extends BaseFactory { matched(matchedBy: MatchCreator, user: User): this { - const params: DeepPartial = { matchedBy, user }; + const params: DeepPartial = { matchedBy, user }; return this.params(params); } } diff --git a/apps/server/src/shared/testing/factory/lesson.factory.ts b/apps/server/src/shared/testing/factory/lesson.factory.ts index af7aff56fc1..a83d47cec80 100644 --- a/apps/server/src/shared/testing/factory/lesson.factory.ts +++ b/apps/server/src/shared/testing/factory/lesson.factory.ts @@ -1,11 +1,11 @@ -import { Course, IComponentProperties, ILessonProperties, LessonEntity } from '@shared/domain'; +import { ComponentProperties, Course, LessonEntity, LessonProperties } from '@shared/domain'; import { BaseFactory } from './base.factory'; import { courseFactory } from './course.factory'; -class LessonFactory extends BaseFactory {} +class LessonFactory extends BaseFactory {} -export const lessonFactory = LessonFactory.define( +export const lessonFactory = LessonFactory.define( LessonEntity, ({ sequence, params }) => { let course: Course; @@ -15,7 +15,7 @@ export const lessonFactory = LessonFactory.define { contents.push(element); diff --git a/apps/server/src/shared/testing/factory/material.factory.ts b/apps/server/src/shared/testing/factory/material.factory.ts index 62d77f1e116..5d0e07e4936 100644 --- a/apps/server/src/shared/testing/factory/material.factory.ts +++ b/apps/server/src/shared/testing/factory/material.factory.ts @@ -1,9 +1,9 @@ -import { IMaterialProperties, Material } from '@shared/domain/entity/materials.entity'; +import { Material, MaterialProperties } from '@shared/domain/entity/materials.entity'; import { BaseFactory } from './base.factory'; -class MaterialFactory extends BaseFactory {} +class MaterialFactory extends BaseFactory {} -export const materialFactory = MaterialFactory.define(Material, ({ sequence }) => { +export const materialFactory = MaterialFactory.define(Material, ({ sequence }) => { return { client: 'test material client', description: 'test material description', diff --git a/apps/server/src/shared/testing/factory/news.factory.ts b/apps/server/src/shared/testing/factory/news.factory.ts index 56b59cedf6c..55788cea9de 100644 --- a/apps/server/src/shared/testing/factory/news.factory.ts +++ b/apps/server/src/shared/testing/factory/news.factory.ts @@ -1,11 +1,11 @@ -import { SchoolNews, CourseNews, TeamNews, INewsProperties } from '@shared/domain'; +import { CourseNews, NewsProperties, SchoolNews, TeamNews } from '@shared/domain'; import { BaseFactory } from './base.factory'; import { courseFactory } from './course.factory'; import { schoolFactory } from './school.factory'; import { teamFactory } from './team.factory'; import { userFactory } from './user.factory'; -export const schoolNewsFactory = BaseFactory.define(SchoolNews, ({ sequence }) => { +export const schoolNewsFactory = BaseFactory.define(SchoolNews, ({ sequence }) => { return { title: `news ${sequence}`, content: `content of news ${sequence}`, @@ -16,7 +16,7 @@ export const schoolNewsFactory = BaseFactory.define }; }); -export const courseNewsFactory = BaseFactory.define(CourseNews, ({ sequence }) => { +export const courseNewsFactory = BaseFactory.define(CourseNews, ({ sequence }) => { return { title: `news ${sequence}`, content: `content of news ${sequence}`, @@ -27,7 +27,7 @@ export const courseNewsFactory = BaseFactory.define }; }); -export const teamNewsFactory = BaseFactory.define(TeamNews, ({ sequence }) => { +export const teamNewsFactory = BaseFactory.define(TeamNews, ({ sequence }) => { return { title: `news ${sequence}`, content: `content of news ${sequence}`, @@ -38,7 +38,7 @@ export const teamNewsFactory = BaseFactory.define(Tea }; }); -export const schoolUnpublishedNewsFactory = BaseFactory.define( +export const schoolUnpublishedNewsFactory = BaseFactory.define( SchoolNews, ({ sequence }) => { return { @@ -52,7 +52,7 @@ export const schoolUnpublishedNewsFactory = BaseFactory.define( +export const courseUnpublishedNewsFactory = BaseFactory.define( CourseNews, ({ sequence }) => { return { @@ -66,7 +66,7 @@ export const courseUnpublishedNewsFactory = BaseFactory.define(TeamNews, ({ sequence }) => { +export const teamUnpublishedNewsFactory = BaseFactory.define(TeamNews, ({ sequence }) => { return { title: `news ${sequence}`, content: `content of news ${sequence}`, diff --git a/apps/server/src/shared/testing/factory/role.factory.ts b/apps/server/src/shared/testing/factory/role.factory.ts index f37236c2587..bd987e198b5 100644 --- a/apps/server/src/shared/testing/factory/role.factory.ts +++ b/apps/server/src/shared/testing/factory/role.factory.ts @@ -1,7 +1,7 @@ -import { IRoleProperties, Role, RoleName } from '@shared/domain'; +import { Role, RoleName, RoleProperties } from '@shared/domain'; import { BaseFactory } from './base.factory'; -export const roleFactory = BaseFactory.define(Role, ({ sequence }) => { +export const roleFactory = BaseFactory.define(Role, ({ sequence }) => { return { name: `role${sequence}` as unknown as RoleName, }; diff --git a/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts b/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts index 456356e6e23..023e3a2626d 100644 --- a/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts @@ -1,11 +1,11 @@ +import { SchoolExternalToolEntity, SchoolExternalToolProperties } from '@modules/tool/school-external-tool/entity'; import { BaseFactory } from '@shared/testing/factory/base.factory'; -import { ISchoolExternalToolProperties, SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { externalToolEntityFactory } from './external-tool-entity.factory'; import { schoolFactory } from './school.factory'; export const schoolExternalToolEntityFactory = BaseFactory.define< SchoolExternalToolEntity, - ISchoolExternalToolProperties + SchoolExternalToolProperties >(SchoolExternalToolEntity, () => { return { tool: externalToolEntityFactory.buildWithId(), diff --git a/apps/server/src/shared/testing/factory/school.factory.ts b/apps/server/src/shared/testing/factory/school.factory.ts index c14dd95130d..1e71e816923 100644 --- a/apps/server/src/shared/testing/factory/school.factory.ts +++ b/apps/server/src/shared/testing/factory/school.factory.ts @@ -1,9 +1,9 @@ -import { ISchoolProperties, SchoolEntity } from '@shared/domain'; +import { SchoolEntity, SchoolProperties } from '@shared/domain'; import { BaseFactory } from './base.factory'; -import { schoolYearFactory } from './schoolyear.factory'; import { federalStateFactory } from './federal-state.factory'; +import { schoolYearFactory } from './schoolyear.factory'; -export const schoolFactory = BaseFactory.define(SchoolEntity, ({ sequence }) => { +export const schoolFactory = BaseFactory.define(SchoolEntity, ({ sequence }) => { return { name: `school #${sequence}`, schoolYear: schoolYearFactory.build(), diff --git a/apps/server/src/shared/testing/factory/schoolyear.factory.ts b/apps/server/src/shared/testing/factory/schoolyear.factory.ts index a1184ed66d3..b60e85c72e9 100644 --- a/apps/server/src/shared/testing/factory/schoolyear.factory.ts +++ b/apps/server/src/shared/testing/factory/schoolyear.factory.ts @@ -1,7 +1,7 @@ -import { ISchoolYearProperties, SchoolYearEntity } from '@shared/domain/entity/schoolyear.entity'; +import { SchoolYearEntity, SchoolYearProperties } from '@shared/domain/entity/schoolyear.entity'; import { BaseFactory } from './base.factory'; -export const schoolYearFactory = BaseFactory.define(SchoolYearEntity, () => { +export const schoolYearFactory = BaseFactory.define(SchoolYearEntity, () => { const year = new Date().getFullYear(); const nextYear = (year + 1).toString().substr(-2); const name = `${year}/${nextYear}`; diff --git a/apps/server/src/shared/testing/factory/storageprovider.factory.ts b/apps/server/src/shared/testing/factory/storageprovider.factory.ts index d0ad53be107..0406b647978 100644 --- a/apps/server/src/shared/testing/factory/storageprovider.factory.ts +++ b/apps/server/src/shared/testing/factory/storageprovider.factory.ts @@ -1,7 +1,7 @@ -import { StorageProviderEntity, IStorageProviderProperties } from '@shared/domain'; +import { StorageProviderEntity, StorageProviderProperties } from '@shared/domain'; import { BaseFactory } from './base.factory'; -export const storageProviderFactory = BaseFactory.define( +export const storageProviderFactory = BaseFactory.define( StorageProviderEntity, () => { return { diff --git a/apps/server/src/shared/testing/factory/submission.factory.ts b/apps/server/src/shared/testing/factory/submission.factory.ts index 09e057da78e..a667e2de37c 100644 --- a/apps/server/src/shared/testing/factory/submission.factory.ts +++ b/apps/server/src/shared/testing/factory/submission.factory.ts @@ -1,32 +1,32 @@ -import { ISubmissionProperties, Submission } from '@shared/domain'; +import { Submission, SubmissionProperties } from '@shared/domain'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; import { schoolFactory } from './school.factory'; import { taskFactory } from './task.factory'; import { userFactory } from './user.factory'; -class SubmissionFactory extends BaseFactory { +class SubmissionFactory extends BaseFactory { graded(): this { - const params: DeepPartial = { graded: true }; + const params: DeepPartial = { graded: true }; return this.params(params); } submitted(): this { - const params: DeepPartial = { submitted: true }; + const params: DeepPartial = { submitted: true }; return this.params(params); } studentWithId(): this { - const params: DeepPartial = { student: userFactory.buildWithId() }; + const params: DeepPartial = { student: userFactory.buildWithId() }; return this.params(params); } teamMembersWithId(numberOfTeamMembers: number): this { const teamMembers = userFactory.buildListWithId(numberOfTeamMembers); - const params: DeepPartial = { teamMembers }; + const params: DeepPartial = { teamMembers }; return this.params(params); } diff --git a/apps/server/src/shared/testing/factory/system.factory.ts b/apps/server/src/shared/testing/factory/system.factory.ts index 3745736b7e1..f686c406851 100644 --- a/apps/server/src/shared/testing/factory/system.factory.ts +++ b/apps/server/src/shared/testing/factory/system.factory.ts @@ -1,11 +1,11 @@ -import { ISystemProperties, LdapConfig, OauthConfig, OidcConfig, SystemEntity } from '@shared/domain'; +import { LdapConfig, OauthConfig, OidcConfig, SystemEntity, SystemProperties } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; -export class SystemFactory extends BaseFactory { +export class SystemFactory extends BaseFactory { withOauthConfig(): this { - const params: DeepPartial = { + const params: DeepPartial = { oauthConfig: new OauthConfig({ clientId: '12345', clientSecret: 'mocksecret', @@ -26,7 +26,7 @@ export class SystemFactory extends BaseFactory } withLdapConfig(otherParams?: DeepPartial): this { - const params: DeepPartial = { + const params: DeepPartial = { ldapConfig: new LdapConfig({ url: 'ldaps:mock.de:389', active: true, diff --git a/apps/server/src/shared/testing/factory/task.factory.ts b/apps/server/src/shared/testing/factory/task.factory.ts index 40162267dd2..c3860940163 100644 --- a/apps/server/src/shared/testing/factory/task.factory.ts +++ b/apps/server/src/shared/testing/factory/task.factory.ts @@ -1,4 +1,4 @@ -import { ITaskProperties, Task } from '@shared/domain'; +import { Task, TaskProperties } from '@shared/domain'; import { User } from '@shared/domain/entity'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; @@ -7,27 +7,27 @@ import { userFactory } from './user.factory'; const yesterday = new Date(Date.now() - 86400000); -class TaskFactory extends BaseFactory { +class TaskFactory extends BaseFactory { draft(): this { - const params: DeepPartial = { private: true }; + const params: DeepPartial = { private: true }; return this.params(params); } isPlanned(): this { - const params: DeepPartial = { private: false, availableDate: new Date(Date.now() + 10000) }; + const params: DeepPartial = { private: false, availableDate: new Date(Date.now() + 10000) }; return this.params(params); } isPublished(): this { - const params: DeepPartial = { private: false, availableDate: new Date(Date.now() - 10000) }; + const params: DeepPartial = { private: false, availableDate: new Date(Date.now() - 10000) }; return this.params(params); } finished(user: User): this { - const params: DeepPartial = { finished: [user] }; + const params: DeepPartial = { finished: [user] }; return this.params(params); } } diff --git a/apps/server/src/shared/testing/factory/team.factory.ts b/apps/server/src/shared/testing/factory/team.factory.ts index 1a72b84969f..332d3190c34 100644 --- a/apps/server/src/shared/testing/factory/team.factory.ts +++ b/apps/server/src/shared/testing/factory/team.factory.ts @@ -1,18 +1,18 @@ -import { ITeamProperties, Role, TeamEntity, TeamUserEntity } from '@shared/domain'; -import { DeepPartial } from 'fishery'; -import { teamUserFactory } from '@shared/testing/factory/teamuser.factory'; +import { Role, TeamEntity, TeamProperties, TeamUserEntity } from '@shared/domain'; import { BaseFactory } from '@shared/testing/factory/base.factory'; +import { teamUserFactory } from '@shared/testing/factory/teamuser.factory'; +import { DeepPartial } from 'fishery'; -class TeamFactory extends BaseFactory { +class TeamFactory extends BaseFactory { withRoleAndUserId(role: Role, userId: string): this { - const params: DeepPartial = { + const params: DeepPartial = { teamUsers: [teamUserFactory.withRoleAndUserId(role, userId).buildWithId()], }; return this.params(params); } withTeamUser(teamUser: TeamUserEntity[]): this { - const params: DeepPartial = { + const params: DeepPartial = { teamUsers: teamUser, }; return this.params(params); diff --git a/apps/server/src/shared/testing/factory/user.factory.ts b/apps/server/src/shared/testing/factory/user.factory.ts index 1557b3ccd35..5b09e5c46b5 100644 --- a/apps/server/src/shared/testing/factory/user.factory.ts +++ b/apps/server/src/shared/testing/factory/user.factory.ts @@ -1,5 +1,5 @@ /* istanbul ignore file */ -import { IUserProperties, Permission, Role, RoleName, User } from '@shared/domain'; +import { Permission, Role, RoleName, User, UserProperties } from '@shared/domain'; import { DeepPartial } from 'fishery'; import _ from 'lodash'; import { adminPermissions, studentPermissions, teacherPermissions, userPermissions } from '../user-role-permissions'; @@ -7,15 +7,15 @@ import { BaseFactory } from './base.factory'; import { roleFactory } from './role.factory'; import { schoolFactory } from './school.factory'; -class UserFactory extends BaseFactory { +class UserFactory extends BaseFactory { withRoleByName(name: RoleName): this { - const params: DeepPartial = { roles: [roleFactory.buildWithId({ name })] }; + const params: DeepPartial = { roles: [roleFactory.buildWithId({ name })] }; return this.params(params); } withRole(role: Role): this { - const params: DeepPartial = { roles: [role] }; + const params: DeepPartial = { roles: [role] }; return this.params(params); } @@ -24,7 +24,7 @@ class UserFactory extends BaseFactory { const permissions = _.union(userPermissions, studentPermissions, additionalPermissions); const role = roleFactory.buildWithId({ permissions, name: RoleName.STUDENT }); - const params: DeepPartial = { roles: [role] }; + const params: DeepPartial = { roles: [role] }; return this.params(params); } @@ -33,7 +33,7 @@ class UserFactory extends BaseFactory { const permissions = _.union(userPermissions, teacherPermissions, additionalPermissions); const role = roleFactory.buildWithId({ permissions, name: RoleName.TEACHER }); - const params: DeepPartial = { roles: [role] }; + const params: DeepPartial = { roles: [role] }; return this.params(params); } @@ -42,7 +42,7 @@ class UserFactory extends BaseFactory { const permissions = _.union(userPermissions, adminPermissions, additionalPermissions); const role = roleFactory.buildWithId({ permissions, name: RoleName.ADMINISTRATOR }); - const params: DeepPartial = { roles: [role] }; + const params: DeepPartial = { roles: [role] }; return this.params(params); } diff --git a/apps/server/src/shared/testing/map-user-to-current-user.ts b/apps/server/src/shared/testing/map-user-to-current-user.ts index b8c975f125d..3c6a1eadc27 100644 --- a/apps/server/src/shared/testing/map-user-to-current-user.ts +++ b/apps/server/src/shared/testing/map-user-to-current-user.ts @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { Account, EntityId, User } from '@shared/domain'; import { ICurrentUser } from '@modules/authentication'; +import { Account, EntityId, User } from '@shared/domain'; export const mapUserToCurrentUser = ( user: User, diff --git a/backup/setup/school-external-tools.json b/backup/setup/school-external-tools.json index 884c8ffe373..59befc464af 100644 --- a/backup/setup/school-external-tools.json +++ b/backup/setup/school-external-tools.json @@ -45,5 +45,28 @@ }, "schoolParameters": [], "toolVersion": 2 + }, + { + "_id": { + "$oid": "647de374cf6a427b9d39e5bb" + }, + "createdAt": { + "$date": { + "$numberLong": "1685971828284" + } + }, + "updatedAt": { + "$date": { + "$numberLong": "1685971828284" + } + }, + "tool": { + "$oid": "647de247cf6a427b9d39e5b9" + }, + "school": { + "$oid": "5fa2c5ccb229544f2c69666c" + }, + "schoolParameters": [], + "toolVersion": 2 } ] diff --git a/config/README.md b/config/README.md index cbdf77a73a9..69081ee6460 100644 --- a/config/README.md +++ b/config/README.md @@ -149,12 +149,12 @@ This code shows a minimal flow. ``` javascript // needed configuration for a module - export interface IUserConfig { + export interface UserConfig { AVAILABLE_LANGUAGES: string[]; } // server.config.ts - export interface IServerConfig extends ICoreModuleConfig, IUserConfig, IFilesStorageClientConfig { + export interface ServerConfig extends ICoreModuleConfig, UserConfig, IFilesStorageClientConfig { NODE_ENV: string; } @@ -176,9 +176,9 @@ This code shows a minimal flow. //use via injections import { ConfigService } from '@nestjs/config'; - import { IUserConfig } from '../interfaces'; + import { UserConfig } from '../interfaces'; - constructor(private readonly configService: ConfigService){} + constructor(private readonly configService: ConfigService){} this.configService.get('AVAILABLE_LANGUAGES'); diff --git a/config/default.schema.json b/config/default.schema.json index b971968a4ae..89d0a328a59 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1343,6 +1343,23 @@ "description": "Number of simultaneously synchronized students, teachers and classes" } } + }, + "ADMIN_API_CLIENT": { + "type": "object", + "description": "Configuration of the schulcloud-server's admin API client.", + "properties": { + "BASE_URL": { + "type": "string", + "description": "Base URL of the Admin API." + }, + "API_KEY": { + "type": "string", + "description": "API key for accessing the Admin API." + } + }, + "default": { + "BASE_URL": "http://localhost:4030" + } } }, "required": [], diff --git a/nest-cli.json b/nest-cli.json index 73dea03c093..8ce5461bb6f 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -45,6 +45,15 @@ "tsConfigPath": "apps/server/tsconfig.app.json" } }, + "deletion-console": { + "type": "application", + "root": "apps/server", + "entryFile": "apps/deletion-console.app", + "sourceRoot": "apps/server/src", + "compilerOptions": { + "tsConfigPath": "apps/server/tsconfig.app.json" + } + }, "files-storage": { "type": "application", "root": "apps/server", diff --git a/package.json b/package.json index 53a0752ee10..4153b7636e7 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,9 @@ "nest:start:console": "nest start console --", "nest:start:console:dev": "nest start console --watch --", "nest:start:console:debug": "nest start console --debug --watch --", + "nest:start:deletion-console": "nest start deletion-console --", + "nest:start:deletion-console:dev": "nest start deletion-console --watch --", + "nest:start:deletion-console:debug": "nest start deletion-console --debug --watch --", "nest:test": "npm run nest:test:cov && npm run nest:lint", "nest:test:all": "jest", "nest:test:unit": "jest \"^((?!\\.api\\.spec\\.ts).)*\\.spec\\.ts$\"",