From c9ace5a9d1d7331c7618b4a17793e6815b44d525 Mon Sep 17 00:00:00 2001 From: Patrick Sachmann Date: Fri, 15 Mar 2024 08:04:12 +0100 Subject: [PATCH] EW-539 adding Learnstore links to the Common Cartridge export (#4599) --- .../keycloak-administration.service.ts | 2 +- .../common-cartridge-file-builder.spec.ts | 110 +++++ .../builders/common-cartridge-file-builder.ts | 80 ++++ ...mon-cartridge-organization-builder.spec.ts | 87 ++++ .../common-cartridge-organization-builder.ts | 82 ++++ .../export/common-cartridge.enums.ts | 28 ++ .../common-cartridge-element-factory.spec.ts | 51 +++ .../common-cartridge-element-factory.ts | 52 +++ .../common-cartridge-element-factory.spec.ts | 63 +++ .../common-cartridge-element-factory.ts | 45 ++ .../common-cartridge-metadata-element.spec.ts | 71 ++++ .../common-cartridge-metadata-element.ts | 42 ++ ...mon-cartridge-organization-element.spec.ts | 101 +++++ .../common-cartridge-organization-element.ts | 53 +++ ...idge-organizations-wrapper-element.spec.ts | 87 ++++ ...cartridge-organizations-wrapper-element.ts | 39 ++ ...artridge-resources-wrapper-element.spec.ts | 78 ++++ ...mon-cartridge-resources-wrapper-element.ts | 28 ++ .../export/elements/v1.1.0/index.ts | 8 + .../common-cartridge-element-factory.spec.ts | 62 +++ .../common-cartridge-element-factory.ts | 45 ++ .../common-cartridge-metadata-element.spec.ts | 71 ++++ .../common-cartridge-metadata-element.ts | 42 ++ ...mon-cartridge-organization-element.spec.ts | 101 +++++ .../common-cartridge-organization-element.ts | 53 +++ ...idge-organizations-wrapper-element.spec.ts | 87 ++++ ...cartridge-organizations-wrapper-element.ts | 39 ++ ...artridge-resources-wrapper-element.spec.ts | 78 ++++ ...mon-cartridge-resources-wrapper-element.ts | 28 ++ .../export/elements/v1.3.0/index.ts | 14 + .../common-cartridge-element.interface.ts | 43 ++ .../common-cartridge-resource.interface.ts | 24 ++ .../export/interfaces/index.ts | 2 + .../common-cartridge-resource-factory.spec.ts | 50 +++ .../common-cartridge-resource-factory.ts | 44 ++ ...common-cartridge-manifest-resource.spec.ts | 147 +++++++ .../common-cartridge-manifest-resource.ts | 68 +++ .../common-cartridge-resource-factory.spec.ts | 51 +++ .../common-cartridge-resource-factory.ts | 37 ++ ...mon-cartridge-web-content-resource.spec.ts | 123 ++++++ .../common-cartridge-web-content-resource.ts | 61 +++ ...common-cartridge-web-link-resource.spec.ts | 129 ++++++ .../common-cartridge-web-link-resource.ts | 67 +++ .../export/resources/v1.1.0/index.ts | 7 + ...common-cartridge-manifest-resource.spec.ts | 148 +++++++ .../common-cartridge-manifest-resource.ts | 71 ++++ .../common-cartridge-resource-factory.spec.ts | 51 +++ .../common-cartridge-resource-factory.ts | 37 ++ ...mon-cartridge-web-content-resource.spec.ts | 123 ++++++ .../common-cartridge-web-content-resource.ts | 62 +++ ...common-cartridge-web-link-resource.spec.ts | 130 ++++++ .../common-cartridge-web-link-resource.ts | 67 +++ .../export/resources/v1.3.0/index.ts | 7 + .../common-cartridge/export/utils.spec.ts | 84 ++++ .../modules/common-cartridge/export/utils.ts | 45 ++ .../common-cartridge-manifest-parser.spec.ts | 4 +- .../src/modules/common-cartridge/index.ts | 18 + .../common-cartridge/testing/assets/README.md | 7 + .../assets}/us_history_since_1877.imscc | Bin .../testing/assets/v1.1.0/manifest.xml | 42 ++ .../testing/assets/v1.1.0/weblink.xml | 5 + .../testing/assets/v1.3.0/manifest.xml | 42 ++ .../testing/assets/v1.3.0/weblink.xml | 5 + .../common-cartridge-element-props.factory.ts | 96 +++++ ...common-cartridge-resource-props.factory.ts | 81 ++++ .../common-cartridge-element.interface.ts | 3 - .../common-cartridge-enums.ts | 18 - .../common-cartridge-file-builder.spec.ts | 105 ----- .../common-cartridge-file-builder.ts | 109 ----- .../common-cartridge-file.interface.ts | 4 - ...common-cartridge-lesson-content-element.ts | 8 - .../common-cartridge-lti-resource.spec.ts | 144 ------- .../common-cartridge-lti-resource.ts | 94 ---- .../common-cartridge-manifest-element.spec.ts | 149 ------- .../common-cartridge-manifest-element.ts | 64 --- .../common-cartridge-metadata-element.spec.ts | 37 -- .../common-cartridge-metadata-element.ts | 35 -- ...artridge-organization-item-element.spec.ts | 44 -- ...mon-cartridge-organization-item-element.ts | 32 -- ...ridge-organization-wrapper-element.spec.ts | 40 -- ...-cartridge-organization-wrapper-element.ts | 26 -- ...on-cartridge-resource-item-element.spec.ts | 15 - .../common-cartridge-resource-item-element.ts | 49 --- ...cartridge-resource-wrapper-element.spec.ts | 25 -- ...mmon-cartridge-resource-wrapper-element.ts | 11 - ...mon-cartridge-web-content-resource.spec.ts | 46 -- .../common-cartridge-web-content-resource.ts | 44 -- ...common-cartridge-web-link-resource.spec.ts | 77 ---- .../common-cartridge-web-link-resource.ts | 74 ---- .../common-cartridge.config.ts | 3 - .../learnroom/common-cartridge/index.ts | 14 - .../learnroom/common-cartridge/utils.ts | 6 - .../controller/api-test/course.api.spec.ts | 8 +- .../learnroom/controller/course.controller.ts | 4 +- .../controller/dto/course.query.params.ts | 2 +- apps/server/src/modules/learnroom/index.ts | 3 +- .../src/modules/learnroom/learnroom.config.ts | 2 + .../src/modules/learnroom/learnroom.module.ts | 10 +- .../mapper/common-cartridge.mapper.spec.ts | 400 ++++++++++++++++++ .../mapper/common-cartridge.mapper.ts | 120 ++++++ .../common-cartridge-export.service.spec.ts | 249 +++++------ .../common-cartridge-export.service.ts | 165 ++------ .../common-cartridge-import.service.spec.ts | 4 +- .../learnroom/uc/course-export.uc.spec.ts | 39 +- .../modules/learnroom/uc/course-export.uc.ts | 14 +- ...mmon-cartridge-file-validator.pipe.spec.ts | 4 +- .../modules/server/api/dto/config.response.ts | 8 +- .../server/api/test/server.api.spec.ts | 3 +- .../src/modules/server/server.config.ts | 27 +- .../context-external-tool.module.ts | 12 +- .../school-external-tool.module.ts | 4 +- config/default.schema.json | 9 +- config/development.json | 2 +- config/test.json | 2 +- src/services/config/publicAppConfigService.js | 2 +- 115 files changed, 4488 insertions(+), 1586 deletions(-) create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.ts create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.ts create mode 100644 apps/server/src/modules/common-cartridge/export/common-cartridge.enums.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.1.0/index.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.ts create mode 100644 apps/server/src/modules/common-cartridge/export/elements/v1.3.0/index.ts create mode 100644 apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-element.interface.ts create mode 100644 apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-resource.interface.ts create mode 100644 apps/server/src/modules/common-cartridge/export/interfaces/index.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.1.0/index.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.ts create mode 100644 apps/server/src/modules/common-cartridge/export/resources/v1.3.0/index.ts create mode 100644 apps/server/src/modules/common-cartridge/export/utils.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/export/utils.ts create mode 100644 apps/server/src/modules/common-cartridge/index.ts create mode 100644 apps/server/src/modules/common-cartridge/testing/assets/README.md rename apps/server/{test/assets/common-cartridge => src/modules/common-cartridge/testing/assets}/us_history_since_1877.imscc (100%) create mode 100644 apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/manifest.xml create mode 100644 apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/weblink.xml create mode 100644 apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/manifest.xml create mode 100644 apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/weblink.xml create mode 100644 apps/server/src/modules/common-cartridge/testing/common-cartridge-element-props.factory.ts create mode 100644 apps/server/src/modules/common-cartridge/testing/common-cartridge-resource-props.factory.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-element.interface.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-enums.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file.interface.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lesson-content-element.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.spec.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/common-cartridge.config.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/index.ts delete mode 100644 apps/server/src/modules/learnroom/common-cartridge/utils.ts create mode 100644 apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts create mode 100644 apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts diff --git a/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts b/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts index a953d0b558a..a6e1669f869 100644 --- a/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts +++ b/apps/server/src/infra/identity-management/keycloak-administration/service/keycloak-administration.service.ts @@ -8,7 +8,7 @@ export class KeycloakAdministrationService { private static AUTHORIZATION_TIMEBOX_MS = 59 * 1000; - public constructor( + constructor( private readonly kcAdminClient: KeycloakAdminClient, @Inject(KeycloakSettings) private readonly kcSettings: IKeycloakSettings ) { diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.spec.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.spec.ts new file mode 100644 index 00000000000..5c5db130cf5 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.spec.ts @@ -0,0 +1,110 @@ +import { faker } from '@faker-js/faker'; +import AdmZip from 'adm-zip'; +import { + CommonCartridgeElementType, + CommonCartridgeIntendedUseType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../common-cartridge.enums'; +import { CommonCartridgeElementProps } from '../elements/common-cartridge-element-factory'; +import { CommonCartridgeResourceProps } from '../resources/common-cartridge-resource-factory'; +import { CommonCartridgeFileBuilder } from './common-cartridge-file-builder'; +import { CommonCartridgeOrganizationBuilderOptions } from './common-cartridge-organization-builder'; + +describe('CommonCartridgeFileBuilder', () => { + const getFileContentAsString = (zip: AdmZip, path: string): string | undefined => + zip.getEntry(path)?.getData().toString(); + + describe('build', () => { + describe('when a common cartridge archive has been created', () => { + const setup = async () => { + const metadataProps: CommonCartridgeElementProps = { + type: CommonCartridgeElementType.METADATA, + title: faker.lorem.words(), + creationDate: new Date(), + copyrightOwners: ['John Doe', 'Jane Doe'], + }; + const organizationOptions: CommonCartridgeOrganizationBuilderOptions = { + identifier: faker.string.uuid(), + title: faker.lorem.words(), + }; + const resourceProps: CommonCartridgeResourceProps = { + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: faker.string.uuid(), + title: faker.lorem.words(), + html: faker.lorem.paragraphs(), + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }; + const builder = new CommonCartridgeFileBuilder({ + version: CommonCartridgeVersion.V_1_1_0, + identifier: faker.string.uuid(), + }); + + builder + .addMetadata(metadataProps) + .addOrganization(organizationOptions) + .addResource(resourceProps) + .addSubOrganization(organizationOptions) + .addResource(resourceProps) + .addSubOrganization(organizationOptions) + .addResource(resourceProps); + + const archive = new AdmZip(await builder.build()); + + return { archive, metadataProps, organizationOptions, resourceProps }; + }; + + it('should have a imsmanifest.xml in archive root', async () => { + const { archive } = await setup(); + + const manifest = getFileContentAsString(archive, 'imsmanifest.xml'); + + expect(manifest).toBeDefined(); + }); + + it('should have included the resource in organization folder', async () => { + const { archive, organizationOptions, resourceProps } = await setup(); + + const resource = getFileContentAsString( + archive, + `${organizationOptions.identifier}/${resourceProps.identifier}.html` + ); + + expect(resource).toBeDefined(); + }); + + it('should have included the resource in sub-organization folder', async () => { + const { archive, organizationOptions, resourceProps } = await setup(); + + const resource = getFileContentAsString( + archive, + `${organizationOptions.identifier}/${organizationOptions.identifier}/${resourceProps.identifier}.html` + ); + + expect(resource).toBeDefined(); + }); + + it('should have included the resource in sub-sub-organization folder', async () => { + const { archive, organizationOptions, resourceProps } = await setup(); + + const resource = getFileContentAsString( + archive, + `${organizationOptions.identifier}/${organizationOptions.identifier}/${organizationOptions.identifier}/${resourceProps.identifier}.html` + ); + + expect(resource).toBeDefined(); + }); + }); + + describe('when metadata has not been provide', () => { + const sut = new CommonCartridgeFileBuilder({ + version: CommonCartridgeVersion.V_1_1_0, + identifier: faker.string.uuid(), + }); + + it('should throw an error', async () => { + await expect(sut.build()).rejects.toThrow('Metadata is not defined'); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.ts new file mode 100644 index 00000000000..0f605b5561d --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-file-builder.ts @@ -0,0 +1,80 @@ +import AdmZip from 'adm-zip'; +import { CommonCartridgeResourceType, CommonCartridgeVersion } from '../common-cartridge.enums'; +import { + CommonCartridgeElementFactory, + CommonCartridgeElementProps, +} from '../elements/common-cartridge-element-factory'; +import { CommonCartridgeElement, CommonCartridgeResource } from '../interfaces'; +import { CommonCartridgeResourceFactory } from '../resources/common-cartridge-resource-factory'; +import { OmitVersion } from '../utils'; +import { + CommonCartridgeOrganizationBuilder, + CommonCartridgeOrganizationBuilderOptions, +} from './common-cartridge-organization-builder'; + +export type CommonCartridgeFileBuilderProps = { + version: CommonCartridgeVersion; + identifier: string; +}; + +export class CommonCartridgeFileBuilder { + private readonly archive: AdmZip = new AdmZip(); + + private readonly organizationBuilders = new Array(); + + private readonly resources = new Array(); + + private metadata?: CommonCartridgeElement; + + constructor(private readonly props: CommonCartridgeFileBuilderProps) {} + + public addMetadata(props: CommonCartridgeElementProps): CommonCartridgeFileBuilder { + this.metadata = CommonCartridgeElementFactory.createElement({ + version: this.props.version, + ...props, + }); + + return this; + } + + public addOrganization( + props: OmitVersion + ): CommonCartridgeOrganizationBuilder { + const builder = new CommonCartridgeOrganizationBuilder( + { ...props, version: this.props.version }, + (resource: CommonCartridgeResource) => this.resources.push(resource) + ); + + this.organizationBuilders.push(builder); + + return builder; + } + + public async build(): Promise { + if (!this.metadata) { + throw new Error('Metadata is not defined'); + } + + const organizations = this.organizationBuilders.map((builder) => builder.build()); + const manifest = CommonCartridgeResourceFactory.createResource({ + type: CommonCartridgeResourceType.MANIFEST, + version: this.props.version, + identifier: this.props.identifier, + metadata: this.metadata, + organizations, + resources: this.resources, + }); + + for (const resources of this.resources) { + if (!resources.canInline()) { + this.archive.addFile(resources.getFilePath(), Buffer.from(resources.getFileContent())); + } + } + + this.archive.addFile(manifest.getFilePath(), Buffer.from(manifest.getFileContent())); + + const buffer = await this.archive.toBufferPromise(); + + return buffer; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.spec.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.spec.ts new file mode 100644 index 00000000000..ba9a36001ae --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.spec.ts @@ -0,0 +1,87 @@ +import { faker } from '@faker-js/faker/locale/af_ZA'; +import { createCommonCartridgeWebContentResourcePropsV110 } from '../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeElement, CommonCartridgeResource } from '../interfaces'; +import { + CommonCartridgeOrganizationBuilder, + CommonCartridgeOrganizationBuilderOptions, +} from './common-cartridge-organization-builder'; + +describe('CommonCartridgeOrganizationBuilder', () => { + describe('build', () => { + describe('when building a Common Cartridge organization with resources', () => { + const setup = () => { + const resources = new Array(); + + const organizationOptions: CommonCartridgeOrganizationBuilderOptions = { + identifier: faker.string.uuid(), + title: faker.lorem.words(), + }; + + const resourceProps = createCommonCartridgeWebContentResourcePropsV110(); + + const sut = new CommonCartridgeOrganizationBuilder( + { + ...organizationOptions, + version: CommonCartridgeVersion.V_1_1_0, + }, + (resource) => resources.push(resource) + ) + .addResource(resourceProps) + .addSubOrganization(organizationOptions) + .addResource(resourceProps) + .addSubOrganization(organizationOptions) + .addResource(resourceProps); + + return { sut, resources }; + }; + + it('should return a common cartridge element', () => { + const { sut, resources } = setup(); + + const element = sut.build(); + + expect(element).toBeInstanceOf(CommonCartridgeElement); + expect(resources.length).toBe(3); + }); + }); + + describe('when building a Common Cartridge organization with items', () => { + const setup = () => { + const resources = new Array(); + + const organizationOptions: CommonCartridgeOrganizationBuilderOptions = { + identifier: faker.string.uuid(), + title: faker.lorem.words(), + }; + + const resourceProps = createCommonCartridgeWebContentResourcePropsV110(); + + const sut = new CommonCartridgeOrganizationBuilder( + { + ...organizationOptions, + version: CommonCartridgeVersion.V_1_1_0, + }, + (resource) => resources.push(resource) + ) + .addResource(resourceProps) + .addSubOrganization(organizationOptions) + .addResource(resourceProps) + .addSubOrganization(organizationOptions) + .addResource(resourceProps) + .addResource(resourceProps); + + return { sut, resources }; + }; + + it('should return a common cartridge element', () => { + const { sut, resources } = setup(); + + const element = sut.build(); + + expect(element).toBeInstanceOf(CommonCartridgeElement); + expect(resources.length).toBe(4); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.ts b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.ts new file mode 100644 index 00000000000..20ff6d3e528 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/builders/common-cartridge-organization-builder.ts @@ -0,0 +1,82 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeElementFactory } from '../elements/common-cartridge-element-factory'; +import { CommonCartridgeElement } from '../interfaces/common-cartridge-element.interface'; +import { CommonCartridgeResource } from '../interfaces/common-cartridge-resource.interface'; +import { + CommonCartridgeResourceFactory, + CommonCartridgeResourceProps, +} from '../resources/common-cartridge-resource-factory'; +import { OmitVersionAndFolder } from '../utils'; + +export type CommonCartridgeOrganizationBuilderOptions = + OmitVersionAndFolder; + +type CommonCartridgeOrganizationBuilderOptionsInternal = { + version: CommonCartridgeVersion; + identifier: string; + title: string; + folder?: string; +}; + +export class CommonCartridgeOrganizationBuilder { + private readonly resources: CommonCartridgeResource[] = []; + + private readonly subOrganizations: CommonCartridgeOrganizationBuilder[] = []; + + constructor( + protected readonly options: CommonCartridgeOrganizationBuilderOptionsInternal, + private readonly addResourceToFileBuilder: (resource: CommonCartridgeResource) => void + ) {} + + private get folder(): string { + return this.options.folder ? `${this.options.folder}/${this.options.identifier}` : this.options.identifier; + } + + public addSubOrganization( + options: OmitVersionAndFolder + ): CommonCartridgeOrganizationBuilder { + const subOrganization = new CommonCartridgeOrganizationBuilder( + { ...options, version: this.options.version, folder: this.folder }, + (resource: CommonCartridgeResource) => this.addResourceToFileBuilder(resource) + ); + + this.subOrganizations.push(subOrganization); + + return subOrganization; + } + + public addResource(props: CommonCartridgeResourceProps): CommonCartridgeOrganizationBuilder { + const resource = CommonCartridgeResourceFactory.createResource({ + version: this.options.version, + folder: this.folder, + ...props, + }); + + this.resources.push(resource); + this.addResourceToFileBuilder(resource); + + return this; + } + + public build(): CommonCartridgeElement { + const organizationElement = CommonCartridgeElementFactory.createElement({ + type: CommonCartridgeElementType.ORGANIZATION, + version: this.options.version, + identifier: this.options.identifier, + title: this.options.title, + items: this.buildItems(), + }); + + return organizationElement; + } + + private buildItems(): (CommonCartridgeElement | CommonCartridgeResource)[] { + if (this.resources.length === 1 && this.subOrganizations.length === 0) { + return [...this.resources]; + } + + const items = [...this.resources, ...this.subOrganizations.map((subOrganization) => subOrganization.build())]; + + return items; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/common-cartridge.enums.ts b/apps/server/src/modules/common-cartridge/export/common-cartridge.enums.ts new file mode 100644 index 00000000000..8e474d7c3df --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/common-cartridge.enums.ts @@ -0,0 +1,28 @@ +export enum CommonCartridgeVersion { + V_1_0_0 = '1.0.0', + V_1_1_0 = '1.1.0', + V_1_2_0 = '1.2.0', + V_1_3_0 = '1.3.0', + V_1_4_0 = '1.4.0', +} + +export enum CommonCartridgeResourceType { + UNKNOWN = 'unknown', + MANIFEST = 'manifest', + WEB_CONTENT = 'webcontent', + WEB_LINK = 'weblink', +} + +export enum CommonCartridgeIntendedUseType { + ASSIGNMENT = 'assignment', + LESSON_PLAN = 'lessonplan', + SYLLABUS = 'syllabus', + UNSPECIFIED = 'unspecified', +} + +export enum CommonCartridgeElementType { + METADATA = 'metadata', + ORGANIZATION = 'organization', + RESOURCES_WRAPPER = 'resourceswrapper', + ORGANIZATIONS_WRAPPER = 'organizationswrapper', +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.spec.ts new file mode 100644 index 00000000000..b891d1aa49a --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.spec.ts @@ -0,0 +1,51 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { + createCommonCartridgeMetadataElementPropsV110, + createCommonCartridgeMetadataElementPropsV130, +} from '../../testing/common-cartridge-element-props.factory'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeElementFactory } from './common-cartridge-element-factory'; +import { CommonCartridgeMetadataElementPropsV110, CommonCartridgeMetadataElementV110 } from './v1.1.0'; +import { CommonCartridgeMetadataElementV130 } from './v1.3.0'; + +describe('CommonCartridgeElementFactory', () => { + describe('createElement', () => { + describe('when Common Cartridge versions is supported', () => { + const propsV110 = createCommonCartridgeMetadataElementPropsV110(); + const propsV130 = createCommonCartridgeMetadataElementPropsV130(); + + it('should return v1.1.0 element', () => { + const result = CommonCartridgeElementFactory.createElement(propsV110); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(CommonCartridgeMetadataElementV110); + }); + + it('should return v1.3.0 element', () => { + const result = CommonCartridgeElementFactory.createElement(propsV130); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(CommonCartridgeMetadataElementV130); + }); + }); + + describe('when versions is not supported', () => { + const notSupportedVersions = [ + CommonCartridgeVersion.V_1_0_0, + CommonCartridgeVersion.V_1_2_0, + CommonCartridgeVersion.V_1_4_0, + ]; + + it('should throw InternalServerErrorException', () => { + notSupportedVersions.forEach((version) => { + expect(() => + CommonCartridgeElementFactory.createElement({ + version, + type: CommonCartridgeElementType.METADATA, + } as CommonCartridgeMetadataElementPropsV110) + ).toThrow(InternalServerErrorException); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.ts b/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.ts new file mode 100644 index 00000000000..046f7033fde --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/common-cartridge-element-factory.ts @@ -0,0 +1,52 @@ +import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeElement } from '../interfaces'; +import { OmitVersionAndFolder, createVersionNotSupportedError } from '../utils'; +import { + CommonCartridgeElementFactoryV110, + CommonCartridgeMetadataElementPropsV110, + CommonCartridgeOrganizationElementPropsV110, + CommonCartridgeOrganizationsWrapperElementPropsV110, + CommonCartridgeResourcesWrapperElementPropsV110, +} from './v1.1.0'; +import { + CommonCartridgeElementFactoryV130, + CommonCartridgeMetadataElementPropsV130, + CommonCartridgeOrganizationElementPropsV130, + CommonCartridgeOrganizationsWrapperElementPropsV130, + CommonCartridgeResourcesWrapperElementPropsV130, +} from './v1.3.0'; + +export type CommonCartridgeElementProps = + | OmitVersionAndFolder + | OmitVersionAndFolder + | OmitVersionAndFolder + | OmitVersionAndFolder + | OmitVersionAndFolder + | OmitVersionAndFolder + | OmitVersionAndFolder + | OmitVersionAndFolder; + +type CommonCartridgeElementPropsInternal = + | CommonCartridgeMetadataElementPropsV110 + | CommonCartridgeOrganizationElementPropsV110 + | CommonCartridgeOrganizationsWrapperElementPropsV110 + | CommonCartridgeResourcesWrapperElementPropsV110 + | CommonCartridgeMetadataElementPropsV130 + | CommonCartridgeOrganizationElementPropsV130 + | CommonCartridgeOrganizationsWrapperElementPropsV130 + | CommonCartridgeResourcesWrapperElementPropsV130; + +export class CommonCartridgeElementFactory { + public static createElement(props: CommonCartridgeElementPropsInternal): CommonCartridgeElement { + const { version } = props; + + switch (version) { + case CommonCartridgeVersion.V_1_1_0: + return CommonCartridgeElementFactoryV110.createElement(props); + case CommonCartridgeVersion.V_1_3_0: + return CommonCartridgeElementFactoryV130.createElement(props); + default: + throw createVersionNotSupportedError(version); + } + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.spec.ts new file mode 100644 index 00000000000..3c43cf3a830 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.spec.ts @@ -0,0 +1,63 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { + createCommonCartridgeMetadataElementPropsV110, + createCommonCartridgeOrganizationElementPropsV110, + createCommonCartridgeOrganizationsWrapperElementPropsV110, + createCommonCartridgeResourcesWrapperElementPropsV110, +} from '../../../testing/common-cartridge-element-props.factory'; +import { CommonCartridgeElementType } from '../../common-cartridge.enums'; +import { CommonCartridgeElementFactoryV110 } from './common-cartridge-element-factory'; +import { CommonCartridgeMetadataElementV110 } from './common-cartridge-metadata-element'; +import { CommonCartridgeOrganizationElementV110 } from './common-cartridge-organization-element'; +import { CommonCartridgeOrganizationsWrapperElementV110 } from './common-cartridge-organizations-wrapper-element'; +import { CommonCartridgeResourcesWrapperElementV110 } from './common-cartridge-resources-wrapper-element'; + +describe('CommonCartridgeElementFactoryV110', () => { + describe('createElement', () => { + describe('when creating elements from props', () => { + it('should return metadata element', () => { + const props = createCommonCartridgeMetadataElementPropsV110(); + + const result = CommonCartridgeElementFactoryV110.createElement(props); + + expect(result).toBeInstanceOf(CommonCartridgeMetadataElementV110); + }); + + it('should return organization element', () => { + const props = createCommonCartridgeOrganizationElementPropsV110(); + + const result = CommonCartridgeElementFactoryV110.createElement(props); + + expect(result).toBeInstanceOf(CommonCartridgeOrganizationElementV110); + }); + + it('should return organization wrapper element', () => { + const props = createCommonCartridgeOrganizationsWrapperElementPropsV110(); + + const result = CommonCartridgeElementFactoryV110.createElement(props); + + expect(result).toBeInstanceOf(CommonCartridgeOrganizationsWrapperElementV110); + }); + + it('should return resources wrapper element', () => { + const props = createCommonCartridgeResourcesWrapperElementPropsV110(); + + const result = CommonCartridgeElementFactoryV110.createElement(props); + + expect(result).toBeInstanceOf(CommonCartridgeResourcesWrapperElementV110); + }); + }); + + describe('when element type is not supported', () => { + const notSupportedProps = createCommonCartridgeMetadataElementPropsV110(); + + notSupportedProps.type = 'not-supported' as CommonCartridgeElementType.METADATA; + + it('should throw error', () => { + expect(() => CommonCartridgeElementFactoryV110.createElement(notSupportedProps)).toThrow( + InternalServerErrorException + ); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.ts new file mode 100644 index 00000000000..88095cf218a --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-element-factory.ts @@ -0,0 +1,45 @@ +import { CommonCartridgeElementType } from '../../common-cartridge.enums'; +import { CommonCartridgeElement } from '../../interfaces'; +import { createElementTypeNotSupportedError } from '../../utils'; +import { + CommonCartridgeMetadataElementPropsV110, + CommonCartridgeMetadataElementV110, +} from './common-cartridge-metadata-element'; +import { + CommonCartridgeOrganizationElementPropsV110, + CommonCartridgeOrganizationElementV110, +} from './common-cartridge-organization-element'; +import { + CommonCartridgeOrganizationsWrapperElementPropsV110, + CommonCartridgeOrganizationsWrapperElementV110, +} from './common-cartridge-organizations-wrapper-element'; +import { + CommonCartridgeResourcesWrapperElementPropsV110, + CommonCartridgeResourcesWrapperElementV110, +} from './common-cartridge-resources-wrapper-element'; + +type CommonCartridgeElementPropsV110 = + | CommonCartridgeMetadataElementPropsV110 + | CommonCartridgeOrganizationElementPropsV110 + | CommonCartridgeOrganizationsWrapperElementPropsV110 + | CommonCartridgeResourcesWrapperElementPropsV110; + +export class CommonCartridgeElementFactoryV110 { + public static createElement(props: CommonCartridgeElementPropsV110): CommonCartridgeElement { + const { type } = props; + + switch (type) { + // AI next 8 lines + case CommonCartridgeElementType.METADATA: + return new CommonCartridgeMetadataElementV110(props); + case CommonCartridgeElementType.ORGANIZATION: + return new CommonCartridgeOrganizationElementV110(props); + case CommonCartridgeElementType.ORGANIZATIONS_WRAPPER: + return new CommonCartridgeOrganizationsWrapperElementV110(props); + case CommonCartridgeElementType.RESOURCES_WRAPPER: + return new CommonCartridgeResourcesWrapperElementV110(props); + default: + throw createElementTypeNotSupportedError(type); + } + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.spec.ts new file mode 100644 index 00000000000..cd12efa1590 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.spec.ts @@ -0,0 +1,71 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { createCommonCartridgeMetadataElementPropsV110 } from '../../../testing/common-cartridge-element-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeMetadataElementV110 } from './common-cartridge-metadata-element'; + +describe('CommonCartridgeMetadataElementV110', () => { + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeMetadataElementPropsV110(); + const sut = new CommonCartridgeMetadataElementV110(props); + + return { sut, props }; + }; + + it('should return correct version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_1_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeMetadataElementPropsV110(); + notSupportedProps.version = CommonCartridgeVersion.V_1_3_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeMetadataElementV110(notSupportedProps)).toThrow(InternalServerErrorException); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using Common Cartridge version 1.1', () => { + const setup = () => { + const props = createCommonCartridgeMetadataElementPropsV110(); + const sut = new CommonCartridgeMetadataElementV110(props); + + return { sut, props }; + }; + + it('should return correct manifest xml object', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toStrictEqual({ + schema: 'IMS Common Cartridge', + schemaversion: '1.1.0', + 'mnf:lom': { + 'mnf:general': { + 'mnf:title': { + 'mnf:string': props.title, + }, + }, + 'mnf:rights': { + 'mnf:copyrightAndOtherRestrictions': { + 'mnf:value': 'yes', + }, + 'mnf:description': { + 'mnf:string': `${props.creationDate.getFullYear()} ${props.copyrightOwners.join(', ')}`, + }, + }, + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.ts new file mode 100644 index 00000000000..c70cbd2c892 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-metadata-element.ts @@ -0,0 +1,42 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElement } from '../../interfaces'; + +export type CommonCartridgeMetadataElementPropsV110 = { + type: CommonCartridgeElementType.METADATA; + version: CommonCartridgeVersion; + title: string; + creationDate: Date; + copyrightOwners: string[]; +}; + +export class CommonCartridgeMetadataElementV110 extends CommonCartridgeElement { + constructor(private readonly props: CommonCartridgeMetadataElementPropsV110) { + super(props); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(): Record { + return { + schema: 'IMS Common Cartridge', + schemaversion: '1.1.0', + 'mnf:lom': { + 'mnf:general': { + 'mnf:title': { + 'mnf:string': this.props.title, + }, + }, + 'mnf:rights': { + 'mnf:copyrightAndOtherRestrictions': { + 'mnf:value': 'yes', + }, + 'mnf:description': { + 'mnf:string': `${this.props.creationDate.getFullYear()} ${this.props.copyrightOwners.join(', ')}`, + }, + }, + }, + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.spec.ts new file mode 100644 index 00000000000..8e1be0c920f --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.spec.ts @@ -0,0 +1,101 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { createCommonCartridgeOrganizationElementPropsV110 } from '../../../testing/common-cartridge-element-props.factory'; +import { createCommonCartridgeWeblinkResourcePropsV110 } from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeResourceFactory } from '../../resources/common-cartridge-resource-factory'; +import { CommonCartridgeElementFactory } from '../common-cartridge-element-factory'; +import { CommonCartridgeOrganizationElementV110 } from './common-cartridge-organization-element'; + +describe('CommonCartridgeOrganizationElementV110', () => { + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeOrganizationElementPropsV110(); + const sut = new CommonCartridgeOrganizationElementV110(props); + + return { sut }; + }; + + it('should return correct version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_1_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeOrganizationElementPropsV110(); + notSupportedProps.version = CommonCartridgeVersion.V_1_3_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeOrganizationElementV110(notSupportedProps)).toThrowError( + InternalServerErrorException + ); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const resourceProps = createCommonCartridgeWeblinkResourcePropsV110(); + + const subOrganization1Props = createCommonCartridgeOrganizationElementPropsV110( + CommonCartridgeResourceFactory.createResource(resourceProps) + ); + + const subOrganization2Props = createCommonCartridgeOrganizationElementPropsV110([ + CommonCartridgeResourceFactory.createResource(resourceProps), + ]); + + const organizationProps = createCommonCartridgeOrganizationElementPropsV110([ + CommonCartridgeElementFactory.createElement(subOrganization1Props), + CommonCartridgeElementFactory.createElement(subOrganization2Props), + ]); + + const sut = new CommonCartridgeOrganizationElementV110(organizationProps); + + return { sut, organizationProps, subOrganization1Props, subOrganization2Props, resourceProps }; + }; + + it('should return correct manifest xml object', () => { + const { sut, organizationProps, subOrganization1Props, subOrganization2Props, resourceProps } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toStrictEqual({ + $: { + identifier: organizationProps.identifier, + }, + title: organizationProps.title, + item: [ + { + $: { + identifier: subOrganization1Props.identifier, + identifierref: resourceProps.identifier, + }, + title: subOrganization1Props.title, + }, + { + $: { + identifier: subOrganization2Props.identifier, + }, + title: subOrganization2Props.title, + item: [ + { + $: { + identifier: expect.any(String), + identifierref: resourceProps.identifier, + }, + title: resourceProps.title, + }, + ], + }, + ], + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.ts new file mode 100644 index 00000000000..ff304d6ea08 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organization-element.ts @@ -0,0 +1,53 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElement, CommonCartridgeResource } from '../../interfaces'; +import { createIdentifier } from '../../utils'; + +export type CommonCartridgeOrganizationElementPropsV110 = { + type: CommonCartridgeElementType.ORGANIZATION; + version: CommonCartridgeVersion; + identifier: string; + title: string; + items: CommonCartridgeResource | Array; +}; + +export class CommonCartridgeOrganizationElementV110 extends CommonCartridgeElement { + constructor(protected readonly props: CommonCartridgeOrganizationElementPropsV110) { + super(props); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(): Record { + if (this.props.items instanceof CommonCartridgeResource) { + return { + $: { + identifier: this.identifier, + identifierref: this.props.items.identifier, + }, + title: this.title, + }; + } + + return { + $: { + identifier: this.identifier, + }, + title: this.title, + item: this.props.items.map((item) => { + if (item instanceof CommonCartridgeResource) { + return { + $: { + identifier: createIdentifier(), + identifierref: item.identifier, + }, + title: item.title, + }; + } + + return item.getManifestXmlObject(); + }), + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.spec.ts new file mode 100644 index 00000000000..fb5ae465a28 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.spec.ts @@ -0,0 +1,87 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { + createCommonCartridgeOrganizationElementPropsV110, + createCommonCartridgeOrganizationsWrapperElementPropsV110, +} from '../../../testing/common-cartridge-element-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementFactory } from '../common-cartridge-element-factory'; +import { CommonCartridgeOrganizationsWrapperElementV110 } from './common-cartridge-organizations-wrapper-element'; + +describe('CommonCartridgeOrganizationsWrapperElementV110', () => { + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeOrganizationsWrapperElementPropsV110(); + const sut = new CommonCartridgeOrganizationsWrapperElementV110(props); + + return { sut, props }; + }; + + it('should return correct version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_1_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeOrganizationsWrapperElementPropsV110(); + notSupportedProps.version = CommonCartridgeVersion.V_1_3_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeOrganizationsWrapperElementV110(notSupportedProps)).toThrowError( + InternalServerErrorException + ); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const organizationProps = createCommonCartridgeOrganizationElementPropsV110(); + const props = createCommonCartridgeOrganizationsWrapperElementPropsV110([ + CommonCartridgeElementFactory.createElement(organizationProps), + ]); + const sut = new CommonCartridgeOrganizationsWrapperElementV110(props); + + return { sut, organizationProps }; + }; + + it('should return correct manifest xml object', () => { + const { sut, organizationProps } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toStrictEqual({ + organization: [ + { + $: { + identifier: 'org-1', + structure: 'rooted-hierarchy', + }, + item: [ + { + $: { + identifier: 'LearningModules', + }, + item: [ + { + $: { + identifier: organizationProps.identifier, + }, + title: organizationProps.title, + item: [], + }, + ], + }, + ], + }, + ], + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.ts new file mode 100644 index 00000000000..9d1c44ec85d --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-organizations-wrapper-element.ts @@ -0,0 +1,39 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElement } from '../../interfaces'; + +export type CommonCartridgeOrganizationsWrapperElementPropsV110 = { + type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER; + version: CommonCartridgeVersion; + items: CommonCartridgeElement[]; +}; + +export class CommonCartridgeOrganizationsWrapperElementV110 extends CommonCartridgeElement { + constructor(private readonly props: CommonCartridgeOrganizationsWrapperElementPropsV110) { + super(props); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(): Record { + return { + organization: [ + { + $: { + identifier: 'org-1', + structure: 'rooted-hierarchy', + }, + item: [ + { + $: { + identifier: 'LearningModules', + }, + item: this.props.items.map((items) => items.getManifestXmlObject()), + }, + ], + }, + ], + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.spec.ts new file mode 100644 index 00000000000..0106eefbee5 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.spec.ts @@ -0,0 +1,78 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { createCommonCartridgeResourcesWrapperElementPropsV110 } from '../../../testing/common-cartridge-element-props.factory'; +import { createCommonCartridgeWeblinkResourcePropsV110 } from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeResourceFactory } from '../../resources/common-cartridge-resource-factory'; +import { CommonCartridgeResourcesWrapperElementV110 } from './common-cartridge-resources-wrapper-element'; + +describe('CommonCartridgeResourcesWrapperElementV110', () => { + describe('getSupportedVersion', () => { + describe('when using common cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeResourcesWrapperElementPropsV110(); + const sut = new CommonCartridgeResourcesWrapperElementV110(props); + + return { sut }; + }; + + it('should return correct version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_1_0); + }); + }); + + describe('when using not supported common cartridge version', () => { + const notSupportedProps = createCommonCartridgeResourcesWrapperElementPropsV110(); + notSupportedProps.version = CommonCartridgeVersion.V_1_3_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeResourcesWrapperElementV110(notSupportedProps)).toThrow( + InternalServerErrorException + ); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using common cartridge version 1.1.0', () => { + const setup = () => { + const resourceProps = createCommonCartridgeWeblinkResourcePropsV110(); + const props = createCommonCartridgeResourcesWrapperElementPropsV110([ + CommonCartridgeResourceFactory.createResource(resourceProps), + ]); + const sut = new CommonCartridgeResourcesWrapperElementV110(props); + + return { sut, resourceProps }; + }; + + it('should return correct manifest xml object', () => { + const { sut, resourceProps } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toStrictEqual({ + resources: [ + { + resource: [ + { + $: { + identifier: resourceProps.identifier, + type: expect.any(String), + }, + file: { + $: { + href: expect.any(String), + }, + }, + }, + ], + }, + ], + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.ts new file mode 100644 index 00000000000..4048787732a --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/common-cartridge-resources-wrapper-element.ts @@ -0,0 +1,28 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElement } from '../../interfaces'; + +export type CommonCartridgeResourcesWrapperElementPropsV110 = { + type: CommonCartridgeElementType.RESOURCES_WRAPPER; + version: CommonCartridgeVersion; + items: CommonCartridgeElement[]; +}; + +export class CommonCartridgeResourcesWrapperElementV110 extends CommonCartridgeElement { + constructor(private readonly props: CommonCartridgeResourcesWrapperElementPropsV110) { + super(props); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(): Record { + return { + resources: [ + { + resource: this.props.items.map((items) => items.getManifestXmlObject()), + }, + ], + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/index.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/index.ts new file mode 100644 index 00000000000..4fa057e5063 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.1.0/index.ts @@ -0,0 +1,8 @@ +export { CommonCartridgeElementFactoryV110 } from './common-cartridge-element-factory'; +export { + CommonCartridgeMetadataElementPropsV110, + CommonCartridgeMetadataElementV110, +} from './common-cartridge-metadata-element'; +export { CommonCartridgeOrganizationElementPropsV110 } from './common-cartridge-organization-element'; +export { CommonCartridgeOrganizationsWrapperElementPropsV110 } from './common-cartridge-organizations-wrapper-element'; +export { CommonCartridgeResourcesWrapperElementPropsV110 } from './common-cartridge-resources-wrapper-element'; diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.spec.ts new file mode 100644 index 00000000000..7fe5e1f0cf5 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.spec.ts @@ -0,0 +1,62 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { + createCommonCartridgeMetadataElementPropsV130, + createCommonCartridgeOrganizationElementPropsV130, + createCommonCartridgeOrganizationsWrapperElementPropsV130, + createCommonCartridgeResourcesWrapperElementPropsV130, +} from '../../../testing/common-cartridge-element-props.factory'; +import { CommonCartridgeElementType } from '../../common-cartridge.enums'; +import { CommonCartridgeElementFactoryV130 } from './common-cartridge-element-factory'; +import { CommonCartridgeMetadataElementV130 } from './common-cartridge-metadata-element'; +import { CommonCartridgeOrganizationElementV130 } from './common-cartridge-organization-element'; +import { CommonCartridgeOrganizationsWrapperElementV130 } from './common-cartridge-organizations-wrapper-element'; +import { CommonCartridgeResourcesWrapperElementV130 } from './common-cartridge-resources-wrapper-element'; + +describe('CommonCartridgeElementFactoryV130', () => { + describe('createElement', () => { + describe('when creating elements from props', () => { + it('should return metadata element', () => { + const props = createCommonCartridgeMetadataElementPropsV130(); + + const result = CommonCartridgeElementFactoryV130.createElement(props); + + expect(result).toBeInstanceOf(CommonCartridgeMetadataElementV130); + }); + + it('should return organization element', () => { + const props = createCommonCartridgeOrganizationElementPropsV130(); + + const result = CommonCartridgeElementFactoryV130.createElement(props); + + expect(result).toBeInstanceOf(CommonCartridgeOrganizationElementV130); + }); + + it('should return organization wrapper element', () => { + const props = createCommonCartridgeOrganizationsWrapperElementPropsV130(); + + const result = CommonCartridgeElementFactoryV130.createElement(props); + + expect(result).toBeInstanceOf(CommonCartridgeOrganizationsWrapperElementV130); + }); + + it('should return resources wrapper element', () => { + const props = createCommonCartridgeResourcesWrapperElementPropsV130(); + + const result = CommonCartridgeElementFactoryV130.createElement(props); + + expect(result).toBeInstanceOf(CommonCartridgeResourcesWrapperElementV130); + }); + }); + + describe('when element type is not supported', () => { + const notSupportedProps = createCommonCartridgeOrganizationsWrapperElementPropsV130(); + notSupportedProps.type = 'not-supported' as CommonCartridgeElementType.ORGANIZATIONS_WRAPPER; + + it('should throw error', () => { + expect(() => CommonCartridgeElementFactoryV130.createElement(notSupportedProps)).toThrow( + InternalServerErrorException + ); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.ts new file mode 100644 index 00000000000..6a1216e405a --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-element-factory.ts @@ -0,0 +1,45 @@ +import { CommonCartridgeElementType } from '../../common-cartridge.enums'; +import { CommonCartridgeElement } from '../../interfaces'; +import { createElementTypeNotSupportedError } from '../../utils'; +import { + CommonCartridgeMetadataElementPropsV130, + CommonCartridgeMetadataElementV130, +} from './common-cartridge-metadata-element'; +import { + CommonCartridgeOrganizationElementPropsV130, + CommonCartridgeOrganizationElementV130, +} from './common-cartridge-organization-element'; +import { + CommonCartridgeOrganizationsWrapperElementPropsV130, + CommonCartridgeOrganizationsWrapperElementV130, +} from './common-cartridge-organizations-wrapper-element'; +import { + CommonCartridgeResourcesWrapperElementPropsV130, + CommonCartridgeResourcesWrapperElementV130, +} from './common-cartridge-resources-wrapper-element'; + +type CommonCartridgeElementProps130 = + | CommonCartridgeMetadataElementPropsV130 + | CommonCartridgeOrganizationElementPropsV130 + | CommonCartridgeOrganizationsWrapperElementPropsV130 + | CommonCartridgeResourcesWrapperElementPropsV130; + +export class CommonCartridgeElementFactoryV130 { + public static createElement(props: CommonCartridgeElementProps130): CommonCartridgeElement { + const { type } = props; + + switch (type) { + // AI next 8 lines + case CommonCartridgeElementType.METADATA: + return new CommonCartridgeMetadataElementV130(props); + case CommonCartridgeElementType.ORGANIZATION: + return new CommonCartridgeOrganizationElementV130(props); + case CommonCartridgeElementType.ORGANIZATIONS_WRAPPER: + return new CommonCartridgeOrganizationsWrapperElementV130(props); + case CommonCartridgeElementType.RESOURCES_WRAPPER: + return new CommonCartridgeResourcesWrapperElementV130(props); + default: + throw createElementTypeNotSupportedError(type); + } + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.spec.ts new file mode 100644 index 00000000000..698b3efea04 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.spec.ts @@ -0,0 +1,71 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { createCommonCartridgeMetadataElementPropsV130 } from '../../../testing/common-cartridge-element-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeMetadataElementV130 } from './common-cartridge-metadata-element'; + +describe('CommonCartridgeMetadataElementV130', () => { + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeMetadataElementPropsV130(); + const sut = new CommonCartridgeMetadataElementV130(props); + + return { sut, props }; + }; + + it('should return correct version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_3_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeMetadataElementPropsV130(); + notSupportedProps.version = CommonCartridgeVersion.V_1_1_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeMetadataElementV130(notSupportedProps)).toThrow(InternalServerErrorException); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using common cartridge version 1.3', () => { + const setup = () => { + const props = createCommonCartridgeMetadataElementPropsV130(); + const sut = new CommonCartridgeMetadataElementV130(props); + + return { sut, props }; + }; + + it('should return correct manifest xml object', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toStrictEqual({ + schema: 'IMS Common Cartridge', + schemaversion: '1.3.0', + 'mnf:lom': { + 'mnf:general': { + 'mnf:title': { + 'mnf:string': props.title, + }, + }, + 'mnf:rights': { + 'mnf:copyrightAndOtherRestrictions': { + 'mnf:value': 'yes', + }, + 'mnf:description': { + 'mnf:string': `${props.creationDate.getFullYear()} ${props.copyrightOwners.join(', ')}`, + }, + }, + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.ts new file mode 100644 index 00000000000..a6477783e57 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-metadata-element.ts @@ -0,0 +1,42 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElement } from '../../interfaces'; + +export type CommonCartridgeMetadataElementPropsV130 = { + type: CommonCartridgeElementType.METADATA; + version: CommonCartridgeVersion; + title: string; + creationDate: Date; + copyrightOwners: string[]; +}; + +export class CommonCartridgeMetadataElementV130 extends CommonCartridgeElement { + constructor(private readonly props: CommonCartridgeMetadataElementPropsV130) { + super(props); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_3_0; + } + + public getManifestXmlObject(): Record { + return { + schema: 'IMS Common Cartridge', + schemaversion: '1.3.0', + 'mnf:lom': { + 'mnf:general': { + 'mnf:title': { + 'mnf:string': this.props.title, + }, + }, + 'mnf:rights': { + 'mnf:copyrightAndOtherRestrictions': { + 'mnf:value': 'yes', + }, + 'mnf:description': { + 'mnf:string': `${this.props.creationDate.getFullYear()} ${this.props.copyrightOwners.join(', ')}`, + }, + }, + }, + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.spec.ts new file mode 100644 index 00000000000..73cb9b1af5a --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.spec.ts @@ -0,0 +1,101 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { createCommonCartridgeOrganizationElementPropsV130 } from '../../../testing/common-cartridge-element-props.factory'; +import { createCommonCartridgeWeblinkResourcePropsV130 } from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeResourceFactory } from '../../resources/common-cartridge-resource-factory'; +import { CommonCartridgeElementFactory } from '../common-cartridge-element-factory'; +import { CommonCartridgeOrganizationElementV130 } from './common-cartridge-organization-element'; + +describe('CommonCartridgeOrganizationElementV130', () => { + describe('getSupportedVersion', () => { + describe('when using common cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeOrganizationElementPropsV130(); + const sut = new CommonCartridgeOrganizationElementV130(props); + + return { sut }; + }; + + it('should return correct version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_3_0); + }); + }); + + describe('when using not supported common cartridge version', () => { + const notSupportedProps = createCommonCartridgeOrganizationElementPropsV130(); + notSupportedProps.version = CommonCartridgeVersion.V_1_1_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeOrganizationElementV130(notSupportedProps)).toThrow( + InternalServerErrorException + ); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using common cartridge version 1.3.0', () => { + const setup = () => { + const resourceProps = createCommonCartridgeWeblinkResourcePropsV130(); + + const subOrganization1Props = createCommonCartridgeOrganizationElementPropsV130( + CommonCartridgeResourceFactory.createResource(resourceProps) + ); + + const subOrganization2Props = createCommonCartridgeOrganizationElementPropsV130([ + CommonCartridgeResourceFactory.createResource(resourceProps), + ]); + + const organizationProps = createCommonCartridgeOrganizationElementPropsV130([ + CommonCartridgeElementFactory.createElement(subOrganization1Props), + CommonCartridgeElementFactory.createElement(subOrganization2Props), + ]); + + const sut = new CommonCartridgeOrganizationElementV130(organizationProps); + + return { sut, organizationProps, subOrganization1Props, subOrganization2Props, resourceProps }; + }; + + it('should return correct manifest xml object', () => { + const { sut, organizationProps, subOrganization1Props, subOrganization2Props, resourceProps } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toStrictEqual({ + $: { + identifier: organizationProps.identifier, + }, + title: organizationProps.title, + item: [ + { + $: { + identifier: subOrganization1Props.identifier, + identifierref: resourceProps.identifier, + }, + title: subOrganization1Props.title, + }, + { + $: { + identifier: subOrganization2Props.identifier, + }, + title: subOrganization2Props.title, + item: [ + { + $: { + identifier: expect.any(String), + identifierref: resourceProps.identifier, + }, + title: resourceProps.title, + }, + ], + }, + ], + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.ts new file mode 100644 index 00000000000..14696edfd95 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organization-element.ts @@ -0,0 +1,53 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElement, CommonCartridgeResource } from '../../interfaces'; +import { createIdentifier } from '../../utils'; + +export type CommonCartridgeOrganizationElementPropsV130 = { + type: CommonCartridgeElementType.ORGANIZATION; + version: CommonCartridgeVersion; + identifier: string; + title: string; + items: CommonCartridgeResource | Array; +}; + +export class CommonCartridgeOrganizationElementV130 extends CommonCartridgeElement { + constructor(private readonly props: CommonCartridgeOrganizationElementPropsV130) { + super(props); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_3_0; + } + + public getManifestXmlObject(): Record { + if (this.props.items instanceof CommonCartridgeResource) { + return { + $: { + identifier: this.identifier, + identifierref: this.props.items.identifier, + }, + title: this.title, + }; + } + + return { + $: { + identifier: this.identifier, + }, + title: this.title, + item: this.props.items.map((item) => { + if (item instanceof CommonCartridgeResource) { + return { + $: { + identifier: createIdentifier(), + identifierref: item.identifier, + }, + title: item.title, + }; + } + + return item.getManifestXmlObject(); + }), + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.spec.ts new file mode 100644 index 00000000000..47c68046764 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.spec.ts @@ -0,0 +1,87 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { + createCommonCartridgeOrganizationElementPropsV130, + createCommonCartridgeOrganizationsWrapperElementPropsV130, +} from '../../../testing/common-cartridge-element-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElementFactory } from '../common-cartridge-element-factory'; +import { CommonCartridgeOrganizationsWrapperElementV130 } from './common-cartridge-organizations-wrapper-element'; + +describe('CommonCartridgeOrganizationsWrapperElementV130', () => { + describe('getSupportedVersion', () => { + describe('when using common cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeOrganizationsWrapperElementPropsV130(); + const sut = new CommonCartridgeOrganizationsWrapperElementV130(props); + + return { sut }; + }; + + it('should return correct version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_3_0); + }); + }); + + describe('when using not supported common cartridge version', () => { + const notSupportedProps = createCommonCartridgeOrganizationsWrapperElementPropsV130(); + notSupportedProps.version = CommonCartridgeVersion.V_1_1_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeOrganizationsWrapperElementV130(notSupportedProps)).toThrow( + InternalServerErrorException + ); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using common cartridge version 1.3.0', () => { + const setup = () => { + const organizationProps = createCommonCartridgeOrganizationElementPropsV130(); + const props = createCommonCartridgeOrganizationsWrapperElementPropsV130([ + CommonCartridgeElementFactory.createElement(organizationProps), + ]); + const sut = new CommonCartridgeOrganizationsWrapperElementV130(props); + + return { sut, organizationProps }; + }; + + it('should return correct manifest xml object', () => { + const { sut, organizationProps } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toStrictEqual({ + organization: [ + { + $: { + identifier: 'org-1', + structure: 'rooted-hierarchy', + }, + item: [ + { + $: { + identifier: 'LearningModules', + }, + item: [ + { + $: { + identifier: organizationProps.identifier, + }, + title: organizationProps.title, + item: [], + }, + ], + }, + ], + }, + ], + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.ts new file mode 100644 index 00000000000..3c00f4844d2 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-organizations-wrapper-element.ts @@ -0,0 +1,39 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElement } from '../../interfaces'; + +export type CommonCartridgeOrganizationsWrapperElementPropsV130 = { + type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER; + version: CommonCartridgeVersion; + items: CommonCartridgeElement[]; +}; + +export class CommonCartridgeOrganizationsWrapperElementV130 extends CommonCartridgeElement { + constructor(private readonly props: CommonCartridgeOrganizationsWrapperElementPropsV130) { + super(props); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_3_0; + } + + public getManifestXmlObject(): Record { + return { + organization: [ + { + $: { + identifier: 'org-1', + structure: 'rooted-hierarchy', + }, + item: [ + { + $: { + identifier: 'LearningModules', + }, + item: this.props.items.map((items) => items.getManifestXmlObject()), + }, + ], + }, + ], + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.spec.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.spec.ts new file mode 100644 index 00000000000..2d71adcc144 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.spec.ts @@ -0,0 +1,78 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { createCommonCartridgeResourcesWrapperElementPropsV130 } from '../../../testing/common-cartridge-element-props.factory'; +import { createCommonCartridgeWeblinkResourcePropsV130 } from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeResourceFactory } from '../../resources/common-cartridge-resource-factory'; +import { CommonCartridgeResourcesWrapperElementV130 } from './common-cartridge-resources-wrapper-element'; + +describe('CommonCartridgeResourcesWrapperElementV130', () => { + describe('getSupportedVersion', () => { + describe('when using common cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeResourcesWrapperElementPropsV130(); + const sut = new CommonCartridgeResourcesWrapperElementV130(props); + + return { sut }; + }; + + it('should return correct version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_3_0); + }); + }); + + describe('when using not supported common cartridge version', () => { + const notSupportedProps = createCommonCartridgeResourcesWrapperElementPropsV130(); + notSupportedProps.version = CommonCartridgeVersion.V_1_1_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeResourcesWrapperElementV130(notSupportedProps)).toThrowError( + InternalServerErrorException + ); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using common cartridge version 1.3.0', () => { + const setup = () => { + const weblinkResourceProps = createCommonCartridgeWeblinkResourcePropsV130(); + const props = createCommonCartridgeResourcesWrapperElementPropsV130([ + CommonCartridgeResourceFactory.createResource(weblinkResourceProps), + ]); + const sut = new CommonCartridgeResourcesWrapperElementV130(props); + + return { sut, weblinkResourceProps }; + }; + + it('should return correct manifest xml object', () => { + const { sut, weblinkResourceProps } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toStrictEqual({ + resources: [ + { + resource: [ + { + $: { + identifier: weblinkResourceProps.identifier, + type: expect.any(String), + }, + file: { + $: { + href: expect.any(String), + }, + }, + }, + ], + }, + ], + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.ts new file mode 100644 index 00000000000..aa11d0f457a --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/common-cartridge-resources-wrapper-element.ts @@ -0,0 +1,28 @@ +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeElement } from '../../interfaces'; + +export type CommonCartridgeResourcesWrapperElementPropsV130 = { + type: CommonCartridgeElementType.RESOURCES_WRAPPER; + version: CommonCartridgeVersion; + items: CommonCartridgeElement[]; +}; + +export class CommonCartridgeResourcesWrapperElementV130 extends CommonCartridgeElement { + constructor(private readonly props: CommonCartridgeResourcesWrapperElementPropsV130) { + super(props); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_3_0; + } + + public getManifestXmlObject(): Record { + return { + resources: [ + { + resource: this.props.items.map((items) => items.getManifestXmlObject()), + }, + ], + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/index.ts b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/index.ts new file mode 100644 index 00000000000..1134c126384 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/elements/v1.3.0/index.ts @@ -0,0 +1,14 @@ +export { CommonCartridgeElementFactoryV130 } from './common-cartridge-element-factory'; +export { + CommonCartridgeMetadataElementPropsV130, + CommonCartridgeMetadataElementV130, +} from './common-cartridge-metadata-element'; +export { CommonCartridgeOrganizationElementPropsV130 } from './common-cartridge-organization-element'; +export { + CommonCartridgeOrganizationsWrapperElementPropsV130, + CommonCartridgeOrganizationsWrapperElementV130, +} from './common-cartridge-organizations-wrapper-element'; +export { + CommonCartridgeResourcesWrapperElementPropsV130, + CommonCartridgeResourcesWrapperElementV130, +} from './common-cartridge-resources-wrapper-element'; diff --git a/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-element.interface.ts b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-element.interface.ts new file mode 100644 index 00000000000..869aa6f576f --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-element.interface.ts @@ -0,0 +1,43 @@ +import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { createVersionNotSupportedError } from '../utils'; + +type CommonCartridgeElementProps = { + version: CommonCartridgeVersion; + identifier?: string; + title?: string; +}; + +/** + * Every element which should be listed in the Common Cartridge manifest must implement this interface. + */ +export abstract class CommonCartridgeElement { + protected constructor(private readonly baseProps: CommonCartridgeElementProps) { + this.checkVersion(baseProps.version); + } + + public get identifier(): string | undefined { + return this.baseProps.identifier; + } + + public get title(): string | undefined { + return this.baseProps.title; + } + + /** + * Every element must know which versions it supports. + * @returns The supported versions for this element. + */ + abstract getSupportedVersion(): CommonCartridgeVersion; + + /** + * This method is used to build the imsmanifest.xml file. + * @returns The XML object representation for the imsmanifest.xml file. + */ + abstract getManifestXmlObject(): Record; + + private checkVersion(target: CommonCartridgeVersion): void { + if (this.getSupportedVersion() !== target) { + throw createVersionNotSupportedError(target); + } + } +} diff --git a/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-resource.interface.ts b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-resource.interface.ts new file mode 100644 index 00000000000..dfa3adbc8b4 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/interfaces/common-cartridge-resource.interface.ts @@ -0,0 +1,24 @@ +import { CommonCartridgeElement } from './common-cartridge-element.interface'; + +/** + * Every resource which should be added to the Common Cartridge archive must implement this interface. + */ +export abstract class CommonCartridgeResource extends CommonCartridgeElement { + /** + * In later Common Cartridge versions, resources can be inlined in the imsmanifest.xml file. + * @returns true if the resource can be inlined, otherwise false. + */ + abstract canInline(): boolean; + + /** + * This method is used to determine the path of the resource in the Common Cartridge archive. + * @returns The path of the resource in the Common Cartridge archive. + */ + abstract getFilePath(): string; + + /** + * This method is used to get the content of the resource. + * @returns The content of the resource. + */ + abstract getFileContent(): string; +} diff --git a/apps/server/src/modules/common-cartridge/export/interfaces/index.ts b/apps/server/src/modules/common-cartridge/export/interfaces/index.ts new file mode 100644 index 00000000000..aaefb39018c --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/interfaces/index.ts @@ -0,0 +1,2 @@ +export { CommonCartridgeElement } from './common-cartridge-element.interface'; +export { CommonCartridgeResource } from './common-cartridge-resource.interface'; diff --git a/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.spec.ts new file mode 100644 index 00000000000..9bd274d4810 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.spec.ts @@ -0,0 +1,50 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { + createCommonCartridgeWebContentResourcePropsV110, + createCommonCartridgeWebContentResourcePropsV130, +} from '../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeResourceType, CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeResourceFactory } from './common-cartridge-resource-factory'; +import { CommonCartridgeWebContentResourcePropsV110, CommonCartridgeWebContentResourceV110 } from './v1.1.0'; +import { CommonCartridgeWebContentResourceV130 } from './v1.3.0'; + +describe('CommonCartridgeResourceVersion', () => { + describe('createResource', () => { + describe('when Common Cartridge version is supported', () => { + it('should return v1.1.0 resource', () => { + const props = createCommonCartridgeWebContentResourcePropsV110(); + + const result = CommonCartridgeResourceFactory.createResource(props); + + expect(result).toBeInstanceOf(CommonCartridgeWebContentResourceV110); + }); + + it('should return v1.3.0 resource', () => { + const props = createCommonCartridgeWebContentResourcePropsV130(); + + const result = CommonCartridgeResourceFactory.createResource(props); + + expect(result).toBeInstanceOf(CommonCartridgeWebContentResourceV130); + }); + }); + + describe('when versions is not supported', () => { + const notSupportedVersions = [ + CommonCartridgeVersion.V_1_0_0, + CommonCartridgeVersion.V_1_2_0, + CommonCartridgeVersion.V_1_4_0, + ]; + + it('should throw InternalServerErrorException', () => { + notSupportedVersions.forEach((version) => { + expect(() => + CommonCartridgeResourceFactory.createResource({ + version, + type: CommonCartridgeResourceType.WEB_CONTENT, + } as CommonCartridgeWebContentResourcePropsV110) + ).toThrow(InternalServerErrorException); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.ts b/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.ts new file mode 100644 index 00000000000..673672c129e --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/common-cartridge-resource-factory.ts @@ -0,0 +1,44 @@ +import { CommonCartridgeVersion } from '../common-cartridge.enums'; +import { CommonCartridgeResource } from '../interfaces'; +import { OmitVersionAndFolder, createVersionNotSupportedError } from '../utils'; +import { + CommonCartridgeManifestResourcePropsV110, + CommonCartridgeResourceFactoryV110, + CommonCartridgeWebContentResourcePropsV110, + CommonCartridgeWebLinkResourcePropsV110, +} from './v1.1.0'; +import { + CommonCartridgeManifestResourcePropsV130, + CommonCartridgeResourceFactoryV130, + CommonCartridgeWebContentResourcePropsV130, + CommonCartridgeWebLinkResourcePropsV130, +} from './v1.3.0'; + +export type CommonCartridgeResourceProps = + | OmitVersionAndFolder + | OmitVersionAndFolder + | OmitVersionAndFolder + | OmitVersionAndFolder; + +type CommonCartridgeResourcePropsInternal = + | CommonCartridgeManifestResourcePropsV110 + | CommonCartridgeWebContentResourcePropsV110 + | CommonCartridgeWebLinkResourcePropsV110 + | CommonCartridgeManifestResourcePropsV130 + | CommonCartridgeWebContentResourcePropsV130 + | CommonCartridgeWebLinkResourcePropsV130; + +export class CommonCartridgeResourceFactory { + public static createResource(props: CommonCartridgeResourcePropsInternal): CommonCartridgeResource { + const { version } = props; + + switch (version) { + case CommonCartridgeVersion.V_1_1_0: + return CommonCartridgeResourceFactoryV110.createResource(props); + case CommonCartridgeVersion.V_1_3_0: + return CommonCartridgeResourceFactoryV130.createResource(props); + default: + throw createVersionNotSupportedError(version); + } + } +} diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.spec.ts new file mode 100644 index 00000000000..aa63bb9237f --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.spec.ts @@ -0,0 +1,147 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { readFile } from 'fs/promises'; +import { createCommonCartridgeManifestResourcePropsV110 } from '../../../testing/common-cartridge-resource-props.factory'; +import { + CommonCartridgeElementType, + CommonCartridgeIntendedUseType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../../common-cartridge.enums'; +import { CommonCartridgeElementFactory } from '../../elements/common-cartridge-element-factory'; +import { CommonCartridgeResourceFactory } from '../common-cartridge-resource-factory'; +import { CommonCartridgeManifestResourceV110 } from './common-cartridge-manifest-resource'; + +describe('CommonCartridgeManifestResourceV110', () => { + describe('canInline', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeManifestResourcePropsV110(); + const sut = new CommonCartridgeManifestResourceV110(props); + + return { sut }; + }; + + it('should return false', () => { + const { sut } = setup(); + + const result = sut.canInline(); + + expect(result).toBe(false); + }); + }); + }); + + describe('getFilePath', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeManifestResourcePropsV110(); + const sut = new CommonCartridgeManifestResourceV110(props); + + return { sut }; + }; + + it('should return constructed file path', () => { + const { sut } = setup(); + + const result = sut.getFilePath(); + + expect(result).toBe('imsmanifest.xml'); + }); + }); + }); + + describe('getFileContent', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const resource1 = CommonCartridgeResourceFactory.createResource({ + type: CommonCartridgeResourceType.WEB_CONTENT, + version: CommonCartridgeVersion.V_1_1_0, + title: 'Title 1', + identifier: 'r1', + folder: 'o1', + html: '

HTML

', + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }); + const resource2 = CommonCartridgeResourceFactory.createResource({ + type: CommonCartridgeResourceType.WEB_LINK, + version: CommonCartridgeVersion.V_1_1_0, + title: 'Title 2', + identifier: 'r2', + folder: 'o2', + url: 'https://www.example.tld', + }); + const organization1 = CommonCartridgeElementFactory.createElement({ + type: CommonCartridgeElementType.ORGANIZATION, + version: CommonCartridgeVersion.V_1_1_0, + title: 'Title 1', + identifier: 'o1', + items: resource1, + }); + const organization2 = CommonCartridgeElementFactory.createElement({ + type: CommonCartridgeElementType.ORGANIZATION, + version: CommonCartridgeVersion.V_1_1_0, + title: 'Title 2', + identifier: 'o2', + items: resource2, + }); + const metadata = CommonCartridgeElementFactory.createElement({ + type: CommonCartridgeElementType.METADATA, + version: CommonCartridgeVersion.V_1_1_0, + title: 'Common Cartridge Manifest', + copyrightOwners: ['John Doe', 'Jane Doe'], + creationDate: new Date('2023-01-01'), + }); + const sut = new CommonCartridgeManifestResourceV110({ + type: CommonCartridgeResourceType.MANIFEST, + version: CommonCartridgeVersion.V_1_1_0, + identifier: 'm1', + metadata, + organizations: [organization1, organization2], + resources: [resource1, resource2], + }); + + return { sut }; + }; + + it('should return constructed file content', async () => { + const { sut } = setup(); + + const expected = await readFile( + './apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/manifest.xml', + 'utf-8' + ); + const result = sut.getFileContent(); + + expect(result).toEqual(expected); + }); + }); + }); + + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeManifestResourcePropsV110(); + const sut = new CommonCartridgeManifestResourceV110(props); + + return { sut }; + }; + + it('should return supported version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_1_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeManifestResourcePropsV110(); + notSupportedProps.version = CommonCartridgeVersion.V_1_3_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeManifestResourceV110(notSupportedProps)).toThrow(InternalServerErrorException); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.ts new file mode 100644 index 00000000000..ae578c34e4a --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-manifest-resource.ts @@ -0,0 +1,68 @@ +import { + CommonCartridgeElementType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../../common-cartridge.enums'; +import { CommonCartridgeElementFactory } from '../../elements/common-cartridge-element-factory'; +import { CommonCartridgeElement, CommonCartridgeResource } from '../../interfaces'; +import { buildXmlString } from '../../utils'; + +export type CommonCartridgeManifestResourcePropsV110 = { + type: CommonCartridgeResourceType.MANIFEST; + version: CommonCartridgeVersion; + identifier: string; + metadata: CommonCartridgeElement; + organizations: CommonCartridgeElement[]; + resources: CommonCartridgeElement[]; +}; + +export class CommonCartridgeManifestResourceV110 extends CommonCartridgeResource { + constructor(private readonly props: CommonCartridgeManifestResourcePropsV110) { + super(props); + } + + public canInline(): boolean { + return false; + } + + public getFilePath(): string { + return 'imsmanifest.xml'; + } + + public getFileContent(): string { + return buildXmlString(this.getManifestXmlObject()); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(): Record { + return { + manifest: { + $: { + identifier: this.props.identifier, + xmlns: 'http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1', + 'xmlns:mnf': 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest', + 'xmlns:res': 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation': + 'http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1 https://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imscp_v1p2_v1p0.xsd ' + + 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest https://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lommanifest_v1p0.xsd ' + + 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource https://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lomresource_v1p0.xsd', + }, + metadata: this.props.metadata.getManifestXmlObject(), + organizations: CommonCartridgeElementFactory.createElement({ + type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER, + version: this.props.version, + items: this.props.organizations, + }).getManifestXmlObject(), + ...CommonCartridgeElementFactory.createElement({ + type: CommonCartridgeElementType.RESOURCES_WRAPPER, + version: this.props.version, + items: this.props.resources, + }).getManifestXmlObject(), + }, + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.spec.ts new file mode 100644 index 00000000000..fd93be44d2d --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.spec.ts @@ -0,0 +1,51 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { + createCommonCartridgeManifestResourcePropsV110, + createCommonCartridgeWebContentResourcePropsV110, + createCommonCartridgeWeblinkResourcePropsV110, +} from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeManifestResourceV110 } from './common-cartridge-manifest-resource'; +import { CommonCartridgeResourceFactoryV110 } from './common-cartridge-resource-factory'; +import { CommonCartridgeWebContentResourceV110 } from './common-cartridge-web-content-resource'; +import { + CommonCartridgeWebLinkResourcePropsV110, + CommonCartridgeWebLinkResourceV110, +} from './common-cartridge-web-link-resource'; + +describe('CommonCartridgeResourceFactoryV110', () => { + describe('createResource', () => { + describe('when creating resources from props', () => { + it('should return manifest resource', () => { + const props = createCommonCartridgeManifestResourcePropsV110(); + + const result = CommonCartridgeResourceFactoryV110.createResource(props); + + expect(result).toBeInstanceOf(CommonCartridgeManifestResourceV110); + }); + + it('should return web content resource', () => { + const props = createCommonCartridgeWebContentResourcePropsV110(); + + const result = CommonCartridgeResourceFactoryV110.createResource(props); + + expect(result).toBeInstanceOf(CommonCartridgeWebContentResourceV110); + }); + + it('should return web link resource', () => { + const props = createCommonCartridgeWeblinkResourcePropsV110(); + + const result = CommonCartridgeResourceFactoryV110.createResource(props); + + expect(result).toBeInstanceOf(CommonCartridgeWebLinkResourceV110); + }); + }); + + describe('when resource type is not supported', () => { + it('should throw error', () => { + expect(() => + CommonCartridgeResourceFactoryV110.createResource({} as CommonCartridgeWebLinkResourcePropsV110) + ).toThrow(InternalServerErrorException); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.ts new file mode 100644 index 00000000000..4e13ec77587 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-resource-factory.ts @@ -0,0 +1,37 @@ +import { CommonCartridgeResourceType } from '../../common-cartridge.enums'; +import { CommonCartridgeResource } from '../../interfaces'; +import { createResourceTypeNotSupportedError } from '../../utils'; +import { + CommonCartridgeManifestResourcePropsV110, + CommonCartridgeManifestResourceV110, +} from './common-cartridge-manifest-resource'; +import { + CommonCartridgeWebContentResourcePropsV110, + CommonCartridgeWebContentResourceV110, +} from './common-cartridge-web-content-resource'; +import { + CommonCartridgeWebLinkResourcePropsV110, + CommonCartridgeWebLinkResourceV110, +} from './common-cartridge-web-link-resource'; + +type CommonCartridgeResourcePropsV110 = + | CommonCartridgeManifestResourcePropsV110 + | CommonCartridgeWebContentResourcePropsV110 + | CommonCartridgeWebLinkResourcePropsV110; + +export class CommonCartridgeResourceFactoryV110 { + public static createResource(props: CommonCartridgeResourcePropsV110): CommonCartridgeResource { + const { type } = props; + + switch (type) { + case CommonCartridgeResourceType.MANIFEST: + return new CommonCartridgeManifestResourceV110(props); + case CommonCartridgeResourceType.WEB_CONTENT: + return new CommonCartridgeWebContentResourceV110(props); + case CommonCartridgeResourceType.WEB_LINK: + return new CommonCartridgeWebLinkResourceV110(props); + default: + throw createResourceTypeNotSupportedError(type); + } + } +} diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.spec.ts new file mode 100644 index 00000000000..169986b451e --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.spec.ts @@ -0,0 +1,123 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { createCommonCartridgeWebContentResourcePropsV110 } from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeWebContentResourceV110 } from './common-cartridge-web-content-resource'; + +describe('CommonCartridgeWebContentResourceV110', () => { + describe('canInline', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV110(); + const sut = new CommonCartridgeWebContentResourceV110(props); + + return { sut }; + }; + + it('should return false', () => { + const { sut } = setup(); + + const result = sut.canInline(); + + expect(result).toBe(false); + }); + }); + }); + + describe('getFilePath', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV110(); + const sut = new CommonCartridgeWebContentResourceV110(props); + + return { sut, props }; + }; + + it('should return the constructed file path', () => { + const { sut, props } = setup(); + + const result = sut.getFilePath(); + + expect(result).toBe(`${props.folder}/${props.identifier}.html`); + }); + }); + }); + + describe('getFileContent', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV110(); + const sut = new CommonCartridgeWebContentResourceV110(props); + + return { sut, props }; + }; + + it('should return the HTML', () => { + const { sut, props } = setup(); + + const result = sut.getFileContent(); + + expect(result).toBe(props.html); + }); + }); + }); + + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV110(); + const sut = new CommonCartridgeWebContentResourceV110(props); + + return { sut }; + }; + + it('should return Common Cartridge version 1.1.0', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_1_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeWebContentResourcePropsV110(); + notSupportedProps.version = CommonCartridgeVersion.V_1_3_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeWebContentResourceV110(notSupportedProps)).toThrow( + InternalServerErrorException + ); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV110(); + const sut = new CommonCartridgeWebContentResourceV110(props); + + return { sut, props }; + }; + + it('should return the correct XML object', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toEqual({ + $: { + identifier: props.identifier, + type: 'webcontent', + intendeduse: props.intendedUse, + }, + file: { + $: { + href: sut.getFilePath(), + }, + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.ts new file mode 100644 index 00000000000..f684709e246 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-content-resource.ts @@ -0,0 +1,61 @@ +import { + CommonCartridgeIntendedUseType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../../common-cartridge.enums'; +import { CommonCartridgeResource } from '../../interfaces'; +import { checkIntendedUse } from '../../utils'; + +export type CommonCartridgeWebContentResourcePropsV110 = { + type: CommonCartridgeResourceType.WEB_CONTENT; + version: CommonCartridgeVersion; + identifier: string; + folder: string; + title: string; + html: string; + intendedUse: CommonCartridgeIntendedUseType; +}; + +export class CommonCartridgeWebContentResourceV110 extends CommonCartridgeResource { + private static readonly SUPPORTED_INTENDED_USES = [ + CommonCartridgeIntendedUseType.LESSON_PLAN, + CommonCartridgeIntendedUseType.SYLLABUS, + CommonCartridgeIntendedUseType.UNSPECIFIED, + ]; + + constructor(private readonly props: CommonCartridgeWebContentResourcePropsV110) { + super(props); + checkIntendedUse(props.intendedUse, CommonCartridgeWebContentResourceV110.SUPPORTED_INTENDED_USES); + } + + public canInline(): boolean { + return false; + } + + public getFilePath(): string { + return `${this.props.folder}/${this.props.identifier}.html`; + } + + public getFileContent(): string { + return this.props.html; + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(): Record { + return { + $: { + identifier: this.props.identifier, + type: this.props.type, + intendeduse: this.props.intendedUse, + }, + file: { + $: { + href: this.getFilePath(), + }, + }, + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.spec.ts new file mode 100644 index 00000000000..79881d285f4 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.spec.ts @@ -0,0 +1,129 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { readFile } from 'fs/promises'; +import { createCommonCartridgeWeblinkResourcePropsV110 } from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeWebLinkResourceV110 } from './common-cartridge-web-link-resource'; + +describe('CommonCartridgeWebLinkResourceV110', () => { + describe('canInline', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV110(); + const sut = new CommonCartridgeWebLinkResourceV110(props); + + return { sut }; + }; + + it('should return false', () => { + const { sut } = setup(); + + const result = sut.canInline(); + + expect(result).toBe(false); + }); + }); + }); + + describe('getFilePath', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV110(); + const sut = new CommonCartridgeWebLinkResourceV110(props); + + return { sut, props }; + }; + + it('should return the constructed file path', () => { + const { sut, props } = setup(); + + const result = sut.getFilePath(); + + expect(result).toBe(`${props.folder}/${props.identifier}.xml`); + }); + }); + }); + + describe('getFileContent', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV110(); + props.title = 'Title'; + props.url = 'http://www.example.tld'; + props.target = '_self'; + props.windowFeatures = 'width=100;height=100;'; + + const sut = new CommonCartridgeWebLinkResourceV110(props); + + return { sut }; + }; + it('should contain correct XML', async () => { + const { sut } = setup(); + + const expected = await readFile( + './apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/weblink.xml', + 'utf8' + ); + const result = sut.getFileContent(); + + expect(result).toEqual(expected); + }); + }); + }); + + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV110(); + const sut = new CommonCartridgeWebLinkResourceV110(props); + + return { sut }; + }; + + it('should return the supported version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_1_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeWeblinkResourcePropsV110(); + notSupportedProps.version = CommonCartridgeVersion.V_1_3_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeWebLinkResourceV110(notSupportedProps)).toThrow(InternalServerErrorException); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using Common Cartridge version 1.1.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV110(); + const sut = new CommonCartridgeWebLinkResourceV110(props); + + return { sut, props }; + }; + + it('should return the manifest XML object', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toEqual({ + $: { + identifier: props.identifier, + type: 'imswl_xmlv1p1', + }, + file: { + $: { + href: sut.getFilePath(), + }, + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.ts new file mode 100644 index 00000000000..4da6c641215 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/common-cartridge-web-link-resource.ts @@ -0,0 +1,67 @@ +import { CommonCartridgeResourceType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeResource } from '../../interfaces'; +import { buildXmlString } from '../../utils'; + +export type CommonCartridgeWebLinkResourcePropsV110 = { + type: CommonCartridgeResourceType.WEB_LINK; + version: CommonCartridgeVersion; + identifier: string; + folder: string; + title: string; + url: string; + target?: string; + windowFeatures?: string; +}; + +export class CommonCartridgeWebLinkResourceV110 extends CommonCartridgeResource { + constructor(private readonly props: CommonCartridgeWebLinkResourcePropsV110) { + super(props); + } + + public canInline(): boolean { + return false; + } + + public getFilePath(): string { + return `${this.props.folder}/${this.props.identifier}.xml`; + } + + public getFileContent(): string { + return buildXmlString({ + webLink: { + $: { + xmlns: 'http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation': + 'http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1 https://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imswl_v1p1.xsd', + }, + title: this.props.title, + url: { + $: { + href: this.props.url, + target: this.props.target, + windowFeatures: this.props.windowFeatures, + }, + }, + }, + }); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_1_0; + } + + public getManifestXmlObject(): Record { + return { + $: { + identifier: this.props.identifier, + type: 'imswl_xmlv1p1', + }, + file: { + $: { + href: this.getFilePath(), + }, + }, + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/index.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/index.ts new file mode 100644 index 00000000000..62d0bde3846 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.1.0/index.ts @@ -0,0 +1,7 @@ +export { CommonCartridgeManifestResourcePropsV110 } from './common-cartridge-manifest-resource'; +export { CommonCartridgeResourceFactoryV110 } from './common-cartridge-resource-factory'; +export { + CommonCartridgeWebContentResourcePropsV110, + CommonCartridgeWebContentResourceV110, +} from './common-cartridge-web-content-resource'; +export { CommonCartridgeWebLinkResourcePropsV110 } from './common-cartridge-web-link-resource'; diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.spec.ts new file mode 100644 index 00000000000..f49caa016c9 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.spec.ts @@ -0,0 +1,148 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { readFile } from 'fs/promises'; +import { createCommonCartridgeManifestResourcePropsV130 } from '../../../testing/common-cartridge-resource-props.factory'; +import { + CommonCartridgeElementType, + CommonCartridgeIntendedUseType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../../common-cartridge.enums'; +import { CommonCartridgeElementFactory } from '../../elements/common-cartridge-element-factory'; +import { CommonCartridgeElementFactoryV130 } from '../../elements/v1.3.0'; +import { CommonCartridgeResourceFactory } from '../common-cartridge-resource-factory'; +import { CommonCartridgeManifestResourceV130 } from './common-cartridge-manifest-resource'; + +describe('CommonCartridgeManifestResourceV130', () => { + describe('canInline', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeManifestResourcePropsV130(); + const sut = new CommonCartridgeManifestResourceV130(props); + + return { sut }; + }; + + it('should return false', () => { + const { sut } = setup(); + + const result = sut.canInline(); + + expect(result).toBe(false); + }); + }); + }); + + describe('getFilePath', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeManifestResourcePropsV130(); + const sut = new CommonCartridgeManifestResourceV130(props); + + return { sut }; + }; + + it('should return constructed file path', () => { + const { sut } = setup(); + + const result = sut.getFilePath(); + + expect(result).toBe('imsmanifest.xml'); + }); + }); + }); + + describe('getFileContent', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const resource1 = CommonCartridgeResourceFactory.createResource({ + type: CommonCartridgeResourceType.WEB_CONTENT, + version: CommonCartridgeVersion.V_1_3_0, + title: 'Title 1', + identifier: 'r1', + folder: 'o1', + html: '

HTML

', + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }); + const resource2 = CommonCartridgeResourceFactory.createResource({ + type: CommonCartridgeResourceType.WEB_LINK, + version: CommonCartridgeVersion.V_1_3_0, + title: 'Title 2', + identifier: 'r2', + folder: 'o2', + url: 'https://www.example.tld', + }); + const organization1 = CommonCartridgeElementFactoryV130.createElement({ + type: CommonCartridgeElementType.ORGANIZATION, + version: CommonCartridgeVersion.V_1_3_0, + title: 'Title 1', + identifier: 'o1', + items: resource1, + }); + const organization2 = CommonCartridgeElementFactory.createElement({ + type: CommonCartridgeElementType.ORGANIZATION, + version: CommonCartridgeVersion.V_1_3_0, + title: 'Title 2', + identifier: 'o2', + items: resource2, + }); + const metadata = CommonCartridgeElementFactoryV130.createElement({ + type: CommonCartridgeElementType.METADATA, + version: CommonCartridgeVersion.V_1_3_0, + title: 'Common Cartridge Manifest', + copyrightOwners: ['John Doe', 'Jane Doe'], + creationDate: new Date('2023-01-01'), + }); + const sut = new CommonCartridgeManifestResourceV130({ + type: CommonCartridgeResourceType.MANIFEST, + version: CommonCartridgeVersion.V_1_3_0, + identifier: 'm1', + metadata, + organizations: [organization1, organization2], + resources: [resource1, resource2], + }); + + return { sut }; + }; + + it('should return constructed file content', async () => { + const { sut } = setup(); + + const expected = await readFile( + './apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/manifest.xml', + 'utf-8' + ); + const result = sut.getFileContent(); + + expect(result).toEqual(expected); + }); + }); + }); + + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeManifestResourcePropsV130(); + const sut = new CommonCartridgeManifestResourceV130(props); + + return { sut }; + }; + + it('should return supported version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_3_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeManifestResourcePropsV130(); + notSupportedProps.version = CommonCartridgeVersion.V_1_1_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeManifestResourceV130(notSupportedProps)).toThrow(InternalServerErrorException); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.ts new file mode 100644 index 00000000000..3da27cb30b8 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-manifest-resource.ts @@ -0,0 +1,71 @@ +import { + CommonCartridgeElementType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../../common-cartridge.enums'; +import { + CommonCartridgeOrganizationsWrapperElementV130, + CommonCartridgeResourcesWrapperElementV130, +} from '../../elements/v1.3.0'; +import { CommonCartridgeElement, CommonCartridgeResource } from '../../interfaces'; +import { buildXmlString } from '../../utils'; + +export type CommonCartridgeManifestResourcePropsV130 = { + type: CommonCartridgeResourceType.MANIFEST; + version: CommonCartridgeVersion; + identifier: string; + metadata: CommonCartridgeElement; + organizations: CommonCartridgeElement[]; + resources: CommonCartridgeElement[]; +}; + +export class CommonCartridgeManifestResourceV130 extends CommonCartridgeResource { + constructor(private readonly props: CommonCartridgeManifestResourcePropsV130) { + super(props); + } + + public canInline(): boolean { + return false; + } + + public getFilePath(): string { + return 'imsmanifest.xml'; + } + + public getFileContent(): string { + return buildXmlString(this.getManifestXmlObject()); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_3_0; + } + + public getManifestXmlObject(): Record { + return { + manifest: { + $: { + identifier: this.props.identifier, + xmlns: 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_v1p1', + 'xmlns:mnf': 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/manifest', + 'xmlns:res': 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/resource', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation': + 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_v1p1 https://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_imscp_v1p2_v1p0.xsd ' + + 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/manifest https://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lommanifest_v1p0.xsd ' + + 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/resource https://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lomresource_v1p0.xsd', + }, + metadata: this.props.metadata.getManifestXmlObject(), + organizations: new CommonCartridgeOrganizationsWrapperElementV130({ + type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER, + version: this.props.version, + items: this.props.organizations, + }).getManifestXmlObject(), + ...new CommonCartridgeResourcesWrapperElementV130({ + type: CommonCartridgeElementType.RESOURCES_WRAPPER, + version: this.props.version, + items: this.props.resources, + }).getManifestXmlObject(), + }, + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.spec.ts new file mode 100644 index 00000000000..ad630ea25d8 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.spec.ts @@ -0,0 +1,51 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { + createCommonCartridgeManifestResourcePropsV130, + createCommonCartridgeWebContentResourcePropsV130, + createCommonCartridgeWeblinkResourcePropsV130, +} from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeManifestResourceV130 } from './common-cartridge-manifest-resource'; +import { CommonCartridgeResourceFactoryV130 } from './common-cartridge-resource-factory'; +import { CommonCartridgeWebContentResourceV130 } from './common-cartridge-web-content-resource'; +import { + CommonCartridgeWebLinkResourcePropsV130, + CommonCartridgeWebLinkResourceV130, +} from './common-cartridge-web-link-resource'; + +describe('CommonCartridgeResourceFactoryV130', () => { + describe('createResource', () => { + describe('when creating resources from props', () => { + it('should return manifest resource', () => { + const props = createCommonCartridgeManifestResourcePropsV130(); + + const result = CommonCartridgeResourceFactoryV130.createResource(props); + + expect(result).toBeInstanceOf(CommonCartridgeManifestResourceV130); + }); + + it('should return web content resource', () => { + const props = createCommonCartridgeWebContentResourcePropsV130(); + + const result = CommonCartridgeResourceFactoryV130.createResource(props); + + expect(result).toBeInstanceOf(CommonCartridgeWebContentResourceV130); + }); + + it('should return web link resource', () => { + const props = createCommonCartridgeWeblinkResourcePropsV130(); + + const result = CommonCartridgeResourceFactoryV130.createResource(props); + + expect(result).toBeInstanceOf(CommonCartridgeWebLinkResourceV130); + }); + }); + + describe('when resource type is not supported', () => { + it('should throw error', () => { + expect(() => + CommonCartridgeResourceFactoryV130.createResource({} as CommonCartridgeWebLinkResourcePropsV130) + ).toThrow(InternalServerErrorException); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.ts new file mode 100644 index 00000000000..9be5fad11c1 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-resource-factory.ts @@ -0,0 +1,37 @@ +import { CommonCartridgeResourceType } from '../../common-cartridge.enums'; +import { CommonCartridgeResource } from '../../interfaces'; +import { createResourceTypeNotSupportedError } from '../../utils'; +import { + CommonCartridgeManifestResourcePropsV130, + CommonCartridgeManifestResourceV130, +} from './common-cartridge-manifest-resource'; +import { + CommonCartridgeWebContentResourcePropsV130, + CommonCartridgeWebContentResourceV130, +} from './common-cartridge-web-content-resource'; +import { + CommonCartridgeWebLinkResourcePropsV130, + CommonCartridgeWebLinkResourceV130, +} from './common-cartridge-web-link-resource'; + +type CommonCartridgeResourcePropsV130 = + | CommonCartridgeManifestResourcePropsV130 + | CommonCartridgeWebContentResourcePropsV130 + | CommonCartridgeWebLinkResourcePropsV130; + +export class CommonCartridgeResourceFactoryV130 { + public static createResource(props: CommonCartridgeResourcePropsV130): CommonCartridgeResource { + const { type } = props; + + switch (type) { + case CommonCartridgeResourceType.MANIFEST: + return new CommonCartridgeManifestResourceV130(props); + case CommonCartridgeResourceType.WEB_CONTENT: + return new CommonCartridgeWebContentResourceV130(props); + case CommonCartridgeResourceType.WEB_LINK: + return new CommonCartridgeWebLinkResourceV130(props); + default: + throw createResourceTypeNotSupportedError(type); + } + } +} diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.spec.ts new file mode 100644 index 00000000000..e1b3334fd7d --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.spec.ts @@ -0,0 +1,123 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { createCommonCartridgeWebContentResourcePropsV130 } from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeWebContentResourceV130 } from './common-cartridge-web-content-resource'; + +describe('CommonCartridgeWebContentResourceV130', () => { + describe('canInline', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV130(); + const sut = new CommonCartridgeWebContentResourceV130(props); + + return { sut }; + }; + + it('should return false', () => { + const { sut } = setup(); + + const result = sut.canInline(); + + expect(result).toBe(false); + }); + }); + }); + + describe('getFilePath', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV130(); + const sut = new CommonCartridgeWebContentResourceV130(props); + + return { sut, props }; + }; + + it('should return the constructed file path', () => { + const { sut, props } = setup(); + + const result = sut.getFilePath(); + + expect(result).toBe(`${props.folder}/${props.identifier}.html`); + }); + }); + }); + + describe('getFileContent', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV130(); + const sut = new CommonCartridgeWebContentResourceV130(props); + + return { sut, props }; + }; + + it('should return the HTML', () => { + const { sut, props } = setup(); + + const result = sut.getFileContent(); + + expect(result).toBe(props.html); + }); + }); + }); + + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV130(); + const sut = new CommonCartridgeWebContentResourceV130(props); + + return { sut }; + }; + + it('should return the supported version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_3_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeWebContentResourcePropsV130(); + notSupportedProps.version = CommonCartridgeVersion.V_1_1_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeWebContentResourceV130(notSupportedProps)).toThrow( + InternalServerErrorException + ); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWebContentResourcePropsV130(); + const sut = new CommonCartridgeWebContentResourceV130(props); + + return { sut, props }; + }; + + it('should return the manifest XML object', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toEqual({ + $: { + identifier: props.identifier, + type: props.type, + intendeduse: props.intendedUse, + }, + file: { + $: { + href: sut.getFilePath(), + }, + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.ts new file mode 100644 index 00000000000..eb168087a52 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-content-resource.ts @@ -0,0 +1,62 @@ +import { + CommonCartridgeIntendedUseType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../../common-cartridge.enums'; +import { CommonCartridgeResource } from '../../interfaces'; +import { checkIntendedUse } from '../../utils'; + +export type CommonCartridgeWebContentResourcePropsV130 = { + type: CommonCartridgeResourceType.WEB_CONTENT; + version: CommonCartridgeVersion; + identifier: string; + folder: string; + title: string; + html: string; + intendedUse: CommonCartridgeIntendedUseType; +}; + +export class CommonCartridgeWebContentResourceV130 extends CommonCartridgeResource { + private static readonly SUPPORTED_INTENDED_USES = [ + CommonCartridgeIntendedUseType.ASSIGNMENT, + CommonCartridgeIntendedUseType.LESSON_PLAN, + CommonCartridgeIntendedUseType.SYLLABUS, + CommonCartridgeIntendedUseType.UNSPECIFIED, + ]; + + constructor(private readonly props: CommonCartridgeWebContentResourcePropsV130) { + super(props); + checkIntendedUse(props.intendedUse, CommonCartridgeWebContentResourceV130.SUPPORTED_INTENDED_USES); + } + + public canInline(): boolean { + return false; + } + + public getFilePath(): string { + return `${this.props.folder}/${this.props.identifier}.html`; + } + + public getFileContent(): string { + return this.props.html; + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_3_0; + } + + public getManifestXmlObject(): Record { + return { + $: { + identifier: this.props.identifier, + type: this.props.type, + intendeduse: this.props.intendedUse, + }, + file: { + $: { + href: this.getFilePath(), + }, + }, + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.spec.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.spec.ts new file mode 100644 index 00000000000..d6aee5e394f --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.spec.ts @@ -0,0 +1,130 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { readFile } from 'node:fs/promises'; +import { createCommonCartridgeWeblinkResourcePropsV130 } from '../../../testing/common-cartridge-resource-props.factory'; +import { CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeWebLinkResourceV130 } from './common-cartridge-web-link-resource'; + +describe('CommonCartridgeWebLinkResourceV130', () => { + describe('canInline', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV130(); + const sut = new CommonCartridgeWebLinkResourceV130(props); + + return { sut }; + }; + + it('should return false', () => { + const { sut } = setup(); + + const result = sut.canInline(); + + expect(result).toBe(false); + }); + }); + }); + + describe('getFilePath', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV130(); + const sut = new CommonCartridgeWebLinkResourceV130(props); + + return { sut, props }; + }; + + it('should return the constructed file path', () => { + const { sut, props } = setup(); + + const result = sut.getFilePath(); + + expect(result).toBe(`${props.folder}/${props.identifier}.xml`); + }); + }); + }); + + describe('getFileContent', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV130(); + props.title = 'Title'; + props.url = 'http://www.example.tld'; + props.target = '_self'; + props.windowFeatures = 'width=100;height=100;'; + + const sut = new CommonCartridgeWebLinkResourceV130(props); + + return { sut }; + }; + + it('should contain correct XML', async () => { + const { sut } = setup(); + + const expected = await readFile( + './apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/weblink.xml', + 'utf8' + ); + const result = sut.getFileContent(); + + expect(result).toEqual(expected); + }); + }); + }); + + describe('getSupportedVersion', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV130(); + const sut = new CommonCartridgeWebLinkResourceV130(props); + + return { sut }; + }; + + it('should return the supported version', () => { + const { sut } = setup(); + + const result = sut.getSupportedVersion(); + + expect(result).toBe(CommonCartridgeVersion.V_1_3_0); + }); + }); + + describe('when using not supported Common Cartridge version', () => { + const notSupportedProps = createCommonCartridgeWeblinkResourcePropsV130(); + notSupportedProps.version = CommonCartridgeVersion.V_1_1_0; + + it('should throw error', () => { + expect(() => new CommonCartridgeWebLinkResourceV130(notSupportedProps)).toThrow(InternalServerErrorException); + }); + }); + }); + + describe('getManifestXmlObject', () => { + describe('when using Common Cartridge version 1.3.0', () => { + const setup = () => { + const props = createCommonCartridgeWeblinkResourcePropsV130(); + const sut = new CommonCartridgeWebLinkResourceV130(props); + + return { sut, props }; + }; + + it('should return the manifest XML object', () => { + const { sut, props } = setup(); + + const result = sut.getManifestXmlObject(); + + expect(result).toEqual({ + $: { + identifier: props.identifier, + type: 'imswl_xmlv1p3', + }, + file: { + $: { + href: sut.getFilePath(), + }, + }, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.ts new file mode 100644 index 00000000000..1cfd8a3df5b --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/common-cartridge-web-link-resource.ts @@ -0,0 +1,67 @@ +import { CommonCartridgeResourceType, CommonCartridgeVersion } from '../../common-cartridge.enums'; +import { CommonCartridgeResource } from '../../interfaces'; +import { buildXmlString } from '../../utils'; + +export type CommonCartridgeWebLinkResourcePropsV130 = { + type: CommonCartridgeResourceType.WEB_LINK; + version: CommonCartridgeVersion; + identifier: string; + folder: string; + title: string; + url: string; + target?: string; + windowFeatures?: string; +}; + +export class CommonCartridgeWebLinkResourceV130 extends CommonCartridgeResource { + constructor(private readonly props: CommonCartridgeWebLinkResourcePropsV130) { + super(props); + } + + public canInline(): boolean { + return false; + } + + public getFilePath(): string { + return `${this.props.folder}/${this.props.identifier}.xml`; + } + + public getFileContent(): string { + return buildXmlString({ + webLink: { + $: { + xmlns: 'http://www.imsglobal.org/xsd/imsccv1p3/imswl_v1p3', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation': + 'http://www.imsglobal.org/xsd/imsccv1p3/imswl_v1p3 https://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_imswl_v1p3.xsd', + }, + title: this.props.title, + url: { + $: { + href: this.props.url, + target: this.props.target, + windowFeatures: this.props.windowFeatures, + }, + }, + }, + }); + } + + public getSupportedVersion(): CommonCartridgeVersion { + return CommonCartridgeVersion.V_1_3_0; + } + + public getManifestXmlObject(): Record { + return { + $: { + identifier: this.props.identifier, + type: 'imswl_xmlv1p3', + }, + file: { + $: { + href: this.getFilePath(), + }, + }, + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/index.ts b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/index.ts new file mode 100644 index 00000000000..dca0baca323 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/resources/v1.3.0/index.ts @@ -0,0 +1,7 @@ +export { CommonCartridgeManifestResourcePropsV130 } from './common-cartridge-manifest-resource'; +export { CommonCartridgeResourceFactoryV130 } from './common-cartridge-resource-factory'; +export { + CommonCartridgeWebContentResourcePropsV130, + CommonCartridgeWebContentResourceV130, +} from './common-cartridge-web-content-resource'; +export { CommonCartridgeWebLinkResourcePropsV130 } from './common-cartridge-web-link-resource'; diff --git a/apps/server/src/modules/common-cartridge/export/utils.spec.ts b/apps/server/src/modules/common-cartridge/export/utils.spec.ts new file mode 100644 index 00000000000..5b3ef9d0b02 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/utils.spec.ts @@ -0,0 +1,84 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ObjectID } from 'bson'; +import { CommonCartridgeVersion } from './common-cartridge.enums'; +import { + buildXmlString, + checkIntendedUse, + createElementTypeNotSupportedError, + createIdentifier, + createResourceTypeNotSupportedError, + createVersionNotSupportedError, +} from './utils'; + +describe('CommonCartridgeUtils', () => { + describe('buildXmlString', () => { + it('should create xml string', () => { + const xml = buildXmlString({ root: { child: 'value' } }); + + expect(xml).toBe('\n\n value\n'); + }); + }); + + describe('createVersionNotSupportedError', () => { + describe('when creating error', () => { + it('should return error with message', () => { + const error = createVersionNotSupportedError(CommonCartridgeVersion.V_1_0_0); + + expect(error).toBeInstanceOf(InternalServerErrorException); + expect(error.message).toBe('Common Cartridge version 1.0.0 is not supported'); + }); + }); + }); + + describe('createIdentifier', () => { + describe('when creating identifier', () => { + it('should return identifier with prefix', () => { + const identifier = new ObjectID(); + + expect(createIdentifier(identifier)).toBe(`i${identifier.toHexString()}`); + }); + + it('should return identifier with prefix when identifier is undefined', () => { + expect(createIdentifier(undefined)).toMatch(/^i[0-9a-f]{24}$/); + }); + }); + }); + + describe('createResourceTypeNotSupportedError', () => { + describe('when creating error', () => { + it('should return error with message', () => { + const resourceType = 'unsupported'; + + const error = createResourceTypeNotSupportedError(resourceType); + + expect(error).toBeInstanceOf(InternalServerErrorException); + expect(error.message).toBe(`Common Cartridge resource type ${resourceType} is not supported`); + }); + }); + }); + + describe('createElementTypeNotSupportedError', () => { + describe('when creating error', () => { + it('should return error with message', () => { + const elementType = 'unsupported'; + + const error = createElementTypeNotSupportedError(elementType); + + expect(error).toBeInstanceOf(InternalServerErrorException); + expect(error.message).toBe(`Common Cartridge element type ${elementType} is not supported`); + }); + }); + }); + + describe('checkIntendedUse', () => { + describe('when intended use is not supported', () => { + it('should throw error', () => { + const supportedIntendedUses = ['use1', 'use2']; + + expect(() => checkIntendedUse('use3', supportedIntendedUses)).toThrowError( + 'Intended use use3 is not supported' + ); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/export/utils.ts b/apps/server/src/modules/common-cartridge/export/utils.ts new file mode 100644 index 00000000000..e510b8633f6 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/export/utils.ts @@ -0,0 +1,45 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import { ObjectID } from 'bson'; +import { Builder } from 'xml2js'; + +export type OmitVersion = Omit; + +export type OmitVersionAndFolder = Omit; + +export type OmitVersionAndType = Omit; + +const xmlBuilder = new Builder({ + xmldec: { version: '1.0', encoding: 'UTF-8' }, + renderOpts: { pretty: true, indent: ' ', newline: '\n' }, +}); + +export function buildXmlString(obj: unknown): string { + return xmlBuilder.buildObject(obj); +} + +export function createVersionNotSupportedError(version: string): Error { + return new InternalServerErrorException(`Common Cartridge version ${version} is not supported`); +} + +export function createResourceTypeNotSupportedError(type: string): Error { + return new InternalServerErrorException(`Common Cartridge resource type ${type} is not supported`); +} + +export function createElementTypeNotSupportedError(type: string): Error { + // AI next 1 line + return new InternalServerErrorException(`Common Cartridge element type ${type} is not supported`); +} + +export function createIdentifier(identifier?: string | ObjectID): string { + if (!identifier) { + return `i${new ObjectID().toString()}`; + } + + return `i${identifier.toString()}`; +} + +export function checkIntendedUse(intendedUse: string, supportedIntendedUses: string[]): void | never { + if (!supportedIntendedUses.includes(intendedUse)) { + throw new Error(`Intended use ${intendedUse} is not supported`); + } +} diff --git a/apps/server/src/modules/common-cartridge/import/common-cartridge-manifest-parser.spec.ts b/apps/server/src/modules/common-cartridge/import/common-cartridge-manifest-parser.spec.ts index 140fa464741..8dd6b2c8541 100644 --- a/apps/server/src/modules/common-cartridge/import/common-cartridge-manifest-parser.spec.ts +++ b/apps/server/src/modules/common-cartridge/import/common-cartridge-manifest-parser.spec.ts @@ -11,7 +11,9 @@ describe('CommonCartridgeManifestParser', () => { return { sut }; } - const buffer = await readFile('./apps/server/test/assets/common-cartridge/us_history_since_1877.imscc'); + const buffer = await readFile( + './apps/server/src/modules/common-cartridge/testing/assets/us_history_since_1877.imscc' + ); const archive = new AdmZip(buffer); const sut = new CommonCartridgeManifestParser(archive.readAsText('imsmanifest.xml'), DEFAULT_FILE_PARSER_OPTIONS); diff --git a/apps/server/src/modules/common-cartridge/index.ts b/apps/server/src/modules/common-cartridge/index.ts new file mode 100644 index 00000000000..c82ed05b595 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/index.ts @@ -0,0 +1,18 @@ +export { + CommonCartridgeFileBuilder, + CommonCartridgeFileBuilderProps, +} from './export/builders/common-cartridge-file-builder'; +export { + CommonCartridgeOrganizationBuilder, + CommonCartridgeOrganizationBuilderOptions, +} from './export/builders/common-cartridge-organization-builder'; +export { + CommonCartridgeElementType, + CommonCartridgeIntendedUseType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from './export/common-cartridge.enums'; +export { CommonCartridgeElementProps } from './export/elements/common-cartridge-element-factory'; +export { CommonCartridgeResourceProps } from './export/resources/common-cartridge-resource-factory'; +export { OmitVersion, createIdentifier } from './export/utils'; +export { CommonCartridgeFileParser } from './import/common-cartridge-file-parser'; diff --git a/apps/server/src/modules/common-cartridge/testing/assets/README.md b/apps/server/src/modules/common-cartridge/testing/assets/README.md new file mode 100644 index 00000000000..7500ae9bee3 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/assets/README.md @@ -0,0 +1,7 @@ +# Important Note + +When working with the XML files in the assets folder in the `common-cartridge` module, please ensure that they are saved with LF (Line Feed) line endings instead of CRLF (Carriage Return + Line Feed). Additionally, make sure that no new line is inserted at the end of the XML files. + +This is important, as the XML files are used for comparison in tests and the Common Cartridge standard does not add a line at the end of the XML files. + +Thank you for your attention to this matter. diff --git a/apps/server/test/assets/common-cartridge/us_history_since_1877.imscc b/apps/server/src/modules/common-cartridge/testing/assets/us_history_since_1877.imscc similarity index 100% rename from apps/server/test/assets/common-cartridge/us_history_since_1877.imscc rename to apps/server/src/modules/common-cartridge/testing/assets/us_history_since_1877.imscc diff --git a/apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/manifest.xml b/apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/manifest.xml new file mode 100644 index 00000000000..661d182508f --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/manifest.xml @@ -0,0 +1,42 @@ + + + + IMS Common Cartridge + 1.1.0 + + + + Common Cartridge Manifest + + + + + yes + + + 2023 John Doe, Jane Doe + + + + + + + + + Title 1 + + + Title 2 + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/weblink.xml b/apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/weblink.xml new file mode 100644 index 00000000000..e202cc742ec --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/assets/v1.1.0/weblink.xml @@ -0,0 +1,5 @@ + + + Title + + \ No newline at end of file diff --git a/apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/manifest.xml b/apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/manifest.xml new file mode 100644 index 00000000000..3b955fa1855 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/manifest.xml @@ -0,0 +1,42 @@ + + + + IMS Common Cartridge + 1.3.0 + + + + Common Cartridge Manifest + + + + + yes + + + 2023 John Doe, Jane Doe + + + + + + + + + Title 1 + + + Title 2 + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/weblink.xml b/apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/weblink.xml new file mode 100644 index 00000000000..47f4a784c22 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/assets/v1.3.0/weblink.xml @@ -0,0 +1,5 @@ + + + Title + + \ No newline at end of file diff --git a/apps/server/src/modules/common-cartridge/testing/common-cartridge-element-props.factory.ts b/apps/server/src/modules/common-cartridge/testing/common-cartridge-element-props.factory.ts new file mode 100644 index 00000000000..000ad586840 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/common-cartridge-element-props.factory.ts @@ -0,0 +1,96 @@ +import { faker } from '@faker-js/faker'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '@modules/common-cartridge'; +import { CommonCartridgeMetadataElementPropsV110 } from '../export/elements/v1.1.0/common-cartridge-metadata-element'; +import { CommonCartridgeOrganizationElementPropsV110 } from '../export/elements/v1.1.0/common-cartridge-organization-element'; +import { CommonCartridgeOrganizationsWrapperElementPropsV110 } from '../export/elements/v1.1.0/common-cartridge-organizations-wrapper-element'; +import { CommonCartridgeResourcesWrapperElementPropsV110 } from '../export/elements/v1.1.0/common-cartridge-resources-wrapper-element'; +import { CommonCartridgeMetadataElementPropsV130 } from '../export/elements/v1.3.0/common-cartridge-metadata-element'; +import { CommonCartridgeOrganizationElementPropsV130 } from '../export/elements/v1.3.0/common-cartridge-organization-element'; +import { CommonCartridgeOrganizationsWrapperElementPropsV130 } from '../export/elements/v1.3.0/common-cartridge-organizations-wrapper-element'; +import { CommonCartridgeResourcesWrapperElementPropsV130 } from '../export/elements/v1.3.0/common-cartridge-resources-wrapper-element'; +import { CommonCartridgeElement } from '../export/interfaces/common-cartridge-element.interface'; +import { CommonCartridgeResource } from '../export/interfaces/common-cartridge-resource.interface'; + +export function createCommonCartridgeMetadataElementPropsV110(): CommonCartridgeMetadataElementPropsV110 { + return { + type: CommonCartridgeElementType.METADATA, + version: CommonCartridgeVersion.V_1_1_0, + title: faker.lorem.words(), + creationDate: faker.date.past(), + copyrightOwners: [faker.person.fullName(), faker.person.fullName()], + }; +} + +export function createCommonCartridgeMetadataElementPropsV130(): CommonCartridgeMetadataElementPropsV130 { + return { + type: CommonCartridgeElementType.METADATA, + version: CommonCartridgeVersion.V_1_3_0, + title: faker.lorem.words(), + creationDate: faker.date.past(), + copyrightOwners: [faker.person.fullName(), faker.person.fullName()], + }; +} + +export function createCommonCartridgeOrganizationElementPropsV110( + items?: CommonCartridgeResource | Array +): CommonCartridgeOrganizationElementPropsV110 { + return { + type: CommonCartridgeElementType.ORGANIZATION, + identifier: faker.string.uuid(), + title: faker.lorem.words(), + items: items || [], + version: CommonCartridgeVersion.V_1_1_0, + }; +} + +export function createCommonCartridgeOrganizationElementPropsV130( + items?: CommonCartridgeResource | Array +): CommonCartridgeOrganizationElementPropsV130 { + return { + type: CommonCartridgeElementType.ORGANIZATION, + identifier: faker.string.uuid(), + title: faker.lorem.words(), + items: items || [], + version: CommonCartridgeVersion.V_1_3_0, + }; +} + +export function createCommonCartridgeOrganizationsWrapperElementPropsV110( + items?: CommonCartridgeElement[] +): CommonCartridgeOrganizationsWrapperElementPropsV110 { + return { + type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER, + version: CommonCartridgeVersion.V_1_1_0, + items: items || [], + }; +} + +export function createCommonCartridgeOrganizationsWrapperElementPropsV130( + items?: CommonCartridgeElement[] +): CommonCartridgeOrganizationsWrapperElementPropsV130 { + return { + type: CommonCartridgeElementType.ORGANIZATIONS_WRAPPER, + version: CommonCartridgeVersion.V_1_3_0, + items: items || [], + }; +} + +export function createCommonCartridgeResourcesWrapperElementPropsV110( + items?: CommonCartridgeResource[] +): CommonCartridgeResourcesWrapperElementPropsV110 { + return { + type: CommonCartridgeElementType.RESOURCES_WRAPPER, + version: CommonCartridgeVersion.V_1_1_0, + items: items || [], + }; +} + +export function createCommonCartridgeResourcesWrapperElementPropsV130( + items?: CommonCartridgeResource[] +): CommonCartridgeResourcesWrapperElementPropsV130 { + return { + type: CommonCartridgeElementType.RESOURCES_WRAPPER, + version: CommonCartridgeVersion.V_1_3_0, + items: items || [], + }; +} diff --git a/apps/server/src/modules/common-cartridge/testing/common-cartridge-resource-props.factory.ts b/apps/server/src/modules/common-cartridge/testing/common-cartridge-resource-props.factory.ts new file mode 100644 index 00000000000..cbe4f31f0c6 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/common-cartridge-resource-props.factory.ts @@ -0,0 +1,81 @@ +import { faker } from '@faker-js/faker'; +import { + CommonCartridgeIntendedUseType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '@modules/common-cartridge'; +import { CommonCartridgeElement } from '../export/interfaces/common-cartridge-element.interface'; +import { CommonCartridgeManifestResourcePropsV110 } from '../export/resources/v1.1.0/common-cartridge-manifest-resource'; +import { CommonCartridgeWebContentResourcePropsV110 } from '../export/resources/v1.1.0/common-cartridge-web-content-resource'; +import { CommonCartridgeWebLinkResourcePropsV110 } from '../export/resources/v1.1.0/common-cartridge-web-link-resource'; +import { CommonCartridgeManifestResourcePropsV130 } from '../export/resources/v1.3.0/common-cartridge-manifest-resource'; +import { CommonCartridgeWebContentResourcePropsV130 } from '../export/resources/v1.3.0/common-cartridge-web-content-resource'; +import { CommonCartridgeWebLinkResourcePropsV130 } from '../export/resources/v1.3.0/common-cartridge-web-link-resource'; + +export function createCommonCartridgeWeblinkResourcePropsV110(): CommonCartridgeWebLinkResourcePropsV110 { + return { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: faker.string.uuid(), + title: faker.lorem.words(), + url: faker.internet.url(), + version: CommonCartridgeVersion.V_1_1_0, + folder: faker.string.alphanumeric(10), + }; +} + +export function createCommonCartridgeWeblinkResourcePropsV130(): CommonCartridgeWebLinkResourcePropsV130 { + return { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: faker.string.uuid(), + title: faker.lorem.words(), + url: faker.internet.url(), + version: CommonCartridgeVersion.V_1_3_0, + folder: faker.string.alphanumeric(10), + }; +} + +export function createCommonCartridgeWebContentResourcePropsV110(): CommonCartridgeWebContentResourcePropsV110 { + return { + type: CommonCartridgeResourceType.WEB_CONTENT, + version: CommonCartridgeVersion.V_1_1_0, + identifier: faker.string.uuid(), + folder: faker.string.alphanumeric(10), + title: faker.lorem.words(), + html: faker.lorem.paragraphs(), + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }; +} + +export function createCommonCartridgeWebContentResourcePropsV130(): CommonCartridgeWebContentResourcePropsV130 { + return { + type: CommonCartridgeResourceType.WEB_CONTENT, + version: CommonCartridgeVersion.V_1_3_0, + identifier: faker.string.uuid(), + folder: faker.string.alphanumeric(10), + title: faker.lorem.words(), + html: faker.lorem.paragraphs(), + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }; +} + +export function createCommonCartridgeManifestResourcePropsV110(): CommonCartridgeManifestResourcePropsV110 { + return { + type: CommonCartridgeResourceType.MANIFEST, + version: CommonCartridgeVersion.V_1_1_0, + identifier: faker.string.uuid(), + metadata: {} as CommonCartridgeElement, + organizations: [], + resources: [], + }; +} + +export function createCommonCartridgeManifestResourcePropsV130(): CommonCartridgeManifestResourcePropsV130 { + return { + type: CommonCartridgeResourceType.MANIFEST, + version: CommonCartridgeVersion.V_1_3_0, + identifier: faker.string.uuid(), + metadata: {} as CommonCartridgeElement, + organizations: [], + resources: [], + }; +} 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 deleted file mode 100644 index 400b31dabb1..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-element.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface CommonCartridgeElement { - transform(): Record; -} diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-enums.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-enums.ts deleted file mode 100644 index 52ccd5c5818..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-enums.ts +++ /dev/null @@ -1,18 +0,0 @@ -export enum CommonCartridgeVersion { - V_1_1_0 = '1.1.0', - V_1_3_0 = '1.3.0', -} - -export enum CommonCartridgeResourceType { - LTI = 'imsbasiclti_xmlv1p0', - WEB_CONTENT = 'webcontent', - WEB_LINK_V1 = 'imswl_xmlv1p1', - WEB_LINK_V3 = 'imswl_xmlv1p3', -} - -export enum CommonCartridgeIntendedUseType { - ASSIGNMENT = 'assignment', - LESSON_PLAN = 'lessonplan', - SYLLABUS = 'syllabus', - UNSPECIFIED = 'unspecified', -} 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 deleted file mode 100644 index 7b5709f8a9e..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import AdmZip from 'adm-zip'; -import { parseStringPromise } from 'xml2js'; -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'; - -describe('CommonCartridgeFileBuilder', () => { - let archive: AdmZip; - - const getFileContentAsString = (path: string): string | undefined => archive.getEntry(path)?.getData().toString(); - const fileBuilderOptions: CommonCartridgeFileBuilderOptions = { - identifier: 'file-identifier', - copyrightOwners: 'Placeholder Copyright', - creationYear: 'Placeholder Creation Year', - title: 'file-title', - version: CommonCartridgeVersion.V_1_1_0, - }; - const organizationProps: ICommonCartridgeOrganizationProps = { - version: CommonCartridgeVersion.V_1_1_0, - identifier: 'organization-identifier', - title: 'organization-title', - resources: [], - }; - const ltiResourceProps: ICommonCartridgeResourceProps = { - version: CommonCartridgeVersion.V_1_1_0, - type: CommonCartridgeResourceType.LTI, - identifier: 'lti-identifier', - href: 'lti-identifier/lti.xml', - title: 'lti-title', - description: 'lti-description', - url: 'https://to-a-lti-tool.tld', - }; - const webContentResourceProps: ICommonCartridgeResourceProps = { - version: CommonCartridgeVersion.V_1_1_0, - type: CommonCartridgeResourceType.WEB_CONTENT, - identifier: 'web-content-identifier', - href: 'web-content-identifier/web-content.html', - title: 'web-content-title', - html: '

Text Resource Title

Text Resource Description

', - }; - - beforeAll(async () => { - const fileBuilder = new CommonCartridgeFileBuilder(fileBuilderOptions).addResourceToFile(webContentResourceProps); - fileBuilder.addOrganization(organizationProps).addResourceToOrganization(ltiResourceProps); - - archive = new AdmZip(await fileBuilder.build()); - }); - - describe('addOrganization', () => { - describe('when adding an organization to the common cartridge file', () => { - it('should add organization to manifest', () => { - const manifest = getFileContentAsString('imsmanifest.xml'); - expect(manifest).toContain(organizationProps.identifier); - expect(manifest).toContain(organizationProps.title); - expect(manifest).toContain(organizationProps.version); - }); - }); - - describe('when adding a resource to an organization', () => { - it('should add resource to organization', () => { - const manifest = getFileContentAsString('imsmanifest.xml'); - expect(manifest).toContain(`${ltiResourceProps.title}`); - }); - - it('should add resource to manifest', () => { - const manifest = getFileContentAsString('imsmanifest.xml'); - expect(manifest).toContain(``); - }); - - it('should create corresponding resource file in archive', () => { - expect(getFileContentAsString(ltiResourceProps.href)).toBeTruthy(); - }); - }); - }); - - describe('addResourceToFile', () => { - describe('when adding a resource to the common cartridge file', () => { - it('should add resource to manifest', () => { - const manifest = getFileContentAsString('imsmanifest.xml'); - expect(manifest).toContain(webContentResourceProps.identifier); - expect(manifest).toContain(``); - expect(manifest).not.toContain(webContentResourceProps.title); - }); - - it('should create corresponding file in archive', () => { - expect(getFileContentAsString(webContentResourceProps.href)).toBeTruthy(); - }); - }); - }); - - describe('build', () => { - describe('when creating common cartridge archive', () => { - it('should create manifest file at archive root', () => { - const manifest = getFileContentAsString('imsmanifest.xml'); - expect(manifest).toBeTruthy(); - }); - - it('should create valid manifest file', async () => { - const manifest = getFileContentAsString('imsmanifest.xml'); - await expect(parseStringPromise(manifest as string)).resolves.not.toThrow(); - }); - }); - }); -}); 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 deleted file mode 100644 index 5a40269c57b..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file-builder.ts +++ /dev/null @@ -1,109 +0,0 @@ -import AdmZip from 'adm-zip'; -import { Builder } from 'xml2js'; -import { CommonCartridgeElement } from './common-cartridge-element.interface'; -import { CommonCartridgeVersion } from './common-cartridge-enums'; -import { CommonCartridgeManifestElement } from './common-cartridge-manifest-element'; -import { - CommonCartridgeOrganizationItemElement, - ICommonCartridgeOrganizationProps, -} from './common-cartridge-organization-item-element'; -import { - CommonCartridgeResourceItemElement, - ICommonCartridgeResourceProps, -} from './common-cartridge-resource-item-element'; - -export type CommonCartridgeFileBuilderOptions = { - identifier: string; - title: string; - copyrightOwners: string; - creationYear: string; - version: CommonCartridgeVersion; -}; - -export interface ICommonCartridgeOrganizationBuilder { - addResourceToOrganization(props: ICommonCartridgeResourceProps): ICommonCartridgeOrganizationBuilder; -} - -export interface ICommonCartridgeFileBuilder { - addOrganization(props: ICommonCartridgeOrganizationProps): ICommonCartridgeOrganizationBuilder; - - addResourceToFile(props: ICommonCartridgeResourceProps): ICommonCartridgeFileBuilder; - - build(): Promise; -} - -class CommonCartridgeOrganizationBuilder implements ICommonCartridgeOrganizationBuilder { - constructor( - private readonly props: ICommonCartridgeOrganizationProps, - private readonly xmlBuilder: Builder, - private readonly zipBuilder: AdmZip - ) {} - - get organization(): CommonCartridgeElement { - return new CommonCartridgeOrganizationItemElement(this.props); - } - - get resources(): CommonCartridgeElement[] { - return this.props.resources.map( - (resourceProps) => new CommonCartridgeResourceItemElement(resourceProps, this.xmlBuilder) - ); - } - - addResourceToOrganization(props: ICommonCartridgeResourceProps): ICommonCartridgeOrganizationBuilder { - const newResource = new CommonCartridgeResourceItemElement(props, this.xmlBuilder); - this.props.resources.push(props); - if (!newResource.canInline()) { - this.zipBuilder.addFile(props.href, Buffer.from(newResource.content())); - } - return this; - } -} - -export class CommonCartridgeFileBuilder implements ICommonCartridgeFileBuilder { - private readonly xmlBuilder = new Builder(); - - private readonly zipBuilder = new AdmZip(); - - private readonly organizations = new Array(); - - private readonly resources = new Array(); - - constructor(private readonly options: CommonCartridgeFileBuilderOptions) {} - - addOrganization(props: ICommonCartridgeOrganizationProps): ICommonCartridgeOrganizationBuilder { - const organizationBuilder = new CommonCartridgeOrganizationBuilder(props, this.xmlBuilder, this.zipBuilder); - this.organizations.push(organizationBuilder); - return organizationBuilder; - } - - addResourceToFile(props: ICommonCartridgeResourceProps): ICommonCartridgeFileBuilder { - const resource = new CommonCartridgeResourceItemElement(props, this.xmlBuilder); - if (!resource.canInline()) { - this.zipBuilder.addFile(props.href, Buffer.from(resource.content())); - } - this.resources.push(resource); - return this; - } - - async build(): Promise { - const organizations = this.organizations.map((organization) => organization.organization); - const resources = this.organizations.flatMap((organization) => organization.resources).concat(this.resources); - const manifest = this.xmlBuilder.buildObject( - new CommonCartridgeManifestElement( - { - identifier: this.options.identifier, - }, - { - title: this.options.title, - copyrightOwners: this.options.copyrightOwners, - creationYear: this.options.creationYear, - version: this.options.version, - }, - organizations, - resources - ).transform() - ); - this.zipBuilder.addFile('imsmanifest.xml', Buffer.from(manifest)); - return this.zipBuilder.toBufferPromise(); - } -} 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 deleted file mode 100644 index 0969e712b05..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-file.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface CommonCartridgeFile { - canInline(): boolean; - content(): string; -} diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lesson-content-element.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lesson-content-element.ts deleted file mode 100644 index edac1e4f30a..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lesson-content-element.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * This type represents the content of a Lesson. - */ -export type ICommonCartridgeLessonContentProps = { - identifier: string; - title: string; - content: string; -}; diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.spec.ts deleted file mode 100644 index f52493e6f98..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Builder } from 'xml2js'; -import { CommonCartridgeResourceType, CommonCartridgeVersion } from './common-cartridge-enums'; -import { CommonCartridgeLtiResource, ICommonCartridgeLtiResourceProps } from './common-cartridge-lti-resource'; - -describe('CommonCartridgeLtiResource', () => { - const propsVersion1: ICommonCartridgeLtiResourceProps = { - type: CommonCartridgeResourceType.LTI, - version: CommonCartridgeVersion.V_1_1_0, - identifier: 'lti-identifier-version1', - href: 'lti-identifier-version1/lti.xml', - title: 'lti-title-version1', - description: 'lti-description-version1', - url: 'https://to-a-lti-tool-version1.tld', - }; - - const propsVersion3: ICommonCartridgeLtiResourceProps = { - type: CommonCartridgeResourceType.LTI, - version: CommonCartridgeVersion.V_1_3_0, - identifier: 'lti-identifier-version3', - href: 'lti-identifier-version3/lti.xml', - title: 'lti-title-version3', - description: 'lti-description-version3', - url: 'https://to-a-lti-tool-version3.tld', - }; - - const ltiResourceVersion1 = new CommonCartridgeLtiResource(propsVersion1, new Builder()); - const ltiResourceVersion3 = new CommonCartridgeLtiResource(propsVersion3, new Builder()); - - describe('content', () => { - describe('When Common Cartridge version 1.1', () => { - it('should return correct content for version 1.1', () => { - const expectedContent = ` - - - lti-title-version1 - lti-description-version1 - https://to-a-lti-tool-version1.tld - https://to-a-lti-tool-version1.tld - - - - `; - - const content = ltiResourceVersion1.content(); - - expect(content.replace(/\s/g, '')).toEqual(expectedContent.replace(/\s/g, '')); - }); - }); - - describe('When Common Cartridge version 1.3', () => { - it('should return correct content for version 1.3', () => { - const expectedContent = ` - - - lti-title-version3 - lti-description-version3 - https://to-a-lti-tool-version3.tld - https://to-a-lti-tool-version3.tld - - - - `; - - const content = ltiResourceVersion3.content(); - - expect(content.replace(/\s/g, '')).toEqual(expectedContent.replace(/\s/g, '')); - }); - }); - }); - - describe('transform', () => { - describe('When Common Cartridge version 1.1', () => { - it('should transform props into the expected resource structure', () => { - const expectedOutput = { - $: { - identifier: propsVersion1.identifier, - type: propsVersion1.type, - }, - file: { - $: { - href: propsVersion1.href, - }, - }, - }; - - const transformed = ltiResourceVersion1.transform(); - expect(transformed).toEqual(expectedOutput); - }); - }); - describe('When Common Cartridge version 1.3', () => { - it('should transform props into the expected resource structure', () => { - const expectedOutput = { - $: { - identifier: propsVersion3.identifier, - type: propsVersion3.type, - }, - file: { - $: { - href: propsVersion3.href, - }, - }, - }; - - const transformed = ltiResourceVersion3.transform(); - expect(transformed).toEqual(expectedOutput); - }); - }); - }); - - describe('canInline', () => { - describe('When Common Cartridge version 1.1', () => { - it('should return false for canInline', () => { - const result = ltiResourceVersion1.canInline(); - expect(result).toBe(false); - }); - }); - describe('When Common Cartridge version 1.3', () => { - it('should return false for canInline', () => { - const result = ltiResourceVersion3.canInline(); - expect(result).toBe(false); - }); - }); - }); -}); 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 deleted file mode 100644 index a374b3687a6..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-lti-resource.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Builder } from 'xml2js'; -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; - version: CommonCartridgeVersion; - identifier: string; - href: string; - title: string; - description?: string; - url: string; -}; - -export class CommonCartridgeLtiResource implements CommonCartridgeElement, CommonCartridgeFile { - constructor(private readonly props: ICommonCartridgeLtiResourceProps, private readonly xmlBuilder: Builder) {} - - canInline(): boolean { - return false; - } - - content(): string { - const commonObject = { - cartridge_basiclti_link: { - $: { - xmlns: '', - 'xmlns:blti': '', - 'xmlns:lticm': '', - 'xmlns:lticp': '', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:schemaLocation': '', - }, - blti: { - title: this.props.title, - description: this.props.description, - launch_url: this.props.url, - secure_launch_url: this.props.url, - cartridge_bundle: { - $: { - identifierref: 'BLTI001_Bundle', - }, - }, - cartridge_icon: { - $: { - identifierref: 'BLTI001_Icon', - }, - }, - }, - }, - }; - - switch (this.props.version) { - case CommonCartridgeVersion.V_1_3_0: - commonObject.cartridge_basiclti_link.$.xmlns = 'http://www.imsglobal.org/xsd/imslticc_v1p3'; - commonObject.cartridge_basiclti_link.$['xmlns:blti'] = 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0'; - commonObject.cartridge_basiclti_link.$['xmlns:lticm'] = 'http://www.imsglobal.org/xsd/imslticm_v1p0'; - commonObject.cartridge_basiclti_link.$['xmlns:lticp'] = 'http://www.imsglobal.org/xsd/imslticp_v1p0'; - commonObject.cartridge_basiclti_link.$['xsi:schemaLocation'] = - 'http://www.imsglobal.org/xsd/imslticc_v1p3 http://www.imsglobal.org/xsd/imslticc_v1p3.xsd' + - 'http://www.imsglobal.org/xsd/imslticp_v1p0 imslticp_v1p0.xsd' + - 'http://www.imsglobal.org/xsd/imslticm_v1p0 imslticm_v1p0.xsd' + - 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0 imsbasiclti_v1p0p1.xsd"'; - break; - default: - commonObject.cartridge_basiclti_link.$.xmlns = '/xsd/imslticc_v1p0'; - commonObject.cartridge_basiclti_link.$['xmlns:blti'] = '/xsd/imsbasiclti_v1p0'; - commonObject.cartridge_basiclti_link.$['xmlns:lticm'] = '/xsd/imslticm_v1p0'; - commonObject.cartridge_basiclti_link.$['xmlns:lticp'] = '/xsd/imslticp_v1p0'; - commonObject.cartridge_basiclti_link.$['xsi:schemaLocation'] = - '/xsd/imslticc_v1p0 /xsd/lti/ltiv1p0/imslticc_v1p0.xsd' + - '/xsd/imsbasiclti_v1p0 /xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd' + - '/xsd/imslticm_v1p0 /xsd/lti/ltiv1p0/imslticm_v1p0.xsd' + - '/xsd/imslticp_v1p0 /xsd/lti/ltiv1p0/imslticp_v1p0.xsd"'; - break; - } - - return this.xmlBuilder.buildObject(commonObject); - } - - transform(): Record { - return { - $: { - identifier: this.props.identifier, - type: this.props.type, - }, - file: { - $: { - href: this.props.href, - }, - }, - }; - } -} diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.spec.ts deleted file mode 100644 index c6d681ab393..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { CommonCartridgeManifestElement } from './common-cartridge-manifest-element'; -import { CommonCartridgeVersion } from './common-cartridge-enums'; -import { ICommonCartridgeMetadataProps } from './common-cartridge-metadata-element'; - -describe('CommonCartridgeManifestElement', () => { - const metadataPropsV3: ICommonCartridgeMetadataProps = { - version: CommonCartridgeVersion.V_1_3_0, - title: 'title of test metadata v3', - copyrightOwners: 'test copy right', - creationYear: 'test year', - }; - - const metadataPropsV1: ICommonCartridgeMetadataProps = { - version: CommonCartridgeVersion.V_1_1_0, - title: 'title of test metadata v1', - copyrightOwners: 'test copy right', - creationYear: 'test year', - }; - - const props = { - identifier: 'manifest-1', - }; - describe('commen cartridge version 3', () => { - it('should transform the manifest based on the provided common cartridge version 3', () => { - const manifestElement = new CommonCartridgeManifestElement(props, metadataPropsV3, [], []); - const result = manifestElement.transform(); - - expect(result).toEqual({ - manifest: { - $: { - identifier: 'manifest-1', - xmlns: 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_v1p1', - 'xmlns:mnf': 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/manifest', - 'xmlns:res': 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/resource', - 'xmlns:ext': 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_extensionv1p2', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:schemaLocation': - 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/resource http://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lomresource_v1p0.xsd ' + - 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_v1p1 http://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_imscp_v1p2_v1p0.xsd ' + - 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/manifest http://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lommanifest_v1p0.xsd ' + - 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_extensionv1p2 http://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_cpextensionv1p2_v1p0.xsd', - }, - metadata: { - schema: 'IMS Common Cartridge', - schemaversion: metadataPropsV3.version, - 'mnf:lom': { - 'mnf:general': { - 'mnf:title': { - 'mnf:string': metadataPropsV3.title, - }, - }, - 'mnf:rights': { - 'mnf:copyrightAndOtherRestrictions': { - 'mnf:value': 'yes', - }, - 'mnf:description': { - 'mnf:string': `${metadataPropsV3.creationYear} ${metadataPropsV3.copyrightOwners}`, - }, - }, - }, - }, - organizations: { - organization: [ - { - $: { - identifier: 'org-1', - structure: 'rooted-hierarchy', - }, - item: [ - { - $: { - identifier: 'LearningModules', - }, - item: [], - }, - ], - }, - ], - }, - resources: { - resource: [], - }, - }, - }); - }); - }); - describe('commen cartridge version 1', () => { - it('should transform the manifest based on the provided common cartridge version 1', () => { - const manifestElement = new CommonCartridgeManifestElement(props, metadataPropsV1, [], []); - const result = manifestElement.transform(); - - expect(result).toEqual({ - manifest: { - $: { - identifier: 'manifest-1', - xmlns: 'http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1', - 'xmlns:mnf': 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest', - 'xmlns:res': 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:schemaLocation': - 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lomresource_v1p0.xsd ' + - 'http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1 http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imscp_v1p2_v1p0.xsd ' + - 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lommanifest_v1p0.xsd ', - }, - metadata: { - schema: 'IMS Common Cartridge', - schemaversion: metadataPropsV1.version, - 'mnf:lom': { - 'mnf:general': { - 'mnf:title': { - 'mnf:string': metadataPropsV1.title, - }, - }, - 'mnf:rights': { - 'mnf:copyrightAndOtherRestrictions': { - 'mnf:value': 'yes', - }, - 'mnf:description': { - 'mnf:string': `${metadataPropsV1.creationYear} ${metadataPropsV1.copyrightOwners}`, - }, - }, - }, - }, - organizations: { - organization: [ - { - $: { - identifier: 'org-1', - structure: 'rooted-hierarchy', - }, - item: [ - { - $: { - identifier: 'LearningModules', - }, - item: [], - }, - ], - }, - ], - }, - resources: { - resource: [], - }, - }, - }); - }); - }); -}); 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 deleted file mode 100644 index 8e71b9adee4..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-manifest-element.ts +++ /dev/null @@ -1,64 +0,0 @@ -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'; - -export type ICommonCartridgeManifestProps = { - identifier: string; -}; - -export class CommonCartridgeManifestElement implements CommonCartridgeElement { - constructor( - private readonly props: ICommonCartridgeManifestProps, - private readonly metadataProps: ICommonCartridgeMetadataProps, - private readonly organizations: CommonCartridgeElement[], - private readonly resources: CommonCartridgeElement[] - ) {} - - transform(): Record { - const versionNumber = this.metadataProps.version; - switch (versionNumber) { - case CommonCartridgeVersion.V_1_3_0: - return { - manifest: { - $: { - identifier: this.props.identifier, - xmlns: 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_v1p1', - 'xmlns:mnf': 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/manifest', - 'xmlns:res': 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/resource', - 'xmlns:ext': 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_extensionv1p2', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:schemaLocation': - 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/resource http://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lomresource_v1p0.xsd ' + - 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_v1p1 http://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_imscp_v1p2_v1p0.xsd ' + - 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/manifest http://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lommanifest_v1p0.xsd ' + - 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_extensionv1p2 http://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_cpextensionv1p2_v1p0.xsd', - }, - metadata: new CommonCartridgeMetadataElement(this.metadataProps).transform(), - organizations: new CommonCartridgeOrganizationWrapperElement(this.organizations).transform(), - resources: new CommonCartridgeResourceWrapperElement(this.resources).transform(), - }, - }; - default: - return { - manifest: { - $: { - identifier: this.props.identifier, - xmlns: 'http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1', - 'xmlns:mnf': 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest', - 'xmlns:res': 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:schemaLocation': - 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lomresource_v1p0.xsd ' + - 'http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1 http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imscp_v1p2_v1p0.xsd ' + - 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lommanifest_v1p0.xsd ', - }, - metadata: new CommonCartridgeMetadataElement(this.metadataProps).transform(), - organizations: new CommonCartridgeOrganizationWrapperElement(this.organizations).transform(), - resources: new CommonCartridgeResourceWrapperElement(this.resources).transform(), - }, - }; - } - } -} diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.spec.ts deleted file mode 100644 index 3e2f648eda0..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ICommonCartridgeMetadataProps, CommonCartridgeMetadataElement } from './common-cartridge-metadata-element'; -import { CommonCartridgeVersion } from './common-cartridge-enums'; - -describe('CommonCartridgeMetadataElement', () => { - describe('transform', () => { - it('should return correct metadata regardless of common cartridge version', () => { - const props: ICommonCartridgeMetadataProps = { - title: 'title of metadata', - copyrightOwners: 'owner of course', - creationYear: '2023', - version: CommonCartridgeVersion.V_1_1_0, - }; - - const metadata = new CommonCartridgeMetadataElement(props); - const transformed = metadata.transform(); - expect(transformed).toEqual({ - schema: 'IMS Common Cartridge', - schemaversion: props.version, - 'mnf:lom': { - 'mnf:general': { - 'mnf:title': { - 'mnf:string': props.title, - }, - }, - 'mnf:rights': { - 'mnf:copyrightAndOtherRestrictions': { - 'mnf:value': 'yes', - }, - 'mnf:description': { - 'mnf:string': `${props.creationYear} ${props.copyrightOwners}`, - }, - }, - }, - }); - }); - }); -}); 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 deleted file mode 100644 index 17a0cf45faa..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-metadata-element.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CommonCartridgeElement } from './common-cartridge-element.interface'; -import { CommonCartridgeVersion } from './common-cartridge-enums'; - -export type ICommonCartridgeMetadataProps = { - title: string; - copyrightOwners: string; - creationYear: string; - version: CommonCartridgeVersion; -}; - -export class CommonCartridgeMetadataElement implements CommonCartridgeElement { - constructor(private readonly props: ICommonCartridgeMetadataProps) {} - - transform(): Record { - return { - schema: 'IMS Common Cartridge', - schemaversion: this.props.version, - 'mnf:lom': { - 'mnf:general': { - 'mnf:title': { - 'mnf:string': this.props.title, - }, - }, - 'mnf:rights': { - 'mnf:copyrightAndOtherRestrictions': { - 'mnf:value': 'yes', - }, - 'mnf:description': { - 'mnf:string': `${this.props.creationYear} ${this.props.copyrightOwners}`, - }, - }, - }, - }; - } -} diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.spec.ts deleted file mode 100644 index 525d301d939..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - ICommonCartridgeOrganizationProps, - CommonCartridgeOrganizationItemElement, -} from './common-cartridge-organization-item-element'; -import { CommonCartridgeVersion, CommonCartridgeResourceType } from './common-cartridge-enums'; -import { ICommonCartridgeResourceProps } from './common-cartridge-resource-item-element'; - -describe('CommonCartridgeOrganizationItemElement', () => { - describe('transform', () => { - it('should return correct organization item element regardless of common cartridge version', () => { - const webContentResourceProps: ICommonCartridgeResourceProps = { - type: CommonCartridgeResourceType.WEB_CONTENT, - version: CommonCartridgeVersion.V_1_3_0, - identifier: 'web-link', - href: 'https://example.com/link', - title: 'Web Link', - html: 'html tags for testing', - }; - const props: ICommonCartridgeOrganizationProps = { - identifier: 'identifier', - title: 'title of organization item element', - version: 'version of common cartridge', - resources: [webContentResourceProps], - }; - const organizationItemElement = new CommonCartridgeOrganizationItemElement(props); - const transformed = organizationItemElement.transform(); - expect(transformed).toEqual({ - $: { - identifier: props.identifier, - }, - title: props.title, - item: [ - { - $: { - identifier: expect.any(String), - identifierref: webContentResourceProps.identifier, - }, - title: webContentResourceProps.title, - }, - ], - }); - }); - }); -}); 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 deleted file mode 100644 index 5d237fc3f98..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-item-element.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { CommonCartridgeElement } from './common-cartridge-element.interface'; -import { ICommonCartridgeResourceProps } from './common-cartridge-resource-item-element'; -import { createIdentifier } from './utils'; - -export type ICommonCartridgeOrganizationProps = { - identifier: string; - title: string; - version: string; - resources: ICommonCartridgeResourceProps[]; -}; - -export class CommonCartridgeOrganizationItemElement implements CommonCartridgeElement { - constructor(private readonly props: ICommonCartridgeOrganizationProps) {} - - transform(): Record { - return { - $: { - identifier: this.props.identifier, - }, - title: this.props.title, - item: this.props.resources.map((content) => { - return { - $: { - identifier: createIdentifier(), - identifierref: content.identifier, - }, - title: content.title, - }; - }), - }; - } -} 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 deleted file mode 100644 index a26e40bc37c..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -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: CommonCartridgeElement[] = [ - { - transform: jest.fn().mockReturnValue({ identifier: 'element-1' }), - }, - { - transform: jest.fn().mockReturnValue({ identifier: 'element-2' }), - }, - ]; - - const organizationWrapperElement = new CommonCartridgeOrganizationWrapperElement(organizationElementsMock); - const result = organizationWrapperElement.transform(); - - expect(result).toEqual({ - organization: [ - { - $: { - identifier: 'org-1', - structure: 'rooted-hierarchy', - }, - item: [ - { - $: { - identifier: 'LearningModules', - }, - item: [{ identifier: 'element-1' }, { identifier: 'element-2' }], - }, - ], - }, - ], - }); - - expect(organizationElementsMock[0].transform).toHaveBeenCalled(); - expect(organizationElementsMock[1].transform).toHaveBeenCalled(); - }); -}); 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 deleted file mode 100644 index 34200b31e37..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-organization-wrapper-element.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CommonCartridgeElement } from './common-cartridge-element.interface'; - -export class CommonCartridgeOrganizationWrapperElement implements CommonCartridgeElement { - constructor(private readonly organizationElements: CommonCartridgeElement[]) {} - - transform(): Record { - return { - organization: [ - { - $: { - identifier: 'org-1', - structure: 'rooted-hierarchy', - }, - item: [ - { - $: { - identifier: 'LearningModules', - }, - item: this.organizationElements.map((organizationElement) => organizationElement.transform()), - }, - ], - }, - ], - }; - } -} diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.spec.ts deleted file mode 100644 index 0c32f9de0b5..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Builder } from 'xml2js'; -import { - CommonCartridgeResourceItemElement, - ICommonCartridgeResourceProps, -} from './common-cartridge-resource-item-element'; - -describe('CommonCartridgeResourceItemElement', () => { - describe('when creating a common cartridge resouce with unkown type', () => { - it('should throw an error', () => { - expect( - () => new CommonCartridgeResourceItemElement({} as ICommonCartridgeResourceProps, new Builder()) - ).toThrowError(); - }); - }); -}); 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 deleted file mode 100644 index 219e7296075..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-item-element.ts +++ /dev/null @@ -1,49 +0,0 @@ -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, - ICommonCartridgeWebContentResourceProps, -} from './common-cartridge-web-content-resource'; -import { - CommonCartridgeWebLinkResourceElement, - ICommonCartridgeWebLinkResourceProps, -} from './common-cartridge-web-link-resource'; - -export type ICommonCartridgeResourceProps = - | ICommonCartridgeLtiResourceProps - | ICommonCartridgeWebContentResourceProps - | ICommonCartridgeWebLinkResourceProps; - -export class CommonCartridgeResourceItemElement implements CommonCartridgeElement, CommonCartridgeFile { - private readonly inner: CommonCartridgeElement & CommonCartridgeFile; - - constructor(props: ICommonCartridgeResourceProps, xmlBuilder: Builder) { - if (props.type === CommonCartridgeResourceType.LTI) { - this.inner = new CommonCartridgeLtiResource(props, xmlBuilder); - } else if (props.type === CommonCartridgeResourceType.WEB_CONTENT) { - this.inner = new CommonCartridgeWebContentResource(props); - } else if ( - props.type === CommonCartridgeResourceType.WEB_LINK_V1 || - props.type === CommonCartridgeResourceType.WEB_LINK_V3 - ) { - this.inner = new CommonCartridgeWebLinkResourceElement(props, xmlBuilder); - } else { - throw new Error('Resource type is unknown!'); - } - } - - canInline(): boolean { - return this.inner.canInline(); - } - - content(): string { - return this.inner.content(); - } - - transform(): Record { - return this.inner.transform(); - } -} 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 deleted file mode 100644 index cd96c9d6784..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -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: CommonCartridgeElement[] = [ - { - transform: jest.fn().mockReturnValue({ identifier: 'resource-1' }), - }, - { - transform: jest.fn().mockReturnValue({ identifier: 'resource-2' }), - }, - ]; - - const resourceWrapperElement = new CommonCartridgeResourceWrapperElement(resourceElementsMock); - const result = resourceWrapperElement.transform(); - - expect(result).toEqual({ - resource: [{ identifier: 'resource-1' }, { identifier: 'resource-2' }], - }); - - expect(resourceElementsMock[0].transform).toHaveBeenCalled(); - expect(resourceElementsMock[1].transform).toHaveBeenCalled(); - }); -}); 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 deleted file mode 100644 index c188651f3d4..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-resource-wrapper-element.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CommonCartridgeElement } from './common-cartridge-element.interface'; - -export class CommonCartridgeResourceWrapperElement implements CommonCartridgeElement { - constructor(private readonly resourceElements: CommonCartridgeElement[]) {} - - transform(): Record { - return { - resource: this.resourceElements.map((resourceElement) => resourceElement.transform()), - }; - } -} diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.spec.ts deleted file mode 100644 index 5bcb07c0b2f..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { CommonCartridgeVersion, CommonCartridgeResourceType } from './common-cartridge-enums'; -import { - ICommonCartridgeWebContentResourceProps, - CommonCartridgeWebContentResource, -} from './common-cartridge-web-content-resource'; - -describe('CommonCartridgeWebContentResource', () => { - const props: ICommonCartridgeWebContentResourceProps = { - type: CommonCartridgeResourceType.WEB_CONTENT, - version: CommonCartridgeVersion.V_1_3_0, - identifier: 'web-link', - href: 'https://example.com/link', - title: 'Web Link', - html: 'html tages for testing', - }; - const webContentResource = new CommonCartridgeWebContentResource(props); - describe('content', () => { - it('should return html content regardless of common cartridge version', () => { - const content = webContentResource.content(); - expect(content).toContain(props.html); - }); - }); - describe('canInline', () => { - it('check the return value of the method Can Inline ', () => { - expect(webContentResource.canInline()).toBe(false); - }); - }); - describe('transform', () => { - it('should transform XML content regardless of common cartridge version', () => { - const transformed = webContentResource.transform(); - expect(webContentResource.canInline()).toBe(false); - expect(transformed).toEqual({ - $: { - identifier: props.identifier, - type: props.type, - intendeduse: 'unspecified', - }, - file: { - $: { - href: props.href, - }, - }, - }); - }); - }); -}); 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 deleted file mode 100644 index c45b981184f..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-content-resource.ts +++ /dev/null @@ -1,44 +0,0 @@ -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; - version: CommonCartridgeVersion; - identifier: string; - href: string; - title: string; - html: string; - intendedUse?: CommonCartridgeIntendedUseType; -}; - -export class CommonCartridgeWebContentResource implements CommonCartridgeElement, CommonCartridgeFile { - constructor(private readonly props: ICommonCartridgeWebContentResourceProps) {} - - canInline(): boolean { - return false; - } - - content(): string { - return this.props.html; - } - - transform(): Record { - return { - $: { - identifier: this.props.identifier, - type: this.props.type, - intendeduse: this.props.intendedUse ?? CommonCartridgeIntendedUseType.UNSPECIFIED, - }, - file: { - $: { - href: this.props.href, - }, - }, - }; - } -} diff --git a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.spec.ts b/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.spec.ts deleted file mode 100644 index cd5c374df60..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Builder } from 'xml2js'; -import { CommonCartridgeVersion, CommonCartridgeResourceType } from './common-cartridge-enums'; -import { - ICommonCartridgeWebLinkResourceProps, - CommonCartridgeWebLinkResourceElement, -} from './common-cartridge-web-link-resource'; - -describe('CommonCartridgeWebLinkResourceElement', () => { - const xmlBuilder = new Builder(); - const propsOfV3: ICommonCartridgeWebLinkResourceProps = { - type: CommonCartridgeResourceType.WEB_LINK_V3, - version: CommonCartridgeVersion.V_1_3_0, - identifier: 'web-link-v3', - href: 'https://example.com/linkv3', - title: 'Web Link v3', - url: 'https://example.com/linkv3', - }; - const propsOfV1: ICommonCartridgeWebLinkResourceProps = { - type: CommonCartridgeResourceType.WEB_LINK_V1, - version: CommonCartridgeVersion.V_1_1_0, - identifier: 'web-link-v1', - href: 'https://example.com/link1', - title: 'Web Link v1', - url: 'https://example.com/link1', - }; - - describe('CommonCartridgeWebLinkResourceElement of version 3', () => { - it('should return XML content of common cartridge version 3', () => { - const webLinkResource = new CommonCartridgeWebLinkResourceElement(propsOfV3, xmlBuilder); - const content = webLinkResource.content(); - const transformed = webLinkResource.transform(); - - expect(content).toContain('webLink'); - expect(content).toContain('http://www.w3.org/2001/XMLSchema-instance'); - expect(content).toContain('http://www.imsglobal.org/xsd/imsccv1p3/imswl_v1p3'); - expect(content).toContain('http://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_imswl_v1p3.xsd'); - expect(transformed).toEqual({ - $: { - identifier: propsOfV3.identifier, - type: propsOfV3.type, - }, - file: { - $: { - href: propsOfV3.href, - }, - }, - }); - expect(webLinkResource.canInline()).toBe(false); - }); - }); - - describe('CommonCartridgeWebLinkResourceElement of version 1', () => { - it('should return XML content of common cartridge version 1', () => { - const webLinkResource = new CommonCartridgeWebLinkResourceElement(propsOfV1, xmlBuilder); - const content = webLinkResource.content(); - const transformed = webLinkResource.transform(); - - expect(content).toContain('webLink'); - expect(content).toContain('http://www.w3.org/2001/XMLSchema-instance'); - expect(content).toContain('http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1'); - expect(content).toContain( - 'https://www.imsglobal.org/sites/default/files/profile/cc/ccv1p1/ccv1p1_imswl_v1p1.xsd' - ); - expect(transformed).toEqual({ - $: { - identifier: propsOfV1.identifier, - type: propsOfV1.type, - }, - file: { - $: { - href: propsOfV1.href, - }, - }, - }); - }); - }); -}); 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 deleted file mode 100644 index 09184d5ef0f..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge-web-link-resource.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Builder } from 'xml2js'; -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; - version: CommonCartridgeVersion; - identifier: string; - href: string; - title: string; - url: string; -}; - -export class CommonCartridgeWebLinkResourceElement implements CommonCartridgeElement, CommonCartridgeFile { - constructor(private readonly props: ICommonCartridgeWebLinkResourceProps, private readonly xmlBuilder: Builder) {} - - canInline(): boolean { - return false; - } - - content(): string { - const commonTags = { - title: this.props.title, - url: { - $: { - href: this.props.url, - target: '_self', - windowFeatures: 'width=100, height=100', - }, - }, - }; - switch (this.props.version) { - case CommonCartridgeVersion.V_1_3_0: - return this.xmlBuilder.buildObject({ - webLink: { - ...commonTags, - $: { - xmlns: 'http://www.imsglobal.org/xsd/imsccv1p3/imswl_v1p3', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:schemaLocation': - 'http://www.imsglobal.org/xsd/imsccv1p3/imswl_v1p3 http://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_imswl_v1p3.xsd', - }, - }, - }); - default: - return this.xmlBuilder.buildObject({ - webLink: { - ...commonTags, - $: { - xmlns: 'http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:schemaLocation': - 'http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1 https://www.imsglobal.org/sites/default/files/profile/cc/ccv1p1/ccv1p1_imswl_v1p1.xsd', - }, - }, - }); - } - } - - transform(): Record { - return { - $: { - identifier: this.props.identifier, - type: this.props.type, - }, - file: { - $: { - href: this.props.href, - }, - }, - }; - } -} 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 deleted file mode 100644 index afbff1098a5..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/common-cartridge.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface CommonCartridgeConfig { - FEATURE_IMSCC_COURSE_EXPORT_ENABLED: boolean; -} diff --git a/apps/server/src/modules/learnroom/common-cartridge/index.ts b/apps/server/src/modules/learnroom/common-cartridge/index.ts deleted file mode 100644 index 581c6529dcb..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * from './common-cartridge-element.interface'; -export * from './common-cartridge-enums'; -export * from './common-cartridge-file-builder'; -export * from './common-cartridge-file.interface'; -export * from './common-cartridge-lti-resource'; -export * from './common-cartridge-manifest-element'; -export * from './common-cartridge-metadata-element'; -export * from './common-cartridge-organization-item-element'; -export * from './common-cartridge-organization-wrapper-element'; -export * from './common-cartridge-resource-item-element'; -export * from './common-cartridge-resource-wrapper-element'; -export * from './common-cartridge-web-content-resource'; -export * from './common-cartridge-web-link-resource'; -export { CommonCartridgeConfig } from './common-cartridge.config'; diff --git a/apps/server/src/modules/learnroom/common-cartridge/utils.ts b/apps/server/src/modules/learnroom/common-cartridge/utils.ts deleted file mode 100644 index b83f8ec220a..00000000000 --- a/apps/server/src/modules/learnroom/common-cartridge/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ObjectId } from 'bson'; - -export function createIdentifier(id?: string | ObjectId): string { - id = id ?? new ObjectId(); - return `i${id.toString()}`; -} diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index bbdf9605b93..caae44a64b3 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -90,14 +90,14 @@ describe('Course Controller (API)', () => { const student2 = createStudent(); const teacher = createTeacher(); const substitutionTeacher = createTeacher(); - const teacherUnkownToCourse = createTeacher(); + const teacherUnknownToCourse = createTeacher(); const course = courseFactory.build({ name: 'course #1', teachers: [teacher.user], students: [student1.user, student2.user], }); - return { course, teacher, teacherUnkownToCourse, substitutionTeacher, student1 }; + return { course, teacher, teacherUnknownToCourse, substitutionTeacher, student1 }; }; it('should find course export', async () => { @@ -122,7 +122,9 @@ describe('Course Controller (API)', () => { describe('[POST] /courses/import', () => { const setup = async () => { const teacher = createTeacher(); - const course = await readFile('./apps/server/test/assets/common-cartridge/us_history_since_1877.imscc'); + const course = await readFile( + './apps/server/src/modules/common-cartridge/testing/assets/us_history_since_1877.imscc' + ); const courseFileName = 'us_history_since_1877.imscc'; await em.persistAndFlush([teacher.account, teacher.user]); diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 48398397c3a..4bcbb5bdead 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -2,7 +2,6 @@ import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication import { Controller, Get, - NotFoundException, Param, Post, Query, @@ -62,12 +61,13 @@ export class CourseController { @Query() queryParams: CourseQueryParams, @Res({ passthrough: true }) response: Response ): Promise { - if (!this.configService.get('FEATURE_IMSCC_COURSE_EXPORT_ENABLED')) throw new NotFoundException(); const result = await this.courseExportUc.exportCourse(urlParams.courseId, currentUser.userId, queryParams.version); + response.set({ 'Content-Type': 'application/zip', 'Content-Disposition': 'attachment;', }); + return new StreamableFile(result); } diff --git a/apps/server/src/modules/learnroom/controller/dto/course.query.params.ts b/apps/server/src/modules/learnroom/controller/dto/course.query.params.ts index 59b99af2f33..3f285dbe354 100644 --- a/apps/server/src/modules/learnroom/controller/dto/course.query.params.ts +++ b/apps/server/src/modules/learnroom/controller/dto/course.query.params.ts @@ -1,6 +1,6 @@ +import { CommonCartridgeVersion } from '@modules/common-cartridge'; import { ApiProperty } from '@nestjs/swagger'; import { IsString, Matches } from 'class-validator'; -import { CommonCartridgeVersion } from '../../common-cartridge'; export class CourseQueryParams { @IsString() diff --git a/apps/server/src/modules/learnroom/index.ts b/apps/server/src/modules/learnroom/index.ts index dbc38969577..0db918062ac 100644 --- a/apps/server/src/modules/learnroom/index.ts +++ b/apps/server/src/modules/learnroom/index.ts @@ -1,3 +1,4 @@ +export { LearnroomConfig } from './learnroom.config'; export * from './learnroom.module'; export { CommonCartridgeExportService, @@ -7,5 +8,3 @@ export { DashboardService, RoomsService, } from './service'; -export { CommonCartridgeConfig } from './common-cartridge'; -export { LearnroomConfig } from './learnroom.config'; diff --git a/apps/server/src/modules/learnroom/learnroom.config.ts b/apps/server/src/modules/learnroom/learnroom.config.ts index e377bfe9c53..b4ed2ff7d96 100644 --- a/apps/server/src/modules/learnroom/learnroom.config.ts +++ b/apps/server/src/modules/learnroom/learnroom.config.ts @@ -1,4 +1,6 @@ export interface LearnroomConfig { + FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED: boolean; + GEOGEBRA_BASE_URL: string; FEATURE_COLUMN_BOARD_ENABLED: boolean; FEATURE_COPY_SERVICE_ENABLED: boolean; FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_ENABLED: boolean; diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 8e3804cc773..96759014d12 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -4,18 +4,20 @@ import { LessonModule } from '@modules/lesson'; import { TaskModule } from '@modules/task'; import { ContextExternalToolModule } from '@modules/tool/context-external-tool'; import { ToolConfigModule } from '@modules/tool/tool-config.module'; -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { - LegacyBoardRepo, CourseGroupRepo, CourseRepo, DashboardElementRepo, DashboardModelMapper, DashboardRepo, + LegacyBoardRepo, UserRepo, } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; +import { BoardNodeRepo } from '../board/repo'; import { COURSE_REPO } from './domain'; +import { CommonCartridgeMapper } from './mapper/common-cartridge.mapper'; import { CourseMikroOrmRepo } from './repo/mikro-orm/course.repo'; import { BoardCopyService, @@ -28,11 +30,10 @@ import { RoomsService, } from './service'; import { CommonCartridgeFileValidatorPipe } from './utils'; -import { BoardNodeRepo } from '../board/repo'; @Module({ imports: [ - BoardModule, + forwardRef(() => BoardModule), CopyHelperModule, ContextExternalToolModule, LessonModule, @@ -50,6 +51,7 @@ import { BoardNodeRepo } from '../board/repo'; CommonCartridgeExportService, CommonCartridgeFileValidatorPipe, CommonCartridgeImportService, + CommonCartridgeMapper, CourseCopyService, CourseGroupRepo, CourseGroupService, diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts new file mode 100644 index 00000000000..566d4488a5e --- /dev/null +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts @@ -0,0 +1,400 @@ +import { faker } from '@faker-js/faker'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { + CommonCartridgeElementProps, + CommonCartridgeElementType, + CommonCartridgeFileBuilderProps, + CommonCartridgeIntendedUseType, + CommonCartridgeOrganizationBuilderOptions, + CommonCartridgeResourceProps, + CommonCartridgeResourceType, + CommonCartridgeVersion, + OmitVersion, + createIdentifier, +} from '@modules/common-cartridge'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ComponentProperties, ComponentType } from '@shared/domain/entity'; +import { courseFactory, lessonFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; +import { LearnroomConfig } from '../learnroom.config'; +import { CommonCartridgeMapper } from './common-cartridge.mapper'; + +describe('CommonCartridgeMapper', () => { + let module: TestingModule; + let sut: CommonCartridgeMapper; + let configServiceMock: DeepMocked>; + + beforeAll(async () => { + await setupEntities(); + module = await Test.createTestingModule({ + providers: [ + CommonCartridgeMapper, + { + provide: ConfigService, + useValue: createMock>(), + }, + ], + }).compile(); + sut = module.get(CommonCartridgeMapper); + configServiceMock = module.get(ConfigService); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('mapCourseToMetadata', () => { + describe('when mapping course to metadata', () => { + const setup = () => { + const course = courseFactory.buildWithId({ + teachers: userFactory.buildListWithId(2), + }); + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { course }; + }; + + it('should map to metadata', () => { + const { course } = setup(); + const metadataProps = sut.mapCourseToMetadata(course); + + expect(metadataProps).toStrictEqual({ + type: CommonCartridgeElementType.METADATA, + title: course.name, + copyrightOwners: course.teachers.toArray().map((teacher) => `${teacher.firstName} ${teacher.lastName}`), + creationDate: course.createdAt, + }); + }); + }); + }); + + describe('mapLessonToOrganization', () => { + describe('when mapping lesson to organization', () => { + const setup = () => { + const lesson = lessonFactory.buildWithId(); + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { lesson }; + }; + + it('should map to organization', () => { + const { lesson } = setup(); + const organizationProps = sut.mapLessonToOrganization(lesson); + + expect(organizationProps).toStrictEqual>({ + identifier: createIdentifier(lesson.id), + title: lesson.name, + }); + }); + }); + }); + + describe('mapContentToOrganization', () => { + describe('when mapping content to organization', () => { + const setup = () => { + const componentProps: ComponentProperties = { + title: 'title', + hidden: false, + component: ComponentType.TEXT, + content: { + text: 'text', + }, + }; + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { componentProps }; + }; + + it('should map to organization', () => { + const { componentProps } = setup(); + const organizationProps = sut.mapContentToOrganization(componentProps); + + expect(organizationProps).toStrictEqual>({ + identifier: expect.any(String), + title: componentProps.title, + }); + }); + }); + }); + + describe('mapTaskToResource', () => { + const setup = () => { + const task = taskFactory.buildWithId(); + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { task }; + }; + + describe('when mapping task with version 1.3.0', () => { + it('should map task to web content', () => { + const { task } = setup(); + const resourceProps = sut.mapTaskToResource(task, CommonCartridgeVersion.V_1_3_0); + + expect(resourceProps).toStrictEqual({ + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: createIdentifier(task.id), + title: task.name, + html: `

${task.name}

${task.description}

`, + intendedUse: CommonCartridgeIntendedUseType.ASSIGNMENT, + }); + }); + }); + + describe('when using other version than 1.3.0', () => { + it('should map to web content', () => { + const { task } = setup(); + const versions = [ + CommonCartridgeVersion.V_1_0_0, + CommonCartridgeVersion.V_1_1_0, + CommonCartridgeVersion.V_1_2_0, + CommonCartridgeVersion.V_1_4_0, + ]; + + versions.forEach((version) => { + const resourceProps = sut.mapTaskToResource(task, version); + + expect(resourceProps).toStrictEqual({ + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: createIdentifier(task.id), + title: task.name, + html: `

${task.name}

${task.description}

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }); + }); + }); + }); + }); + + describe('mapTaskToOrganization', () => { + describe('when mapping task', () => { + const setup = () => { + const task = taskFactory.buildWithId(); + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { task }; + }; + + it('should map to organization', () => { + const { task } = setup(); + const organizationProps = sut.mapTaskToOrganization(task); + + expect(organizationProps).toStrictEqual>({ + identifier: expect.any(String), + title: task.name, + }); + }); + }); + }); + + describe('mapContentToResources', () => { + describe('when mapping text content', () => { + const setup = () => { + const componentProps: ComponentProperties = { + title: 'title', + hidden: false, + component: ComponentType.TEXT, + content: { + text: 'text', + }, + }; + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { componentProps }; + }; + + it('should map to web content', () => { + const { componentProps } = setup(); + const resourceProps = sut.mapContentToResources(componentProps); + + expect(resourceProps).toStrictEqual({ + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: expect.any(String), + title: componentProps.title, + html: `

${componentProps.title}

${componentProps?.content.text}

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }); + }); + }); + + describe('when mapping geogebra content', () => { + const setup = () => { + const componentProps: ComponentProperties = { + title: 'title', + hidden: false, + component: ComponentType.GEOGEBRA, + content: { + materialId: 'material-id', + }, + }; + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { componentProps }; + }; + + it('should map to web link', () => { + const { componentProps } = setup(); + const resourceProps = sut.mapContentToResources(componentProps); + + expect(resourceProps).toStrictEqual({ + type: CommonCartridgeResourceType.WEB_LINK, + title: componentProps.title, + identifier: expect.any(String), + url: `${configServiceMock.getOrThrow('FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED')}/m/${ + componentProps.content.materialId + }`, + }); + }); + }); + + describe('when mapping etherpad content', () => { + const setup = () => { + const componentProps: ComponentProperties = { + title: 'title', + hidden: false, + component: ComponentType.ETHERPAD, + content: { + description: 'description', + title: 'title', + url: 'url', + }, + }; + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { componentProps }; + }; + + it('should map to web link', () => { + const { componentProps } = setup(); + const resourceProps = sut.mapContentToResources(componentProps); + + expect(resourceProps).toStrictEqual({ + type: CommonCartridgeResourceType.WEB_LINK, + identifier: expect.any(String), + title: `${componentProps.content.title} - ${componentProps.content.description}`, + url: componentProps.content.url, + }); + }); + }); + + describe('when mapping learn store content to resources', () => { + const setup = () => { + const componentProps: ComponentProperties = { + _id: 'id', + title: 'title', + hidden: false, + component: ComponentType.LERNSTORE, + content: { + resources: [ + { + client: 'client', + description: 'description', + title: 'title', + url: 'url', + }, + ], + }, + }; + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { componentProps }; + }; + + it('should map to web link', () => { + const { componentProps } = setup(); + const resourceProps = sut.mapContentToResources(componentProps); + + expect(resourceProps).toStrictEqual([ + { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: expect.any(String), + title: componentProps.content?.resources[0].title as string, + url: componentProps.content?.resources[0].url as string, + }, + ]); + }); + }); + + describe('when no learn store content is provided', () => { + // AI next 16 lines + const setup = () => { + const componentProps: ComponentProperties = { + _id: 'id', + title: 'title', + hidden: false, + component: ComponentType.LERNSTORE, + }; + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { componentProps }; + }; + + it('should map to empty array', () => { + const { componentProps } = setup(); + const resourceProps = sut.mapContentToResources(componentProps); + + expect(resourceProps).toEqual([]); + }); + }); + + describe('when mapping unknown content', () => { + const setup = () => { + const unknownComponentProps: ComponentProperties = { + title: 'title', + hidden: false, + component: ComponentType.INTERNAL, + content: { + url: 'url', + }, + }; + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { unknownComponentProps }; + }; + + it('should map to empty array', () => { + const { unknownComponentProps } = setup(); + const resourceProps = sut.mapContentToResources(unknownComponentProps); + + expect(resourceProps).toEqual([]); + }); + }); + }); + + describe('mapCourseToManifest', () => { + describe('when mapping course', () => { + const setup = () => { + const course = courseFactory.buildWithId(); + const version = CommonCartridgeVersion.V_1_1_0; + + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + return { course, version }; + }; + + it('should map to manifest', () => { + const { course, version } = setup(); + const fileBuilderProps = sut.mapCourseToManifest(version, course); + + expect(fileBuilderProps).toStrictEqual({ + version: CommonCartridgeVersion.V_1_1_0, + identifier: createIdentifier(course.id), + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts new file mode 100644 index 00000000000..b5e89980278 --- /dev/null +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts @@ -0,0 +1,120 @@ +import { + CommonCartridgeElementProps, + CommonCartridgeElementType, + CommonCartridgeFileBuilderProps, + CommonCartridgeIntendedUseType, + CommonCartridgeOrganizationBuilderOptions, + CommonCartridgeResourceProps, + CommonCartridgeResourceType, + CommonCartridgeVersion, + createIdentifier, +} from '@modules/common-cartridge'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ComponentProperties, ComponentType, Course, LessonEntity, Task } from '@shared/domain/entity'; +import { LearnroomConfig } from '../learnroom.config'; + +@Injectable() +export class CommonCartridgeMapper { + constructor(private readonly configService: ConfigService) {} + + public mapCourseToMetadata(course: Course): CommonCartridgeElementProps { + return { + type: CommonCartridgeElementType.METADATA, + title: course.name, + copyrightOwners: course.teachers.toArray().map((teacher) => `${teacher.firstName} ${teacher.lastName}`), + creationDate: course.createdAt, + }; + } + + public mapLessonToOrganization(lesson: LessonEntity): CommonCartridgeOrganizationBuilderOptions { + return { + identifier: createIdentifier(lesson.id), + title: lesson.name, + }; + } + + public mapContentToOrganization(content: ComponentProperties): CommonCartridgeOrganizationBuilderOptions { + return { + identifier: createIdentifier(content._id), + title: content.title, + }; + } + + public mapTaskToOrganization(task: Task): CommonCartridgeOrganizationBuilderOptions { + return { + identifier: createIdentifier(), + title: task.name, + }; + } + + public mapTaskToResource(task: Task, version: CommonCartridgeVersion): CommonCartridgeResourceProps { + const intendedUse = (() => { + switch (version) { + case CommonCartridgeVersion.V_1_1_0: + return CommonCartridgeIntendedUseType.UNSPECIFIED; + case CommonCartridgeVersion.V_1_3_0: + return CommonCartridgeIntendedUseType.ASSIGNMENT; + default: + return CommonCartridgeIntendedUseType.UNSPECIFIED; + } + })(); + + return { + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: createIdentifier(task.id), + title: task.name, + html: `

${task.name}

${task.description}

`, + intendedUse, + }; + } + + public mapContentToResources( + content: ComponentProperties + ): CommonCartridgeResourceProps | CommonCartridgeResourceProps[] { + switch (content.component) { + case ComponentType.TEXT: + return { + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: createIdentifier(content._id), + title: content.title, + html: `

${content.title}

${content.content.text}

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }; + case ComponentType.GEOGEBRA: + return { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: createIdentifier(content._id), + title: content.title, + url: `${this.configService.getOrThrow('GEOGEBRA_BASE_URL')}/m/${content.content.materialId}`, + }; + case ComponentType.ETHERPAD: + return { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: createIdentifier(content._id), + title: `${content.title} - ${content.content.description}`, + url: content.content.url, + }; + case ComponentType.LERNSTORE: + return ( + content.content?.resources.map((resource) => { + return { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: createIdentifier(), + title: resource.title, + url: resource.url, + }; + }) || [] + ); + default: + return []; + } + } + + public mapCourseToManifest(version: CommonCartridgeVersion, course: Course): CommonCartridgeFileBuilderProps { + return { + version, + identifier: createIdentifier(course.id), + }; + } +} 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 9f964682836..b8c057459df 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,37 +1,85 @@ +import { faker } from '@faker-js/faker'; 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 { CommonCartridgeVersion } from '@modules/common-cartridge'; +import { CommonCartridgeExportService, CourseService, LearnroomConfig } from '@modules/learnroom'; +import { LessonService } from '@modules/lesson'; +import { TaskService } from '@modules/task'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { - ComponentProperties, - ComponentTextProperties, - ComponentType, - Course, - LessonEntity, - Task, -} from '@shared/domain/entity'; +import { ComponentType } from '@shared/domain/entity'; import { courseFactory, lessonFactory, setupEntities, taskFactory } from '@shared/testing'; import AdmZip from 'adm-zip'; -import { CommonCartridgeVersion } from '../common-cartridge'; +import { CommonCartridgeMapper } from '../mapper/common-cartridge.mapper'; describe('CommonCartridgeExportService', () => { let module: TestingModule; - let courseExportService: CommonCartridgeExportService; + let sut: CommonCartridgeExportService; let courseServiceMock: DeepMocked; let lessonServiceMock: DeepMocked; let taskServiceMock: DeepMocked; + let configServiceMock: DeepMocked>; + + const createXmlString = (nodeName: string, value: boolean | number | string): string => + `<${nodeName}>${value.toString()}`; + const getFileContent = (archive: AdmZip, filePath: string): string | undefined => + archive.getEntry(filePath)?.getData().toString(); + const setupParams = async (version: CommonCartridgeVersion) => { + const course = courseFactory.teachersWithId(2).buildWithId(); + const tasks = taskFactory.buildListWithId(2); + const lessons = lessonFactory.buildListWithId(1, { + contents: [ + { + title: 'text-title', + hidden: false, + component: ComponentType.TEXT, + content: { + text: 'text', + }, + }, + { + title: 'lernstore-title', + hidden: false, + component: ComponentType.LERNSTORE, + content: { + resources: [ + { + client: 'client-1', + description: 'description-1', + title: 'title-1', + url: 'url-1', + }, + { + client: 'client-2', + description: 'description-2', + title: 'title-2', + url: 'url-2', + }, + ], + }, + }, + ], + }); + const [lesson] = lessons; + const taskFromLesson = taskFactory.buildWithId({ course, lesson }); - let course: Course; - let lessons: LessonEntity[]; - let tasks: Task[]; + lessonServiceMock.findById.mockResolvedValue(lesson); + courseServiceMock.findById.mockResolvedValue(course); + lessonServiceMock.findByCourseIds.mockResolvedValue([lessons, lessons.length]); + taskServiceMock.findBySingleParent.mockResolvedValue([tasks, tasks.length]); + configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); + + const buffer = await sut.exportCourse(course.id, faker.string.uuid(), version); + const archive = new AdmZip(buffer); + + return { archive, course, lessons, tasks, taskFromLesson }; + }; beforeAll(async () => { await setupEntities(); module = await Test.createTestingModule({ providers: [ CommonCartridgeExportService, + CommonCartridgeMapper, { provide: CourseService, useValue: createMock(), @@ -44,43 +92,17 @@ describe('CommonCartridgeExportService', () => { provide: TaskService, useValue: createMock(), }, + { + provide: ConfigService, + useValue: createMock>(), + }, ], }).compile(); - courseExportService = module.get(CommonCartridgeExportService); + sut = module.get(CommonCartridgeExportService); courseServiceMock = module.get(CourseService); lessonServiceMock = module.get(LessonService); taskServiceMock = module.get(TaskService); - course = courseFactory.teachersWithId(2).buildWithId(); - lessons = lessonFactory.buildListWithId(5, { - contents: [ - { - component: ComponentType.TEXT, - title: 'Text', - content: { - text: 'text', - }, - } as ComponentProperties, - { - component: ComponentType.ETHERPAD, - title: 'Etherpad', - content: { - url: 'url', - }, - } as ComponentProperties, - { - component: ComponentType.GEOGEBRA, - title: 'Geogebra', - content: { - materialId: 'materialId', - }, - } as ComponentProperties, - {} as ComponentProperties, - ], - }); - tasks = taskFactory.buildListWithId(5, { - name: 'Task of a lesson', - lesson: lessons[0], - }); + configServiceMock = module.get(ConfigService); }); afterAll(async () => { @@ -88,139 +110,88 @@ describe('CommonCartridgeExportService', () => { }); describe('exportCourse', () => { - const setupExport = async (version: CommonCartridgeVersion) => { - const [lesson] = lessons; - const textContent = { text: 'Some random text' } as ComponentTextProperties; - const lessonContent: ComponentProperties = { - _id: 'random_id', - title: 'A random title', - hidden: false, - component: ComponentType.TEXT, - content: textContent, - }; - lesson.contents = [lessonContent]; - lessonServiceMock.findById.mockResolvedValueOnce(lesson); - courseServiceMock.findById.mockResolvedValueOnce(course); - lessonServiceMock.findByCourseIds.mockResolvedValueOnce([lessons, lessons.length]); - taskServiceMock.findBySingleParent.mockResolvedValueOnce([tasks, tasks.length]); - const archive = new AdmZip(await courseExportService.exportCourse(course.id, '', version)); - return archive; - }; - - describe('When Common Cartridge version 1.1', () => { - let archive: AdmZip; - - beforeAll(async () => { - archive = await setupExport(CommonCartridgeVersion.V_1_1_0); - }); + describe('when using version 1.1', () => { + const setup = async () => setupParams(CommonCartridgeVersion.V_1_1_0); - it('should create manifest file', () => { - expect(archive.getEntry('imsmanifest.xml')).toBeDefined(); + it('should use schema version 1.1.0', async () => { + const { archive } = await setup(); + + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('schemaversion', '1.1.0')); }); - it('should add title to manifest file', () => { - expect(archive.getEntry('imsmanifest.xml')?.getData().toString()).toContain(course.name); + it('should add course', async () => { + const { archive, course } = await setup(); + + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('mnf:string', course.name)); }); - it('should add lessons as organization items to manifest file', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); + it('should add lessons', async () => { + const { archive, lessons } = await setup(); + lessons.forEach((lesson) => { - expect(manifest).toContain(lesson.name); + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('title', lesson.name)); }); }); - it('should add lesson text content to manifest file', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); - expect(manifest).toContain(lessons[0].contents[0].title); - }); - - it('should add copyright information to manifest file', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); - expect(manifest).toContain(course.teachers[0].firstName); - expect(manifest).toContain(course.teachers[0].lastName); - expect(manifest).toContain(course.teachers[1].firstName); - expect(manifest).toContain(course.teachers[1].lastName); - expect(manifest).toContain(course.createdAt.getFullYear().toString()); - }); + it('should add tasks', async () => { + const { archive, tasks } = await setup(); - it('should add tasks as assignments', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); tasks.forEach((task) => { - expect(manifest).toContain(`${task.name}`); - expect(manifest).toContain(`identifier="i${task.id}" type="webcontent" intendeduse="unspecified"`); + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(` { + it('should add tasks of lesson to manifest file', async () => { + const { archive, lessons } = await setup(); const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); + lessons[0].tasks.getItems().forEach((task) => { expect(manifest).toContain(`${task.name}`); expect(manifest).toContain(`identifier="i${task.id}" type="webcontent" intendeduse="unspecified"`); }); }); - - it('should add version 1 information to manifest file', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); - expect(manifest).toContain(CommonCartridgeVersion.V_1_1_0); - }); }); - describe('When Common Cartridge version 1.3', () => { - let archive: AdmZip; + describe('when using version 1.3', () => { + const setup = async () => setupParams(CommonCartridgeVersion.V_1_3_0); - beforeAll(async () => { - archive = await setupExport(CommonCartridgeVersion.V_1_3_0); - }); + it('should use schema version 1.3.0', async () => { + const { archive } = await setup(); - it('should create manifest file', () => { - expect(archive.getEntry('imsmanifest.xml')).toBeDefined(); + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('schemaversion', '1.3.0')); }); - it('should add title to manifest file', () => { - expect(archive.getEntry('imsmanifest.xml')?.getData().toString()).toContain(course.name); + it('should add course', async () => { + const { archive, course } = await setup(); + + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('mnf:string', course.name)); }); - it('should add lessons as organization items to manifest file', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); + it('should add lessons', async () => { + const { archive, lessons } = await setup(); + lessons.forEach((lesson) => { - expect(manifest).toContain(lesson.name); + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('title', lesson.name)); }); }); - it('should add lesson text content to manifest file', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); - expect(manifest).toContain(lessons[0].contents[0].title); - }); - - it('should add copyright information to manifest file', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); - expect(manifest).toContain(course.teachers[0].firstName); - expect(manifest).toContain(course.teachers[0].lastName); - expect(manifest).toContain(course.teachers[1].firstName); - expect(manifest).toContain(course.teachers[1].lastName); - expect(manifest).toContain(course.createdAt.getFullYear().toString()); - }); + it('should add tasks', async () => { + const { archive, tasks } = await setup(); - it('should add tasks as assignments', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); tasks.forEach((task) => { - expect(manifest).toContain(`${task.name}`); - expect(manifest).toContain(`identifier="i${task.id}" type="webcontent" intendeduse="assignment"`); + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(` { + it('should add tasks of lesson to manifest file', async () => { + const { archive, lessons } = await setup(); const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); + lessons[0].tasks.getItems().forEach((task) => { expect(manifest).toContain(`${task.name}`); expect(manifest).toContain(`identifier="i${task.id}" type="webcontent" intendeduse="assignment"`); }); }); - - it('should add version 3 information to manifest file', () => { - const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); - expect(manifest).toContain(CommonCartridgeVersion.V_1_3_0); - }); }); }); }); 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 7f9e253da16..7ec713712a0 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,18 +1,15 @@ -import { LessonService } from '@modules/lesson/service'; -import { TaskService } from '@modules/task/service'; -import { Injectable } from '@nestjs/common'; -import { ComponentProperties, Course, Task } from '@shared/domain/entity'; -import { EntityId } from '@shared/domain/types'; -import { ComponentType } from '@src/shared/domain/entity/lesson.entity'; import { CommonCartridgeFileBuilder, - CommonCartridgeIntendedUseType, - CommonCartridgeResourceType, + CommonCartridgeOrganizationBuilder, CommonCartridgeVersion, - ICommonCartridgeResourceProps, - ICommonCartridgeWebContentResourceProps, -} from '../common-cartridge'; -import { createIdentifier } from '../common-cartridge/utils'; +} from '@modules/common-cartridge'; +import { LessonService } from '@modules/lesson'; +import { TaskService } from '@modules/task'; +import { Injectable } from '@nestjs/common'; +import { ComponentProperties } from '@shared/domain/entity'; +import { EntityId } from '@shared/domain/types'; +import { createIdentifier } from '@src/modules/common-cartridge/export/utils'; +import { CommonCartridgeMapper } from '../mapper/common-cartridge.mapper'; import { CourseService } from './course.service'; @Injectable() @@ -20,153 +17,75 @@ export class CommonCartridgeExportService { constructor( private readonly courseService: CourseService, private readonly lessonService: LessonService, - private readonly taskService: TaskService + private readonly taskService: TaskService, + private readonly commonCartridgeMapper: CommonCartridgeMapper ) {} - async exportCourse(courseId: EntityId, userId: EntityId, version: CommonCartridgeVersion): Promise { + public async exportCourse(courseId: EntityId, userId: EntityId, version: CommonCartridgeVersion): Promise { const course = await this.courseService.findById(courseId); - const builder = new CommonCartridgeFileBuilder({ - identifier: createIdentifier(courseId), - title: course.name, - version, - copyrightOwners: this.mapCourseTeachersToCopyrightOwners(course), - creationYear: course.createdAt.getFullYear().toString(), - }); + const builder = new CommonCartridgeFileBuilder(this.commonCartridgeMapper.mapCourseToManifest(version, course)); + + builder.addMetadata(this.commonCartridgeMapper.mapCourseToMetadata(course)); - await this.addLessons(builder, version, courseId); - await this.addTasks(builder, version, courseId, userId); + await this.addLessons(builder, courseId, version); + await this.addTasks(builder, courseId, userId, version); return builder.build(); } private async addLessons( builder: CommonCartridgeFileBuilder, - version: CommonCartridgeVersion, - courseId: EntityId + courseId: EntityId, + version: CommonCartridgeVersion ): Promise { const [lessons] = await this.lessonService.findByCourseIds([courseId]); lessons.forEach((lesson) => { - const organizationBuilder = builder.addOrganization({ - version, - identifier: createIdentifier(lesson.id), - title: lesson.name, - resources: [], - }); + const organizationBuilder = builder.addOrganization(this.commonCartridgeMapper.mapLessonToOrganization(lesson)); lesson.contents.forEach((content) => { - const resourceProps = this.mapContentToResource(lesson.id, content, version); - if (resourceProps) { - organizationBuilder.addResourceToOrganization(resourceProps); - } + this.addComponentToOrganization(organizationBuilder, content); }); - const tasks = lesson.tasks.getItems(); - tasks.forEach((task) => { - organizationBuilder.addResourceToOrganization(this.mapTaskToWebContentResource(task, version)); + lesson.getLessonLinkedTasks().forEach((task) => { + organizationBuilder.addResource(this.commonCartridgeMapper.mapTaskToResource(task, version)); }); }); } private async addTasks( builder: CommonCartridgeFileBuilder, - version: CommonCartridgeVersion, courseId: EntityId, - userId: EntityId + userId: EntityId, + version: CommonCartridgeVersion ): Promise { const [tasks] = await this.taskService.findBySingleParent(userId, courseId); - const organizationBuilder = builder.addOrganization({ - version, - identifier: createIdentifier(), - // FIXME: change the title for tasks organization + const organization = builder.addOrganization({ title: '', - resources: [], + identifier: createIdentifier(), }); tasks.forEach((task) => { - organizationBuilder.addResourceToOrganization(this.mapTaskToWebContentResource(task, version)); + organization.addResource(this.commonCartridgeMapper.mapTaskToResource(task, version)); }); } - private mapContentToResource( - lessonId: string, - content: ComponentProperties, - version: CommonCartridgeVersion - ): ICommonCartridgeResourceProps | undefined { - const commonProps = { - version, - identifier: createIdentifier(content._id), - href: `${createIdentifier(lessonId)}/${createIdentifier(content._id)}.html`, - title: content.title, - }; - - if (content.component === ComponentType.TEXT) { - return { - version, - identifier: createIdentifier(content._id), - href: `${createIdentifier(lessonId)}/${createIdentifier(content._id)}.html`, - title: content.title, - type: CommonCartridgeResourceType.WEB_CONTENT, - intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, - html: `

${content.title}

${content.content.text}

`, - }; - } + private addComponentToOrganization( + organizationBuilder: CommonCartridgeOrganizationBuilder, + component: ComponentProperties + ): void { + const resources = this.commonCartridgeMapper.mapContentToResources(component); - if (content.component === ComponentType.GEOGEBRA) { - const url = `https://www.geogebra.org/m/${content.content.materialId}`; - return version === CommonCartridgeVersion.V_1_3_0 - ? { ...commonProps, type: CommonCartridgeResourceType.WEB_LINK_V3, url } - : { ...commonProps, type: CommonCartridgeResourceType.WEB_LINK_V1, url }; - } + if (Array.isArray(resources)) { + const subOrganizationBuilder = organizationBuilder.addSubOrganization( + this.commonCartridgeMapper.mapContentToOrganization(component) + ); - if (content.component === ComponentType.ETHERPAD) { - return version === CommonCartridgeVersion.V_1_3_0 - ? { - ...commonProps, - type: CommonCartridgeResourceType.WEB_LINK_V3, - url: content.content.url, - title: content.content.description, - } - : { - ...commonProps, - type: CommonCartridgeResourceType.WEB_LINK_V1, - url: content.content.url, - title: content.content.description, - }; + resources.forEach((resource) => { + subOrganizationBuilder.addResource(resource); + }); + } else { + organizationBuilder.addResource(resources); } - - return undefined; - } - - /** - * This method gets the course as parameter and maps the contained teacher names within the teachers Collection to a string. - * @param Course - * @return string - * */ - private mapCourseTeachersToCopyrightOwners(course: Course): string { - const result = course.teachers - .toArray() - .map((teacher) => `${teacher.firstName} ${teacher.lastName}`) - .reduce((previousTeachers, currentTeacher) => `${previousTeachers}, ${currentTeacher}`); - return result; - } - - private mapTaskToWebContentResource( - task: Task, - version: CommonCartridgeVersion - ): ICommonCartridgeWebContentResourceProps { - const taskIdentifier = createIdentifier(task.id); - return { - version, - identifier: taskIdentifier, - href: `${taskIdentifier}/${taskIdentifier}.html`, - title: task.name, - type: CommonCartridgeResourceType.WEB_CONTENT, - html: `

${task.name}

${task.description}

`, - intendedUse: - version === CommonCartridgeVersion.V_1_1_0 - ? CommonCartridgeIntendedUseType.UNSPECIFIED - : CommonCartridgeIntendedUseType.ASSIGNMENT, - }; } } diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts index 940aa15804a..8d7ed362906 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts @@ -58,7 +58,9 @@ describe('CommonCartridgeImportService', () => { describe('when the common cartridge is valid', () => { const setup = async () => { const user = userFactory.buildWithId(); - const buffer = await readFile('./apps/server/test/assets/common-cartridge/us_history_since_1877.imscc'); + const buffer = await readFile( + './apps/server/src/modules/common-cartridge/testing/assets/us_history_since_1877.imscc' + ); return { user, buffer }; }; diff --git a/apps/server/src/modules/learnroom/uc/course-export.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-export.uc.spec.ts index 8ca36158f5d..101298cd96f 100644 --- a/apps/server/src/modules/learnroom/uc/course-export.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-export.uc.spec.ts @@ -1,17 +1,20 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { CommonCartridgeVersion } from '@modules/common-cartridge'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { CommonCartridgeExportService } from '@modules/learnroom/service/common-cartridge-export.service'; -import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { ObjectId } from 'bson'; -import { ForbiddenException } from '@nestjs/common'; +import { AuthorizationReferenceService } from '../../authorization/domain'; +import { LearnroomConfig } from '../learnroom.config'; +import { CommonCartridgeExportService } from '../service/common-cartridge-export.service'; import { CourseExportUc } from './course-export.uc'; -import { CommonCartridgeVersion } from '../common-cartridge'; describe('CourseExportUc', () => { let module: TestingModule; let courseExportUc: CourseExportUc; let courseExportServiceMock: DeepMocked; let authorizationServiceMock: DeepMocked; + let configServiceMock: DeepMocked>; beforeAll(async () => { module = await Test.createTestingModule({ @@ -25,11 +28,16 @@ describe('CourseExportUc', () => { provide: AuthorizationReferenceService, useValue: createMock(), }, + { + provide: ConfigService, + useValue: createMock>(), + }, ], }).compile(); courseExportUc = module.get(CourseExportUc); courseExportServiceMock = module.get(CommonCartridgeExportService); authorizationServiceMock = module.get(AuthorizationReferenceService); + configServiceMock = module.get(ConfigService); }); afterAll(async () => { @@ -54,6 +62,7 @@ describe('CourseExportUc', () => { const setup = () => { authorizationServiceMock.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); courseExportServiceMock.exportCourse.mockResolvedValueOnce(Buffer.from('')); + configServiceMock.get.mockReturnValueOnce(true); return setupParams(); }; @@ -71,6 +80,7 @@ describe('CourseExportUc', () => { const setup = () => { authorizationServiceMock.checkPermissionByReferences.mockResolvedValueOnce(); courseExportServiceMock.exportCourse.mockRejectedValueOnce(new Error()); + configServiceMock.get.mockReturnValueOnce(true); return setupParams(); }; @@ -86,6 +96,7 @@ describe('CourseExportUc', () => { const setup = () => { authorizationServiceMock.checkPermissionByReferences.mockResolvedValueOnce(); courseExportServiceMock.exportCourse.mockResolvedValueOnce(Buffer.from('')); + configServiceMock.get.mockReturnValueOnce(true); return setupParams(); }; @@ -103,5 +114,23 @@ describe('CourseExportUc', () => { await expect(courseExportUc.exportCourse(courseId, userId, version)).resolves.toBeInstanceOf(Buffer); }); }); + + describe('when feature is disabled', () => { + const setup = () => { + authorizationServiceMock.checkPermissionByReferences.mockResolvedValueOnce(); + courseExportServiceMock.exportCourse.mockResolvedValueOnce(Buffer.from('')); + configServiceMock.get.mockReturnValueOnce(false); + + return setupParams(); + }; + + it('should throw a NotFoundException', async () => { + const { courseId, userId, version } = setup(); + + await expect(courseExportUc.exportCourse(courseId, userId, version)).rejects.toThrowError( + new NotFoundException() + ); + }); + }); }); }); diff --git a/apps/server/src/modules/learnroom/uc/course-export.uc.ts b/apps/server/src/modules/learnroom/uc/course-export.uc.ts index bbb4a388157..320f35431d9 100644 --- a/apps/server/src/modules/learnroom/uc/course-export.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-export.uc.ts @@ -1,19 +1,23 @@ import { AuthorizationContextBuilder } from '@modules/authorization'; import { AuthorizableReferenceType, AuthorizationReferenceService } from '@modules/authorization/domain'; -import { Injectable } from '@nestjs/common'; +import { CommonCartridgeVersion } from '@modules/common-cartridge'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { CommonCartridgeVersion } from '../common-cartridge'; +import { LearnroomConfig } from '../learnroom.config'; import { CommonCartridgeExportService } from '../service/common-cartridge-export.service'; @Injectable() export class CourseExportUc { constructor( + private readonly configService: ConfigService, private readonly courseExportService: CommonCartridgeExportService, private readonly authorizationService: AuthorizationReferenceService ) {} async exportCourse(courseId: EntityId, userId: EntityId, version: CommonCartridgeVersion): Promise { + this.checkFeatureEnabled(); const context = AuthorizationContextBuilder.read([Permission.COURSE_EDIT]); await this.authorizationService.checkPermissionByReferences( userId, @@ -24,4 +28,10 @@ export class CourseExportUc { return this.courseExportService.exportCourse(courseId, userId, version); } + + private checkFeatureEnabled(): void { + if (!this.configService.get('FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED')) { + throw new NotFoundException(); + } + } } diff --git a/apps/server/src/modules/learnroom/utils/common-cartridge-file-validator.pipe.spec.ts b/apps/server/src/modules/learnroom/utils/common-cartridge-file-validator.pipe.spec.ts index 76466b78e49..94b9ba79bff 100644 --- a/apps/server/src/modules/learnroom/utils/common-cartridge-file-validator.pipe.spec.ts +++ b/apps/server/src/modules/learnroom/utils/common-cartridge-file-validator.pipe.spec.ts @@ -99,7 +99,9 @@ describe('CommonCartridgeFileValidatorPipe', () => { describe('when the file is valid', () => { const setup = async () => { - const buffer = await readFile('./apps/server/test/assets/common-cartridge/us_history_since_1877.imscc'); + const buffer = await readFile( + './apps/server/src/modules/common-cartridge/testing/assets/us_history_since_1877.imscc' + ); configServiceMock.getOrThrow.mockReturnValue(1000); diff --git a/apps/server/src/modules/server/api/dto/config.response.ts b/apps/server/src/modules/server/api/dto/config.response.ts index 6e0fcb5e0e3..c0cdcc20caf 100644 --- a/apps/server/src/modules/server/api/dto/config.response.ts +++ b/apps/server/src/modules/server/api/dto/config.response.ts @@ -126,7 +126,10 @@ export class ConfigResponse { FEATURE_CONSENT_NECESSARY: boolean; @ApiProperty() - FEATURE_IMSCC_COURSE_EXPORT_ENABLED: boolean; + FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED: boolean; + + @ApiProperty() + FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_ENABLED: boolean; @ApiProperty() FEATURE_SCHOOL_SANIS_USER_MIGRATION_ENABLED: boolean; @@ -202,7 +205,8 @@ export class ConfigResponse { this.FEATURE_USER_MIGRATION_ENABLED = config.userMigrationEnabled; this.FEATURE_COPY_SERVICE_ENABLED = config.FEATURE_COPY_SERVICE_ENABLED; this.FEATURE_CONSENT_NECESSARY = config.FEATURE_CONSENT_NECESSARY; - this.FEATURE_IMSCC_COURSE_EXPORT_ENABLED = config.FEATURE_IMSCC_COURSE_EXPORT_ENABLED; + this.FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED = config.FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED; + this.FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_ENABLED = config.FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_ENABLED; this.FEATURE_SCHOOL_SANIS_USER_MIGRATION_ENABLED = config.FEATURE_SCHOOL_SANIS_USER_MIGRATION_ENABLED; this.FEATURE_ALLOW_INSECURE_LDAP_URL_ENABLED = config.FEATURE_ALLOW_INSECURE_LDAP_URL_ENABLED; this.FEATURE_NEST_SYSTEMS_API_ENABLED = config.FEATURE_NEST_SYSTEMS_API_ENABLED; diff --git a/apps/server/src/modules/server/api/test/server.api.spec.ts b/apps/server/src/modules/server/api/test/server.api.spec.ts index 947b4f380b6..0afd7c88ad1 100644 --- a/apps/server/src/modules/server/api/test/server.api.spec.ts +++ b/apps/server/src/modules/server/api/test/server.api.spec.ts @@ -53,7 +53,8 @@ describe('Server Controller (API)', () => { 'FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION', 'FEATURE_ES_COLLECTIONS_ENABLED', 'FEATURE_EXTENSIONS_ENABLED', - 'FEATURE_IMSCC_COURSE_EXPORT_ENABLED', + 'FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED', + 'FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_ENABLED', 'FEATURE_LERNSTORE_ENABLED', 'FEATURE_LESSON_SHARE', 'FEATURE_LOGIN_LINK_ENABLED', diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index f59f9e4959b..7c133156065 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -3,21 +3,21 @@ import type { IdentityManagementConfig } from '@infra/identity-management'; import type { SchulconnexClientConfig } from '@infra/schulconnex-client'; import type { AccountConfig } from '@modules/account'; import type { AuthenticationConfig, XApiKeyConfig } from '@modules/authentication'; +import type { BoardConfig } from '@modules/board'; import type { FilesStorageClientConfig } from '@modules/files-storage-client'; -import type { CommonCartridgeConfig, LearnroomConfig } from '@modules/learnroom'; +import type { LearnroomConfig } from '@modules/learnroom'; +import type { LessonConfig } from '@modules/lesson'; import type { SchoolConfig } from '@modules/school'; +import type { SharingConfig } from '@modules/sharing'; +import type { SystemConfig } from '@modules/system'; +import { getTldrawClientConfig, type TldrawClientConfig } from '@modules/tldraw-client'; +import { ToolConfiguration, type IToolFeatures } from '@modules/tool'; import type { UserConfig } from '@modules/user'; -import { type IUserImportFeatures, UserImportConfiguration } from '@modules/user-import'; +import { UserImportConfiguration, type IUserImportFeatures } from '@modules/user-import'; +import type { UserLoginMigrationConfig } from '@modules/user-login-migration'; +import { VideoConferenceConfiguration, type IVideoConferenceSettings } from '@modules/video-conference'; import type { CoreModuleConfig } from '@src/core'; import type { MailConfig } from '@src/infra/mail/interfaces/mail-config'; -import { ToolConfiguration, type IToolFeatures } from '@modules/tool'; -import { getTldrawClientConfig, type TldrawClientConfig } from '@modules/tldraw-client'; -import { VideoConferenceConfiguration, type IVideoConferenceSettings } from '@modules/video-conference'; -import type { UserLoginMigrationConfig } from '@modules/user-login-migration'; -import type { LessonConfig } from '@modules/lesson'; -import type { BoardConfig } from '@modules/board'; -import type { SharingConfig } from '@modules/sharing'; -import type { SystemConfig } from '@modules/system'; export enum NodeEnvType { TEST = 'test', @@ -34,7 +34,6 @@ export interface ServerConfig FilesStorageClientConfig, AccountConfig, IdentityManagementConfig, - CommonCartridgeConfig, SchoolConfig, MailConfig, XApiKeyConfig, @@ -46,7 +45,6 @@ export interface ServerConfig LessonConfig, IVideoConferenceSettings, BoardConfig, - LearnroomConfig, SharingConfig, IUserImportFeatures, SchulconnexClientConfig, @@ -152,7 +150,10 @@ const config: ServerConfig = { TEACHER_STUDENT_VISIBILITY__IS_CONFIGURABLE: Configuration.get( 'TEACHER_STUDENT_VISIBILITY__IS_CONFIGURABLE' ) as boolean, - FEATURE_IMSCC_COURSE_EXPORT_ENABLED: Configuration.get('FEATURE_IMSCC_COURSE_EXPORT_ENABLED') as boolean, + FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED: Configuration.get( + 'FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED' + ) as boolean, + GEOGEBRA_BASE_URL: Configuration.get('GEOGEBRA_BASE_URL') as string, FEATURE_IDENTITY_MANAGEMENT_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_ENABLED') as boolean, FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED') as boolean, FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED') as boolean, diff --git a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts index d73e8a25171..102e033cb6b 100644 --- a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts +++ b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts @@ -1,15 +1,21 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; import { SchoolExternalToolModule } from '../school-external-tool'; +import { ToolConfigModule } from '../tool-config.module'; import { ContextExternalToolAuthorizableService, ContextExternalToolService, ToolReferenceService } from './service'; import { ContextExternalToolValidationService } from './service/context-external-tool-validation.service'; -import { ToolConfigModule } from '../tool-config.module'; import { ToolVersionService } from './service/tool-version-service'; @Module({ - imports: [CommonToolModule, ExternalToolModule, SchoolExternalToolModule, LoggerModule, ToolConfigModule], + imports: [ + forwardRef(() => CommonToolModule), + forwardRef(() => ExternalToolModule), + SchoolExternalToolModule, + LoggerModule, + ToolConfigModule, + ], providers: [ ContextExternalToolService, ContextExternalToolValidationService, 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 2ae4f66b2d8..2ff671cb62b 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,11 +1,11 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; import { ToolConfigModule } from '../tool-config.module'; import { SchoolExternalToolService, SchoolExternalToolValidationService } from './service'; @Module({ - imports: [CommonToolModule, ExternalToolModule, ToolConfigModule], + imports: [forwardRef(() => CommonToolModule), forwardRef(() => ExternalToolModule), ToolConfigModule], providers: [SchoolExternalToolService, SchoolExternalToolValidationService], exports: [SchoolExternalToolService, SchoolExternalToolValidationService], }) diff --git a/config/default.schema.json b/config/default.schema.json index f201801ed6f..8a47d26fb77 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1165,11 +1165,18 @@ "default": false, "description": "Toggle for copy course feature." }, - "FEATURE_IMSCC_COURSE_EXPORT_ENABLED": { + "FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED": { "type": "boolean", "default": false, "description": "Toggle for the IMSCC course download feature." }, + "GEOGEBRA_BASE_URL": { + "type": "string", + "format": "uri", + "default": "https://www.geogebra.org", + "pattern": ".*(?