diff --git a/.build/ory/kratos/kratos.yml b/.build/ory/kratos/kratos.yml index c3f5e69a27..3354d06a6b 100644 --- a/.build/ory/kratos/kratos.yml +++ b/.build/ory/kratos/kratos.yml @@ -82,7 +82,6 @@ selfservice: - hook: require_verified_address registration: - enable_legacy_one_step: true lifespan: 10m ui_url: http://localhost:3000/registration after: diff --git a/.env.docker b/.env.docker index 6812ed6bf1..64c97e3f28 100644 --- a/.env.docker +++ b/.env.docker @@ -11,6 +11,7 @@ SYNAPSE_SHARED_SECRET=n#P.uIl8IDOYPR-fiLzDoFw9ZPvTIlYg7*F9*~eaDZFK#;.KRg SYNAPSE_ENABLE_REGISTRATION=true SYNAPSE_NO_TLS=true SYNAPSE_SERVER_URL=http://synapse:8008 +SYNAPSE_ADMIN_USERNAME=matrixadmin44@alkem.io POSTGRES_USER=synapse diff --git a/package-lock.json b/package-lock.json index 896f28ffc6..1761e1a26f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "alkemio-server", - "version": "0.84.10", + "version": "0.86.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "alkemio-server", - "version": "0.84.10", + "version": "0.86.0", "license": "EUPL-1.2", "dependencies": { "@alkemio/matrix-adapter-lib": "^0.4.1", diff --git a/package.json b/package.json index 2768a55fa2..1449d0056c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alkemio-server", - "version": "0.84.10", + "version": "0.86.0", "description": "Alkemio server, responsible for managing the shared Alkemio platform", "author": "Alkemio Foundation", "private": false, diff --git a/quickstart-services.yml b/quickstart-services.yml index 299ab72360..09d81da6b5 100644 --- a/quickstart-services.yml +++ b/quickstart-services.yml @@ -206,6 +206,8 @@ services: hostname: notifications image: alkemio/notifications:v0.19.0 platform: linux/x86_64 + depends_on: + - rabbitmq environment: - RABBITMQ_HOST - RABBITMQ_USER @@ -236,6 +238,8 @@ services: hostname: matrix-adapter image: alkemio/matrix-adapter:v0.4.5 platform: linux/x86_64 + depends_on: + - rabbitmq environment: - RABBITMQ_HOST - RABBITMQ_USER @@ -274,6 +278,8 @@ services: hostname: whiteboard-collaboration image: alkemio/whiteboard-collaboration-service:v0.3.1 platform: linux/x86_64 + depends_on: + - rabbitmq environment: - RABBITMQ_HOST - RABBITMQ_USER @@ -292,6 +298,8 @@ services: hostname: file-service image: alkemio/file-service:v0.1.2 platform: linux/x86_64 + depends_on: + - rabbitmq environment: - RABBITMQ_HOST - RABBITMQ_USER diff --git a/src/app.module.ts b/src/app.module.ts index d035934008..357b735ad6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -78,6 +78,7 @@ import { PlatformSettingsModule } from '@platform/settings/platform.settings.mod import { FileIntegrationModule } from '@services/file-integration'; import { AdminLicensingModule } from '@platform/admin/licensing/admin.licensing.module'; import { PlatformRoleModule } from '@platform/platfrom.role/platform.role.module'; +import { LookupByNameModule } from '@services/api/lookup-by-name'; @Module({ imports: [ @@ -263,6 +264,7 @@ import { PlatformRoleModule } from '@platform/platfrom.role/platform.role.module ChatGuidanceModule, VirtualContributorModule, LookupModule, + LookupByNameModule, AuthResetSubscriberModule, TaskGraphqlModule, ActivityFeedModule, diff --git a/src/common/constants/authorization/credential.rule.constants.ts b/src/common/constants/authorization/credential.rule.constants.ts index 20305619e2..6a73dea97e 100644 --- a/src/common/constants/authorization/credential.rule.constants.ts +++ b/src/common/constants/authorization/credential.rule.constants.ts @@ -62,5 +62,3 @@ export const CREDENTIAL_RULE_CALENDAR_EVENT_CREATED_BY = 'credentialRule-calendarEventCreatedBy'; export const CREDENTIAL_RULE_DOCUMENT_CREATED_BY = 'credentialRule-documentCreatedBy'; -export const CREDENTIAL_RULE_LIBRARY_INNOVATION_PACK_PROVIDER_ADMIN = - 'credentialRule-libraryInnovationPackProvider'; diff --git a/src/common/constants/authorization/credential.rule.types.constants.ts b/src/common/constants/authorization/credential.rule.types.constants.ts index 8d5e8e2f73..abfb8e3199 100644 --- a/src/common/constants/authorization/credential.rule.types.constants.ts +++ b/src/common/constants/authorization/credential.rule.types.constants.ts @@ -76,8 +76,6 @@ export const CREDENTIAL_RULE_TYPES_PLATFORM_ACCESS_GUIDANCE = 'credentialRuleTypes-platformAccessGuidance'; export const CREDENTIAL_RULE_TYPES_PLATFORM_ACCESS_DASHBOARD = 'credentialRuleTypes-platformAccessDashboard'; -export const CREDENTIAL_RULE_TYPES_LIBRARY_FILE_UPLOAD_ANY_USER = - 'credentialRuleTypes-libraryFileUploadAnyUser'; export const CREDENTIAL_RULE_TYPES_PLATFORM_FILE_UPLOAD_ANY_USER = 'credentialRuleTypes-platformFileUploadAnyUser'; export const CREDENTIAL_RULE_TYPES_UPDATE_FORUM_DISCUSSION = diff --git a/src/common/enums/alkemio.error.status.ts b/src/common/enums/alkemio.error.status.ts index 80752ea830..6a38b91e05 100644 --- a/src/common/enums/alkemio.error.status.ts +++ b/src/common/enums/alkemio.error.status.ts @@ -9,6 +9,7 @@ export enum AlkemioErrorStatus { ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND', FORMAT_NOT_SUPPORTED = 'FORMAT_NOT_SUPPORTED', INVALID_TOKEN = 'INVALID_TOKEN', + INVALID_UUID = 'INVALID_UUID', ACCOUNT_NOT_FOUND = 'ACCOUNT_NOT_FOUND', INVALID_STATE_TRANSITION = 'INVALID_STATE_TRANSITION', UNAUTHENTICATED = 'UNAUTHENTICATED', diff --git a/src/common/enums/authorization.credential.ts b/src/common/enums/authorization.credential.ts index f408a293c5..1317d0f9bb 100644 --- a/src/common/enums/authorization.credential.ts +++ b/src/common/enums/authorization.credential.ts @@ -24,9 +24,6 @@ export enum AuthorizationCredential { USER_GROUP_MEMBER = 'user-group-member', // Able to be a part of an user group - // Library related credentials - INNOVATION_PACK_PROVIDER = 'innovation-pack-provider', - // Roles to allow easier management of users BETA_TESTER = 'beta-tester', VC_CAMPAIGN = 'vc-campaign', diff --git a/src/common/enums/space.reserved.name.ts b/src/common/enums/space.reserved.name.ts new file mode 100644 index 0000000000..9caa208dd3 --- /dev/null +++ b/src/common/enums/space.reserved.name.ts @@ -0,0 +1,16 @@ +export enum SpaceReservedName { + USER = 'user', + ORGANIZATION = 'organization', + VIRTUAL_CONTRIBUTOR = 'vc', + ADMIN = 'admin', + INNOVATION_LIBRARY = 'innovation-library', + INNOVATION_PACKS = 'innovation-packs', + CREATE_SPACE = 'create-space', + HOME = 'home', + SPACES = 'spaces', + CONTRIBUTORS = 'contributors', + FORUM = 'forum', + ABOUT = 'about', + PROFILE = 'profile', + RESTRICTED = 'restricted', +} diff --git a/src/common/exceptions/invalid.uuid.ts b/src/common/exceptions/invalid.uuid.ts new file mode 100644 index 0000000000..44fcef1712 --- /dev/null +++ b/src/common/exceptions/invalid.uuid.ts @@ -0,0 +1,9 @@ +import { LogContext, AlkemioErrorStatus } from '@common/enums'; +import { BaseException } from './base.exception'; +import { ExceptionDetails } from './exception.details'; + +export class InvalidUUID extends BaseException { + constructor(error: string, context: LogContext, details?: ExceptionDetails) { + super(error, context, AlkemioErrorStatus.INVALID_UUID, details); + } +} diff --git a/src/common/interceptors/innovation.hub.interceptor.ts b/src/common/interceptors/innovation.hub.interceptor.ts index 4906aa260e..9255df17c7 100644 --- a/src/common/interceptors/innovation.hub.interceptor.ts +++ b/src/common/interceptors/innovation.hub.interceptor.ts @@ -66,7 +66,7 @@ export class InnovationHubInterceptor implements NestInterceptor { try { ctx[INNOVATION_HUB_INJECT_TOKEN] = - await this.innovationHubService.getInnovationHubOrFail({ + await this.innovationHubService.getInnovationHubFlexOrFail({ subdomain: subDomain, }); } catch (e) { diff --git a/src/common/utils/stringify.util.ts b/src/common/utils/stringify.util.ts index 593a9611f4..4dca213b6a 100644 --- a/src/common/utils/stringify.util.ts +++ b/src/common/utils/stringify.util.ts @@ -7,7 +7,9 @@ export function stringifyWithoutAuthorizationMetaInfo(object: any): string { key === 'version' || key === 'allowedTypes' || key === 'storageBucket' || - key === 'visuals' + key === 'visuals' || + key === 'issuer' || + key === 'expires' ) { return undefined; } diff --git a/src/core/bootstrap/bootstrap.service.ts b/src/core/bootstrap/bootstrap.service.ts index e0358072ad..3d414bbfc4 100644 --- a/src/core/bootstrap/bootstrap.service.ts +++ b/src/core/bootstrap/bootstrap.service.ts @@ -266,18 +266,18 @@ export class BootstrapService { this.logger.verbose?.('...No account present...', LogContext.BOOTSTRAP); this.logger.verbose?.('........creating...', LogContext.BOOTSTRAP); // create a default host org - const hostOrganization = await this.organizationService.getOrganization( + let hostOrganization = await this.organizationService.getOrganization( DEFAULT_HOST_ORG_NAMEID ); if (!hostOrganization) { - const hostOrg = await this.organizationService.createOrganization({ + hostOrganization = await this.organizationService.createOrganization({ nameID: DEFAULT_HOST_ORG_NAMEID, profileData: { displayName: DEFAULT_HOST_ORG_DISPLAY_NAME, }, }); await this.organizationAuthorizationService.applyAuthorizationPolicy( - hostOrg + hostOrganization ); } @@ -291,7 +291,7 @@ export class BootstrapService { level: SpaceLevel.SPACE, type: SpaceType.SPACE, }, - hostID: DEFAULT_HOST_ORG_NAMEID, + hostID: hostOrganization.id, }; let account = await this.accountService.createAccount(spaceInput); diff --git a/src/core/dataloader/creators/loader.creators/account/account.license.loader.creator.ts b/src/core/dataloader/creators/loader.creators/account/account.innovation.hubs.loader.creator.ts similarity index 67% rename from src/core/dataloader/creators/loader.creators/account/account.license.loader.creator.ts rename to src/core/dataloader/creators/loader.creators/account/account.innovation.hubs.loader.creator.ts index 22eb7b7338..8c4325d84d 100644 --- a/src/core/dataloader/creators/loader.creators/account/account.license.loader.creator.ts +++ b/src/core/dataloader/creators/loader.creators/account/account.innovation.hubs.loader.creator.ts @@ -4,19 +4,19 @@ import { InjectEntityManager } from '@nestjs/typeorm'; import { Account } from '@domain/space/account/account.entity'; import { createTypedRelationDataLoader } from '../../../utils'; import { DataLoaderCreator, DataLoaderCreatorOptions } from '../../base'; -import { ILicense } from '@domain/license/license/license.interface'; +import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; @Injectable() -export class AccountLicenseLoaderCreator - implements DataLoaderCreator +export class AccountInnovationHubsLoaderCreator + implements DataLoaderCreator { constructor(@InjectEntityManager() private manager: EntityManager) {} - create(options?: DataLoaderCreatorOptions) { + create(options?: DataLoaderCreatorOptions) { return createTypedRelationDataLoader( this.manager, Account, - { license: true }, + { innovationHubs: true }, this.constructor.name, options ); diff --git a/src/core/dataloader/creators/loader.creators/account/account.innovation.pack.loader.creator.ts b/src/core/dataloader/creators/loader.creators/account/account.innovation.pack.loader.creator.ts new file mode 100644 index 0000000000..70478e3393 --- /dev/null +++ b/src/core/dataloader/creators/loader.creators/account/account.innovation.pack.loader.creator.ts @@ -0,0 +1,24 @@ +import { EntityManager } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager } from '@nestjs/typeorm'; +import { Account } from '@domain/space/account/account.entity'; +import { createTypedRelationDataLoader } from '../../../utils'; +import { DataLoaderCreator, DataLoaderCreatorOptions } from '../../base'; +import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; + +@Injectable() +export class AccountInnovationPacksLoaderCreator + implements DataLoaderCreator +{ + constructor(@InjectEntityManager() private manager: EntityManager) {} + + create(options?: DataLoaderCreatorOptions) { + return createTypedRelationDataLoader( + this.manager, + Account, + { innovationPacks: true }, + this.constructor.name, + options + ); + } +} diff --git a/src/core/dataloader/creators/loader.creators/index.ts b/src/core/dataloader/creators/loader.creators/index.ts index c52cea88df..4736d73f60 100644 --- a/src/core/dataloader/creators/loader.creators/index.ts +++ b/src/core/dataloader/creators/loader.creators/index.ts @@ -17,8 +17,9 @@ export * from './visual.loader.creator'; export * from './account/account.library.loader.creator'; export * from './account/account.defaults.loader.creator'; -export * from './account/account.license.loader.creator'; export * from './account/account.virtual.contributors.loader.creator'; +export * from './account/account.innovation.hubs.loader.creator'; +export * from './account/account.innovation.pack.loader.creator'; export * from './account/account.loader.creator'; export * from './collaboration/collaboration.timeline.loader.creator'; diff --git a/src/core/validation/handlers/base/base.handler.ts b/src/core/validation/handlers/base/base.handler.ts index 39974ed11c..5804c80f7e 100644 --- a/src/core/validation/handlers/base/base.handler.ts +++ b/src/core/validation/handlers/base/base.handler.ts @@ -64,7 +64,6 @@ import { import { UpdateCalloutTemplateInput } from '@domain/template/callout-template/dto/callout.template.dto.update'; import { CreateCalloutTemplateInput } from '@domain/template/callout-template/dto/callout.template.dto.create'; import { CreateContributionOnCalloutInput } from '@domain/collaboration/callout/dto/callout.dto.create.contribution'; -import { UpdateLicenseInput } from '@domain/license/license/dto/license.dto.update'; import { CreateLinkInput, UpdateLinkInput, @@ -133,7 +132,6 @@ export class BaseHandler extends AbstractHandler { UpdateSpaceInput, UpdateSpaceSettingsEntityInput, UpdateOrganizationInput, - UpdateLicenseInput, UpdateLinkInput, UpdateCalendarEventInput, UpdateInnovationFlowStateInput, diff --git a/src/domain/collaboration/callout/callout.resolver.mutations.ts b/src/domain/collaboration/callout/callout.resolver.mutations.ts index 1199a0b24c..42ac353c2d 100644 --- a/src/domain/collaboration/callout/callout.resolver.mutations.ts +++ b/src/domain/collaboration/callout/callout.resolver.mutations.ts @@ -1,6 +1,6 @@ import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { CurrentUser, Profiling } from '@src/common/decorators'; -import { AuthorizationPrivilege } from '@common/enums'; +import { AuthorizationPrivilege, LogContext } from '@common/enums'; import { GraphqlGuard } from '@core/authorization'; import { Inject, UseGuards } from '@nestjs/common/decorators'; import { AuthorizationService } from '@core/authorization/authorization.service'; @@ -38,6 +38,7 @@ import { ICalloutContribution } from '../callout-contribution/callout.contributi import { CalloutContributionAuthorizationService } from '../callout-contribution/callout.contribution.service.authorization'; import { CalloutContributionService } from '../callout-contribution/callout.contribution.service'; import { ILink } from '../link/link.interface'; +import { RelationshipNotFoundException } from '@common/exceptions'; @Resolver() export class CalloutResolverMutations { @@ -180,8 +181,19 @@ export class CalloutResolverMutations { @Args('contributionData') contributionData: CreateContributionOnCalloutInput ): Promise { const callout = await this.calloutService.getCalloutOrFail( - contributionData.calloutID + contributionData.calloutID, + { + relations: { + collaboration: true, + }, + } ); + if (!callout.collaboration) { + throw new RelationshipNotFoundException( + `Callout ${callout.id} has no collaboration relationship`, + LogContext.COLLABORATION + ); + } this.authorizationService.grantAccessOrFail( agentInfo, callout.authorization, @@ -189,6 +201,12 @@ export class CalloutResolverMutations { `create contribution on callout: ${callout.id}` ); + // Get the levelZeroSpaceID for the callout + const levelZeroSpaceID = + await this.communityResolverService.getLevelZeroSpaceIdForCollaboration( + callout.collaboration.id + ); + if (callout.contributionPolicy.state === CalloutState.CLOSED) { if ( !this.authorizationService.isAccessGranted( @@ -237,6 +255,7 @@ export class CalloutResolverMutations { callout, contribution, contribution.post, + levelZeroSpaceID, agentInfo ); } @@ -247,6 +266,7 @@ export class CalloutResolverMutations { await this.processActivityLinkCreated( callout, contribution.link, + levelZeroSpaceID, agentInfo ); } @@ -258,6 +278,7 @@ export class CalloutResolverMutations { callout, contribution, contribution.whiteboard, + levelZeroSpaceID, agentInfo ); } @@ -269,6 +290,7 @@ export class CalloutResolverMutations { private async processActivityLinkCreated( callout: ICallout, link: ILink, + levelZeroSpaceID: string, agentInfo: AgentInfo ) { const activityLogInput: ActivityInputCalloutLinkCreated = { @@ -278,20 +300,11 @@ export class CalloutResolverMutations { }; this.activityAdapter.calloutLinkCreated(activityLogInput); - const community = - await this.communityResolverService.getCommunityFromCalloutOrFail( - callout.id - ); - const spaceID = - await this.communityResolverService.getRootSpaceIDFromCommunityOrFail( - community - ); - this.contributionReporter.calloutLinkCreated( { id: link.id, name: link.profile.displayName, - space: spaceID, + space: levelZeroSpaceID, }, { id: agentInfo.userID, @@ -304,6 +317,7 @@ export class CalloutResolverMutations { callout: ICallout, contribution: ICalloutContribution, whiteboard: IWhiteboard, + levelZeroSpaceID: string, agentInfo: AgentInfo ) { const notificationInput: NotificationInputWhiteboardCreated = { @@ -320,20 +334,11 @@ export class CalloutResolverMutations { callout: callout, }); - const community = - await this.communityResolverService.getCommunityFromCalloutOrFail( - callout.id - ); - const spaceID = - await this.communityResolverService.getRootSpaceIDFromCommunityOrFail( - community - ); - this.contributionReporter.calloutWhiteboardCreated( { id: whiteboard.id, name: whiteboard.nameID, - space: spaceID, + space: levelZeroSpaceID, }, { id: agentInfo.userID, @@ -346,6 +351,7 @@ export class CalloutResolverMutations { callout: ICallout, contribution: ICalloutContribution, post: IPost, + levelZeroSpaceID: string, agentInfo: AgentInfo ) { const notificationInput: NotificationInputPostCreated = { @@ -363,20 +369,11 @@ export class CalloutResolverMutations { }; this.activityAdapter.calloutPostCreated(activityLogInput); - const community = - await this.communityResolverService.getCommunityFromCalloutOrFail( - callout.id - ); - const spaceID = - await this.communityResolverService.getRootSpaceIDFromCommunityOrFail( - community - ); - this.contributionReporter.calloutPostCreated( { id: post.id, name: post.profile.displayName, - space: spaceID, + space: levelZeroSpaceID, }, { id: agentInfo.userID, diff --git a/src/domain/collaboration/collaboration/collaboration.resolver.mutations.ts b/src/domain/collaboration/collaboration/collaboration.resolver.mutations.ts index aee52df148..28d53eff1a 100644 --- a/src/domain/collaboration/collaboration/collaboration.resolver.mutations.ts +++ b/src/domain/collaboration/collaboration/collaboration.resolver.mutations.ts @@ -166,16 +166,16 @@ export class CollaborationResolverMutations { this.activityAdapter.calloutPublished(activityLogInput); } - const rootSpaceID = - await this.communityResolverService.getRootSpaceIDFromCalloutOrFail( - callout.id + const levelZeroSpaceID = + await this.communityResolverService.getLevelZeroSpaceIdForCollaboration( + collaboration.id ); this.contributionReporter.calloutCreated( { id: callout.id, name: callout.nameID, - space: rootSpaceID, + space: levelZeroSpaceID, }, { id: agentInfo.userID, diff --git a/src/domain/collaboration/collaboration/collaboration.service.ts b/src/domain/collaboration/collaboration/collaboration.service.ts index 09715122f0..7011a561ec 100644 --- a/src/domain/collaboration/collaboration/collaboration.service.ts +++ b/src/domain/collaboration/collaboration/collaboration.service.ts @@ -247,7 +247,6 @@ export class CollaborationService { const space = await this.entityManager.findOne(Space, { where: { collaboration: { id: collaborationID } }, relations: { - account: true, subspaces: { collaboration: true, }, @@ -259,15 +258,12 @@ export class CollaborationService { LogContext.COLLABORATION ); } - const accountID = space.account.id; switch (space.level) { case SpaceLevel.SPACE: const spacesInAccount = await this.entityManager.find(Space, { where: { - account: { - id: accountID, - }, + levelZeroSpaceID: space.id, }, relations: { collaboration: true, diff --git a/src/domain/communication/room/room.service.events.ts b/src/domain/communication/room/room.service.events.ts index b98a8dcb38..7533a22f49 100644 --- a/src/domain/communication/room/room.service.events.ts +++ b/src/domain/communication/room/room.service.events.ts @@ -107,15 +107,15 @@ export class RoomServiceEvents { await this.communityResolverService.getCommunityFromPostRoomOrFail( room.id ); - const spaceID = - await this.communityResolverService.getRootSpaceIDFromCommunityOrFail( - community + const levelZeroSpaceID = + await this.communityResolverService.getLevelZeroSpaceIdForCommunity( + community.id ); this.contributionReporter.calloutPostCommentCreated( { id: post.id, name: post.profile.displayName, - space: spaceID, + space: levelZeroSpaceID, }, { id: agentInfo.userID, @@ -151,16 +151,16 @@ export class RoomServiceEvents { await this.communityResolverService.getCommunityFromUpdatesOrFail( room.id ); - const spaceID = - await this.communityResolverService.getRootSpaceIDFromCommunityOrFail( - community + const levelZeroSpaceID = + await this.communityResolverService.getLevelZeroSpaceIdForCommunity( + community.id ); this.contributionReporter.updateCreated( { id: room.id, name: '', - space: spaceID, + space: levelZeroSpaceID, }, { id: agentInfo.userID, @@ -196,16 +196,16 @@ export class RoomServiceEvents { await this.communityResolverService.getCommunityFromCalloutOrFail( callout.id ); - const spaceID = - await this.communityResolverService.getRootSpaceIDFromCommunityOrFail( - community + const levelZeroSpaceID = + await this.communityResolverService.getLevelZeroSpaceIdForCommunity( + community.id ); this.contributionReporter.calloutCommentCreated( { id: callout.id, name: callout.nameID, - space: spaceID, + space: levelZeroSpaceID, }, { id: agentInfo.userID, diff --git a/src/domain/community/community-role/community.role.resolver.mutations.ts b/src/domain/community/community-role/community.role.resolver.mutations.ts index 23ae6bae55..48dbec8823 100644 --- a/src/domain/community/community-role/community.role.resolver.mutations.ts +++ b/src/domain/community/community-role/community.role.resolver.mutations.ts @@ -414,14 +414,15 @@ export class CommunityRoleResolverMutations { const contributors: IContributor[] = []; for (const contributorID of invitationData.invitedContributors) { - const contributor = await this.contributorService.getContributorOrFail( - contributorID, - { - relations: { - agent: true, - }, - } - ); + const contributor = + await this.contributorService.getContributorByUuidOrFail( + contributorID, + { + relations: { + agent: true, + }, + } + ); contributors.push(contributor); } @@ -502,9 +503,8 @@ export class CommunityRoleResolverMutations { invitation = await this.invitationService.save(invitation); if (invitedContributor instanceof VirtualContributor) { - const accountHost = await this.virtualContributorService.getAccountHost( - invitedContributor - ); + const accountHost = + await this.virtualContributorService.getAccountHost(invitedContributor); const notificationInput: NotificationInputCommunityInvitationVirtualContributor = { triggeredBy: agentInfo.userID, @@ -610,9 +610,8 @@ export class CommunityRoleResolverMutations { platformInvitation, community.authorization ); - platformInvitation = await this.platformInvitationService.save( - platformInvitation - ); + platformInvitation = + await this.platformInvitationService.save(platformInvitation); const notificationInput: NotificationInputPlatformInvitation = { triggeredBy: agentInfo.userID, @@ -720,9 +719,8 @@ export class CommunityRoleResolverMutations { @Args({ name: 'communityID', type: () => String }) communityID: string, @CurrentUser() agentInfo: AgentInfo ): Promise { - const community = await this.communityService.getCommunityOrFail( - communityID - ); + const community = + await this.communityService.getCommunityOrFail(communityID); await this.authorizationService.grantAccessOrFail( agentInfo, community.authorization, diff --git a/src/domain/community/community-role/community.role.service.events.ts b/src/domain/community/community-role/community.role.service.events.ts index 8b6ac6d828..2514ac2029 100644 --- a/src/domain/community/community-role/community.role.service.events.ts +++ b/src/domain/community/community-role/community.role.service.events.ts @@ -34,7 +34,7 @@ export class CommunityRoleEventsService { public async processCommunityNewMemberEvents( community: ICommunity, - rootSpaceID: string, + levelZeroSpaceID: string, displayName: string, agentInfo: AgentInfo, newContributor: IContributor @@ -59,7 +59,7 @@ export class CommunityRoleEventsService { { id: community.parentID, name: displayName, - space: rootSpaceID, + space: levelZeroSpaceID, }, { id: agentInfo.userID, @@ -72,7 +72,7 @@ export class CommunityRoleEventsService { { id: community.parentID, name: displayName, - space: rootSpaceID, + space: levelZeroSpaceID, }, { id: agentInfo.userID, diff --git a/src/domain/community/community-role/community.role.service.ts b/src/domain/community/community-role/community.role.service.ts index 8d2702171d..90235c4eea 100644 --- a/src/domain/community/community-role/community.role.service.ts +++ b/src/domain/community/community-role/community.role.service.ts @@ -392,15 +392,15 @@ export class CommunityRoleService { ); if (triggerNewMemberEvents) { - const rootSpaceID = await this.communityService.getRootSpaceID( - community - ); - const displayName = await this.communityService.getDisplayName( - community - ); + const levelZeroSpaceID = + await this.communityService.getLevelZeroSpaceIdForCommunity( + community + ); + const displayName = + await this.communityService.getDisplayName(community); await this.communityRoleEventsService.processCommunityNewMemberEvents( community, - rootSpaceID, + levelZeroSpaceID, displayName, agentInfo, contributor @@ -526,9 +526,8 @@ export class CommunityRoleService { validatePolicyLimits ); - const parentCommunity = await this.communityService.getParentCommunity( - community - ); + const parentCommunity = + await this.communityService.getParentCommunity(community); if (role === CommunityRole.ADMIN && parentCommunity) { // Check if an admin anywhere else in the community const peerCommunities = await this.communityService.getPeerCommunites( @@ -873,9 +872,8 @@ export class CommunityRoleService { await this.validateApplicationFromUser(user, agent, community); - const application = await this.applicationService.createApplication( - applicationData - ); + const application = + await this.applicationService.createApplication(applicationData); application.community = community; return await this.applicationService.save(application); } diff --git a/src/domain/community/community/community.service.ts b/src/domain/community/community/community.service.ts index 0b89b31978..28fb329a64 100644 --- a/src/domain/community/community/community.service.ts +++ b/src/domain/community/community/community.service.ts @@ -416,9 +416,11 @@ export class CommunityService { ); } - public async getRootSpaceID(community: ICommunity): Promise { - return await this.communityResolverService.getRootSpaceIDFromCommunityOrFail( - community + public async getLevelZeroSpaceIdForCommunity( + community: ICommunity + ): Promise { + return await this.communityResolverService.getLevelZeroSpaceIdForCommunity( + community.id ); } diff --git a/src/domain/community/contributor/contributor.service.ts b/src/domain/community/contributor/contributor.service.ts index b0501f0070..540bc0a646 100644 --- a/src/domain/community/contributor/contributor.service.ts +++ b/src/domain/community/contributor/contributor.service.ts @@ -42,13 +42,13 @@ export class ContributorService { contributorID: string, options?: FindOneOptions ): Promise { - return await this.contributorLookupService.getContributor( + return await this.contributorLookupService.getContributorByUUID( contributorID, options ); } - async getContributorOrFail( + async getContributorByUuidOrFail( contributorID: string, options?: FindOneOptions ): Promise { @@ -64,7 +64,7 @@ export class ContributorService { async getContributorAndAgent( contributorID: string ): Promise<{ contributor: IContributor; agent: IAgent }> { - const contributor = await this.getContributorOrFail(contributorID, { + const contributor = await this.getContributorByUuidOrFail(contributorID, { relations: { agent: true }, }); diff --git a/src/domain/community/invitation/invitation.service.ts b/src/domain/community/invitation/invitation.service.ts index 21284cbb74..ff45992b0d 100644 --- a/src/domain/community/invitation/invitation.service.ts +++ b/src/domain/community/invitation/invitation.service.ts @@ -106,9 +106,10 @@ export class InvitationService { } async getInvitedContributor(invitation: IInvitation): Promise { - const contributor = await this.contributorService.getContributorOrFail( - invitation.invitedContributor - ); + const contributor = + await this.contributorService.getContributorByUuidOrFail( + invitation.invitedContributor + ); if (!contributor) throw new RelationshipNotFoundException( `Unable to load contributor for invitation ${invitation.id} `, diff --git a/src/domain/community/user/user.service.ts b/src/domain/community/user/user.service.ts index 095b15f798..bc352f69e1 100644 --- a/src/domain/community/user/user.service.ts +++ b/src/domain/community/user/user.service.ts @@ -421,6 +421,7 @@ export class UserService { ); } const { id } = user; + await this.clearUserCache(user); if (user.profile) { diff --git a/src/domain/community/virtual-contributor/virtual.contributor.entity.ts b/src/domain/community/virtual-contributor/virtual.contributor.entity.ts index 7be3981272..428a124819 100644 --- a/src/domain/community/virtual-contributor/virtual.contributor.entity.ts +++ b/src/domain/community/virtual-contributor/virtual.contributor.entity.ts @@ -14,7 +14,6 @@ export class VirtualContributor eager: false, onDelete: 'SET NULL', }) - @JoinColumn() account!: Account; @OneToOne(() => AiPersona, { diff --git a/src/domain/community/virtual-contributor/virtual.contributor.service.ts b/src/domain/community/virtual-contributor/virtual.contributor.service.ts index 530c443ccc..6567ae3690 100644 --- a/src/domain/community/virtual-contributor/virtual.contributor.service.ts +++ b/src/domain/community/virtual-contributor/virtual.contributor.service.ts @@ -73,9 +73,6 @@ export class VirtualContributorService { virtualContributorData.profileData?.displayName || '' ); } - await this.checkDisplayNameOrFail( - virtualContributorData.profileData?.displayName - ); let virtualContributor: IVirtualContributor = VirtualContributor.create( virtualContributorData diff --git a/src/domain/innovation-hub/dto/innovation.hub.dto.create.ts b/src/domain/innovation-hub/dto/innovation.hub.dto.create.ts index d4cc82a430..b68aa8de42 100644 --- a/src/domain/innovation-hub/dto/innovation.hub.dto.create.ts +++ b/src/domain/innovation-hub/dto/innovation.hub.dto.create.ts @@ -44,10 +44,4 @@ export class CreateInnovationHubInput extends CreateNameableInput { description: 'A readable identifier, unique within the containing scope.', }) nameID!: string; - - @Field(() => UUID, { - nullable: true, - description: 'Account ID, associated with the Innovation Hub.', - }) - accountID?: string; } diff --git a/src/domain/innovation-hub/dto/innovation.hub.dto.update.ts b/src/domain/innovation-hub/dto/innovation.hub.dto.update.ts index 09866b7fb6..6cc9316159 100644 --- a/src/domain/innovation-hub/dto/innovation.hub.dto.update.ts +++ b/src/domain/innovation-hub/dto/innovation.hub.dto.update.ts @@ -4,6 +4,7 @@ import { SpaceVisibility } from '@common/enums/space.visibility'; import { UUID_NAMEID } from '@domain/common/scalars'; import { UpdateNameableInput } from '@domain/common/entity/nameable-entity'; import { InnovationHubType } from '../types'; +import { SearchVisibility } from '@common/enums/search.visibility'; @InputType() export class UpdateInnovationHubInput extends UpdateNameableInput { @@ -20,4 +21,19 @@ export class UpdateInnovationHubInput extends UpdateNameableInput { description: `Spaces with which visibility this Innovation Hub will display. Only valid when type '${InnovationHubType.VISIBILITY}' is used.`, }) spaceVisibilityFilter?: SpaceVisibility; + + @Field(() => Boolean, { + nullable: true, + description: + 'Flag to control the visibility of the InnovationHub in the platform store.', + }) + @IsOptional() + listedInStore?: boolean; + + @Field(() => SearchVisibility, { + description: 'Visibility of the InnovationHub in searches.', + nullable: true, + }) + @IsOptional() + searchVisibility?: SearchVisibility; } diff --git a/src/domain/innovation-hub/innovation.hub.entity.ts b/src/domain/innovation-hub/innovation.hub.entity.ts index 173d45020a..8689af5457 100644 --- a/src/domain/innovation-hub/innovation.hub.entity.ts +++ b/src/domain/innovation-hub/innovation.hub.entity.ts @@ -1,13 +1,20 @@ -import { Column, Entity, JoinColumn, OneToOne } from 'typeorm'; +import { Column, Entity, ManyToOne } from 'typeorm'; import { NameableEntity } from '@domain/common/entity/nameable-entity'; import { SpaceVisibility } from '@common/enums/space.visibility'; import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; import { SUBDOMAIN_LENGTH } from '@common/constants'; import { InnovationHubType } from './innovation.hub.type.enum'; import { Account } from '@domain/space/account/account.entity'; +import { SearchVisibility } from '@common/enums/search.visibility'; @Entity() export class InnovationHub extends NameableEntity implements IInnovationHub { + @ManyToOne(() => Account, account => account.innovationHubs, { + eager: false, + onDelete: 'SET NULL', + }) + account!: Account; + @Column({ unique: true, }) @@ -33,11 +40,13 @@ export class InnovationHub extends NameableEntity implements IInnovationHub { }) spaceListFilter?: string[]; - @OneToOne(() => Account, { - eager: false, - cascade: true, - onDelete: 'SET NULL', + @Column() + listedInStore!: boolean; + + @Column('varchar', { + length: 36, + nullable: false, + default: SearchVisibility.ACCOUNT, }) - @JoinColumn() - account!: Account; + searchVisibility!: SearchVisibility; } diff --git a/src/domain/innovation-hub/innovation.hub.interface.ts b/src/domain/innovation-hub/innovation.hub.interface.ts index 68f1f53161..7893f381e4 100644 --- a/src/domain/innovation-hub/innovation.hub.interface.ts +++ b/src/domain/innovation-hub/innovation.hub.interface.ts @@ -3,6 +3,7 @@ import { INameable } from '@domain/common/entity/nameable-entity'; import { SpaceVisibility } from '@common/enums/space.visibility'; import { InnovationHubType } from './innovation.hub.type.enum'; import { IAccount } from '@domain/space/account/account.interface'; +import { SearchVisibility } from '@common/enums/search.visibility'; @ObjectType('InnovationHub') export abstract class IInnovationHub extends INameable { @@ -27,4 +28,17 @@ export abstract class IInnovationHub extends INameable { spaceListFilter?: string[]; account!: IAccount; + + @Field(() => SearchVisibility, { + description: 'Visibility of the InnovationHub in searches.', + nullable: false, + }) + searchVisibility!: SearchVisibility; + + @Field(() => Boolean, { + nullable: false, + description: + 'Flag to control if this InnovationHub is listed in the platform store.', + }) + listedInStore!: boolean; } diff --git a/src/domain/innovation-hub/innovation.hub.module.ts b/src/domain/innovation-hub/innovation.hub.module.ts index fe017eb207..6985cfe9cc 100644 --- a/src/domain/innovation-hub/innovation.hub.module.ts +++ b/src/domain/innovation-hub/innovation.hub.module.ts @@ -2,33 +2,29 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { InnovationHubService } from './innovation.hub.service'; import { InnovationHub } from './innovation.hub.entity'; -import { InnovationHubFieldResolver } from './innovation.hub.field.resolver'; import { SpaceModule } from '@domain/space/space/space.module'; import { ProfileModule } from '@domain/common/profile/profile.module'; import { InnovationHubAuthorizationService } from '@domain/innovation-hub/innovation.hub.service.authorization'; -import { PlatformAuthorizationPolicyModule } from '@platform/authorization/platform.authorization.policy.module'; import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; import { InnovationHubResolverMutations } from './innovation.hub.resolver.mutations'; import { AuthorizationModule } from '@core/authorization/authorization.module'; import { NamingModule } from '@services/infrastructure/naming/naming.module'; import { StorageAggregatorResolverModule } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.module'; -import { AccountModule } from '@domain/space/account/account.module'; +import { InnovationHubResolverFields } from './innovation.hub.resolver.fields'; @Module({ imports: [ TypeOrmModule.forFeature([InnovationHub]), SpaceModule, ProfileModule, - PlatformAuthorizationPolicyModule, AuthorizationPolicyModule, AuthorizationModule, NamingModule, StorageAggregatorResolverModule, - AccountModule, ], providers: [ InnovationHubService, - InnovationHubFieldResolver, + InnovationHubResolverFields, InnovationHubResolverMutations, InnovationHubAuthorizationService, ], diff --git a/src/domain/innovation-hub/innovation.hub.field.resolver.ts b/src/domain/innovation-hub/innovation.hub.resolver.fields.ts similarity index 98% rename from src/domain/innovation-hub/innovation.hub.field.resolver.ts rename to src/domain/innovation-hub/innovation.hub.resolver.fields.ts index a9e27e30e3..d1692c39a9 100644 --- a/src/domain/innovation-hub/innovation.hub.field.resolver.ts +++ b/src/domain/innovation-hub/innovation.hub.resolver.fields.ts @@ -13,7 +13,7 @@ import { IAccount } from '@domain/space/account/account.interface'; import { AccountLoaderCreator } from '@core/dataloader/creators/loader.creators/account/account.loader.creator'; @Resolver(() => IInnovationHub) -export class InnovationHubFieldResolver { +export class InnovationHubResolverFields { constructor( private innovationHubService: InnovationHubService, private spaceService: SpaceService diff --git a/src/domain/innovation-hub/innovation.hub.resolver.mutations.ts b/src/domain/innovation-hub/innovation.hub.resolver.mutations.ts index 5c8915977f..7cbf34df66 100644 --- a/src/domain/innovation-hub/innovation.hub.resolver.mutations.ts +++ b/src/domain/innovation-hub/innovation.hub.resolver.mutations.ts @@ -8,39 +8,15 @@ import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; import { DeleteInnovationHubInput } from './dto/innovation.hub.dto.delete'; import { InnovationHubService } from './innovation.hub.service'; import { AuthorizationService } from '@core/authorization/authorization.service'; -import { CreateInnovationHubInput } from './dto/innovation.hub.dto.create'; -import { PlatformAuthorizationPolicyService } from '@platform/authorization/platform.authorization.policy.service'; import { UpdateInnovationHubInput } from './dto/innovation.hub.dto.update'; @Resolver() export class InnovationHubResolverMutations { constructor( private authorizationService: AuthorizationService, - private innovationHubService: InnovationHubService, - private platformAuthorizationService: PlatformAuthorizationPolicyService + private innovationHubService: InnovationHubService ) {} - @UseGuards(GraphqlGuard) - @Mutation(() => IInnovationHub, { - description: 'Create Innovation Hub.', - }) - @Profiling.api - async createInnovationHub( - @CurrentUser() agentInfo: AgentInfo, - @Args('createData') createData: CreateInnovationHubInput - ): Promise { - const authorizationPolicy = - await this.platformAuthorizationService.getPlatformAuthorizationPolicy(); - await this.authorizationService.grantAccessOrFail( - agentInfo, - authorizationPolicy, - AuthorizationPrivilege.PLATFORM_ADMIN, - 'create innovation space' - ); - - return await this.innovationHubService.createOrFail(createData); - } - @UseGuards(GraphqlGuard) @Mutation(() => IInnovationHub, { description: 'Update Innovation Hub.', @@ -50,13 +26,13 @@ export class InnovationHubResolverMutations { @CurrentUser() agentInfo: AgentInfo, @Args('updateData') updateData: UpdateInnovationHubInput ): Promise { - const authorizationPolicy = - await this.platformAuthorizationService.getPlatformAuthorizationPolicy(); + const innovationHub = + await this.innovationHubService.getInnovationHubOrFail(updateData.ID); await this.authorizationService.grantAccessOrFail( agentInfo, - authorizationPolicy, + innovationHub.authorization, AuthorizationPrivilege.UPDATE, - 'update innovation space' + 'update innovation hub' ); return await this.innovationHubService.updateOrFail(updateData); @@ -71,14 +47,14 @@ export class InnovationHubResolverMutations { @CurrentUser() agentInfo: AgentInfo, @Args('deleteData') deleteData: DeleteInnovationHubInput ): Promise { - const authorizationPolicy = - await this.platformAuthorizationService.getPlatformAuthorizationPolicy(); + const innovationHub = + await this.innovationHubService.getInnovationHubOrFail(deleteData.ID); await this.authorizationService.grantAccessOrFail( agentInfo, - authorizationPolicy, + innovationHub.authorization, AuthorizationPrivilege.PLATFORM_ADMIN, - 'delete innovation space' + 'delete innovation hub' ); - return await this.innovationHubService.deleteOrFail(deleteData.ID); + return await this.innovationHubService.delete(deleteData.ID); } } diff --git a/src/domain/innovation-hub/innovation.hub.service.authorization.ts b/src/domain/innovation-hub/innovation.hub.service.authorization.ts index 80086970e4..8d0ca0d491 100644 --- a/src/domain/innovation-hub/innovation.hub.service.authorization.ts +++ b/src/domain/innovation-hub/innovation.hub.service.authorization.ts @@ -2,7 +2,6 @@ import { Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; -import { PlatformAuthorizationPolicyService } from '@platform/authorization/platform.authorization.policy.service'; import { IInnovationHub, InnovationHub } from './types'; import { ProfileAuthorizationService } from '@domain/common/profile/profile.service.authorization'; import { IAuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.interface'; @@ -12,49 +11,63 @@ import { IAuthorizationPolicyRuleCredential } from '@core/authorization/authoriz import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; import { AuthorizationCredential } from '@common/enums/authorization.credential'; import { CREDENTIAL_RULE_TYPES_INNOVATION_HUBS } from '@common/constants/authorization/credential.rule.types.constants'; +import { InnovationHubService } from './innovation.hub.service'; @Injectable() export class InnovationHubAuthorizationService { constructor( private authorizationPolicyService: AuthorizationPolicyService, - private platformAuthorizationService: PlatformAuthorizationPolicyService, private profileAuthorizationService: ProfileAuthorizationService, + private innovationHubService: InnovationHubService, @InjectRepository(InnovationHub) private spaceRepository: Repository ) {} public async applyAuthorizationPolicyAndSave( - hub: IInnovationHub + hubInput: IInnovationHub, + parentAuthorization: IAuthorizationPolicy | undefined ): Promise { - hub.authorization = this.authorizationPolicyService.reset( - hub.authorization + const hub = await this.innovationHubService.getInnovationHubOrFail( + hubInput.id, + { + relations: { + profile: true, + }, + } ); + + if (!hub.profile) { + throw new EntityNotInitializedException( + `authorization: Unable to load InnovationHub entities for auth reset: ${hubInput.id}`, + LogContext.INNOVATION_HUB + ); + } + + // Clone the authorization policy + allow anonymous read access to ensure + // pages are visible / loadable by all users + const clonedAuthorization = + this.authorizationPolicyService.cloneAuthorizationPolicy( + parentAuthorization + ); + clonedAuthorization.anonymousReadAccess = true; + hub.authorization = - this.platformAuthorizationService.inheritRootAuthorizationPolicy( - hub.authorization + this.authorizationPolicyService.inheritParentAuthorization( + hub.authorization, + clonedAuthorization ); - hub.authorization.anonymousReadAccess = true; + hub.authorization = this.extendAuthorizationPolicyRules(hub.authorization); - hub = await this.cascadeAuthorization(hub); + hub.profile = + await this.profileAuthorizationService.applyAuthorizationPolicy( + hub.profile, + hub.authorization + ); return this.spaceRepository.save(hub); } - private async cascadeAuthorization( - innovationHub: IInnovationHub - ): Promise { - if (innovationHub.profile) { - innovationHub.profile = - await this.profileAuthorizationService.applyAuthorizationPolicy( - innovationHub.profile, - innovationHub.authorization - ); - } - - return innovationHub; - } - private extendAuthorizationPolicyRules( hubAuthorization: IAuthorizationPolicy ): IAuthorizationPolicy { diff --git a/src/domain/innovation-hub/innovation.hub.service.ts b/src/domain/innovation-hub/innovation.hub.service.ts index 7cc96f136a..c4410d7e73 100644 --- a/src/domain/innovation-hub/innovation.hub.service.ts +++ b/src/domain/innovation-hub/innovation.hub.service.ts @@ -11,13 +11,12 @@ import { VisualType } from '@common/enums/visual.type'; import { AuthorizationPolicy } from '@domain/common/authorization-policy'; import { IInnovationHub, InnovationHub, InnovationHubType } from './types'; import { CreateInnovationHubInput, UpdateInnovationHubInput } from './dto'; -import { InnovationHubAuthorizationService } from './innovation.hub.service.authorization'; import { SpaceService } from '@domain/space/space/space.service'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; import { NamingService } from '@services/infrastructure/naming/naming.service'; import { TagsetReservedName } from '@common/enums/tagset.reserved.name'; -import { StorageAggregatorResolverService } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service'; -import { AccountService } from '@domain/space/account/account.service'; +import { SearchVisibility } from '@common/enums/search.visibility'; +import { IAccount } from '@domain/space/account/account.interface'; @Injectable() export class InnovationHubService { @@ -25,16 +24,14 @@ export class InnovationHubService { @InjectRepository(InnovationHub) private readonly innovationHubRepository: Repository, private readonly profileService: ProfileService, - private readonly authService: InnovationHubAuthorizationService, private readonly authorizationPolicyService: AuthorizationPolicyService, - private storageAggregatorResolverService: StorageAggregatorResolverService, private readonly spaceService: SpaceService, - private readonly accountService: AccountService, private namingService: NamingService ) {} - public async createOrFail( - createData: CreateInnovationHubInput + public async createInnovationHub( + createData: CreateInnovationHubInput, + account: IAccount ): Promise { try { await this.validateCreateInput(createData); @@ -46,6 +43,13 @@ export class InnovationHubService { ); } + if (!account.storageAggregator) { + throw new EntityNotFoundException( + `Unable to load storage aggregator on account for creating innovation Hub: ${account.id}`, + LogContext.ACCOUNT + ); + } + const subdomainAvailable = await this.namingService.isInnovationHubSubdomainAvailable( createData.subdomain @@ -72,17 +76,16 @@ export class InnovationHubService { ); } - const { accountID, ...createDataProps } = createData; - const hub: IInnovationHub = InnovationHub.create(createDataProps); + const hub: IInnovationHub = InnovationHub.create(createData); hub.authorization = new AuthorizationPolicy(); - - const storageAggregator = - await this.storageAggregatorResolverService.getPlatformStorageAggregator(); + hub.listedInStore = true; + hub.searchVisibility = SearchVisibility.ACCOUNT; + hub.account = account; hub.profile = await this.profileService.createProfile( createData.profileData, ProfileType.INNOVATION_HUB, - storageAggregator + account.storageAggregator ); await this.profileService.addTagsetOnProfile(hub.profile, { @@ -95,14 +98,7 @@ export class InnovationHubService { VisualType.BANNER_WIDE ); - if (accountID) { - const account = await this.accountService.getAccountOrFail(accountID); - hub.account = account; - } - - await this.save(hub); - - return this.authService.applyAuthorizationPolicyAndSave(hub); + return await this.save(hub); } public save(hub: IInnovationHub): Promise { @@ -113,9 +109,7 @@ export class InnovationHubService { input: UpdateInnovationHubInput ): Promise { const innovationHub: IInnovationHub = await this.getInnovationHubOrFail( - { - idOrNameId: input.ID, - }, + input.ID, { relations: { profile: true } } ); @@ -168,17 +162,21 @@ export class InnovationHubService { input.profileData ); } + if (typeof input.listedInStore === 'boolean') { + innovationHub.listedInStore = !!input.listedInStore; + } + + if (input.searchVisibility) { + innovationHub.searchVisibility = input.searchVisibility; + } return await this.save(innovationHub); } - public async deleteOrFail(innovationHubID: string): Promise { - const hub = await this.getInnovationHubOrFail( - { idOrNameId: innovationHubID }, - { - relations: { profile: true }, - } - ); + public async delete(innovationHubID: string): Promise { + const hub = await this.getInnovationHubOrFail(innovationHubID, { + relations: { profile: true }, + }); if (hub.profile) { await this.profileService.deleteProfile(hub.profile.id); @@ -200,6 +198,23 @@ export class InnovationHubService { } public async getInnovationHubOrFail( + innovationHubID: string, + options?: FindOneOptions + ): Promise { + const innovationHub = await this.innovationHubRepository.findOne({ + where: { id: innovationHubID }, + ...options, + }); + + if (!innovationHub) + throw new EntityNotFoundException( + `Unable to find InnovationHub with ID: ${innovationHubID}`, + LogContext.SPACES + ); + return innovationHub; + } + + public async getInnovationHubFlexOrFail( args: { subdomain?: string; idOrNameId?: string }, options?: FindOneOptions ): Promise { diff --git a/src/domain/license/license/dto/license.dto.create.ts b/src/domain/license/license/dto/license.dto.create.ts deleted file mode 100644 index 9d8cec1ff5..0000000000 --- a/src/domain/license/license/dto/license.dto.create.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Field, InputType } from '@nestjs/graphql'; -import { SpaceVisibility } from '@common/enums/space.visibility'; -import { IsOptional } from 'class-validator'; - -@InputType() -export class CreateLicenseInput { - @Field(() => SpaceVisibility, { - nullable: true, - description: 'Visibility of the Space.', - }) - @IsOptional() - visibility?: SpaceVisibility; -} diff --git a/src/domain/license/license/dto/license.dto.update.ts b/src/domain/license/license/dto/license.dto.update.ts deleted file mode 100644 index 8c0c6c2ab7..0000000000 --- a/src/domain/license/license/dto/license.dto.update.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Field, InputType } from '@nestjs/graphql'; -import { SpaceVisibility } from '@common/enums/space.visibility'; -import { IsOptional } from 'class-validator'; - -@InputType() -export class UpdateLicenseInput { - @Field(() => SpaceVisibility, { - nullable: true, - description: 'Visibility of the Space.', - }) - @IsOptional() - visibility?: SpaceVisibility; -} diff --git a/src/domain/license/license/license.entity.ts b/src/domain/license/license/license.entity.ts deleted file mode 100644 index 0b5b18c757..0000000000 --- a/src/domain/license/license/license.entity.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AuthorizableEntity } from '@domain/common/entity/authorizable-entity'; -import { Column, Entity } from 'typeorm'; -import { ILicense } from './license.interface'; -import { SpaceVisibility } from '@common/enums/space.visibility'; - -@Entity() -export class License extends AuthorizableEntity implements ILicense { - @Column('varchar', { - length: 36, - nullable: false, - default: SpaceVisibility.ACTIVE, - }) - visibility!: SpaceVisibility; -} diff --git a/src/domain/license/license/license.interface.ts b/src/domain/license/license/license.interface.ts deleted file mode 100644 index 264cbb2a4b..0000000000 --- a/src/domain/license/license/license.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { SpaceVisibility } from '@common/enums/space.visibility'; -import { IAuthorizable } from '@domain/common/entity/authorizable-entity'; -import { Field, ObjectType } from '@nestjs/graphql'; - -@ObjectType('License') -export abstract class ILicense extends IAuthorizable { - @Field(() => SpaceVisibility, { - description: 'Visibility of the Space.', - nullable: false, - }) - visibility!: SpaceVisibility; -} diff --git a/src/domain/license/license/license.module.ts b/src/domain/license/license/license.module.ts deleted file mode 100644 index 7868828137..0000000000 --- a/src/domain/license/license/license.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { AuthorizationModule } from '@core/authorization/authorization.module'; -import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { License } from './license.entity'; -import { LicenseService } from './license.service'; -import { LicenseAuthorizationService } from './license.service.authorization'; - -@Module({ - imports: [ - AuthorizationModule, - AuthorizationPolicyModule, - TypeOrmModule.forFeature([License]), - ], - providers: [LicenseService, LicenseAuthorizationService], - exports: [LicenseService, LicenseAuthorizationService], -}) -export class LicenseModule {} diff --git a/src/domain/license/license/license.service.authorization.ts b/src/domain/license/license/license.service.authorization.ts deleted file mode 100644 index b3c85592cf..0000000000 --- a/src/domain/license/license/license.service.authorization.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ILicense } from './license.interface'; -import { IAuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.interface'; -import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; - -@Injectable() -export class LicenseAuthorizationService { - constructor(private authorizationPolicyService: AuthorizationPolicyService) {} - - applyAuthorizationPolicy( - license: ILicense, - parentAuthorization: IAuthorizationPolicy | undefined - ): ILicense { - license.authorization = - this.authorizationPolicyService.inheritParentAuthorization( - license.authorization, - parentAuthorization - ); - - return license; - } -} diff --git a/src/domain/license/license/license.service.ts b/src/domain/license/license/license.service.ts deleted file mode 100644 index ed67f46e4c..0000000000 --- a/src/domain/license/license/license.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { LogContext } from '@common/enums/logging.context'; -import { EntityNotFoundException } from '@common/exceptions/entity.not.found.exception'; -import { AuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.entity'; -import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; -import { Inject, Injectable, LoggerService } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { FindOneOptions, Repository } from 'typeorm'; -import { License } from './license.entity'; -import { ILicense } from './license.interface'; -import { UpdateLicenseInput } from './dto/license.dto.update'; -import { SpaceVisibility } from '@common/enums/space.visibility'; -import { CreateLicenseInput } from './dto/license.dto.create'; - -@Injectable() -export class LicenseService { - constructor( - private authorizationPolicyService: AuthorizationPolicyService, - @InjectRepository(License) - private licenseRepository: Repository, - @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService - ) {} - - public async createLicense( - licenseInput: CreateLicenseInput - ): Promise { - const license: ILicense = License.create(); - this.updateLicense(license, licenseInput); - - license.authorization = AuthorizationPolicy.create(); - // default to active space - license.visibility = licenseInput.visibility || SpaceVisibility.ACTIVE; - - return await this.licenseRepository.save(license); - } - - async delete(licenseID: string): Promise { - const license = await this.getLicenseOrFail(licenseID); - - if (license.authorization) - await this.authorizationPolicyService.delete(license.authorization); - - return await this.licenseRepository.remove(license as License); - } - - public async getLicenseOrFail( - licenseID: string, - options?: FindOneOptions - ): Promise { - const license = await this.licenseRepository.findOne({ - where: { id: licenseID }, - ...options, - }); - if (!license) - throw new EntityNotFoundException( - `License not found: ${licenseID}`, - LogContext.LICENSE - ); - return license; - } - - public async updateLicense( - license: ILicense, - licenseUpdateData: UpdateLicenseInput - ): Promise { - if (licenseUpdateData.visibility) { - license.visibility = licenseUpdateData.visibility; - } - - return await this.save(license); - } - - async save(license: ILicense): Promise { - return await this.licenseRepository.save(license); - } -} diff --git a/src/domain/space/account.host/account.host.service.ts b/src/domain/space/account.host/account.host.service.ts index 4c964d976f..08a00c8c25 100644 --- a/src/domain/space/account.host/account.host.service.ts +++ b/src/domain/space/account.host/account.host.service.ts @@ -88,7 +88,7 @@ export class AccountHostService { } public async getHostByID(contributorID: string): Promise { - return this.contributorService.getContributorOrFail(contributorID, { + return this.contributorService.getContributorByUuidOrFail(contributorID, { relations: { agent: true, }, diff --git a/src/domain/space/account/account.entity.ts b/src/domain/space/account/account.entity.ts index eb11c60665..92546a1cf3 100644 --- a/src/domain/space/account/account.entity.ts +++ b/src/domain/space/account/account.entity.ts @@ -1,13 +1,14 @@ import { Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm'; import { IAccount } from '@domain/space/account/account.interface'; import { TemplatesSet } from '@domain/template/templates-set/templates.set.entity'; -import { License } from '@domain/license/license/license.entity'; import { SpaceDefaults } from '../space.defaults/space.defaults.entity'; import { AuthorizableEntity } from '@domain/common/entity/authorizable-entity'; import { Space } from '../space/space.entity'; import { Agent } from '@domain/agent/agent/agent.entity'; import { VirtualContributor } from '@domain/community/virtual-contributor'; import { StorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.entity'; +import { InnovationHub } from '@domain/innovation-hub/innovation.hub.entity'; +import { InnovationPack } from '@library/innovation-pack/innovation.pack.entity'; @Entity() export class Account extends AuthorizableEntity implements IAccount { @OneToOne(() => Space, { @@ -34,14 +35,6 @@ export class Account extends AuthorizableEntity implements IAccount { @JoinColumn() storageAggregator?: StorageAggregator; - @OneToOne(() => License, { - eager: false, - cascade: true, - onDelete: 'SET NULL', - }) - @JoinColumn() - license?: License; - @OneToOne(() => TemplatesSet, { eager: false, cascade: true, @@ -63,4 +56,16 @@ export class Account extends AuthorizableEntity implements IAccount { cascade: true, }) virtualContributors!: VirtualContributor[]; + + @OneToMany(() => InnovationHub, hub => hub.account, { + eager: false, + cascade: true, + }) + innovationHubs!: InnovationHub[]; + + @OneToMany(() => InnovationPack, innovationPack => innovationPack.account, { + eager: false, + cascade: true, + }) + innovationPacks!: InnovationPack[]; } diff --git a/src/domain/space/account/account.interface.ts b/src/domain/space/account/account.interface.ts index eb45fdfcb0..c24bb843c2 100644 --- a/src/domain/space/account/account.interface.ts +++ b/src/domain/space/account/account.interface.ts @@ -1,12 +1,13 @@ import { ObjectType } from '@nestjs/graphql'; import { ITemplatesSet } from '@domain/template/templates-set'; -import { ILicense } from '@domain/license/license/license.interface'; import { IAuthorizable } from '@domain/common/entity/authorizable-entity'; import { ISpaceDefaults } from '../space.defaults/space.defaults.interface'; import { ISpace } from '../space/space.interface'; import { IAgent } from '@domain/agent/agent/agent.interface'; import { IVirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.interface'; import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; +import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; +import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; @ObjectType('Account') export class IAccount extends IAuthorizable { @@ -14,7 +15,8 @@ export class IAccount extends IAuthorizable { space?: ISpace; library?: ITemplatesSet; defaults?: ISpaceDefaults; - license?: ILicense; virtualContributors!: IVirtualContributor[]; + innovationHubs!: IInnovationHub[]; + innovationPacks!: IInnovationPack[]; storageAggregator?: IStorageAggregator; } diff --git a/src/domain/space/account/account.module.ts b/src/domain/space/account/account.module.ts index 06366c0389..201c2da713 100644 --- a/src/domain/space/account/account.module.ts +++ b/src/domain/space/account/account.module.ts @@ -6,7 +6,6 @@ import { AccountResolverFields } from '@domain/space/account/account.resolver.fi import { AccountAuthorizationService } from '@domain/space/account/account.service.authorization'; import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; import { TemplatesSetModule } from '@domain/template/templates-set/templates.set.module'; -import { LicenseModule } from '@domain/license/license/license.module'; import { SpaceDefaultsModule } from '../space.defaults/space.defaults.module'; import { AgentModule } from '@domain/agent/agent/agent.module'; import { AuthorizationModule } from '@core/authorization/authorization.module'; @@ -26,7 +25,10 @@ import { StorageAggregatorModule } from '@domain/storage/storage-aggregator/stor import { CommunityPolicyModule } from '@domain/community/community-policy/community.policy.module'; import { NotificationAdapterModule } from '@services/adapters/notification-adapter/notification.adapter.module'; import { CommunityModule } from '@domain/community/community/community.module'; +import { InnovationHubModule } from '@domain/innovation-hub'; +import { InnovationPackModule } from '@library/innovation-pack/innovation.pack.module'; import { SpaceSettingssModule } from '../space.settings/space.settings.module'; +import { NamingModule } from '@services/infrastructure/naming/naming.module'; @Module({ imports: [ @@ -43,15 +45,17 @@ import { SpaceSettingssModule } from '../space.settings/space.settings.module'; StorageAggregatorModule, PlatformAuthorizationPolicyModule, InnovationFlowTemplateModule, - LicenseModule, LicensingModule, LicenseIssuerModule, LicenseEngineModule, + InnovationHubModule, + NamingModule, NameReporterModule, CommunityPolicyModule, TypeOrmModule.forFeature([Account]), NotificationAdapterModule, CommunityModule, + InnovationPackModule, ], providers: [ AccountService, diff --git a/src/domain/space/account/account.resolver.fields.ts b/src/domain/space/account/account.resolver.fields.ts index 2b83d45210..902d836003 100644 --- a/src/domain/space/account/account.resolver.fields.ts +++ b/src/domain/space/account/account.resolver.fields.ts @@ -12,15 +12,15 @@ import { IAccount } from '@domain/space/account/account.interface'; import { ITemplatesSet } from '@domain/template/templates-set'; import { Loader } from '@core/dataloader/decorators'; import { ILoader } from '@core/dataloader/loader.interface'; -import { ILicense } from '@domain/license/license/license.interface'; import { ISpaceDefaults } from '../space.defaults/space.defaults.interface'; import { AccountDefaultsLoaderCreator, - AccountLicenseLoaderCreator, AccountLibraryLoaderCreator, AuthorizationLoaderCreator, AgentLoaderCreator, AccountVirtualContributorsLoaderCreator, + AccountInnovationHubsLoaderCreator, + AccountInnovationPacksLoaderCreator, } from '@core/dataloader/creators/loader.creators'; import { IAuthorizationPolicy } from '@domain/common/authorization-policy'; import { AuthorizationService } from '@core/authorization/authorization.service'; @@ -28,20 +28,21 @@ import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { IAgent } from '@domain/agent/agent/agent.interface'; import { IContributor } from '@domain/community/contributor/contributor.interface'; import { IAccountSubscription } from './account.license.subscription.interface'; -import { LicensingService } from '@platform/licensing/licensing.service'; import { IVirtualContributor, VirtualContributor, } from '@domain/community/virtual-contributor'; import { AccountHostService } from '../account.host/account.host.service'; import { LicensePrivilege } from '@common/enums/license.privilege'; +import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; +import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; +import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; @Resolver(() => IAccount) export class AccountResolverFields { constructor( private accountService: AccountService, private accountHostService: AccountHostService, - private licensingService: LicensingService, private authorizationService: AuthorizationService ) {} @@ -106,18 +107,6 @@ export class AccountResolverFields { return loader.load(account.id); } - @ResolveField('license', () => ILicense, { - nullable: false, - description: - 'The License governing platform functionality in use by this Account', - }) - async license( - @Parent() account: Account, - @Loader(AccountLicenseLoaderCreator) loader: ILoader - ): Promise { - return loader.load(account.id); - } - @ResolveField('licensePrivileges', () => [LicensePrivilege], { nullable: true, description: @@ -183,8 +172,48 @@ export class AccountResolverFields { @Loader(AccountVirtualContributorsLoaderCreator, { parentClassRef: Account, }) - loader: ILoader - ) { + loader: ILoader + ): Promise { + return loader.load(account.id); + } + + @ResolveField('innovationHubs', () => [IInnovationHub], { + nullable: false, + description: 'The InnovationHubs for this Account.', + }) + async innovationHubs( + @Parent() account: Account, + @Loader(AccountInnovationHubsLoaderCreator, { + parentClassRef: Account, + }) + loader: ILoader + ): Promise { return loader.load(account.id); } + + @ResolveField('innovationPacks', () => [IInnovationPack], { + nullable: false, + description: 'The InnovationPacks for this Account.', + }) + async innovationPacks( + @Parent() account: Account, + @Loader(AccountInnovationPacksLoaderCreator, { + parentClassRef: Account, + }) + loader: ILoader + ): Promise { + return loader.load(account.id); + } + + @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) + @UseGuards(GraphqlGuard) + @ResolveField('storageAggregator', () => IStorageAggregator, { + nullable: false, + description: 'The StorageAggregator in use by this Account', + }) + async storageAggregator( + @Parent() account: Account + ): Promise { + return await this.accountService.getStorageAggregatorOrFail(account.id); + } } diff --git a/src/domain/space/account/account.resolver.mutations.ts b/src/domain/space/account/account.resolver.mutations.ts index dd83acb666..69057b971b 100644 --- a/src/domain/space/account/account.resolver.mutations.ts +++ b/src/domain/space/account/account.resolver.mutations.ts @@ -34,6 +34,14 @@ import { NotificationAdapter } from '@services/adapters/notification-adapter/not import { NotificationInputSpaceCreated } from '@services/adapters/notification-adapter/dto/notification.dto.input.space.created'; import { CreateSpaceOnAccountInput } from './dto/account.dto.create.space'; import { CommunityService } from '@domain/community/community/community.service'; +import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; +import { CreateInnovationHubOnAccountInput } from './dto/account.dto.create.innovation.hub'; +import { InnovationHubService } from '@domain/innovation-hub'; +import { InnovationHubAuthorizationService } from '@domain/innovation-hub/innovation.hub.service.authorization'; +import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; +import { CreateInnovationPackOnAccountInput } from './dto/account.dto.create.innovation.pack'; +import { InnovationPackAuthorizationService } from '@library/innovation-pack/innovation.pack.service.authorization'; +import { InnovationPackService } from '@library/innovation-pack/innovaton.pack.service'; @Resolver() export class AccountResolverMutations { @@ -45,6 +53,10 @@ export class AccountResolverMutations { private innovationFlowTemplateService: InnovationFlowTemplateService, private virtualContributorService: VirtualContributorService, private virtualContributorAuthorizationService: VirtualContributorAuthorizationService, + private innovationHubService: InnovationHubService, + private innovationHubAuthorizationService: InnovationHubAuthorizationService, + private innovationPackService: InnovationPackService, + private innovationPackAuthorizationService: InnovationPackAuthorizationService, private spaceDefaultsService: SpaceDefaultsService, private namingReporter: NameReporterService, private spaceService: SpaceService, @@ -85,9 +97,8 @@ export class AccountResolverMutations { createSpaceOnAccountData, agentInfo ); - account = await this.accountAuthorizationService.applyAuthorizationPolicy( - account - ); + account = + await this.accountAuthorizationService.applyAuthorizationPolicy(account); account = await this.accountService.save(account); const rootSpace = await this.accountService.getRootSpace(account, { @@ -214,16 +225,14 @@ export class AccountResolverMutations { `update platform settings on space: ${account.id}` ); - const result = await this.accountService.updateAccountPlatformSettings( - updateData - ); + const result = + await this.accountService.updateAccountPlatformSettings(updateData); await this.accountService.save(result); // Update the authorization policy as most of the changes imply auth policy updates - account = await this.accountAuthorizationService.applyAuthorizationPolicy( - result - ); + account = + await this.accountAuthorizationService.applyAuthorizationPolicy(result); return await this.accountService.save(account); } @@ -275,6 +284,44 @@ export class AccountResolverMutations { return spaceDefaults; } + @UseGuards(GraphqlGuard) + @Mutation(() => IInnovationHub, { + description: 'Create Innovation Hub.', + }) + async createInnovationHub( + @CurrentUser() agentInfo: AgentInfo, + @Args('createData') createData: CreateInnovationHubOnAccountInput + ): Promise { + // InnovationHubs still require platform admin for now + const authorizationPolicy = + await this.platformAuthorizationService.getPlatformAuthorizationPolicy(); + await this.authorizationService.grantAccessOrFail( + agentInfo, + authorizationPolicy, + AuthorizationPrivilege.PLATFORM_ADMIN, + 'create innovation space' + ); + const account = await this.accountService.getAccountOrFail( + createData.accountID, + { + relations: { + storageAggregator: true, + }, + } + ); + + let innovationHub = await this.innovationHubService.createInnovationHub( + createData, + account + ); + innovationHub = + await this.innovationHubAuthorizationService.applyAuthorizationPolicyAndSave( + innovationHub, + account.authorization + ); + return await this.innovationHubService.save(innovationHub); + } + @UseGuards(GraphqlGuard) @Mutation(() => IVirtualContributor, { description: 'Creates a new VirtualContributor on an Account.', @@ -339,4 +386,56 @@ export class AccountResolverMutations { virtual.id ); } + + @UseGuards(GraphqlGuard) + @Mutation(() => IInnovationPack, { + description: 'Creates a new InnovationPack on an Account.', + }) + async createInnovationPack( + @CurrentUser() agentInfo: AgentInfo, + @Args('innovationPackData') + innovationPackData: CreateInnovationPackOnAccountInput + ): Promise { + const account = await this.accountService.getAccountOrFail( + innovationPackData.accountID, + { + relations: { + space: { + community: true, + }, + }, + } + ); + if (!account.space || !account.space.community) { + throw new EntityNotInitializedException( + `Account space or community is not initialized: ${account.id}`, + LogContext.ACCOUNT + ); + } + + this.authorizationService.grantAccessOrFail( + agentInfo, + account.authorization, + AuthorizationPrivilege.CREATE, + `create Innovation Pack on account: ${innovationPackData.nameID}` + ); + + let innovationPack = + await this.accountService.createInnovationPackOnAccount( + innovationPackData + ); + + const clonedAccountAuth = + await this.accountAuthorizationService.getClonedAccountAuthExtendedForChildEntities( + account + ); + + innovationPack = + await this.innovationPackAuthorizationService.applyAuthorizationPolicy( + innovationPack, + clonedAccountAuth + ); + + return await this.innovationPackService.save(innovationPack); + } } diff --git a/src/domain/space/account/account.service.authorization.ts b/src/domain/space/account/account.service.authorization.ts index 72c8448011..84211dbb9c 100644 --- a/src/domain/space/account/account.service.authorization.ts +++ b/src/domain/space/account/account.service.authorization.ts @@ -12,7 +12,6 @@ import { import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; import { IAccount } from './account.interface'; import { TemplatesSetAuthorizationService } from '@domain/template/templates-set/templates.set.service.authorization'; -import { LicenseAuthorizationService } from '@domain/license/license/license.service.authorization'; import { PlatformAuthorizationPolicyService } from '@platform/authorization/platform.authorization.policy.service'; import { SpaceAuthorizationService } from '../space/space.service.authorization'; import { IAuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.interface'; @@ -34,8 +33,12 @@ import { CommunityRole } from '@common/enums/community.role'; import { StorageAggregatorAuthorizationService } from '@domain/storage/storage-aggregator/storage.aggregator.service.authorization'; import { AuthorizationPolicyRulePrivilege } from '@core/authorization/authorization.policy.rule.privilege'; import { POLICY_RULE_ACCOUNT_CREATE_VC } from '@common/constants/authorization/policy.rule.constants'; +import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; +import { InnovationPackAuthorizationService } from '@library/innovation-pack/innovation.pack.service.authorization'; import { ISpaceSettings } from '../space.settings/space.settings.interface'; import { SpaceSettingsService } from '../space.settings/space.settings.service'; +import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; +import { InnovationHubAuthorizationService } from '@domain/innovation-hub/innovation.hub.service.authorization'; @Injectable() export class AccountAuthorizationService { @@ -43,12 +46,13 @@ export class AccountAuthorizationService { private authorizationPolicyService: AuthorizationPolicyService, private agentAuthorizationService: AgentAuthorizationService, private templatesSetAuthorizationService: TemplatesSetAuthorizationService, - private licenseAuthorizationService: LicenseAuthorizationService, private platformAuthorizationService: PlatformAuthorizationPolicyService, private spaceAuthorizationService: SpaceAuthorizationService, private virtualContributorAuthorizationService: VirtualContributorAuthorizationService, + private innovationPackAuthorizationService: InnovationPackAuthorizationService, private communityPolicyService: CommunityPolicyService, private storageAggregatorAuthorizationService: StorageAggregatorAuthorizationService, + private innovationHubAuthorizationService: InnovationHubAuthorizationService, private spaceSettingsService: SpaceSettingsService, private accountService: AccountService, private accountHostService: AccountHostService @@ -63,10 +67,11 @@ export class AccountAuthorizationService { policy: true, }, }, - license: true, library: true, defaults: true, virtualContributors: true, + innovationPacks: true, + innovationHubs: true, storageAggregator: true, }, }); @@ -154,10 +159,11 @@ export class AccountAuthorizationService { !account.space.community || !account.space.community.policy || !account.library || - !account.license || !account.defaults || !account.virtualContributors || - !account.storageAggregator + !account.innovationPacks || + !account.storageAggregator || + !account.innovationHubs ) { throw new RelationshipNotFoundException( `Unable to load Account with entities at start of auth reset: ${account.id} `, @@ -173,11 +179,6 @@ export class AccountAuthorizationService { account.authorization ); - account.license = this.licenseAuthorizationService.applyAuthorizationPolicy( - account.license, - account.authorization - ); - // For certain child entities allow the space admin also pretty much full control account.library = await this.templatesSetAuthorizationService.applyAuthorizationPolicy( @@ -199,14 +200,36 @@ export class AccountAuthorizationService { const updatedVCs: IVirtualContributor[] = []; for (const vc of account.virtualContributors) { - const udpatedVC = + const updatedVC = await this.virtualContributorAuthorizationService.applyAuthorizationPolicy( vc, clonedAccountAuth ); - updatedVCs.push(udpatedVC); + updatedVCs.push(updatedVC); } account.virtualContributors = updatedVCs; + + const updatedIPs: IInnovationPack[] = []; + for (const ip of account.innovationPacks) { + const updatedIP = + await this.innovationPackAuthorizationService.applyAuthorizationPolicy( + ip, + clonedAccountAuth + ); + updatedIPs.push(updatedIP); + } + account.innovationPacks = updatedIPs; + + const updatedInnovationHubs: IInnovationHub[] = []; + for (const innovationHub of account.innovationHubs) { + const updatedInnovationHub = + await this.innovationHubAuthorizationService.applyAuthorizationPolicyAndSave( + innovationHub, + clonedAccountAuth + ); + updatedInnovationHubs.push(updatedInnovationHub); + } + account.innovationHubs = updatedInnovationHubs; return account; } diff --git a/src/domain/space/account/account.service.ts b/src/domain/space/account/account.service.ts index e0efd519cd..d54d666ab6 100644 --- a/src/domain/space/account/account.service.ts +++ b/src/domain/space/account/account.service.ts @@ -14,8 +14,6 @@ import { IAccount } from './account.interface'; import { AgentService } from '@domain/agent/agent/agent.service'; import { ITemplatesSet } from '@domain/template/templates-set/templates.set.interface'; import { TemplatesSetService } from '@domain/template/templates-set/templates.set.service'; -import { ILicense } from '@domain/license/license/license.interface'; -import { LicenseService } from '@domain/license/license/license.service'; import { SpaceDefaultsService } from '../space.defaults/space.defaults.service'; import { UpdateAccountDefaultsInput } from './dto/account.dto.update.defaults'; import { ISpaceDefaults } from '../space.defaults/space.defaults.interface'; @@ -24,9 +22,7 @@ import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { ISpace } from '../space/space.interface'; import { UpdateAccountPlatformSettingsInput } from './dto/account.dto.update.platform.settings'; import { AuthorizationPolicy } from '@domain/common/authorization-policy'; -import { SpaceVisibility } from '@common/enums/space.visibility'; import { CreateAccountInput } from './dto/account.dto.create'; -import { CreateSpaceInput } from '../space/dto/space.dto.create'; import { LicensingService } from '@platform/licensing/licensing.service'; import { ILicensePlan } from '@platform/license-plan/license.plan.interface'; import { IAccountSubscription } from './account.license.subscription.interface'; @@ -44,6 +40,15 @@ import { StorageAggregatorService } from '@domain/storage/storage-aggregator/sto import { CreateSpaceOnAccountInput } from './dto/account.dto.create.space'; import { Space } from '../space/space.entity'; import { LicensePlanType } from '@common/enums/license.plan.type'; +import { CreateInnovationHubOnAccountInput } from './dto/account.dto.create.innovation.hub'; +import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; +import { InnovationHubService } from '@domain/innovation-hub'; +import { SpaceLevel } from '@common/enums/space.level'; +import { InnovationPackService } from '@library/innovation-pack/innovaton.pack.service'; +import { CreateInnovationPackOnAccountInput } from './dto/account.dto.create.innovation.pack'; +import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; +import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; +import { NamingService } from '@services/infrastructure/naming/naming.service'; @Injectable() export class AccountService { @@ -53,12 +58,14 @@ export class AccountService { private agentService: AgentService, private templatesSetService: TemplatesSetService, private spaceDefaultsService: SpaceDefaultsService, - private licenseService: LicenseService, private licensingService: LicensingService, private licenseEngineService: LicenseEngineService, private licenseIssuerService: LicenseIssuerService, private storageAggregatorService: StorageAggregatorService, private virtualContributorService: VirtualContributorService, + private innovationHubService: InnovationHubService, + private innovationPackService: InnovationPackService, + private namingService: NamingService, @InjectRepository(Account) private accountRepository: Repository, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService @@ -74,9 +81,6 @@ export class AccountService { await this.storageAggregatorService.createStorageAggregator(); account.library = await this.templatesSetService.createTemplatesSet(); account.defaults = await this.spaceDefaultsService.createSpaceDefaults(); - account.license = await this.licenseService.createLicense({ - visibility: SpaceVisibility.ACTIVE, - }); // And set the defaults account.library = @@ -142,9 +146,25 @@ export class AccountService { ); } const spaceData = spaceOnAccountData.spaceData; - await this.validateSpaceData(spaceData); + + const reservedNameIDs = + await this.namingService.getReservedNameIDsLevelZeroSpaces(); + if (!spaceData.nameID) { + spaceData.nameID = this.namingService.createNameIdAvoidingReservedNameIDs( + spaceData.profileData.displayName, + reservedNameIDs + ); + } else { + if (reservedNameIDs.includes(spaceData.nameID)) { + throw new ValidationException( + `Unable to create entity: the provided nameID is already taken: ${spaceData.nameID}`, + LogContext.SPACES + ); + } + } + // Set data for the root space - spaceData.level = 0; + spaceData.level = SpaceLevel.SPACE; spaceData.storageAggregatorParent = account.storageAggregator; const space = await this.spaceService.createSpace( @@ -159,13 +179,6 @@ export class AccountService { return savedAccount; } - async validateSpaceData(spaceData: CreateSpaceInput) { - if (!(await this.spaceService.isNameIdAvailable(spaceData.nameID))) - throw new ValidationException( - `Unable to create Space: the provided nameID is already taken: ${spaceData.nameID}`, - LogContext.SPACES - ); - } async save(account: IAccount): Promise { return await this.accountRepository.save(account); } @@ -213,30 +226,13 @@ export class AccountService { updateData: UpdateAccountPlatformSettingsInput ): Promise { const account = await this.getAccountOrFail(updateData.accountID, { - relations: { - license: true, - space: true, - }, + relations: {}, }); - if (!account.license) { - throw new RelationshipNotFoundException( - `Unable to load license for account ${account.id} `, - LogContext.ACCOUNT - ); - } - if (updateData.hostID) { await this.accountHostService.setAccountHost(account, updateData.hostID); } - if (updateData.license) { - account.license = await this.licenseService.updateLicense( - account.license, - updateData.license - ); - } - return await this.save(account); } @@ -247,21 +243,23 @@ export class AccountService { agent: true, space: true, library: true, - license: true, defaults: true, virtualContributors: true, + innovationPacks: true, storageAggregator: true, + innovationHubs: true, }, }); if ( !account.agent || !account.space || - !account.license || !account.defaults || !account.library || !account.virtualContributors || - !account.storageAggregator + !account.storageAggregator || + !account.innovationHubs || + !account.innovationPacks ) { throw new RelationshipNotFoundException( `Unable to load all entities for deletion of account ${account.id} `, @@ -278,7 +276,6 @@ export class AccountService { await this.templatesSetService.deleteTemplatesSet(account.library.id); - await this.licenseService.delete(account.license.id); await this.spaceDefaultsService.deleteSpaceDefaults(account.defaults.id); await this.storageAggregatorService.delete(account.storageAggregator.id); @@ -291,6 +288,13 @@ export class AccountService { for (const vc of account.virtualContributors) { await this.virtualContributorService.deleteVirtualContributor(vc.id); } + for (const ip of account.innovationPacks) { + await this.innovationPackService.deleteInnovationPack({ ID: ip.id }); + } + + for (const hub of account.innovationHubs) { + await this.innovationHubService.delete(hub.id); + } const result = await this.accountRepository.remove(account as Account); result.id = accountID; @@ -374,24 +378,6 @@ export class AccountService { return templatesSet; } - async getLicenseOrFail(accountId: string): Promise { - const account = await this.getAccountOrFail(accountId, { - relations: { - license: true, - }, - }); - const license = account.license; - - if (!license) { - throw new EntityNotFoundException( - `Unable to find license for account with nameID: ${accountId}`, - LogContext.ACCOUNT - ); - } - - return license; - } - async getRootSpace( accountInput: IAccount, options?: FindOneOptions @@ -474,6 +460,50 @@ export class AccountService { return await this.virtualContributorService.save(vc); } + public async createInnovationHubOnAccount( + innovationHubData: CreateInnovationHubOnAccountInput + ): Promise { + const accountID = innovationHubData.accountID; + const account = await this.getAccountOrFail(accountID, { + relations: { storageAggregator: true }, + }); + + if (!account.storageAggregator) { + throw new RelationshipNotFoundException( + `Unable to load Account with required entities for creating an InnovationHub: ${account.id} `, + LogContext.ACCOUNT + ); + } + const hub = await this.innovationHubService.createInnovationHub( + innovationHubData, + account + ); + hub.account = account; + return await this.innovationHubService.save(hub); + } + + public async createInnovationPackOnAccount( + ipData: CreateInnovationPackOnAccountInput + ): Promise { + const accountID = ipData.accountID; + const account = await this.getAccountOrFail(accountID, { + relations: { storageAggregator: true }, + }); + + if (!account.storageAggregator) { + throw new RelationshipNotFoundException( + `Unable to load Account with required entities for creating Innovation Pack: ${account.id} `, + LogContext.ACCOUNT + ); + } + const ip = await this.innovationPackService.createInnovationPack( + ipData, + account.storageAggregator + ); + ip.account = account; + return await this.innovationPackService.save(ip); + } + public async activeSubscription(account: IAccount) { const licensingFramework = await this.licensingService.getDefaultLicensingOrFail(); @@ -498,4 +528,21 @@ export class AccountService { .filter(item => item.plan?.type === LicensePlanType.SPACE_PLAN) .sort((a, b) => b.plan!.sortOrder - a.plan!.sortOrder)?.[0].subscription; } + + public async getStorageAggregatorOrFail( + accountID: string + ): Promise { + const space = await this.getAccountOrFail(accountID, { + relations: { + storageAggregator: true, + }, + }); + const storageAggregator = space.storageAggregator; + if (!storageAggregator) + throw new RelationshipNotFoundException( + `Unable to load storage aggregator for account ${accountID} `, + LogContext.ACCOUNT + ); + return storageAggregator; + } } diff --git a/src/domain/space/account/dto/account.dto.create.innovation.hub.ts b/src/domain/space/account/dto/account.dto.create.innovation.hub.ts new file mode 100644 index 0000000000..4f8328211d --- /dev/null +++ b/src/domain/space/account/dto/account.dto.create.innovation.hub.ts @@ -0,0 +1,9 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { UUID } from '@domain/common/scalars/scalar.uuid'; +import { CreateInnovationHubInput } from '@domain/innovation-hub/dto'; + +@InputType() +export class CreateInnovationHubOnAccountInput extends CreateInnovationHubInput { + @Field(() => UUID, { nullable: false }) + accountID!: string; +} diff --git a/src/domain/space/account/dto/account.dto.create.innovation.pack.ts b/src/domain/space/account/dto/account.dto.create.innovation.pack.ts new file mode 100644 index 0000000000..9cb4493c99 --- /dev/null +++ b/src/domain/space/account/dto/account.dto.create.innovation.pack.ts @@ -0,0 +1,9 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { UUID } from '@domain/common/scalars/scalar.uuid'; +import { CreateInnovationPackInput } from '@library/innovation-pack/dto/innovation.pack.dto.create'; + +@InputType() +export class CreateInnovationPackOnAccountInput extends CreateInnovationPackInput { + @Field(() => UUID, { nullable: false }) + accountID!: string; +} diff --git a/src/domain/space/account/dto/account.dto.update.platform.settings.ts b/src/domain/space/account/dto/account.dto.update.platform.settings.ts index c340d888cc..1c74b85545 100644 --- a/src/domain/space/account/dto/account.dto.update.platform.settings.ts +++ b/src/domain/space/account/dto/account.dto.update.platform.settings.ts @@ -1,9 +1,7 @@ import { UUID } from '@domain/common/scalars'; import { UUID_NAMEID } from '@domain/common/scalars/scalar.uuid.nameid'; -import { UpdateLicenseInput } from '@domain/license/license/dto/license.dto.update'; import { Field, InputType } from '@nestjs/graphql'; -import { Type } from 'class-transformer'; -import { IsOptional, ValidateNested } from 'class-validator'; +import { IsOptional } from 'class-validator'; @InputType() export class UpdateAccountPlatformSettingsInput { @@ -20,13 +18,4 @@ export class UpdateAccountPlatformSettingsInput { }) @IsOptional() hostID?: string; - - @Field(() => UpdateLicenseInput, { - nullable: true, - description: 'Update the license settings for the Account.', - }) - @IsOptional() - @ValidateNested() - @Type(() => UpdateLicenseInput) - license?: UpdateLicenseInput; } diff --git a/src/domain/space/space/dto/space.dto.update.platform.settings.ts b/src/domain/space/space/dto/space.dto.update.platform.settings.ts index c10295f53b..c03990264a 100644 --- a/src/domain/space/space/dto/space.dto.update.platform.settings.ts +++ b/src/domain/space/space/dto/space.dto.update.platform.settings.ts @@ -1,6 +1,8 @@ +import { SpaceVisibility } from '@common/enums/space.visibility'; import { UUID } from '@domain/common/scalars'; import { NameID } from '@domain/common/scalars/scalar.nameid'; import { Field, InputType } from '@nestjs/graphql'; +import { IsOptional } from 'class-validator'; @InputType() export class UpdateSpacePlatformSettingsInput { @@ -12,8 +14,16 @@ export class UpdateSpacePlatformSettingsInput { spaceID!: string; @Field(() => NameID, { - nullable: false, + nullable: true, description: 'Upate the URL path for the Space.', }) - nameID!: string; + @IsOptional() + nameID?: string; + + @Field(() => SpaceVisibility, { + nullable: true, + description: 'Visibility of the Space, only on L0 spaces.', + }) + @IsOptional() + visibility?: SpaceVisibility; } diff --git a/src/domain/space/space/sort.spaces.by.activity.spec.ts b/src/domain/space/space/sort.spaces.by.activity.spec.ts index 70123a5de6..c3e82a65d1 100644 --- a/src/domain/space/space/sort.spaces.by.activity.spec.ts +++ b/src/domain/space/space/sort.spaces.by.activity.spec.ts @@ -4,6 +4,7 @@ import { LatestActivitiesPerSpace } from '@services/api/me/space.membership.type import { ISpace } from './space.interface'; import { sortSpacesByActivity } from './sort.spaces.by.activity'; import { SpaceType } from '@common/enums/space.type'; +import { SpaceVisibility } from '@common/enums/space.visibility'; const createTestActivity = (createdDate: Date): IActivity => { return { @@ -25,6 +26,8 @@ const createTestSpace = (id: string): ISpace => { rowId: 1, nameID: 'space1', settingsStr: '', + levelZeroSpaceID: '', + visibility: SpaceVisibility.ACTIVE, profile: { id: '1', displayName: 'Space 1', @@ -35,6 +38,8 @@ const createTestSpace = (id: string): ISpace => { account: { id: `account${id}`, virtualContributors: [], + innovationHubs: [], + innovationPacks: [], }, type: SpaceType.SPACE, level: 0, diff --git a/src/domain/space/space/space.entity.ts b/src/domain/space/space/space.entity.ts index 5236537286..586733d5a9 100644 --- a/src/domain/space/space/space.entity.ts +++ b/src/domain/space/space/space.entity.ts @@ -9,7 +9,7 @@ import { } from 'typeorm'; import { ISpace } from '@domain/space/space/space.interface'; import { NameableEntity } from '@domain/common/entity/nameable-entity/nameable.entity'; -import { TINY_TEXT_LENGTH } from '@common/constants'; +import { TINY_TEXT_LENGTH, UUID_LENGTH } from '@common/constants'; import { SpaceType } from '@common/enums/space.type'; import { Collaboration } from '@domain/collaboration/collaboration'; import { Community } from '@domain/community/community'; @@ -17,6 +17,7 @@ import { StorageAggregator } from '@domain/storage/storage-aggregator/storage.ag import { Account } from '../account/account.entity'; import { Context } from '@domain/context/context/context.entity'; import { Agent } from '@domain/agent/agent/agent.entity'; +import { SpaceVisibility } from '@common/enums/space.visibility'; @Entity() export class Space extends NameableEntity implements ISpace { @OneToMany(() => Space, space => space.parentSpace, { @@ -89,9 +90,21 @@ export class Space extends NameableEntity implements ISpace { }) type!: SpaceType; + @Column({ + length: UUID_LENGTH, + }) + levelZeroSpaceID!: string; + @Column('int', { nullable: false }) level!: number; + @Column('varchar', { + length: 36, + nullable: false, + default: SpaceVisibility.ACTIVE, + }) + visibility!: SpaceVisibility; + constructor() { super(); this.nameID = ''; diff --git a/src/domain/space/space/space.interface.ts b/src/domain/space/space/space.interface.ts index 7a2a98e746..421670ac61 100644 --- a/src/domain/space/space/space.interface.ts +++ b/src/domain/space/space/space.interface.ts @@ -7,6 +7,7 @@ import { ICommunity } from '@domain/community/community'; import { IContext } from '@domain/context/context/context.interface'; import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; import { IAccount } from '../account/account.interface'; +import { SpaceVisibility } from '@common/enums/space.visibility'; @ObjectType('Space') export class ISpace extends INameable { @@ -33,6 +34,12 @@ export class ISpace extends INameable { }) type!: SpaceType; + @Field(() => SpaceVisibility, { + description: 'Visibility of the Space.', + nullable: false, + }) + visibility!: SpaceVisibility; + agent?: IAgent; collaboration?: ICollaboration; @@ -43,4 +50,9 @@ export class ISpace extends INameable { settingsStr!: string; storageAggregator?: IStorageAggregator; + + @Field(() => String, { + description: 'The ID of the level zero space for this tree.', + }) + levelZeroSpaceID!: string; } diff --git a/src/domain/space/space/space.resolver.fields.ts b/src/domain/space/space/space.resolver.fields.ts index ad18a9a29a..32fe9f7c3d 100644 --- a/src/domain/space/space/space.resolver.fields.ts +++ b/src/domain/space/space/space.resolver.fields.ts @@ -169,9 +169,9 @@ export class SpaceResolverFields { @CurrentUser() agentInfo: AgentInfo, @Parent() space: ISpace ): Promise { - const subspace = await this.spaceService.getSubspaceInAccount( + const subspace = await this.spaceService.getSubspaceInLevelZeroSpace( id, - space.account.id + space.levelZeroSpaceID ); if (!subspace) { throw new EntityNotFoundException( diff --git a/src/domain/space/space/space.resolver.mutations.ts b/src/domain/space/space/space.resolver.mutations.ts index 06a5c19c17..f7fb9b599b 100644 --- a/src/domain/space/space/space.resolver.mutations.ts +++ b/src/domain/space/space/space.resolver.mutations.ts @@ -119,15 +119,17 @@ export class SpaceResolverMutations { `space settings update: ${space.id}` ); - const updatedSpace = await this.spaceService.updateSpaceSettings( + let updatedSpace = await this.spaceService.updateSpaceSettings( space, settingsData ); // As the settings may update the authorization for the Space, the authorization policy will need to be reset - return this.spaceAuthorizationService - .applyAuthorizationPolicy(updatedSpace) - .then(space => this.spaceService.save(space)); + updatedSpace = + await this.spaceAuthorizationService.applyAuthorizationPolicy( + updatedSpace + ); + return await this.spaceService.save(updatedSpace); } @UseGuards(GraphqlGuard) @@ -139,7 +141,7 @@ export class SpaceResolverMutations { @CurrentUser() agentInfo: AgentInfo, @Args('updateData') updateData: UpdateSpacePlatformSettingsInput ): Promise { - const space = await this.spaceService.getSpaceOrFail(updateData.spaceID); + let space = await this.spaceService.getSpaceOrFail(updateData.spaceID); this.authorizationService.grantAccessOrFail( agentInfo, space.authorization, @@ -147,10 +149,13 @@ export class SpaceResolverMutations { `update platform settings on space: ${space.id}` ); - return await this.spaceService.updateSpacePlatformSettings( + space = await this.spaceService.updateSpacePlatformSettings( space, updateData ); + space = + await this.spaceAuthorizationService.applyAuthorizationPolicy(space); + return await this.spaceService.save(space); } @UseGuards(GraphqlGuard) @@ -204,9 +209,8 @@ export class SpaceResolverMutations { // Save here so can reuse it later without another load const displayName = subspace.profile.displayName; - subspace = await this.spaceAuthorizationService.applyAuthorizationPolicy( - subspace - ); + subspace = + await this.spaceAuthorizationService.applyAuthorizationPolicy(subspace); subspace = await this.spaceService.save(subspace); this.activityAdapter.subspaceCreated({ diff --git a/src/domain/space/space/space.service.authorization.ts b/src/domain/space/space/space.service.authorization.ts index 25e8f85e57..d971596ba4 100644 --- a/src/domain/space/space/space.service.authorization.ts +++ b/src/domain/space/space/space.service.authorization.ts @@ -71,7 +71,6 @@ export class SpaceAuthorizationService { }, }, account: { - license: true, agent: { credentials: true, }, @@ -81,7 +80,6 @@ export class SpaceAuthorizationService { ); if ( !spaceAccountLicense.account || - !spaceAccountLicense.account.license || !spaceAccountLicense.account.agent || !spaceAccountLicense.account.agent.credentials ) { @@ -91,7 +89,7 @@ export class SpaceAuthorizationService { ); } - const spaceVisibility = spaceAccountLicense.account.license.visibility; + const spaceVisibility = spaceAccountLicense.visibility; const accountAgent = spaceAccountLicense.account.agent; // Allow the parent admins to also delete subspaces @@ -182,6 +180,9 @@ export class SpaceAuthorizationService { parentAuthorization ); + space.authorization = await this.extendAuthorizationPolicy( + space.authorization + ); if (privateSpace) { space.authorization.anonymousReadAccess = false; } @@ -453,19 +454,6 @@ export class SpaceAuthorizationService { newRules.push(createSubspacePrilegeRule); } - // Allow global admins to manage platform settings - const platformSettings = - this.authorizationPolicyService.createCredentialRuleUsingTypesOnly( - [AuthorizationPrivilege.PLATFORM_ADMIN], - [ - AuthorizationCredential.GLOBAL_ADMIN, - AuthorizationCredential.GLOBAL_SUPPORT, - ], - CREDENTIAL_RULE_TYPES_SPACE_PLATFORM_SETTINGS - ); - platformSettings.cascade = false; - newRules.push(platformSettings); - this.authorizationPolicyService.appendCredentialAuthorizationRules( authorization, newRules @@ -679,4 +667,35 @@ export class SpaceAuthorizationService { } return memberCriteria; } + + private async extendAuthorizationPolicy( + authorization: IAuthorizationPolicy | undefined + ): Promise { + if (!authorization) { + throw new EntityNotInitializedException( + 'Authorization definition not found for account', + LogContext.ACCOUNT + ); + } + + const newRules: IAuthorizationPolicyRuleCredential[] = []; + + // Allow global admins to manage platform settings + const platformSettings = + this.authorizationPolicyService.createCredentialRuleUsingTypesOnly( + [AuthorizationPrivilege.PLATFORM_ADMIN], + [ + AuthorizationCredential.GLOBAL_ADMIN, + AuthorizationCredential.GLOBAL_SUPPORT, + ], + CREDENTIAL_RULE_TYPES_SPACE_PLATFORM_SETTINGS + ); + platformSettings.cascade = false; + newRules.push(platformSettings); + + return this.authorizationPolicyService.appendCredentialAuthorizationRules( + authorization, + newRules + ); + } } diff --git a/src/domain/space/space/space.service.spec.ts b/src/domain/space/space/space.service.spec.ts index 44141dd153..2259a13040 100644 --- a/src/domain/space/space/space.service.spec.ts +++ b/src/domain/space/space/space.service.spec.ts @@ -12,7 +12,6 @@ import { SpaceFilterService } from '@services/infrastructure/space-filter/space. import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; import { InnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.entity'; import { ProfileType } from '@common/enums'; -import { License } from '@domain/license/license/license.entity'; import { Collaboration } from '@domain/collaboration/collaboration/collaboration.entity'; import { Account } from '../account/account.entity'; import { SpaceType } from '@common/enums/space.type'; @@ -93,13 +92,17 @@ const getSubspacesMock = ( rowId: i, nameID: `challenge-${spaceId}.${i}`, settingsStr: JSON.stringify({}), + levelZeroSpaceID: spaceId, account: { id: `account-${spaceId}.${i}`, virtualContributors: [], + innovationHubs: [], + innovationPacks: [], ...getEntityMock(), }, type: SpaceType.CHALLENGE, level: SpaceLevel.CHALLENGE, + visibility: SpaceVisibility.ACTIVE, collaboration: { id: '', groupsStr: JSON.stringify([ @@ -182,13 +185,17 @@ const getSubsubspacesMock = (subsubspaceId: string, count: number): Space[] => { rowId: i, nameID: `subsubspace-${subsubspaceId}.${i}`, settingsStr: JSON.stringify({}), + levelZeroSpaceID: subsubspaceId, account: { id: `account-${subsubspaceId}.${i}`, virtualContributors: [], + innovationHubs: [], + innovationPacks: [], ...getEntityMock(), }, type: SpaceType.OPPORTUNITY, level: SpaceLevel.OPPORTUNITY, + visibility: SpaceVisibility.ACTIVE, collaboration: { id: '', groupsStr: JSON.stringify([ @@ -277,6 +284,7 @@ const getSpaceMock = ({ rowId: parseInt(id), nameID: `space-${id}`, settingsStr: JSON.stringify({}), + levelZeroSpaceID: '', profile: { id: `profile-${id}`, displayName: `Space ${id}`, @@ -287,15 +295,12 @@ const getSpaceMock = ({ }, type: SpaceType.SPACE, level: 0, + visibility, account: { id: `account-${id}`, virtualContributors: [], - license: { - id, - visibility, - ...getEntityMock(), - }, - + innovationHubs: [], + innovationPacks: [], ...getEntityMock(), }, authorization: getAuthorizationPolicyMock( @@ -312,8 +317,7 @@ const getFilteredSpaces = ( visibilities: SpaceVisibility[] ): Space[] => { return spaces.filter(space => { - const visibility = - space.account.license?.visibility || SpaceVisibility.ACTIVE; + const visibility = space.visibility || SpaceVisibility.ACTIVE; return visibilities.includes(visibility); }); }; diff --git a/src/domain/space/space/space.service.ts b/src/domain/space/space/space.service.ts index 70e462c605..06b0253c5d 100644 --- a/src/domain/space/space/space.service.ts +++ b/src/domain/space/space/space.service.ts @@ -35,7 +35,6 @@ import { IPaginatedType } from '@core/pagination/paginated.type'; import { SpaceFilterInput } from '@services/infrastructure/space-filter/dto/space.filter.dto.input'; import { PaginationArgs } from '@core/pagination'; import { getPaginationResults } from '@core/pagination/pagination.fn'; -import { SpaceMembershipCollaborationInfo } from '@services/api/me/space.membership.type'; import { ISpaceSettings } from '../space.settings/space.settings.interface'; import { SpaceType } from '@common/enums/space.type'; import { IAccount } from '../account/account.interface'; @@ -61,6 +60,7 @@ import { UpdateSpaceSettingsInput } from './dto/space.dto.update.settings'; import { IContributor } from '@domain/community/contributor/contributor.interface'; import { CommunityContributorType } from '@common/enums/community.contributor.type'; import { CommunityRoleService } from '@domain/community/community-role/community.role.service'; +import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; @Injectable() export class SpaceService { @@ -116,16 +116,9 @@ export class SpaceService { } const space: ISpace = Space.create(spaceData); + // default to demo space + space.visibility = SpaceVisibility.ACTIVE; - return await this.initialise(space, spaceData, account, agentInfo); - } - - public async initialise( - space: ISpace, - spaceData: CreateSpaceInput, - account: IAccount, - agentInfo: AgentInfo | undefined - ): Promise { space.authorization = new AuthorizationPolicy(); space.account = account; space.settingsStr = this.spaceSettingsService.serializeSettings( @@ -238,6 +231,10 @@ export class SpaceService { await this.save(space); + if (spaceData.level === SpaceLevel.SPACE) { + space.levelZeroSpaceID = space.id; + } + ////// Community // set immediate community parent + resourceID space.community.parentID = space.id; @@ -297,11 +294,8 @@ export class SpaceService { } return this.spaceRepository.findBy({ - account: { - license: { - visibility: spaceVisibilityFilter, - }, - }, + visibility: spaceVisibilityFilter, + level: SpaceLevel.SPACE, }); } @@ -385,22 +379,15 @@ export class SpaceService { where: { id: In(args.IDs), level: SpaceLevel.SPACE, - account: { - license: { - visibility: In(visibilities), - }, - }, + visibility: In(visibilities), }, ...options, }); } else { spaces = await this.spaceRepository.find({ where: { - account: { - license: { - visibility: In(visibilities), - }, - }, + visibility: In(visibilities), + level: SpaceLevel.SPACE, }, ...options, }); @@ -411,99 +398,25 @@ export class SpaceService { return spaces; } - public async getSpacesWithChildJourneys( - args: SpacesQueryArgs, - options?: FindManyOptions - ): Promise { - const visibilities = this.spacesFilterService.getAllowedVisibilities( - args.filter - ); - // Load the spaces - let spaces: ISpace[]; - if (args && args.IDs) { - spaces = await this.spaceRepository.find({ - where: { - id: In(args.IDs), - account: { - license: { - visibility: In(visibilities), - }, - }, - }, - ...options, - relations: { - ...options?.relations, - collaboration: true, - subspaces: { - collaboration: true, - subspaces: { - collaboration: true, - }, - }, - }, - }); - } else { - spaces = await this.spaceRepository.find({ - where: { - account: { - license: { - visibility: In(visibilities), - }, - }, - }, - ...options, - relations: { - ...options?.relations, - collaboration: true, - subspaces: { - collaboration: true, - subspaces: { - collaboration: true, - }, - }, - }, - }); - } + public async getSpacesInList(spaceIDs: string[]): Promise { + const visibilities = [SpaceVisibility.ACTIVE, SpaceVisibility.DEMO]; + + const spaces = await this.spaceRepository.find({ + where: { + id: In(spaceIDs), + visibility: In(visibilities), + }, + relations: { + parentSpace: true, + collaboration: true, + }, + }); if (spaces.length === 0) return []; return spaces; } - // Returns a map of all collaboration IDs with parent space ID - public getSpaceMembershipCollaborationInfo( - spaces: ISpace[] - ): SpaceMembershipCollaborationInfo { - const spaceMembershipCollaborationInfo: SpaceMembershipCollaborationInfo = - new Map(); - - for (const space of spaces) { - if (space.collaboration?.id) - spaceMembershipCollaborationInfo.set(space.collaboration.id, space.id); - - if (space.subspaces) { - for (const subspace of space.subspaces) { - if (subspace.collaboration?.id) - spaceMembershipCollaborationInfo.set( - subspace.collaboration.id, - space.id - ); - - if (subspace.subspaces) { - for (const subsubspace of subspace.subspaces) { - if (subsubspace.collaboration?.id) - spaceMembershipCollaborationInfo.set( - subsubspace.collaboration.id, - space.id - ); - } - } - } - } - } - return spaceMembershipCollaborationInfo; - } - public async orderSpacesDefault(spaces: ISpace[]): Promise { // Get the order to return the data in const sortedIDs = await this.getSpacesWithSortOrderDefault( @@ -534,16 +447,10 @@ export class SpaceService { const qb = this.spaceRepository.createQueryBuilder('space'); if (visibilities) { - qb.leftJoinAndSelect('space.account', 'account'); - qb.leftJoinAndSelect('account.license', 'license'); qb.leftJoinAndSelect('space.authorization', 'authorization'); qb.where({ level: SpaceLevel.SPACE, - account: { - license: { - visibility: In(visibilities), - }, - }, + visibility: In(visibilities), }); } @@ -557,8 +464,6 @@ export class SpaceService { const qb = this.spaceRepository.createQueryBuilder('space'); qb.leftJoinAndSelect('space.subspaces', 'subspace'); - qb.leftJoinAndSelect('space.account', 'account'); - qb.leftJoinAndSelect('account.license', 'license'); qb.leftJoinAndSelect('space.authorization', 'authorization_policy'); qb.leftJoinAndSelect('subspace.subspaces', 'subspaces'); qb.where({ @@ -572,8 +477,8 @@ export class SpaceService { private sortSpacesDefault(spacesData: Space[]): string[] { const sortedSpaces = spacesData.sort((a, b) => { - const visibilityA = a.account?.license?.visibility; - const visibilityB = b.account?.license?.visibility; + const visibilityA = a.visibility; + const visibilityB = b.visibility; if ( visibilityA !== visibilityB && (visibilityA === SpaceVisibility.DEMO || @@ -640,11 +545,7 @@ export class SpaceService { return this.spaceRepository.find({ where: { id: In(spaceIds), - account: { - license: { - visibility: visibilities.length ? In(visibilities) : undefined, - }, - }, + visibility: visibilities.length ? In(visibilities) : undefined, }, }); } @@ -718,10 +619,35 @@ export class SpaceService { space: ISpace, updateData: UpdateSpacePlatformSettingsInput ): Promise { - if (updateData.nameID !== space.nameID) { + if (updateData.visibility && updateData.visibility !== space.visibility) { + // Only update visibility on L0 spaces + if (space.level !== SpaceLevel.SPACE) { + throw new ValidationException( + `Unable to update visibility on Space ${space.id} as it is not a L0 space`, + LogContext.SPACES + ); + } + await this.updateSpaceVisibilityAllSubspaces( + space.id, + updateData.visibility + ); + + space.visibility = updateData.visibility; + } + if (updateData.nameID && updateData.nameID !== space.nameID) { + let reservedNameIDs: string[] = []; + if (space.level === SpaceLevel.SPACE) { + reservedNameIDs = + await this.namingService.getReservedNameIDsLevelZeroSpaces(); + } else { + reservedNameIDs = + await this.namingService.getReservedNameIDsInLevelZeroSpace( + space.levelZeroSpaceID + ); + } // updating the nameID, check new value is allowed - const updateAllowed = await this.isNameIdAvailable(updateData.nameID); - if (!updateAllowed) { + const existingNameID = reservedNameIDs.includes(updateData.nameID); + if (existingNameID) { throw new ValidationException( `Unable to update Space nameID: the provided nameID is already taken: ${updateData.nameID}`, LogContext.ACCOUNT @@ -729,9 +655,25 @@ export class SpaceService { } space.nameID = updateData.nameID; } + return await this.save(space); } + private async updateSpaceVisibilityAllSubspaces( + levelZeroSpaceID: string, + visibility: SpaceVisibility + ) { + const spaces = await this.spaceRepository.find({ + where: { + levelZeroSpaceID: levelZeroSpaceID, + }, + }); + for (const space of spaces) { + space.visibility = visibility; + await this.save(space); + } + } + public async updateSpaceSettings( space: ISpace, settingsData: UpdateSpaceSettingsInput @@ -739,19 +681,6 @@ export class SpaceService { return await this.updateSettings(space, settingsData.settings); } - async isNameIdAvailable(nameID: string): Promise { - const spaceCount = await this.spaceRepository.countBy({ - nameID: nameID, - }); - if (spaceCount != 0) return false; - - // check restricted space names - const restrictedSpaceNames = ['user', 'organization']; - if (restrictedSpaceNames.includes(nameID.toLowerCase())) return false; - - return true; - } - async getSubspaces( space: ISpace, args?: LimitAndShuffleIdsQueryArgs @@ -820,7 +749,9 @@ export class SpaceService { ); } const reservedNameIDs = - await this.namingService.getReservedNameIDsInAccount(space.account.id); + await this.namingService.getReservedNameIDsInLevelZeroSpace( + space.levelZeroSpaceID + ); if (!subspaceData.nameID) { subspaceData.nameID = this.namingService.createNameIdAvoidingReservedNameIDs( @@ -871,6 +802,7 @@ export class SpaceService { // Set the parent space directly, avoiding saving the whole parent subspace.parentSpace = space; + subspace.levelZeroSpaceID = space.levelZeroSpaceID; // Finally set the community relationship await this.setCommunityHierarchyForSubspace( @@ -878,13 +810,13 @@ export class SpaceService { subspace.community ); - return await this.spaceRepository.save(subspace); + return await this.save(subspace); } async getSubspace(subspaceID: string, space: ISpace): Promise { - return await this.getSubspaceInAccountScopeOrFail( + return await this.getSubspaceInLevelZeroScopeOrFail( subspaceID, - space.account.id + space.levelZeroSpaceID ); } @@ -1018,9 +950,9 @@ export class SpaceService { await this.storageAggregatorService.delete(space.storageAggregator.id); } - async getSubspaceInAccount( + async getSubspaceInLevelZeroSpace( subspaceID: string, - accountID: string, + levelZeroSpaceID: string, options?: FindOneOptions ): Promise { let subspace: ISpace | null = null; @@ -1028,9 +960,7 @@ export class SpaceService { subspace = await this.spaceRepository.findOne({ where: { id: subspaceID, - account: { - id: accountID, - }, + levelZeroSpaceID: levelZeroSpaceID, }, ...options, }); @@ -1040,9 +970,7 @@ export class SpaceService { subspace = await this.spaceRepository.findOne({ where: { nameID: subspaceID, - account: { - id: accountID, - }, + levelZeroSpaceID: levelZeroSpaceID, }, ...options, }); @@ -1051,14 +979,14 @@ export class SpaceService { return subspace; } - async getSubspaceInAccountScopeOrFail( + async getSubspaceInLevelZeroScopeOrFail( subspaceID: string, - accountID: string, + levelZeroSpaceID: string, options?: FindOneOptions ): Promise { - const subspace = await this.getSubspaceInAccount( + const subspace = await this.getSubspaceInLevelZeroSpace( subspaceID, - accountID, + levelZeroSpaceID, options ); @@ -1093,52 +1021,6 @@ export class SpaceService { ); } - public async getSpaceForCommunityOrFail( - communityId: string - ): Promise { - const space = await this.spaceRepository.findOne({ - where: { - community: { - id: communityId, - }, - }, - relations: { - profile: true, - }, - }); - if (!space) { - throw new EntityNotFoundException( - `Unable to find space for community: ${communityId}`, - LogContext.SPACES - ); - } - return space; - } - - public async getSpaceForCollaborationOrFail( - collaborationID: string, - options?: FindOneOptions - ): Promise { - const space = await this.spaceRepository.findOne({ - where: { - collaboration: { - id: collaborationID, - }, - }, - relations: { - ...options?.relations, - profile: true, - }, - }); - if (!space) { - throw new EntityNotFoundException( - `Unable to find space for collaboration: ${collaborationID}`, - LogContext.SPACES - ); - } - return space; - } - public async updateSettings( space: ISpace, settingsData: UpdateSpaceSettingsEntityInput @@ -1204,7 +1086,9 @@ export class SpaceService { return context; } - public async getStorageAggregatorOrFail(spaceID: string): Promise { + public async getStorageAggregatorOrFail( + spaceID: string + ): Promise { const space = await this.getSpaceOrFail(spaceID, { relations: { storageAggregator: true, @@ -1214,7 +1098,7 @@ export class SpaceService { if (!storageAggregator) throw new RelationshipNotFoundException( `Unable to load storage aggregator for space ${spaceID} `, - LogContext.CONTEXT + LogContext.SPACES ); return storageAggregator; } @@ -1297,12 +1181,6 @@ export class SpaceService { async getMetrics(space: ISpace): Promise { const metrics: INVP[] = []; - if (!space.account) { - throw new EntityNotInitializedException( - 'Space account not initialized', - LogContext.SPACES - ); - } // Subspaces const subspacesCount = await this.getSubspacesInSpaceCount(space.id); const subspacesTopic = new NVP('subspaces', subspacesCount.toString()); @@ -1312,9 +1190,8 @@ export class SpaceService { const community = await this.getCommunity(space.id); // Members - const membersCount = await this.communityRoleService.getMembersCount( - community - ); + const membersCount = + await this.communityRoleService.getMembersCount(community); const membersTopic = new NVP('members', membersCount.toString()); membersTopic.id = `members-${space.id}`; metrics.push(membersTopic); diff --git a/src/library/innovation-pack/dto/innovation.pack.dto.create.ts b/src/library/innovation-pack/dto/innovation.pack.dto.create.ts index 2ed139868e..a860a671c7 100644 --- a/src/library/innovation-pack/dto/innovation.pack.dto.create.ts +++ b/src/library/innovation-pack/dto/innovation.pack.dto.create.ts @@ -1,16 +1,9 @@ import { Field, InputType } from '@nestjs/graphql'; -import { UUID_NAMEID } from '@domain/common/scalars/scalar.uuid.nameid'; import { CreateNameableInput } from '@domain/common/entity/nameable-entity/dto/nameable.dto.create'; import { IsOptional } from 'class-validator'; @InputType() export class CreateInnovationPackInput extends CreateNameableInput { - @Field(() => UUID_NAMEID, { - nullable: false, - description: 'The provider Organization for the InnovationPack', - }) - providerID!: string; - @Field(() => [String], { nullable: true }) @IsOptional() tags?: string[]; diff --git a/src/library/innovation-pack/dto/innovation.pack.dto.update.ts b/src/library/innovation-pack/dto/innovation.pack.dto.update.ts index 98ce49e64f..b514f20e04 100644 --- a/src/library/innovation-pack/dto/innovation.pack.dto.update.ts +++ b/src/library/innovation-pack/dto/innovation.pack.dto.update.ts @@ -1,21 +1,22 @@ import { Field, InputType } from '@nestjs/graphql'; import { IsOptional } from 'class-validator'; -import { UUID_NAMEID } from '@domain/common/scalars'; import { UpdateNameableInput } from '@domain/common/entity/nameable-entity/dto/nameable.dto.update'; +import { SearchVisibility } from '@common/enums/search.visibility'; @InputType() export class UpdateInnovationPackInput extends UpdateNameableInput { - @Field(() => UUID_NAMEID, { + @Field(() => Boolean, { nullable: true, - description: 'Update the provider Organization for the InnovationPack.', + description: + 'Flag to control the visibility of the InnovationPack in the platform Library.', }) @IsOptional() - providerOrgID?: string; + listedInStore?: boolean; - // Override the type of entry accepted to accept the nameID also - @Field(() => UUID_NAMEID, { - nullable: false, - description: 'The ID or NameID of the InnovationPack.', + @Field(() => SearchVisibility, { + description: 'Visibility of the InnovationPack in searches.', + nullable: true, }) - ID!: string; + @IsOptional() + searchVisibility?: SearchVisibility; } diff --git a/src/library/innovation-pack/innovation.pack.entity.ts b/src/library/innovation-pack/innovation.pack.entity.ts index c409403720..c3558f7198 100644 --- a/src/library/innovation-pack/innovation.pack.entity.ts +++ b/src/library/innovation-pack/innovation.pack.entity.ts @@ -1,17 +1,18 @@ -import { Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; import { TemplatesSet } from '@domain/template/templates-set/templates.set.entity'; -import { Library } from '../library/library.entity'; import { IInnovationPack } from './innovation.pack.interface'; import { NameableEntity } from '@domain/common/entity/nameable-entity/nameable.entity'; +import { Account } from '@domain/space/account/account.entity'; +import { SearchVisibility } from '@common/enums/search.visibility'; @Entity() export class InnovationPack extends NameableEntity implements IInnovationPack { - @ManyToOne(() => Library, library => library.innovationPacks, { + @ManyToOne(() => Account, account => account.innovationPacks, { eager: false, cascade: false, onDelete: 'CASCADE', }) - library?: Library; + account?: Account; @OneToOne(() => TemplatesSet, { eager: false, @@ -21,5 +22,15 @@ export class InnovationPack extends NameableEntity implements IInnovationPack { @JoinColumn() templatesSet?: TemplatesSet; + @Column() + listedInStore!: boolean; + + @Column('varchar', { + length: 36, + nullable: false, + default: SearchVisibility.ACCOUNT, + }) + searchVisibility!: SearchVisibility; + templatesCount = 0; } diff --git a/src/library/innovation-pack/innovation.pack.interface.ts b/src/library/innovation-pack/innovation.pack.interface.ts index 094e2dd678..4e30b847f7 100644 --- a/src/library/innovation-pack/innovation.pack.interface.ts +++ b/src/library/innovation-pack/innovation.pack.interface.ts @@ -1,11 +1,28 @@ -import { ObjectType } from '@nestjs/graphql'; +import { Field, ObjectType } from '@nestjs/graphql'; import { ITemplatesSet } from '@domain/template/templates-set'; import { INameable } from '@domain/common/entity/nameable-entity/nameable.interface'; +import { SearchVisibility } from '@common/enums/search.visibility'; +import { IAccount } from '@domain/space/account/account.interface'; @ObjectType('InnovationPack') export abstract class IInnovationPack extends INameable { templatesSet?: ITemplatesSet; + @Field(() => SearchVisibility, { + description: 'Visibility of the InnovationPack in searches.', + nullable: false, + }) + searchVisibility!: SearchVisibility; + + @Field(() => Boolean, { + nullable: false, + description: + 'Flag to control if this InnovationPack is listed in the platform store.', + }) + listedInStore!: boolean; + + account?: IAccount; + // Only used internally templatesCount!: number; } diff --git a/src/library/innovation-pack/innovation.pack.module.ts b/src/library/innovation-pack/innovation.pack.module.ts index 4c278e01cc..873179129c 100644 --- a/src/library/innovation-pack/innovation.pack.module.ts +++ b/src/library/innovation-pack/innovation.pack.module.ts @@ -8,18 +8,16 @@ import { InnovationPackResolverFields } from './innovation.pack.resolver.fields' import { InnovationPackResolverMutations } from './innovation.pack.resolver.mutations'; import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; import { AuthorizationModule } from '@core/authorization/authorization.module'; -import { OrganizationModule } from '@domain/community/organization/organization.module'; -import { AgentModule } from '@domain/agent/agent/agent.module'; import { ProfileModule } from '@domain/common/profile/profile.module'; +import { AccountHostModule } from '@domain/space/account.host/account.host.module'; @Module({ imports: [ TemplatesSetModule, AuthorizationModule, AuthorizationPolicyModule, - OrganizationModule, ProfileModule, - AgentModule, + AccountHostModule, TypeOrmModule.forFeature([InnovationPack]), ], providers: [ diff --git a/src/library/innovation-pack/innovation.pack.resolver.fields.ts b/src/library/innovation-pack/innovation.pack.resolver.fields.ts index fb88732954..7d8b2ae9c9 100644 --- a/src/library/innovation-pack/innovation.pack.resolver.fields.ts +++ b/src/library/innovation-pack/innovation.pack.resolver.fields.ts @@ -1,6 +1,5 @@ import { AuthorizationPrivilege } from '@common/enums'; import { GraphqlGuard } from '@core/authorization'; -import { IOrganization } from '@domain/community/organization/'; import { UseGuards } from '@nestjs/common'; import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthorizationAgentPrivilege, Profiling } from '@src/common/decorators'; @@ -12,22 +11,12 @@ import { ProfileLoaderCreator } from '@core/dataloader/creators'; import { Loader } from '@core/dataloader/decorators'; import { ILoader } from '@core/dataloader/loader.interface'; import { InnovationPack } from './innovation.pack.entity'; +import { IContributor } from '@domain/community/contributor/contributor.interface'; @Resolver(() => IInnovationPack) export class InnovationPackResolverFields { constructor(private innovationPackService: InnovationPackService) {} - @ResolveField('provider', () => IOrganization, { - nullable: true, - description: 'The InnovationPack provider.', - }) - @Profiling.api - async provider( - @Parent() innovationPack: IInnovationPack - ): Promise { - return await this.innovationPackService.getProvider(innovationPack.id); - } - @UseGuards(GraphqlGuard) @ResolveField('profile', () => IProfile, { nullable: false, @@ -55,4 +44,15 @@ export class InnovationPackResolverFields { innovationPack.id ); } + + @ResolveField('provider', () => IContributor, { + nullable: false, + description: 'The InnovationPack provider.', + }) + @Profiling.api + async provider( + @Parent() innovationPack: IInnovationPack + ): Promise { + return await this.innovationPackService.getProvider(innovationPack.id); + } } diff --git a/src/library/innovation-pack/innovation.pack.service.authorization.ts b/src/library/innovation-pack/innovation.pack.service.authorization.ts index 4ef3f1f262..46e0a4ef11 100644 --- a/src/library/innovation-pack/innovation.pack.service.authorization.ts +++ b/src/library/innovation-pack/innovation.pack.service.authorization.ts @@ -1,21 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { InnovationPackService } from './innovaton.pack.service'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; import { IInnovationPack } from './innovation.pack.interface'; -import { InnovationPack } from './innovation.pack.entity'; import { TemplatesSetAuthorizationService } from '@domain/template/templates-set/templates.set.service.authorization'; import { IAuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.interface'; import { ProfileAuthorizationService } from '@domain/common/profile/profile.service.authorization'; -import { EntityNotInitializedException } from '@common/exceptions'; import { - AuthorizationCredential, - AuthorizationPrivilege, - LogContext, -} from '@common/enums'; + EntityNotInitializedException, + RelationshipNotFoundException, +} from '@common/exceptions'; +import { LogContext } from '@common/enums'; import { IAuthorizationPolicyRuleCredential } from '@core/authorization/authorization.policy.rule.credential.interface'; -import { CREDENTIAL_RULE_LIBRARY_INNOVATION_PACK_PROVIDER_ADMIN } from '@common/constants/authorization/credential.rule.constants'; @Injectable() export class InnovationPackAuthorizationService { @@ -23,15 +18,29 @@ export class InnovationPackAuthorizationService { private authorizationPolicyService: AuthorizationPolicyService, private templatesSetAuthorizationService: TemplatesSetAuthorizationService, private profileAuthorizationService: ProfileAuthorizationService, - private innovationPackService: InnovationPackService, - @InjectRepository(InnovationPack) - private innovationPackRepository: Repository + private innovationPackService: InnovationPackService ) {} async applyAuthorizationPolicy( - innovationPack: IInnovationPack, + innovationPackInput: IInnovationPack, parentAuthorization: IAuthorizationPolicy | undefined ): Promise { + const innovationPack = + await this.innovationPackService.getInnovationPackOrFail( + innovationPackInput.id, + { + relations: { + profile: true, + templatesSet: true, + }, + } + ); + if (!innovationPack.profile || !innovationPack.templatesSet) { + throw new RelationshipNotFoundException( + `Unable to load entities for innovation pack auth reset: ${innovationPack.id} `, + LogContext.COMMUNITY + ); + } // Ensure always applying from a clean state innovationPack.authorization = this.authorizationPolicyService.reset( innovationPack.authorization @@ -42,42 +51,24 @@ export class InnovationPackAuthorizationService { parentAuthorization ); - innovationPack.authorization = await this.appendCredentialRules( - innovationPack - ); - - // Cascade down - const innovationPackPropagated = - await this.propagateAuthorizationToChildEntities(innovationPack); - - return await this.innovationPackRepository.save(innovationPackPropagated); - } - - private async propagateAuthorizationToChildEntities( - innovationPack: IInnovationPack - ): Promise { - innovationPack.profile = await this.innovationPackService.getProfile( - innovationPack - ); + innovationPack.authorization = this.appendCredentialRules(innovationPack); innovationPack.profile = await this.profileAuthorizationService.applyAuthorizationPolicy( innovationPack.profile, innovationPack.authorization ); - innovationPack.templatesSet = - await this.innovationPackService.getTemplatesSetOrFail(innovationPack.id); innovationPack.templatesSet = await this.templatesSetAuthorizationService.applyAuthorizationPolicy( innovationPack.templatesSet, innovationPack.authorization ); - return innovationPack; + return await this.innovationPackService.save(innovationPack); } - private async appendCredentialRules( + private appendCredentialRules( innovationPack: IInnovationPack - ): Promise { + ): IAuthorizationPolicy { const authorization = innovationPack.authorization; if (!authorization) throw new EntityNotInitializedException( @@ -87,37 +78,40 @@ export class InnovationPackAuthorizationService { const newRules: IAuthorizationPolicyRuleCredential[] = []; - const providerOrg = await this.innovationPackService.getProvider( - innovationPack.id - ); - if (!providerOrg) { - throw new EntityNotInitializedException( - `Providing organization not found for InnovationPack: ${innovationPack.id}`, - LogContext.LIBRARY - ); - } - - const providerOrgAdmins = - this.authorizationPolicyService.createCredentialRule( - [ - AuthorizationPrivilege.CREATE, - AuthorizationPrivilege.READ, - AuthorizationPrivilege.UPDATE, - AuthorizationPrivilege.DELETE, - ], - [ - { - type: AuthorizationCredential.ORGANIZATION_ADMIN, - resourceID: providerOrg.id, - }, - ], - CREDENTIAL_RULE_LIBRARY_INNOVATION_PACK_PROVIDER_ADMIN - ); - newRules.push(providerOrgAdmins); - return this.authorizationPolicyService.appendCredentialAuthorizationRules( authorization, newRules ); } + + // // TODO: what does this look like after the move? Prviously the library explicitly allowed read access to anonymous users + + // private extendStorageAuthorizationPolicy( + // storageAuthorization: IAuthorizationPolicy | undefined + // ): IAuthorizationPolicy { + // if (!storageAuthorization) + // throw new EntityNotInitializedException( + // 'Authorization definition not found', + // LogContext.LIBRARY + // ); + + // const newRules: IAuthorizationPolicyRuleCredential[] = []; + + // // Any member can upload + // const registeredUsersCanUpload = + // this.authorizationPolicyService.createCredentialRuleUsingTypesOnly( + // [AuthorizationPrivilege.FILE_UPLOAD], + // [AuthorizationCredential.GLOBAL_REGISTERED], + // CREDENTIAL_RULE_TYPES_LIBRARY_FILE_UPLOAD_ANY_USER + // ); + // registeredUsersCanUpload.cascade = false; + // newRules.push(registeredUsersCanUpload); + + // this.authorizationPolicyService.appendCredentialAuthorizationRules( + // storageAuthorization, + // newRules + // ); + + // return storageAuthorization; + // } } diff --git a/src/library/innovation-pack/innovaton.pack.service.ts b/src/library/innovation-pack/innovaton.pack.service.ts index 404d3c4ce6..8b9dbee401 100644 --- a/src/library/innovation-pack/innovaton.pack.service.ts +++ b/src/library/innovation-pack/innovaton.pack.service.ts @@ -1,16 +1,10 @@ import { UUID_LENGTH } from '@common/constants'; -import { - AuthorizationCredential, - LogContext, - ProfileType, -} from '@common/enums'; +import { LogContext, ProfileType } from '@common/enums'; import { EntityNotFoundException, RelationshipNotFoundException, ValidationException, } from '@common/exceptions'; - -import { IOrganization } from '@domain/community/organization/organization.interface'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOneOptions, FindOptionsRelations, Repository } from 'typeorm'; @@ -20,8 +14,6 @@ import { IInnovationPack } from './innovation.pack.interface'; import { UpdateInnovationPackInput } from './dto/innovation.pack.dto.update'; import { ITemplatesSet } from '@domain/template/templates-set/templates.set.interface'; import { TemplatesSetService } from '@domain/template/templates-set/templates.set.service'; -import { OrganizationService } from '@domain/community/organization/organization.service'; -import { AgentService } from '@domain/agent/agent/agent.service'; import { CreateInnovationPackInput } from './dto/innovation.pack.dto.create'; import { DeleteInnovationPackInput } from './dto/innovationPack.dto.delete'; import { AuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.entity'; @@ -30,14 +22,16 @@ import { ProfileService } from '@domain/common/profile/profile.service'; import { VisualType } from '@common/enums/visual.type'; import { TagsetReservedName } from '@common/enums/tagset.reserved.name'; import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; +import { SearchVisibility } from '@common/enums/search.visibility'; +import { IContributor } from '@domain/community/contributor/contributor.interface'; +import { AccountHostService } from '@domain/space/account.host/account.host.service'; @Injectable() export class InnovationPackService { constructor( - private organizationService: OrganizationService, - private agentService: AgentService, private profileService: ProfileService, private templatesSetService: TemplatesSetService, + private accountHostService: AccountHostService, @InjectRepository(InnovationPack) private innovationPackRepository: Repository, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService @@ -61,6 +55,9 @@ export class InnovationPackService { VisualType.CARD ); + innovationPack.listedInStore = true; + innovationPack.searchVisibility = SearchVisibility.ACCOUNT; + await this.profileService.addTagsetOnProfile(innovationPack.profile, { name: TagsetReservedName.DEFAULT, tags: innovationPackData.tags ?? [], @@ -69,17 +66,7 @@ export class InnovationPackService { innovationPack.templatesSet = await this.templatesSetService.createTemplatesSet(); - // save before assigning host in case that fails - const savedInnovationPack = await this.innovationPackRepository.save( - innovationPack - ); - - await this.setInnovationPackProvider( - innovationPack.id, - innovationPackData.providerID - ); - - return savedInnovationPack; + return await this.save(innovationPack); } async save(innovationPack: IInnovationPack): Promise { @@ -119,14 +106,15 @@ export class InnovationPackService { ); } - if (innovationPackData.providerOrgID) { - await this.setInnovationPackProvider( - innovationPack.id, - innovationPackData.providerOrgID - ); + if (typeof innovationPackData.listedInStore === 'boolean') { + innovationPack.listedInStore = !!innovationPackData.listedInStore; } - return await this.innovationPackRepository.save(innovationPack); + if (innovationPackData.searchVisibility) { + innovationPack.searchVisibility = innovationPackData.searchVisibility; + } + + return await this.save(innovationPack); } async deleteInnovationPack( @@ -136,18 +124,6 @@ export class InnovationPackService { relations: { templatesSet: true, profile: true }, }); - // Remove any host credentials - const providerOrg = await this.getProvider(innovationPack.id); - if (providerOrg) { - const agentHostOrg = await this.organizationService.getAgent(providerOrg); - providerOrg.agent = await this.agentService.revokeCredential({ - agentID: agentHostOrg.id, - type: AuthorizationCredential.INNOVATION_PACK_PROVIDER, - resourceID: innovationPack.id, - }); - await this.organizationService.save(providerOrg); - } - if (innovationPack.templatesSet) { await this.templatesSetService.deleteTemplatesSet( innovationPack.templatesSet.id @@ -186,7 +162,7 @@ export class InnovationPackService { if (!innovationPack) throw new EntityNotFoundException( `Unable to find InnovationPack with ID: ${innovationPackID}`, - LogContext.SPACES + LogContext.LIBRARY ); return innovationPack; } @@ -230,40 +206,6 @@ export class InnovationPackService { return templatesSet; } - async setInnovationPackProvider( - innovationPackID: string, - hostOrgID: string - ): Promise { - const organization = await this.organizationService.getOrganizationOrFail( - hostOrgID, - { relations: { agent: true } } - ); - - const existingHost = await this.getProvider(innovationPackID); - - if (existingHost) { - const agentExisting = await this.organizationService.getAgent( - existingHost - ); - organization.agent = await this.agentService.revokeCredential({ - agentID: agentExisting.id, - type: AuthorizationCredential.INNOVATION_PACK_PROVIDER, - resourceID: innovationPackID, - }); - } - - // assign the credential - const agent = await this.organizationService.getAgent(organization); - organization.agent = await this.agentService.grantCredential({ - agentID: agent.id, - type: AuthorizationCredential.INNOVATION_PACK_PROVIDER, - resourceID: innovationPackID, - }); - - await this.organizationService.save(organization); - return await this.getInnovationPackOrFail(innovationPackID); - } - async isNameIdAvailable(nameID: string): Promise { const innovationPackCount = await this.innovationPackRepository.countBy({ nameID: nameID, @@ -271,26 +213,6 @@ export class InnovationPackService { return innovationPackCount == 0; } - async getProvider( - innovationPackID: string - ): Promise { - const organizations = - await this.organizationService.organizationsWithCredentials({ - type: AuthorizationCredential.INNOVATION_PACK_PROVIDER, - resourceID: innovationPackID, - }); - if (organizations.length == 0) { - return undefined; - } - if (organizations.length > 1) { - throw new RelationshipNotFoundException( - `More than one provider for InnovationPack ${innovationPackID} `, - LogContext.SPACES - ); - } - return organizations[0]; - } - async getTemplatesCount(innovationPackID: string): Promise { const innovationPack = await this.getInnovationPackOrFail( innovationPackID, @@ -309,4 +231,29 @@ export class InnovationPackService { } return await this.templatesSetService.getTemplatesCount(templatesSetId); } + + public async getProvider(innovationPackID: string): Promise { + const innovationPack = await this.innovationPackRepository.findOne({ + where: { id: innovationPackID }, + relations: { + account: true, + }, + }); + if (!innovationPack || !innovationPack.account) { + throw new RelationshipNotFoundException( + `Unable to load innovation pack with account to get Provider for InnovationPack ${innovationPackID} `, + LogContext.LIBRARY + ); + } + const provider = await this.accountHostService.getHost( + innovationPack.account + ); + if (!provider) { + throw new RelationshipNotFoundException( + `Unable to load provider for InnovationPack ${innovationPackID} `, + LogContext.LIBRARY + ); + } + return provider; + } } diff --git a/src/library/library/library.entity.ts b/src/library/library/library.entity.ts index fff1006ff6..38ab8b0218 100644 --- a/src/library/library/library.entity.ts +++ b/src/library/library/library.entity.ts @@ -1,22 +1,5 @@ import { AuthorizableEntity } from '@domain/common/entity/authorizable-entity'; -import { InnovationPack } from '@library/innovation-pack/innovation.pack.entity'; -import { Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm'; +import { Entity } from 'typeorm'; import { ILibrary } from './library.interface'; -import { StorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.entity'; - @Entity() -export class Library extends AuthorizableEntity implements ILibrary { - @OneToMany(() => InnovationPack, innovationPack => innovationPack.library, { - eager: true, - cascade: true, - }) - innovationPacks?: InnovationPack[]; - - @OneToOne(() => StorageAggregator, { - eager: false, - cascade: true, - onDelete: 'SET NULL', - }) - @JoinColumn() - storageAggregator!: StorageAggregator; -} +export class Library extends AuthorizableEntity implements ILibrary {} diff --git a/src/library/library/library.interface.ts b/src/library/library/library.interface.ts index eb0c437a59..a7ef673cae 100644 --- a/src/library/library/library.interface.ts +++ b/src/library/library/library.interface.ts @@ -1,11 +1,5 @@ import { IAuthorizable } from '@domain/common/entity/authorizable-entity'; -import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; -import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; import { ObjectType } from '@nestjs/graphql'; @ObjectType('Library') -export abstract class ILibrary extends IAuthorizable { - innovationPacks?: IInnovationPack[]; - - storageAggregator!: IStorageAggregator; -} +export abstract class ILibrary extends IAuthorizable {} diff --git a/src/library/library/library.module.ts b/src/library/library/library.module.ts index 5e028807a4..5ded71c52a 100644 --- a/src/library/library/library.module.ts +++ b/src/library/library/library.module.ts @@ -1,27 +1,21 @@ import { AuthorizationModule } from '@core/authorization/authorization.module'; import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; -import { InnovationPackModule } from '@library/innovation-pack/innovation.pack.module'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { NamingModule } from '@services/infrastructure/naming/naming.module'; import { Library } from './library.entity'; import { LibraryResolverFields } from './library.resolver.fields'; -import { LibraryResolverMutations } from './library.resolver.mutations'; import { LibraryService } from './library.service'; import { LibraryAuthorizationService } from './library.service.authorization'; -import { StorageAggregatorModule } from '@domain/storage/storage-aggregator/storage.aggregator.module'; +import { InnovationPackModule } from '@library/innovation-pack/innovation.pack.module'; @Module({ imports: [ - InnovationPackModule, - NamingModule, AuthorizationModule, - StorageAggregatorModule, AuthorizationPolicyModule, + InnovationPackModule, TypeOrmModule.forFeature([Library]), ], providers: [ - LibraryResolverMutations, LibraryResolverFields, LibraryService, LibraryAuthorizationService, diff --git a/src/library/library/library.resolver.fields.ts b/src/library/library/library.resolver.fields.ts index b0998c1172..5ee7edef0a 100644 --- a/src/library/library/library.resolver.fields.ts +++ b/src/library/library/library.resolver.fields.ts @@ -1,54 +1,23 @@ -import { AuthorizationPrivilege } from '@common/enums'; +import { AuthorizationPrivilege, LogContext } from '@common/enums'; import { GraphqlGuard } from '@core/authorization'; -import { UseGuards } from '@nestjs/common'; -import { Args, Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { Inject, LoggerService, UseGuards } from '@nestjs/common'; +import { Args, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthorizationAgentPrivilege } from '@src/common/decorators'; import { ILibrary } from './library.interface'; -import { UUID_NAMEID } from '@domain/common/scalars'; -import { InnovationPackService } from '@library/innovation-pack/innovaton.pack.service'; import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; import { LibraryService } from './library.service'; -import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; import { InnovationPacksInput } from './dto/library.dto.innovationPacks.input'; import { IVirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.interface'; +import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; @Resolver(() => ILibrary) export class LibraryResolverFields { constructor( - private innovationPackService: InnovationPackService, - private libraryService: LibraryService + private libraryService: LibraryService, + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} - @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) - @ResolveField('innovationPack', () => IInnovationPack, { - nullable: true, - description: 'A single Innovation Pack', - }) - @UseGuards(GraphqlGuard) - async innovationPack( - @Args({ - name: 'ID', - nullable: false, - type: () => UUID_NAMEID, - description: 'The ID or NAMEID of the Innovation Pack', - }) - ID: string - ): Promise { - return await this.innovationPackService.getInnovationPackOrFail(ID); - } - - @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) - @ResolveField('storageAggregator', () => IStorageAggregator, { - nullable: true, - description: 'The StorageAggregator for storage used by this Library', - }) - @UseGuards(GraphqlGuard) - async storageAggregator( - @Parent() library: ILibrary - ): Promise { - return await this.libraryService.getStorageAggregator(library); - } - @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) @ResolveField('innovationPacks', () => [IInnovationPack], { nullable: false, @@ -56,12 +25,14 @@ export class LibraryResolverFields { }) @UseGuards(GraphqlGuard) async innovationPacks( - @Parent() library: ILibrary, @Args('queryData', { type: () => InnovationPacksInput, nullable: true }) queryData?: InnovationPacksInput ): Promise { - return await this.libraryService.getInnovationPacks( - library, + this.logger.verbose?.( + `Ignoring query data ${JSON.stringify(queryData)} for now; to be added back in later`, + LogContext.LIBRARY + ); + return await this.libraryService.getListedInnovationPacks( queryData?.limit, queryData?.orderBy ); @@ -76,4 +47,13 @@ export class LibraryResolverFields { async virtualContributors(): Promise { return await this.libraryService.getListedVirtualContributors(); } + + @UseGuards(GraphqlGuard) + @ResolveField(() => [IInnovationHub], { + nullable: false, + description: 'The InnovationHub listed on this platform', + }) + async innovationHubs(): Promise { + return await this.libraryService.getListedInnovationHubs(); + } } diff --git a/src/library/library/library.resolver.mutations.ts b/src/library/library/library.resolver.mutations.ts deleted file mode 100644 index 643ebf2726..0000000000 --- a/src/library/library/library.resolver.mutations.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { UseGuards } from '@nestjs/common'; -import { Resolver, Mutation, Args } from '@nestjs/graphql'; -import { CurrentUser, Profiling } from '@src/common/decorators'; -import { GraphqlGuard } from '@core/authorization'; -import { AgentInfo } from '@core/authentication.agent.info/agent.info'; -import { AuthorizationService } from '@core/authorization/authorization.service'; -import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; -import { LibraryService } from './library.service'; -import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; -import { CreateInnovationPackOnLibraryInput } from './dto/library.dto.create.innovation.pack'; -import { InnovationPackAuthorizationService } from '@library/innovation-pack/innovation.pack.service.authorization'; - -@Resolver() -export class LibraryResolverMutations { - constructor( - private authorizationService: AuthorizationService, - private libraryService: LibraryService, - private innovationPackAuthorizationService: InnovationPackAuthorizationService - ) {} - - @UseGuards(GraphqlGuard) - @Mutation(() => IInnovationPack, { - description: 'Create a new InnovatonPack on the Library.', - }) - @Profiling.api - async createInnovationPackOnLibrary( - @CurrentUser() agentInfo: AgentInfo, - @Args('packData') packData: CreateInnovationPackOnLibraryInput - ): Promise { - const library = await this.libraryService.getLibraryOrFail(); - - this.authorizationService.grantAccessOrFail( - agentInfo, - library.authorization, - AuthorizationPrivilege.CREATE, - `create innovationPack on library: ${library.id}` - ); - - const innovationPack = await this.libraryService.createInnovationPack( - packData - ); - const innovationPackAuthorized = - await this.innovationPackAuthorizationService.applyAuthorizationPolicy( - innovationPack, - library.authorization - ); - return innovationPackAuthorized; - } -} diff --git a/src/library/library/library.service.authorization.ts b/src/library/library/library.service.authorization.ts index f5510c1272..f557a7ccd2 100644 --- a/src/library/library/library.service.authorization.ts +++ b/src/library/library/library.service.authorization.ts @@ -1,31 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; -import { Library } from './library.entity'; -import { LibraryService } from './library.service'; import { ILibrary } from './library.interface'; -import { InnovationPackAuthorizationService } from '@library/innovation-pack/innovation.pack.service.authorization'; import { IAuthorizationPolicy } from '@domain/common/authorization-policy'; -import { EntityNotInitializedException } from '@common/exceptions'; -import { - AuthorizationCredential, - AuthorizationPrivilege, - LogContext, -} from '@common/enums'; -import { IAuthorizationPolicyRuleCredential } from '@core/authorization/authorization.policy.rule.credential.interface'; -import { CREDENTIAL_RULE_TYPES_LIBRARY_FILE_UPLOAD_ANY_USER } from '@common/constants'; -import { StorageAggregatorAuthorizationService } from '@domain/storage/storage-aggregator/storage.aggregator.service.authorization'; +import { LibraryService } from './library.service'; @Injectable() export class LibraryAuthorizationService { constructor( private authorizationPolicyService: AuthorizationPolicyService, - private innovationPackAuthorizationService: InnovationPackAuthorizationService, - private storageAggregatorAuthorizationService: StorageAggregatorAuthorizationService, - private libraryService: LibraryService, - @InjectRepository(Library) - private libraryRepository: Repository + private libraryService: LibraryService ) {} async applyAuthorizationPolicy( @@ -44,71 +27,6 @@ export class LibraryAuthorizationService { // For now the library is world visible library.authorization.anonymousReadAccess = true; - // Cascade down - const libraryPropagated = await this.propagateAuthorizationToChildEntities( - library - ); - - return await this.libraryRepository.save(libraryPropagated); - } - - private async propagateAuthorizationToChildEntities( - library: ILibrary - ): Promise { - library.innovationPacks = await this.libraryService.getInnovationPacks( - library - ); - for (const innovationPack of library.innovationPacks) { - await this.innovationPackAuthorizationService.applyAuthorizationPolicy( - innovationPack, - library.authorization - ); - } - - library.storageAggregator = await this.libraryService.getStorageAggregator( - library - ); - library.storageAggregator = - await this.storageAggregatorAuthorizationService.applyAuthorizationPolicy( - library.storageAggregator, - library.authorization - ); - library.storageAggregator.authorization = - this.extendStorageAuthorizationPolicy( - library.storageAggregator.authorization, - library - ); - - return library; - } - - private extendStorageAuthorizationPolicy( - storageAuthorization: IAuthorizationPolicy | undefined, - library: ILibrary - ): IAuthorizationPolicy { - if (!storageAuthorization) - throw new EntityNotInitializedException( - `Authorization definition not found for: ${library.id}`, - LogContext.LIBRARY - ); - - const newRules: IAuthorizationPolicyRuleCredential[] = []; - - // Any member can upload - const registeredUsersCanUpload = - this.authorizationPolicyService.createCredentialRuleUsingTypesOnly( - [AuthorizationPrivilege.FILE_UPLOAD], - [AuthorizationCredential.GLOBAL_REGISTERED], - CREDENTIAL_RULE_TYPES_LIBRARY_FILE_UPLOAD_ANY_USER - ); - registeredUsersCanUpload.cascade = false; - newRules.push(registeredUsersCanUpload); - - this.authorizationPolicyService.appendCredentialAuthorizationRules( - storageAuthorization, - newRules - ); - - return storageAuthorization; + return await this.libraryService.save(library); } } diff --git a/src/library/library/library.service.ts b/src/library/library/library.service.ts index ea0483d45f..e1e0172aad 100644 --- a/src/library/library/library.service.ts +++ b/src/library/library/library.service.ts @@ -1,30 +1,27 @@ import { LogContext } from '@common/enums/logging.context'; import { EntityNotFoundException } from '@common/exceptions/entity.not.found.exception'; -import { EntityNotInitializedException } from '@common/exceptions/entity.not.initialized.exception'; -import { ValidationException } from '@common/exceptions/validation.exception'; import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; import { InnovationPackService } from '@library/innovation-pack/innovaton.pack.service'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; -import { NamingService } from '@services/infrastructure/naming/naming.service'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { EntityManager, FindOneOptions, Repository } from 'typeorm'; -import { CreateInnovationPackOnLibraryInput } from './dto/library.dto.create.innovation.pack'; import { Library } from './library.entity'; import { ILibrary } from './library.interface'; -import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; import { InnovationPacksOrderBy } from '@common/enums/innovation.packs.orderBy'; import { IVirtualContributor, VirtualContributor, } from '@domain/community/virtual-contributor'; import { SearchVisibility } from '@common/enums/search.visibility'; +import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; +import { InnovationHub } from '@domain/innovation-hub/innovation.hub.entity'; +import { InnovationPack } from '@library/innovation-pack/innovation.pack.entity'; @Injectable() export class LibraryService { constructor( private innovationPackService: InnovationPackService, - private namingService: NamingService, @InjectEntityManager('default') private entityManager: EntityManager, @InjectRepository(Library) @@ -43,6 +40,9 @@ export class LibraryService { ); return library; } + public async save(library: ILibrary): Promise { + return await this.libraryRepository.save(library); + } public async getListedVirtualContributors(): Promise { const virtualContributors = await this.entityManager.find( @@ -60,17 +60,29 @@ export class LibraryService { return virtualContributors; } - public async getInnovationPacks( - library: ILibrary, + public async getListedInnovationHubs(): Promise { + const innovationHubs = await this.entityManager.find(InnovationHub, { + where: { + listedInStore: true, + searchVisibility: SearchVisibility.PUBLIC, + }, + }); + return innovationHubs; + } + + public async getListedInnovationPacks( limit?: number, orderBy: InnovationPacksOrderBy = InnovationPacksOrderBy.NUMBER_OF_TEMPLATES_DESC ): Promise { - const innovationPacks = library.innovationPacks; - if (!innovationPacks) - throw new EntityNotFoundException( - `Undefined innovation packs found: ${library.id}`, - LogContext.LIBRARY - ); + const innovationPacks = await this.entityManager.find(InnovationPack, { + where: { + listedInStore: true, + searchVisibility: SearchVisibility.PUBLIC, + }, + relations: { + templatesSet: true, + }, + }); // Sort based on the amount of Templates in the InnovationPacks const innovationPacksWithCounts = await Promise.all( @@ -94,66 +106,4 @@ export class LibraryService { }); return limit && limit > 0 ? sortedPacks.slice(0, limit) : sortedPacks; } - - public async createInnovationPack( - innovationPackData: CreateInnovationPackOnLibraryInput - ): Promise { - const library = await this.getLibraryOrFail({ - relations: { - storageAggregator: true, - }, - }); - if (!library.innovationPacks || !library.storageAggregator) - throw new EntityNotInitializedException( - `Library (${library}) not initialised`, - LogContext.LIBRARY - ); - - const reservedNameIDs = - await this.namingService.getReservedNameIDsInLibrary(library.id); - if (innovationPackData.nameID && innovationPackData.nameID.length > 0) { - const nameTaken = reservedNameIDs.includes(innovationPackData.nameID); - if (nameTaken) - throw new ValidationException( - `Unable to create InnovationPack: the provided nameID is already taken: ${innovationPackData.nameID}`, - LogContext.LIBRARY - ); - } else { - innovationPackData.nameID = - this.namingService.createNameIdAvoidingReservedNameIDs( - `${innovationPackData.profileData.displayName}`, - reservedNameIDs - ); - } - - const innovationPack = - await this.innovationPackService.createInnovationPack( - innovationPackData, - library.storageAggregator - ); - library.innovationPacks.push(innovationPack); - await this.libraryRepository.save(library); - - return innovationPack; - } - - async getStorageAggregator( - libraryInput: ILibrary - ): Promise { - const library = await this.getLibraryOrFail({ - relations: { - storageAggregator: true, - }, - }); - const storageAggregator = library.storageAggregator; - - if (!storageAggregator) { - throw new EntityNotFoundException( - `Unable to find storage aggregator for Library: ${libraryInput.id}`, - LogContext.STORAGE_BUCKET - ); - } - - return storageAggregator; - } } diff --git a/src/migrations/1721649196399-innovationPacksAccount.ts b/src/migrations/1721649196399-innovationPacksAccount.ts new file mode 100644 index 0000000000..f81f193dd4 --- /dev/null +++ b/src/migrations/1721649196399-innovationPacksAccount.ts @@ -0,0 +1,208 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { safelyDropFK } from './utils/safely-drop-foreignKey'; + +export class InnovationPacksAccount1721649196399 implements MigrationInterface { + name = 'InnovationPacksAccount1721649196399'; + + public async up(queryRunner: QueryRunner): Promise { + await safelyDropFK( + queryRunner, + 'innovation_pack', + 'FK_77777450cf75dc486700ca034c6' + ); + await safelyDropFK( + queryRunner, + 'library', + 'FK_6664d59c0b805c9c1ecb0070e16' + ); + + await queryRunner.query( + `ALTER TABLE \`innovation_pack\` DROP COLUMN \`libraryId\`` + ); + await queryRunner.query( + `ALTER TABLE \`innovation_pack\` ADD \`listedInStore\` tinyint NOT NULL` + ); + await queryRunner.query( + `ALTER TABLE \`innovation_pack\` ADD \`searchVisibility\` varchar(36) NOT NULL DEFAULT 'account'` + ); + await queryRunner.query( + `ALTER TABLE \`innovation_pack\` ADD \`accountId\` char(36) NULL` + ); + + const innovationPacks = await queryRunner.query( + `SELECT id FROM \`innovation_pack\`` + ); + if (innovationPacks.length > 0) { + const defaultHostAccount = await this.getDefaultHostAccount(queryRunner); + console.log(`Default host account: ${defaultHostAccount}`); + + const libraryStorageAggregatorID = + await this.getLibraryStorageAggregatorID(queryRunner); + + for (const innovationPack of innovationPacks) { + let accountID = defaultHostAccount; + + const providerCredential = await this.getProviderCredential( + queryRunner, + innovationPack.id + ); + if (providerCredential) { + const organization = await this.getOrganization( + queryRunner, + providerCredential.agentId + ); + if (organization) { + const accountHostCredential = await this.getAccountHostCredential( + queryRunner, + organization.agentId + ); + if (accountHostCredential) { + accountID = accountHostCredential.resourceID; + } + } + } + + if (!accountID) { + throw new Error( + `Account ID not found for innovation pack: ${innovationPack.id}` + ); + } + + const account = await this.getAccount(queryRunner, accountID); + if (!account) { + console.log( + `Account ${accountID} does not have a storage aggregator` + ); + continue; + } + const listedInStore = true; + const searchVisibility = 'public'; + await queryRunner.query( + `UPDATE innovation_pack SET accountId = '${accountID}', listedInStore = ${listedInStore}, searchVisibility='${searchVisibility}' WHERE id = '${innovationPack.id}'` + ); + + await this.updateStorageBuckets( + queryRunner, + libraryStorageAggregatorID, + account.storageAggregatorId + ); + } + } + + await queryRunner.query( + `ALTER TABLE \`library\` DROP COLUMN \`storageAggregatorId\`` + ); + await queryRunner.query( + `DELETE FROM credential WHERE type = 'innovation-pack-provider'` + ); + + await queryRunner.query( + `ALTER TABLE \`innovation_pack\` ADD CONSTRAINT \`FK_77777450cf75dc486700ca034c6\` FOREIGN KEY (\`accountId\`) REFERENCES \`account\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise {} + + private async getDefaultHostAccount( + queryRunner: QueryRunner + ): Promise { + const orgNamePattern = '%Alkemio%'; + let result = await queryRunner.query( + `SELECT account.id as id FROM credential + JOIN account ON credential.resourceID = account.id + JOIN organization ON credential.agentId = organization.agentId + JOIN profile ON organization.profileId = profile.id + WHERE credential.type = 'account-host' AND profile.displayName LIKE ?`, + [orgNamePattern] + ); + + if (!result || result.length === 0) { + result = await queryRunner.query( + `SELECT account.id as id FROM credential + JOIN account ON credential.resourceID = account.id + JOIN organization ON credential.agentId = organization.agentId + WHERE credential.type = 'account-host' LIMIT 1` + ); + + if (!result || result.length === 0) { + throw new Error(`No account-host credentials found.`); + } + } + + return result[0].id; + } + + private async getLibraryStorageAggregatorID( + queryRunner: QueryRunner + ): Promise { + const library = await queryRunner.query( + `SELECT id, storageAggregatorId FROM library` + ); + if (!library || library.length !== 1) { + throw new Error(`Unable to retrieve storage aggregator on library`); + } + return library[0].storageAggregatorId; + } + + private async getProviderCredential( + queryRunner: QueryRunner, + resourceId: string + ): Promise { + const result = await queryRunner.query( + `SELECT id, agentId FROM credential WHERE resourceID = ? and type = 'innovation-pack-provider'`, + [resourceId] + ); + return result[0]; + } + + private async getOrganization( + queryRunner: QueryRunner, + agentId: string + ): Promise { + const result = await queryRunner.query( + `SELECT id, agentId FROM organization WHERE agentId = ?`, + [agentId] + ); + return result[0]; + } + + private async getAccountHostCredential( + queryRunner: QueryRunner, + agentId: string + ): Promise { + const result = await queryRunner.query( + `SELECT id, agentId, resourceID FROM credential WHERE agentId = ? and type = 'account-host'`, + [agentId] + ); + return result[0]; + } + + private async getAccount( + queryRunner: QueryRunner, + accountId: string + ): Promise { + const result = await queryRunner.query( + `SELECT id, storageAggregatorId FROM account WHERE id = ?`, + [accountId] + ); + return result[0]; + } + + private async updateStorageBuckets( + queryRunner: QueryRunner, + oldStorageAggregatorId: string, + newStorageAggregatorId: string + ): Promise { + const storageBuckets = await queryRunner.query( + `SELECT id FROM \`storage_bucket\` WHERE storageAggregatorId = ?`, + [oldStorageAggregatorId] + ); + + for (const storageBucket of storageBuckets) { + await queryRunner.query( + `UPDATE storage_bucket SET storageAggregatorId = ? WHERE id = ?`, + [newStorageAggregatorId, storageBucket.id] + ); + } + } +} diff --git a/src/migrations/1721737522043-innovationHubAccount.ts b/src/migrations/1721737522043-innovationHubAccount.ts new file mode 100644 index 0000000000..4060dc77a4 --- /dev/null +++ b/src/migrations/1721737522043-innovationHubAccount.ts @@ -0,0 +1,104 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InnovationHubAccount1721737522043 implements MigrationInterface { + name = 'InnovationHubAccount1721737522043'; + + public async up(queryRunner: QueryRunner): Promise { + // Temporarily remove the constraint + await queryRunner.query( + 'ALTER TABLE `innovation_hub` DROP CONSTRAINT `FK_156fd30246eb151b9d17716abf5`' + ); + + // Remove uniqueness of accountId index: Just copied from accountInnovationHub1715936821326 + await queryRunner.query( + 'DROP INDEX `REL_156fd30246eb151b9d17716abf` ON `innovation_hub`' + ); + await queryRunner.query( + 'ALTER TABLE `innovation_hub` DROP INDEX `IDX_156fd30246eb151b9d17716abf`' + ); + await queryRunner.query( + 'ALTER TABLE `innovation_hub` ADD INDEX `IDX_156fd30246eb151b9d17716abf` (`accountId`)' + ); + await queryRunner.query( + 'CREATE INDEX `REL_156fd30246eb151b9d17716abf` ON `innovation_hub` (`accountId`)' + ); + + // Add the two new columns + await queryRunner.query( + `ALTER TABLE \`innovation_hub\` ADD \`listedInStore\` tinyint NOT NULL DEFAULT(1)` + ); + await queryRunner.query( + `ALTER TABLE \`innovation_hub\` ADD \`searchVisibility\` varchar(36) NOT NULL DEFAULT 'account'` + ); + + // All existing innovation hubs to default to be public + listed + await queryRunner.query( + `UPDATE innovation_hub SET listedInStore = '1', searchVisibility = 'public';` + ); + + // Find the account that will host all innovation hubs that don't have an account already + const innovationHubs = await queryRunner.query( + `SELECT id FROM \`innovation_hub\`` + ); + if (innovationHubs.length > 0) { + const defaultHostAccount = await this.getDefaultHostAccount(queryRunner); + console.log(`Default host account: ${defaultHostAccount}`); + await queryRunner.query( + `UPDATE innovation_hub SET accountId = '${defaultHostAccount}' WHERE accountId IS NULL;` + ); + } + + // Make accountId not nullable + await queryRunner.query( + `ALTER TABLE \`innovation_hub\` MODIFY \`accountId\` varchar(36) NOT NULL;` + ); + + // Add the constraint back + await queryRunner.query( + 'ALTER TABLE `innovation_hub` ADD CONSTRAINT `FK_156fd30246eb151b9d17716abf5` FOREIGN KEY (`accountId`) REFERENCES `account`(`id`) ON DELETE CASCADE ON UPDATE CASCADE' + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`innovation_hub\` DROP COLUMN \`listedInStore\`` + ); + + await queryRunner.query( + `ALTER TABLE \`innovation_hub\` DROP COLUMN \`searchVisibility\`` + ); + + await queryRunner.query( + `ALTER TABLE \`innovation_hub\` MODIFY \`accountId\` varchar(36) NOT NULL;` + ); + } + + private async getDefaultHostAccount( + queryRunner: QueryRunner + ): Promise { + const orgNamePattern = '%Alkemio%'; + let result = await queryRunner.query( + `SELECT account.id as id FROM credential + JOIN account ON credential.resourceID = account.id + JOIN organization ON credential.agentId = organization.agentId + JOIN profile ON organization.profileId = profile.id + WHERE credential.type = 'account-host' AND profile.displayName LIKE ?`, + [orgNamePattern] + ); + + if (!result || result.length === 0) { + result = await queryRunner.query( + `SELECT account.id as id FROM credential + JOIN account ON credential.resourceID = account.id + JOIN organization ON credential.agentId = organization.agentId + WHERE credential.type = 'account-host' LIMIT 1` + ); + + if (!result || result.length === 0) { + throw new Error(`No account-host credentials found.`); + } + } + + return result[0].id; + } +} diff --git a/src/migrations/1721803817721-levelZeroSpaceID.ts b/src/migrations/1721803817721-levelZeroSpaceID.ts new file mode 100644 index 0000000000..5478bb3585 --- /dev/null +++ b/src/migrations/1721803817721-levelZeroSpaceID.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class LevelZeroSpaceID1721803817721 implements MigrationInterface { + name = 'LevelZeroSpaceID1721803817721'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE `space` ADD `levelZeroSpaceID` char(36) NULL' + ); + const spaces: { + id: string; + accountId: string; + level: number; + }[] = await queryRunner.query(`SELECT id, accountId, level FROM \`space\``); + for (const space of spaces) { + const [account]: { + id: string; + spaceId: string; + }[] = await queryRunner.query( + `SELECT id, spaceId FROM account WHERE id = '${space.accountId}'` + ); + if (account) { + await queryRunner.query( + `UPDATE space SET levelZeroSpaceID = '${account.spaceId}' WHERE id = '${space.id}'` + ); + } else { + console.log(`No root space found for account ${space.id}`); + } + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/src/migrations/1721830892863-spaceVisibility.ts b/src/migrations/1721830892863-spaceVisibility.ts new file mode 100644 index 0000000000..1b5d28d799 --- /dev/null +++ b/src/migrations/1721830892863-spaceVisibility.ts @@ -0,0 +1,72 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SpaceVisibility1721830892863 implements MigrationInterface { + name = 'SpaceVisibility1721830892863'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE `space` ADD `visibility` varchar(36) NULL' + ); + + // Drop the foreign key constraint on License + await queryRunner.query( + `ALTER TABLE \`license\` DROP FOREIGN KEY \`FK_bfd01743815f0dd68ac1c5c45c0\`` + ); + await queryRunner.query( + `ALTER TABLE \`account\` DROP FOREIGN KEY \`FK_8339e62882f239dc00ff5866f8c\`` + ); + await queryRunner.query( + `ALTER TABLE \`feature_flag\` DROP FOREIGN KEY \`FK_7e3e0a8b6d3e9b4a3a0d6e3a3e3\`` + ); + + const spaces: { + id: string; + accountId: string; + level: number; + }[] = await queryRunner.query(`SELECT id, accountId, level FROM \`space\``); + for (const space of spaces) { + const [account]: { + id: string; + licenseId: string; + }[] = await queryRunner.query( + `SELECT id, licenseId FROM account WHERE id = '${space.accountId}'` + ); + if (account) { + const [license]: { + id: string; + visibility: string; + }[] = await queryRunner.query( + `SELECT id, visibility FROM license WHERE id = '${account.licenseId}'` + ); + if (license) { + await queryRunner.query( + `UPDATE \`space\` SET visibility = '${license.visibility}' WHERE id = '${space.id}'` + ); + } else { + console.log(`No license found for account ${account.id}`); + } + } else { + console.log(`No root space found for account ${space.id}`); + } + } + + const licenses: { + id: string; + authorizationId: string; + }[] = await queryRunner.query( + `SELECT id, authorizationId FROM \`license\`` + ); + for (const license of licenses) { + // delete the authorization associated with the license + await queryRunner.query( + `DELETE FROM authorization_policy WHERE id = '${license.authorizationId}'` + ); + } + // Drop License table + column on Account + await queryRunner.query('ALTER TABLE `account` DROP COLUMN `licenseId`'); + await queryRunner.query('DROP TABLE `license`'); + await queryRunner.query('DROP TABLE `feature_flag`'); + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/src/migrations/1722591262858-innovationHubs-storageAggregators.ts b/src/migrations/1722591262858-innovationHubs-storageAggregators.ts new file mode 100644 index 0000000000..dab8d50e50 --- /dev/null +++ b/src/migrations/1722591262858-innovationHubs-storageAggregators.ts @@ -0,0 +1,42 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InnovationHubsStorageAggregators1722591262858 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + const innovationHubs: { + innovationHubId: string; + storageBucketId: string; + currentStorageAggregatorId: string; + accountStorageAggregator: string; + }[] = await queryRunner.query( + `SELECT + innovation_hub.id AS innovationHubId, + profile.storageBucketId AS storageBucketId, + storage_bucket.storageAggregatorId AS currentStorageAggregatorId, + account.storageAggregatorId AS accountStorageAggregator + FROM innovation_hub + JOIN profile ON innovation_hub.profileId = profile.Id + JOIN storage_bucket ON profile.storageBucketId = storage_bucket.id + JOIN account ON innovation_hub.accountId = account.id;` + ); + if (innovationHubs.length > 0) { + for (const innovationHub of innovationHubs) { + if ( + innovationHub.currentStorageAggregatorId !== + innovationHub.accountStorageAggregator + ) { + await queryRunner.query( + `UPDATE storage_bucket SET storageAggregatorId = ? WHERE id = ?`, + [ + innovationHub.accountStorageAggregator, + innovationHub.storageBucketId, + ] + ); + } + } + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/src/migrations/utils/safely-drop-foreignKey.ts b/src/migrations/utils/safely-drop-foreignKey.ts new file mode 100644 index 0000000000..f71da2109b --- /dev/null +++ b/src/migrations/utils/safely-drop-foreignKey.ts @@ -0,0 +1,24 @@ +import { QueryRunner } from 'typeorm'; + +export const safelyDropFK = async ( + queryRunner: QueryRunner, + tableName: string, + foreignKey: string +) => { + const result = await queryRunner.query( + ` + SELECT CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + AND CONSTRAINT_NAME = ? + `, + [tableName, foreignKey] + ); + + if (result && result.length && result.length > 0) { + await queryRunner.query( + `ALTER TABLE \`${tableName}\` DROP FOREIGN KEY \`${foreignKey}\`` + ); + } +}; diff --git a/src/platform/platfrom/platform.resolver.fields.ts b/src/platform/platfrom/platform.resolver.fields.ts index 79ffc4a259..bce83193b2 100644 --- a/src/platform/platfrom/platform.resolver.fields.ts +++ b/src/platform/platfrom/platform.resolver.fields.ts @@ -45,7 +45,7 @@ export class PlatformResolverFields { }) library(): Promise { return this.platformService.getLibraryOrFail({ - library: { innovationPacks: true }, + library: {}, }); } @@ -76,17 +76,8 @@ export class PlatformResolverFields { return this.platformService.getLicensing(platform); } - @ResolveField(() => [IInnovationHub], { - nullable: false, - description: 'List of Innovation Hubs on the platform', - }) - public innovationHubs(): Promise { - return this.innovationHubService.getInnovationHubs(); - } - @ResolveField(() => IInnovationHub, { - description: - 'Details about an Innovation Hubs on the platform. If the arguments are omitted, the current Innovation Hub you are in will be returned.', + description: 'Details about the current Innovation Hub you are in.', nullable: true, }) public innovationHub( @@ -102,7 +93,7 @@ export class PlatformResolverFields { return Promise.resolve(innovationHub as IInnovationHub); } - return this.innovationHubService.getInnovationHubOrFail({ + return this.innovationHubService.getInnovationHubFlexOrFail({ subdomain: args.subdomain, idOrNameId: args.id, }); diff --git a/src/platform/platfrom/platform.service.authorization.ts b/src/platform/platfrom/platform.service.authorization.ts index 4c89ab75a6..91490f8ad0 100644 --- a/src/platform/platfrom/platform.service.authorization.ts +++ b/src/platform/platfrom/platform.service.authorization.ts @@ -15,8 +15,6 @@ import { LogContext, } from '@common/enums'; import { IAuthorizationPolicyRuleCredential } from '@core/authorization/authorization.policy.rule.credential.interface'; -import { InnovationHubService } from '@domain/innovation-hub'; -import { InnovationHubAuthorizationService } from '@domain/innovation-hub/innovation.hub.service.authorization'; import { CREDENTIAL_RULE_PLATFORM_CREATE_ORGANIZATION, CREDENTIAL_RULE_PLATFORM_CREATE_SPACE, @@ -43,8 +41,6 @@ export class PlatformAuthorizationService { private libraryAuthorizationService: LibraryAuthorizationService, private forumAuthorizationService: ForumAuthorizationService, private platformService: PlatformService, - private innovationHubService: InnovationHubService, - private innovationHubAuthorizationService: InnovationHubAuthorizationService, private storageAggregatorAuthorizationService: StorageAggregatorAuthorizationService, private platformInvitationAuthorizationService: PlatformInvitationAuthorizationService, private licensingAuthorizationService: LicensingAuthorizationService, @@ -123,9 +119,7 @@ export class PlatformAuthorizationService { ): Promise { const platform = await this.platformService.getPlatformOrFail({ relations: { - library: { - innovationPacks: true, - }, + library: true, forum: true, storageAggregator: true, licensing: true, @@ -179,15 +173,6 @@ export class PlatformAuthorizationService { platformStorageAuth ); - const innovationHubs = await this.innovationHubService.getInnovationHubs({ - relations: {}, - }); - for (const innovationHub of innovationHubs) { - this.innovationHubAuthorizationService.applyAuthorizationPolicyAndSave( - innovationHub - ); - } - platform.licensing = await this.licensingAuthorizationService.applyAuthorizationPolicy( platform.licensing, diff --git a/src/platform/settings/platform.settings.service.ts b/src/platform/settings/platform.settings.service.ts index f742e5924e..711dc45acc 100644 --- a/src/platform/settings/platform.settings.service.ts +++ b/src/platform/settings/platform.settings.service.ts @@ -31,12 +31,9 @@ export class PlatformSettingsService { input: UpdateInnovationHubPlatformSettingsInput ): Promise { const innovationHub: IInnovationHub = - await this.innovationHubService.getInnovationHubOrFail( - { - idOrNameId: input.ID, - }, - { relations: { account: true } } - ); + await this.innovationHubService.getInnovationHubOrFail(input.ID, { + relations: { account: true }, + }); if (!innovationHub.account) this.logger.warn( diff --git a/src/services/adapters/notification-adapter/notification.payload.builder.ts b/src/services/adapters/notification-adapter/notification.payload.builder.ts index 15218d6570..57dbbf9ddc 100644 --- a/src/services/adapters/notification-adapter/notification.payload.builder.ts +++ b/src/services/adapters/notification-adapter/notification.payload.builder.ts @@ -80,9 +80,8 @@ export class NotificationPayloadBuilder { community, applicationCreatorID ); - const applicantPayload = await this.getContributorPayloadOrFail( - applicantID - ); + const applicantPayload = + await this.getContributorPayloadOrFail(applicantID); const payload: CommunityApplicationCreatedEventPayload = { applicant: applicantPayload, ...spacePayload, @@ -101,9 +100,8 @@ export class NotificationPayloadBuilder { community, invitationCreatorID ); - const inviteePayload = await this.getContributorPayloadOrFail( - invitedUserID - ); + const inviteePayload = + await this.getContributorPayloadOrFail(invitedUserID); const payload: CommunityInvitationCreatedEventPayload = { invitee: inviteePayload, welcomeMessage, @@ -550,14 +548,12 @@ export class NotificationPayloadBuilder { private async getContributorPayloadOrFail( contributorID: string ): Promise { - const contributor = await this.contributorLookupService.getContributor( - contributorID, - { + const contributor = + await this.contributorLookupService.getContributorByUUID(contributorID, { relations: { profile: true, }, - } - ); + }); if (!contributor || !contributor.profile) { throw new EntityNotFoundException( @@ -569,9 +565,8 @@ export class NotificationPayloadBuilder { const contributorType = this.contributorLookupService.getContributorType(contributor); - const userURL = await this.urlGeneratorService.createUrlForContributor( - contributor - ); + const userURL = + await this.urlGeneratorService.createUrlForContributor(contributor); const result: ContributorPayload = { id: contributor.id, nameID: contributor.nameID, @@ -590,9 +585,8 @@ export class NotificationPayloadBuilder { organizationID: string ): Promise { const basePayload = this.buildBaseEventPayload(senderID); - const orgContribtor = await this.getContributorPayloadOrFail( - organizationID - ); + const orgContribtor = + await this.getContributorPayloadOrFail(organizationID); const payload: CommunicationOrganizationMessageEventPayload = { message, organization: orgContribtor, @@ -625,9 +619,11 @@ export class NotificationPayloadBuilder { originEntityDisplayName: string, commentType: RoomType ): Promise { - const userContributor = await this.getContributorPayloadOrFail( - mentionedUserNameID - ); + const user = + await this.contributorLookupService.getUserByNameIdOrFail( + mentionedUserNameID + ); + const userContributor = await this.getContributorPayloadOrFail(user.id); const commentOriginUrl = await this.buildCommentOriginUrl( commentType, @@ -657,7 +653,11 @@ export class NotificationPayloadBuilder { originEntityDisplayName: string, commentType: RoomType ): Promise { - const orgData = await this.getContributorPayloadOrFail(mentionedOrgNameID); + const org = + await this.contributorLookupService.getOrganizationByNameIdOrFail( + mentionedOrgNameID + ); + const orgData = await this.getContributorPayloadOrFail(org.id); const commentOriginUrl = await this.buildCommentOriginUrl( commentType, diff --git a/src/services/api/activity-log/activity.log.builder.service.ts b/src/services/api/activity-log/activity.log.builder.service.ts index afd09ff242..ed6df011d3 100644 --- a/src/services/api/activity-log/activity.log.builder.service.ts +++ b/src/services/api/activity-log/activity.log.builder.service.ts @@ -54,7 +54,7 @@ export default class ActivityLogBuilderService implements IActivityLogBuilder { rawActivity.parentID ); const contributorJoining = - await this.contributorLookupService.getContributorOrFail( + await this.contributorLookupService.getContributorByUuidOrFail( rawActivity.resourceID ); diff --git a/src/services/api/activity-log/activity.log.module.ts b/src/services/api/activity-log/activity.log.module.ts index cb1aa6d8bd..cb262f7530 100644 --- a/src/services/api/activity-log/activity.log.module.ts +++ b/src/services/api/activity-log/activity.log.module.ts @@ -20,6 +20,7 @@ import { SpaceModule } from '@domain/space/space/space.module'; import { LinkModule } from '@domain/collaboration/link/link.module'; import { UrlGeneratorModule } from '@services/infrastructure/url-generator'; import { ContributorLookupModule } from '@services/infrastructure/contributor-lookup/contributor.lookup.module'; +import { EntityResolverModule } from '@services/infrastructure/entity-resolver/entity.resolver.module'; @Module({ imports: [ @@ -41,6 +42,7 @@ import { ContributorLookupModule } from '@services/infrastructure/contributor-lo SubscriptionServiceModule, PlatformAuthorizationPolicyModule, UrlGeneratorModule, + EntityResolverModule, ], providers: [ ActivityLogService, diff --git a/src/services/api/activity-log/activity.log.service.ts b/src/services/api/activity-log/activity.log.service.ts index 28c14d4b9b..f0617c8e29 100644 --- a/src/services/api/activity-log/activity.log.service.ts +++ b/src/services/api/activity-log/activity.log.service.ts @@ -22,12 +22,14 @@ import { UrlGeneratorService } from '@services/infrastructure/url-generator/url. import { EntityManager } from 'typeorm/entity-manager/EntityManager'; import { InjectEntityManager } from '@nestjs/typeorm'; import { ContributorLookupService } from '@services/infrastructure/contributor-lookup/contributor.lookup.service'; +import { CommunityResolverService } from '@services/infrastructure/entity-resolver/community.resolver.service'; export class ActivityLogService { constructor( private activityService: ActivityService, private userService: UserService, private contributorLookupService: ContributorLookupService, + private communityResolverService: CommunityResolverService, private calloutService: CalloutService, private postService: PostService, private whiteboardService: WhiteboardService, @@ -111,14 +113,15 @@ export class ActivityLogService { ); } - const space = await this.spaceService.getSpaceForCollaborationOrFail( - rawActivity.collaborationID, - { - relations: { - community: true, - }, - } - ); + const space = + await this.communityResolverService.getSpaceForCollaborationOrFail( + rawActivity.collaborationID, + { + relations: { + community: true, + }, + } + ); const activityLogEntryBase: IActivityLogEntry = { id: rawActivity.id, @@ -162,9 +165,10 @@ export class ActivityLogService { private async getParentDetailsByCollaboration( collaborationID: string ): Promise<{ nameID: string; displayName: string } | undefined> { - const space = await this.spaceService.getSpaceForCollaborationOrFail( - collaborationID - ); + const space = + await this.communityResolverService.getSpaceForCollaborationOrFail( + collaborationID + ); return { displayName: space.profile.displayName, diff --git a/src/services/api/conversion/conversion.service.ts b/src/services/api/conversion/conversion.service.ts index d532333960..103bef2f97 100644 --- a/src/services/api/conversion/conversion.service.ts +++ b/src/services/api/conversion/conversion.service.ts @@ -102,9 +102,8 @@ export class ConversionService { type: SpaceType.SPACE, }, }; - const emptyAccount = await this.accountService.createAccount( - createAccountInput - ); + const emptyAccount = + await this.accountService.createAccount(createAccountInput); if (!emptyAccount.space) { throw new EntityNotInitializedException( @@ -267,7 +266,7 @@ export class ConversionService { } const reservedNameIDs = - await this.namingService.getReservedNameIDsInAccount( + await this.namingService.getReservedNameIDsInLevelZeroSpace( subsubspace.account.id ); const subspaceNameID = diff --git a/src/services/api/lookup-by-name/dto/index.ts b/src/services/api/lookup-by-name/dto/index.ts new file mode 100644 index 0000000000..83942b0404 --- /dev/null +++ b/src/services/api/lookup-by-name/dto/index.ts @@ -0,0 +1 @@ +export * from './lookup.by.name.query.results'; diff --git a/src/services/api/lookup-by-name/dto/lookup.by.name.query.results.ts b/src/services/api/lookup-by-name/dto/lookup.by.name.query.results.ts new file mode 100644 index 0000000000..4ee4ec2339 --- /dev/null +++ b/src/services/api/lookup-by-name/dto/lookup.by.name.query.results.ts @@ -0,0 +1,8 @@ +import { ObjectType } from '@nestjs/graphql'; +import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; + +@ObjectType() +export class LookupByNameQueryResults { + // exposed through the field resolver + innovationPack!: IInnovationPack; +} diff --git a/src/services/api/lookup-by-name/index.ts b/src/services/api/lookup-by-name/index.ts new file mode 100644 index 0000000000..1094fdf668 --- /dev/null +++ b/src/services/api/lookup-by-name/index.ts @@ -0,0 +1,3 @@ +export * from './lookup.by.name.module'; +export * from './lookup.by.name.service'; +export * from './lookup.by.name.resolver.queries'; diff --git a/src/services/api/lookup-by-name/lookup.by.name.module.ts b/src/services/api/lookup-by-name/lookup.by.name.module.ts new file mode 100644 index 0000000000..f958ca1170 --- /dev/null +++ b/src/services/api/lookup-by-name/lookup.by.name.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { AuthorizationModule } from '@core/authorization/authorization.module'; +import { LookupByNameService } from './lookup.by.name.service'; +import { LookupByNameResolverQueries } from './lookup.by.name.resolver.queries'; +import { LookupByNameResolverFields } from './lookup.by.name.resolver.fields'; +import { InnovationPackModule } from '@library/innovation-pack/innovation.pack.module'; + +@Module({ + imports: [AuthorizationModule, InnovationPackModule], + providers: [ + LookupByNameService, + LookupByNameResolverQueries, + LookupByNameResolverFields, + ], + exports: [LookupByNameService], +}) +export class LookupByNameModule {} diff --git a/src/services/api/lookup-by-name/lookup.by.name.resolver.fields.ts b/src/services/api/lookup-by-name/lookup.by.name.resolver.fields.ts new file mode 100644 index 0000000000..4aee9deb4f --- /dev/null +++ b/src/services/api/lookup-by-name/lookup.by.name.resolver.fields.ts @@ -0,0 +1,41 @@ +import { Args, Resolver } from '@nestjs/graphql'; +import { GraphqlGuard } from '@core/authorization'; +import { UseGuards } from '@nestjs/common'; +import { CurrentUser } from '@src/common/decorators'; +import { ResolveField } from '@nestjs/graphql'; +import { AgentInfo } from '@core/authentication.agent.info/agent.info'; +import { LookupByNameQueryResults } from './dto/lookup.by.name.query.results'; +import { AuthorizationService } from '@core/authorization/authorization.service'; +import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; +import { InnovationPackService } from '@library/innovation-pack/innovaton.pack.service'; +import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; +import { NameID } from '@domain/common/scalars'; + +@Resolver(() => LookupByNameQueryResults) +export class LookupByNameResolverFields { + constructor( + private authorizationService: AuthorizationService, + private innovationPackService: InnovationPackService + ) {} + + @UseGuards(GraphqlGuard) + @ResolveField(() => IInnovationPack, { + nullable: true, + description: 'Lookup the specified InnovationPack using a NameID', + }) + async innovationPack( + @CurrentUser() agentInfo: AgentInfo, + @Args('NAMEID', { type: () => NameID }) nameid: string + ): Promise { + const innovationPack = + await this.innovationPackService.getInnovationPackOrFail(nameid); + this.authorizationService.grantAccessOrFail( + agentInfo, + innovationPack.authorization, + AuthorizationPrivilege.READ, + `lookup InnovationPack by NameID: ${innovationPack.id}` + ); + + return innovationPack; + } +} diff --git a/src/services/api/lookup-by-name/lookup.by.name.resolver.queries.ts b/src/services/api/lookup-by-name/lookup.by.name.resolver.queries.ts new file mode 100644 index 0000000000..0f477da0bd --- /dev/null +++ b/src/services/api/lookup-by-name/lookup.by.name.resolver.queries.ts @@ -0,0 +1,18 @@ +import { Query, Resolver } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; +import { GraphqlGuard } from '@core/authorization'; +import { Profiling } from '@common/decorators'; +import { LookupByNameQueryResults } from './dto/lookup.by.name.query.results'; + +@Resolver() +export class LookupByNameResolverQueries { + @UseGuards(GraphqlGuard) + @Query(() => LookupByNameQueryResults, { + nullable: false, + description: 'Allow direct lookup of entities using their NameIDs', + }) + @Profiling.api + async lookupByName(): Promise { + return {} as LookupByNameQueryResults; + } +} diff --git a/src/services/api/lookup-by-name/lookup.by.name.service.ts b/src/services/api/lookup-by-name/lookup.by.name.service.ts new file mode 100644 index 0000000000..641f8128f2 --- /dev/null +++ b/src/services/api/lookup-by-name/lookup.by.name.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class LookupByNameService {} diff --git a/src/services/api/lookup/lookup.module.ts b/src/services/api/lookup/lookup.module.ts index 2216d36b07..9c76f1b4b3 100644 --- a/src/services/api/lookup/lookup.module.ts +++ b/src/services/api/lookup/lookup.module.ts @@ -29,6 +29,8 @@ import { CommunityGuidelinesModule } from '@domain/community/community-guideline import { CommunityGuidelinesTemplateModule } from '@domain/template/community-guidelines-template/community.guidelines.template.module'; import { VirtualContributorModule } from '@domain/community/virtual-contributor/virtual.contributor.module'; import { StorageBucketModule } from '@domain/storage/storage-bucket/storage.bucket.module'; +import { InnovationHubModule } from '@domain/innovation-hub'; +import { InnovationPackModule } from '@library/innovation-pack/innovation.pack.module'; @Module({ imports: [ @@ -41,6 +43,7 @@ import { StorageBucketModule } from '@domain/storage/storage-bucket/storage.buck WhiteboardModule, WhiteboardTemplateModule, InnovationFlowModule, + InnovationPackModule, InnovationFlowTemplateModule, PostModule, ProfileModule, @@ -50,6 +53,7 @@ import { StorageBucketModule } from '@domain/storage/storage-bucket/storage.buck RoomModule, ApplicationModule, InvitationModule, + InnovationHubModule, DocumentModule, StorageAggregatorModule, StorageBucketModule, diff --git a/src/services/api/lookup/lookup.resolver.fields.ts b/src/services/api/lookup/lookup.resolver.fields.ts index 94f56213b6..da323c7514 100644 --- a/src/services/api/lookup/lookup.resolver.fields.ts +++ b/src/services/api/lookup/lookup.resolver.fields.ts @@ -58,6 +58,10 @@ import { IVirtualContributor } from '@domain/community/virtual-contributor/virtu import { VirtualContributorService } from '@domain/community/virtual-contributor/virtual.contributor.service'; import { StorageBucketService } from '@domain/storage/storage-bucket/storage.bucket.service'; import { IStorageBucket } from '@domain/storage/storage-bucket/storage.bucket.interface'; +import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; +import { InnovationHubService } from '@domain/innovation-hub/innovation.hub.service'; +import { InnovationPackService } from '@library/innovation-pack/innovaton.pack.service'; +import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; @Resolver(() => LookupQueryResults) export class LookupResolverFields { @@ -73,6 +77,7 @@ export class LookupResolverFields { private whiteboardService: WhiteboardService, private whiteboardTemplateService: WhiteboardTemplateService, private calloutTemplateService: CalloutTemplateService, + private innovationPackService: InnovationPackService, private profileService: ProfileService, private postService: PostService, private calloutService: CalloutService, @@ -88,7 +93,8 @@ export class LookupResolverFields { private userService: UserService, private guidelinesService: CommunityGuidelinesService, private guidelinesTemplateService: CommunityGuidelinesTemplateService, - private virtualContributorService: VirtualContributorService + private virtualContributorService: VirtualContributorService, + private innovationHubService: InnovationHubService ) {} @UseGuards(GraphqlGuard) @@ -212,6 +218,27 @@ export class LookupResolverFields { return document; } + @UseGuards(GraphqlGuard) + @ResolveField(() => IInnovationPack, { + nullable: true, + description: 'Lookup the specified InnovationPack', + }) + async innovationPack( + @CurrentUser() agentInfo: AgentInfo, + @Args('ID', { type: () => UUID }) id: string + ): Promise { + const innovationPack = + await this.innovationPackService.getInnovationPackOrFail(id); + this.authorizationService.grantAccessOrFail( + agentInfo, + innovationPack.authorization, + AuthorizationPrivilege.READ, + `lookup InnovationPack: ${innovationPack.id}` + ); + + return innovationPack; + } + @UseGuards(GraphqlGuard) @ResolveField(() => IStorageBucket, { nullable: true, @@ -232,6 +259,27 @@ export class LookupResolverFields { return document; } + @UseGuards(GraphqlGuard) + @ResolveField(() => IInnovationHub, { + nullable: true, + description: 'Lookup the specified InnovationHub', + }) + async innovationHub( + @CurrentUser() agentInfo: AgentInfo, + @Args('ID', { type: () => UUID }) id: string + ): Promise { + const innovationHub = + await this.innovationHubService.getInnovationHubOrFail(id); + this.authorizationService.grantAccessOrFail( + agentInfo, + innovationHub.authorization, + AuthorizationPrivilege.READ, + `lookup InnovationHub: ${innovationHub.id}` + ); + + return innovationHub; + } + @UseGuards(GraphqlGuard) @ResolveField(() => IApplication, { nullable: true, diff --git a/src/services/api/me/dto/me.membership.result.ts b/src/services/api/me/dto/me.membership.result.ts new file mode 100644 index 0000000000..8511dbacdc --- /dev/null +++ b/src/services/api/me/dto/me.membership.result.ts @@ -0,0 +1,21 @@ +import { UUID } from '@domain/common/scalars'; +import { Field, ObjectType } from '@nestjs/graphql'; +import { ISpace } from '@domain/space/space/space.interface'; + +@ObjectType() +export class CommunityMembershipResult { + @Field(() => UUID, { + description: 'ID for the membership', + }) + id!: string; + + @Field(() => ISpace, { + description: 'The space for the membership is for', + }) + space!: ISpace; + + @Field(() => [CommunityMembershipResult], { + description: 'The child community memberships', + }) + childMemberships!: CommunityMembershipResult[]; +} diff --git a/src/services/api/me/dto/me.query.results.ts b/src/services/api/me/dto/me.query.results.ts index 12ede678f6..6f16d1ff8f 100644 --- a/src/services/api/me/dto/me.query.results.ts +++ b/src/services/api/me/dto/me.query.results.ts @@ -2,7 +2,7 @@ import { ObjectType } from '@nestjs/graphql'; import { IUser } from '@domain/community/user/user.interface'; import { IInvitation } from '@domain/community/invitation'; import { IApplication } from '@domain/community/application'; -import { ISpace } from '@domain/space/space/space.interface'; +import { CommunityMembershipResult } from './me.membership.result'; @ObjectType() export class MeQueryResults { @@ -10,5 +10,6 @@ export class MeQueryResults { user!: IUser; invitations!: IInvitation[]; applications!: IApplication[]; - spaceMemberships!: ISpace[]; + spaceMembershipsHierarchical!: CommunityMembershipResult[]; + spaceMembershipsFlat!: CommunityMembershipResult[]; } diff --git a/src/services/api/me/me.resolver.fields.ts b/src/services/api/me/me.resolver.fields.ts index 9e5b0bf480..abe4c5e86e 100644 --- a/src/services/api/me/me.resolver.fields.ts +++ b/src/services/api/me/me.resolver.fields.ts @@ -12,12 +12,12 @@ import { } from '@common/exceptions'; import { UserService } from '@domain/community/user/user.service'; import { ISpace } from '@domain/space/space/space.interface'; -import { SpaceVisibility } from '@common/enums/space.visibility'; import { MeService } from './me.service'; import { LogContext } from '@common/enums'; import { MySpaceResults } from './dto/my.journeys.results'; import { CommunityInvitationResult } from './dto/me.invitation.result'; import { CommunityApplicationResult } from './dto/me.application.result'; +import { CommunityMembershipResult } from './dto/me.membership.result'; @Resolver(() => MeQueryResults) export class MeResolverFields { @@ -106,20 +106,23 @@ export class MeResolverFields { } @UseGuards(GraphqlGuard) - @ResolveField(() => [ISpace], { - description: 'The applications of the current authenticated user', + @ResolveField(() => [CommunityMembershipResult], { + description: 'The hierarchy of the Spaces the current user is a member.', }) - public spaceMemberships( - @CurrentUser() agentInfo: AgentInfo, - @Args({ - name: 'visibilities', - nullable: true, - type: () => [SpaceVisibility], - description: 'The Space visibilities you want to filter on', - }) - visibilities: SpaceVisibility[] - ): Promise { - return this.meService.getSpaceMemberships(agentInfo, visibilities); + public spaceMembershipsHierarchical( + @CurrentUser() agentInfo: AgentInfo + ): Promise { + return this.meService.getSpaceMembershipsHierarchical(agentInfo); + } + + @UseGuards(GraphqlGuard) + @ResolveField(() => [CommunityMembershipResult], { + description: 'The Spaces the current user is a member of as a flat list.', + }) + public spaceMembershipsFlat( + @CurrentUser() agentInfo: AgentInfo + ): Promise { + return this.meService.getSpaceMembershipsFlat(agentInfo); } @UseGuards(GraphqlGuard) diff --git a/src/services/api/me/me.service.ts b/src/services/api/me/me.service.ts index 29b46a8787..29bba8f75d 100644 --- a/src/services/api/me/me.service.ts +++ b/src/services/api/me/me.service.ts @@ -1,10 +1,8 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; -import { SpaceVisibility } from '@common/enums/space.visibility'; import { groupCredentialsByEntity } from '@services/api/roles/util/group.credentials.by.entity'; import { SpaceService } from '@domain/space/space/space.service'; import { RolesService } from '../roles/roles.service'; import { ISpace } from '@domain/space/space/space.interface'; -import { SpacesQueryArgs } from '@domain/space/space/dto/space.args.query.spaces'; import { ActivityLogService } from '../activity-log'; import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { MySpaceResults } from './dto/my.journeys.results'; @@ -14,12 +12,17 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { sortSpacesByActivity } from '@domain/space/space/sort.spaces.by.activity'; import { CommunityInvitationResult } from './dto/me.invitation.result'; import { CommunityResolverService } from '@services/infrastructure/entity-resolver/community.resolver.service'; -import { EntityNotFoundException } from '@common/exceptions'; +import { + EntityNotFoundException, + RelationshipNotFoundException, +} from '@common/exceptions'; import { CommunityApplicationResult } from './dto/me.application.result'; -import { CommunityRoleService } from '@domain/community/community-role/community.role.service'; import { ContributorService } from '@domain/community/contributor/contributor.service'; import { UserService } from '@domain/community/user/user.service'; import { compact } from 'lodash'; +import { SpaceMembershipCollaborationInfo } from './space.membership.type'; +import { CommunityMembershipResult } from './dto/me.membership.result'; +import { SpaceLevel } from '@common/enums/space.level'; @Injectable() export class MeService { @@ -30,7 +33,6 @@ export class MeService { private rolesService: RolesService, private activityLogService: ActivityLogService, private activityService: ActivityService, - private communityRoleService: CommunityRoleService, private communityResolverService: CommunityResolverService, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService @@ -92,35 +94,121 @@ export class MeService { return results; } - public async getSpaceMemberships( - agentInfo: AgentInfo, - visibilities: SpaceVisibility[] = [ - SpaceVisibility.ACTIVE, - SpaceVisibility.DEMO, - ] + private async getSpaceMembershipsForAgentInfo( + agentInfo: AgentInfo ): Promise { const credentialMap = groupCredentialsByEntity(agentInfo.credentials); const spaceIds = Array.from(credentialMap.get('spaces')?.keys() ?? []); - const args: SpacesQueryArgs = { - IDs: spaceIds, - filter: { - visibilities, - }, - }; - - // get spaces and their subspaces - const spaces = await this.spaceService.getSpacesWithChildJourneys(args); + const allSpaces = await this.spaceService.getSpacesInList(spaceIds); + for (const space of allSpaces) { + if ( + (space.level !== SpaceLevel.SPACE && !space.parentSpace) || + !space.collaboration + ) { + throw new RelationshipNotFoundException( + `Space ${space.id} is missing parent space or collaboration`, + LogContext.COMMUNITY + ); + } + } const spaceMembershipCollaborationInfo = - this.spaceService.getSpaceMembershipCollaborationInfo(spaces); - + this.getSpaceMembershipCollaborationInfo(allSpaces); const latestActivitiesPerSpace = await this.activityService.getLatestActivitiesPerSpaceMembership( agentInfo.userID, spaceMembershipCollaborationInfo ); + return sortSpacesByActivity(allSpaces, latestActivitiesPerSpace); + } + + public async getSpaceMembershipsFlat( + agentInfo: AgentInfo + ): Promise { + const sortedFlatListSpacesWithMembership = + await this.getSpaceMembershipsForAgentInfo(agentInfo); + const spaceMemberships: CommunityMembershipResult[] = []; + + for (const space of sortedFlatListSpacesWithMembership) { + const levelZeroMembership: CommunityMembershipResult = { + id: space.id, + space: space, + childMemberships: [], + }; + spaceMemberships.push(levelZeroMembership); + } + return spaceMemberships; + } + + public async getSpaceMembershipsHierarchical( + agentInfo: AgentInfo + ): Promise { + const sortedFlatListSpacesWithMembership = + await this.getSpaceMembershipsForAgentInfo(agentInfo); + + const levelZeroSpaces = sortedFlatListSpacesWithMembership.filter( + space => space.level === SpaceLevel.SPACE + ); + const levelOneSpaces = sortedFlatListSpacesWithMembership.filter( + space => space.level === SpaceLevel.CHALLENGE + ); + const levelTwoSpaces = sortedFlatListSpacesWithMembership.filter( + space => space.level === SpaceLevel.OPPORTUNITY + ); + const levelZeroMemberships: CommunityMembershipResult[] = []; + for (const levelZeroSpace of levelZeroSpaces) { + const levelZeroMembership: CommunityMembershipResult = { + id: levelZeroSpace.id, + space: levelZeroSpace, + childMemberships: [], + }; + for (const levelOneSpace of levelOneSpaces) { + if (levelOneSpace.parentSpace?.id === levelZeroSpace.id) { + const levelOneMembership: CommunityMembershipResult = { + id: levelOneSpace.id, + space: levelOneSpace, + childMemberships: [], + }; + for (const levelTwoSpace of levelTwoSpaces) { + if (levelTwoSpace.parentSpace?.id === levelOneSpace.id) { + const levelTwoMembership: CommunityMembershipResult = { + id: levelTwoSpace.id, + space: levelTwoSpace, + childMemberships: [], + }; + levelOneMembership.childMemberships.push(levelTwoMembership); + } + } + levelZeroMembership.childMemberships.push(levelOneMembership); + } + } + levelZeroMemberships.push(levelZeroMembership); + } + + return levelZeroMemberships; + } + + // Returns a map of all collaboration IDs with parent space ID + private getSpaceMembershipCollaborationInfo( + spaces: ISpace[] + ): SpaceMembershipCollaborationInfo { + const spaceMembershipCollaborationInfo: SpaceMembershipCollaborationInfo = + new Map(); + + for (const space of spaces) { + if (!space.collaboration) { + throw new EntityNotFoundException( + `Space ${space.id} is missing collaboration`, + LogContext.COMMUNITY + ); + } + spaceMembershipCollaborationInfo.set( + space.collaboration.id, + space.levelZeroSpaceID + ); + } - return sortSpacesByActivity(spaces, latestActivitiesPerSpace); + return spaceMembershipCollaborationInfo; } public async getMySpaces( diff --git a/src/services/api/roles/dto/roles.dto.result.space.ts b/src/services/api/roles/dto/roles.dto.result.space.ts index 7090d3e734..b761ec379f 100644 --- a/src/services/api/roles/dto/roles.dto.result.space.ts +++ b/src/services/api/roles/dto/roles.dto.result.space.ts @@ -33,7 +33,6 @@ export class RolesResultSpace extends RolesResultCommunity { ); this.spaceID = space.id; this.space = space; - this.visibility = - space.account?.license?.visibility ?? SpaceVisibility.ACTIVE; + this.visibility = space.visibility ?? SpaceVisibility.ACTIVE; } } diff --git a/src/services/api/roles/roles.service.spec.ts b/src/services/api/roles/roles.service.spec.ts index 112114d5a7..cee55ed458 100644 --- a/src/services/api/roles/roles.service.spec.ts +++ b/src/services/api/roles/roles.service.spec.ts @@ -23,18 +23,17 @@ import * as getOrganizationRolesForUserEntityData from './util/get.organization. import * as getSpaceRolesForContributorQueryResult from './util/get.space.roles.for.contributor.query.result'; import { MockInvitationService } from '@test/mocks/invitation.service.mock'; import { MockCommunityResolverService } from '@test/mocks/community.resolver.service.mock'; -import { SpaceService } from '@domain/space/space/space.service'; import { RolesResultSpace } from './dto/roles.dto.result.space'; import { ProfileType } from '@common/enums/profile.type'; import { Profile } from '@domain/common/profile/profile.entity'; import { SpaceType } from '@common/enums/space.type'; import { SpaceLevel } from '@common/enums/space.level'; import { Space } from '@domain/space/space/space.entity'; -import { License } from '@domain/license/license/license.entity'; import { RolesResultCommunity } from './dto/roles.dto.result.community'; import { MockUserLookupService } from '@test/mocks/user.lookup.service.mock'; import { MockVirtualContributorService } from '@test/mocks/virtual.contributor.service.mock'; import { IUser } from '@domain/community/user'; +import { CommunityResolverService } from '@services/infrastructure/entity-resolver/community.resolver.service'; describe('RolesService', () => { let rolesService: RolesService; @@ -43,7 +42,7 @@ describe('RolesService', () => { let applicationService: ApplicationService; let organizationService: OrganizationService; let communityService: CommunityService; - let spaceService: SpaceService; + let communityResolverService: CommunityResolverService; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ @@ -70,8 +69,8 @@ describe('RolesService', () => { applicationService = moduleRef.get(ApplicationService); organizationService = moduleRef.get(OrganizationService); communityService = moduleRef.get(CommunityService); + communityResolverService = moduleRef.get(CommunityResolverService); spaceFilterService = moduleRef.get(SpaceFilterService); - spaceService = moduleRef.get(SpaceService); }); describe('User Roles', () => { @@ -134,7 +133,7 @@ describe('RolesService', () => { jest.spyOn(communityService, 'isSpaceCommunity').mockResolvedValue(true); jest - .spyOn(spaceService, 'getSpaceForCommunityOrFail') + .spyOn(communityResolverService, 'getSpaceForCommunityOrFail') .mockResolvedValue(testData.space as any); }); @@ -246,6 +245,7 @@ const getSpaceRoleResultMock = ({ settingsStr: JSON.stringify({}), rowId: parseInt(id), nameID: `space-${id}`, + levelZeroSpaceID: '', profile: { id: `profile-${id}`, displayName: `Space ${id}`, @@ -256,14 +256,12 @@ const getSpaceRoleResultMock = ({ }, type: SpaceType.SPACE, level: SpaceLevel.SPACE, + visibility: SpaceVisibility.ACTIVE, account: { id: `account-${id}`, virtualContributors: [], - license: { - id: `license-${id}`, - visibility: SpaceVisibility.ACTIVE, - ...getEntityMock(), - }, + innovationHubs: [], + innovationPacks: [], }, ...getEntityMock(), }, diff --git a/src/services/api/roles/roles.service.ts b/src/services/api/roles/roles.service.ts index a474130189..f318fa5298 100644 --- a/src/services/api/roles/roles.service.ts +++ b/src/services/api/roles/roles.service.ts @@ -156,9 +156,10 @@ export class RolesService { community.id ); - const space = await this.spaceService.getSpaceForCommunityOrFail( - community.id - ); + const space = + await this.communityResolverService.getSpaceForCommunityOrFail( + community.id + ); const applicationResult = new CommunityApplicationForRoleResult( community.id, @@ -229,9 +230,10 @@ export class RolesService { community.id ); - const space = await this.spaceService.getSpaceForCommunityOrFail( - community.id - ); + const space = + await this.communityResolverService.getSpaceForCommunityOrFail( + community.id + ); const invitationResult = new CommunityInvitationForRoleResult( community.id, diff --git a/src/services/api/roles/util/get.space.roles.for.contributor.entity.data.ts b/src/services/api/roles/util/get.space.roles.for.contributor.entity.data.ts index d1081ceed1..9d9aabd903 100644 --- a/src/services/api/roles/util/get.space.roles.for.contributor.entity.data.ts +++ b/src/services/api/roles/util/get.space.roles.for.contributor.entity.data.ts @@ -28,17 +28,11 @@ export const getSpaceRolesForContributorEntityData = async ( where = { id: In(ids), level: In(levels), - account: { - license: { - visibility: In(visibility), - }, - }, + visibility: In(visibility), }; relations = { profile: true, - account: { - license: true, - }, + account: true, }; } const results = entityManager.find(ref, { @@ -48,10 +42,7 @@ export const getSpaceRolesForContributorEntityData = async ( profile: { displayName: true, }, - account: { - id: true, - license: true, - }, + account: true, }, } as FindManyOptions); return results; diff --git a/src/services/api/roles/util/get.space.roles.for.contributor.query.result.ts b/src/services/api/roles/util/get.space.roles.for.contributor.query.result.ts index 1c0e59e28a..59059fc690 100644 --- a/src/services/api/roles/util/get.space.roles.for.contributor.query.result.ts +++ b/src/services/api/roles/util/get.space.roles.for.contributor.query.result.ts @@ -34,8 +34,8 @@ export const getSpaceRolesForContributorQueryResult = ( ); if (readAccessSpace) { - const accountID = space.account?.id; - if (!accountID) { + const levelZeroSpaceID = space.levelZeroSpaceID; + if (!levelZeroSpaceID) { throw new RelationshipNotFoundException( `Unable to load account on Space in roles user: ${space.nameID}`, LogContext.ROLES @@ -43,14 +43,14 @@ export const getSpaceRolesForContributorQueryResult = ( } const subspaceResults: RolesResultCommunity[] = []; for (const subspace of subspaces) { - const challengeAccountID = subspace.account?.id; - if (!challengeAccountID) { + const challengeLevelZeroSpaceID = subspace.levelZeroSpaceID; + if (!challengeLevelZeroSpaceID) { throw new RelationshipNotFoundException( - `Unable to load account on Challenge in roles user: ${space.nameID}`, + `Unable to load L0 space ID on Challenge in roles user: ${space.nameID}`, LogContext.ROLES ); } - if (challengeAccountID === accountID) { + if (challengeLevelZeroSpaceID === levelZeroSpaceID) { const subspaceResult = new RolesResultCommunity( subspace.nameID, subspace.id, diff --git a/src/services/api/search/v1/search.service.ts b/src/services/api/search/v1/search.service.ts index a24274cbd4..6760b9e786 100644 --- a/src/services/api/search/v1/search.service.ts +++ b/src/services/api/search/v1/search.service.ts @@ -398,9 +398,6 @@ export class SearchService { location: true, tagsets: true, }, - account: { - license: true, - }, }, }); @@ -426,11 +423,7 @@ export class SearchService { // Filter the spaces + score them for (const space of filteredSpaceMatches) { - if ( - space.account && - space.account.license && - space.account.license.visibility !== SpaceVisibility.ARCHIVED - ) { + if (space.visibility !== SpaceVisibility.ARCHIVED) { // Score depends on various factors, hardcoded for now const score_increment = this.getScoreIncrementSpace(space, agentInfo); @@ -448,7 +441,7 @@ export class SearchService { // Determine the score increment based on whether the user has read access or not private getScoreIncrementSpace(space: ISpace, agentInfo: AgentInfo): number { - switch (space.account?.license?.visibility) { + switch (space.visibility) { case SpaceVisibility.ACTIVE: return this.getScoreIncrementReadAccess(space.authorization, agentInfo); case SpaceVisibility.DEMO: @@ -498,9 +491,6 @@ export class SearchService { location: true, tagsets: true, }, - account: { - license: true, - }, }, }); const lowerCasedTerm = term.toLowerCase(); @@ -527,8 +517,7 @@ export class SearchService { // Only show challenges that the current user has read access to for (const filteredSubspace of filteredChallengeMatches) { if ( - filteredSubspace.account?.license?.visibility !== - SpaceVisibility.ARCHIVED && + filteredSubspace.visibility !== SpaceVisibility.ARCHIVED && this.authorizationService.isAccessGranted( agentInfo, filteredSubspace.authorization, @@ -569,9 +558,6 @@ export class SearchService { relations: { context: true, collaboration: true, - account: { - license: true, - }, profile: { location: true, tagsets: true, @@ -608,8 +594,7 @@ export class SearchService { // Only show challenges that the current user has read access to for (const filteredSubsubspace of filteredOpportunityMatches) { if ( - filteredSubsubspace.account.license?.visibility !== - SpaceVisibility.ARCHIVED && + filteredSubsubspace.visibility !== SpaceVisibility.ARCHIVED && this.authorizationService.isAccessGranted( agentInfo, filteredSubsubspace.authorization, @@ -901,9 +886,8 @@ export class SearchService { ); const searchResultType = searchResultBase.type as SearchResultType; try { - const searchResult = await searchResultBuilder[searchResultType]( - searchResultBase - ); + const searchResult = + await searchResultBuilder[searchResultType](searchResultBase); searchResults.push(searchResult); } catch (error: any) { this.logger.error( @@ -959,13 +943,11 @@ export class SearchService { spaceIDsFilter = [searchInSpace.id]; const accountIDsFilter = [searchInSpace.account.id]; - const subspacesFilter = await this.getSubspacesInAccountFilter( - accountIDsFilter - ); + const subspacesFilter = + await this.getSubspacesInAccountFilter(accountIDsFilter); challengeIDsFilter = subspacesFilter.map(subspace => subspace.id); - const subsubspacesFilter = await this.getSubsubspacesInAccountFilter( - accountIDsFilter - ); + const subsubspacesFilter = + await this.getSubsubspacesInAccountFilter(accountIDsFilter); opportunityIDsFilter = subsubspacesFilter.map(opp => opp.id); userIDsFilter = await this.getUsersFilter(searchInSpace); organizationIDsFilter = await this.getOrganizationsFilter(searchInSpace); diff --git a/src/services/api/search/v2/ingest/search.ingest.service.ts b/src/services/api/search/v2/ingest/search.ingest.service.ts index 342788c0f6..ed216c8f59 100644 --- a/src/services/api/search/v2/ingest/search.ingest.service.ts +++ b/src/services/api/search/v2/ingest/search.ingest.service.ts @@ -393,7 +393,7 @@ export class SearchIngestService { private fetchSpacesLevel0Count() { return this.entityManager.count(Space, { where: { - account: { license: { visibility: Not(SpaceVisibility.ARCHIVED) } }, + visibility: Not(SpaceVisibility.ARCHIVED), level: SpaceLevel.SPACE, }, }); @@ -403,16 +403,15 @@ export class SearchIngestService { .find(Space, { ...journeyFindOptions, where: { - account: { license: { visibility: Not(SpaceVisibility.ARCHIVED) } }, + visibility: Not(SpaceVisibility.ARCHIVED), level: SpaceLevel.SPACE, }, relations: { ...journeyFindOptions.relations, - account: { license: true }, }, select: { ...journeyFindOptions.select, - account: { id: true, license: { visibility: true } }, + visibility: true, }, skip: start, take: limit, @@ -422,7 +421,7 @@ export class SearchIngestService { ...space, account: undefined, type: SearchEntityTypes.SPACE, - license: { visibility: space?.account?.license?.visibility }, + visibility: space?.visibility, spaceID: space.id, // spaceID is the same as the space's id profile: { ...space.profile, @@ -436,7 +435,7 @@ export class SearchIngestService { private fetchSpacesLevel1Count() { return this.entityManager.count(Space, { where: { - account: { license: { visibility: Not(SpaceVisibility.ARCHIVED) } }, + visibility: Not(SpaceVisibility.ARCHIVED), level: SpaceLevel.CHALLENGE, }, }); @@ -446,17 +445,16 @@ export class SearchIngestService { .find(Space, { ...journeyFindOptions, where: { - account: { license: { visibility: Not(SpaceVisibility.ARCHIVED) } }, + visibility: Not(SpaceVisibility.ARCHIVED), level: SpaceLevel.CHALLENGE, }, relations: { ...journeyFindOptions.relations, - account: { license: true }, parentSpace: true, }, select: { ...journeyFindOptions.select, - account: { id: true, license: { visibility: true } }, + visibility: true, parentSpace: { id: true }, }, skip: start, @@ -468,7 +466,7 @@ export class SearchIngestService { account: undefined, parentSpace: undefined, type: SearchEntityTypes.SPACE, - license: { visibility: space?.account?.license?.visibility }, + visibility: space?.visibility, spaceID: space.parentSpace?.id ?? EMPTY_VALUE, profile: { ...space.profile, @@ -482,7 +480,7 @@ export class SearchIngestService { private fetchSpacesLevel2Count() { return this.entityManager.count(Space, { where: { - account: { license: { visibility: Not(SpaceVisibility.ARCHIVED) } }, + visibility: Not(SpaceVisibility.ARCHIVED), level: SpaceLevel.OPPORTUNITY, }, }); @@ -492,17 +490,16 @@ export class SearchIngestService { .find(Space, { ...journeyFindOptions, where: { - account: { license: { visibility: Not(SpaceVisibility.ARCHIVED) } }, + visibility: Not(SpaceVisibility.ARCHIVED), level: SpaceLevel.OPPORTUNITY, }, relations: { ...journeyFindOptions.relations, - account: { license: true }, parentSpace: { parentSpace: true }, }, select: { ...journeyFindOptions.select, - account: { id: true, license: { visibility: true } }, + visibility: true, parentSpace: { id: true, parentSpace: { id: true } }, }, skip: start, @@ -514,7 +511,7 @@ export class SearchIngestService { account: undefined, parentSpace: undefined, type: SearchEntityTypes.SPACE, - license: { visibility: space?.account?.license?.visibility }, + visibility: space?.visibility, spaceID: space.parentSpace?.parentSpace?.id ?? EMPTY_VALUE, profile: { ...space.profile, @@ -595,9 +592,7 @@ export class SearchIngestService { return this.entityManager.count(Space, { loadEagerRelations: false, where: { - account: { - license: { visibility: Not(SpaceVisibility.ARCHIVED) }, - }, + visibility: Not(SpaceVisibility.ARCHIVED), }, }); } @@ -606,12 +601,9 @@ export class SearchIngestService { .find(Space, { loadEagerRelations: false, where: { - account: { - license: { visibility: Not(SpaceVisibility.ARCHIVED) }, - }, + visibility: Not(SpaceVisibility.ARCHIVED), }, relations: { - account: { license: true }, parentSpace: { parentSpace: true, }, @@ -625,7 +617,7 @@ export class SearchIngestService { }, select: { id: true, - account: { id: true, license: { visibility: true } }, + visibility: true, parentSpace: { id: true, parentSpace: { id: true } }, collaboration: { id: true, @@ -651,7 +643,7 @@ export class SearchIngestService { framing: undefined, type: SearchEntityTypes.CALLOUT, license: { - visibility: space?.account?.license?.visibility ?? EMPTY_VALUE, + visibility: space?.visibility ?? EMPTY_VALUE, }, spaceID: space.parentSpace?.parentSpace?.id ?? @@ -672,9 +664,7 @@ export class SearchIngestService { return this.entityManager.count(Space, { loadEagerRelations: false, where: { - account: { - license: { visibility: Not(SpaceVisibility.ARCHIVED) }, - }, + visibility: Not(SpaceVisibility.ARCHIVED), }, }); } @@ -683,12 +673,9 @@ export class SearchIngestService { .find(Space, { loadEagerRelations: false, where: { - account: { - license: { visibility: Not(SpaceVisibility.ARCHIVED) }, - }, + visibility: Not(SpaceVisibility.ARCHIVED), }, relations: { - account: { license: true }, collaboration: { callouts: { framing: { @@ -709,7 +696,7 @@ export class SearchIngestService { }, select: { id: true, - account: { id: true, license: { visibility: true } }, + visibility: true, collaboration: { id: true, callouts: { @@ -767,8 +754,7 @@ export class SearchIngestService { content, type: SearchEntityTypes.WHITEBOARD, license: { - visibility: - space?.account?.license?.visibility ?? EMPTY_VALUE, + visibility: space?.visibility ?? EMPTY_VALUE, }, spaceID: space?.parentSpace?.parentSpace?.id ?? @@ -806,8 +792,7 @@ export class SearchIngestService { ), type: SearchEntityTypes.WHITEBOARD, license: { - visibility: - space?.account?.license?.visibility ?? EMPTY_VALUE, + visibility: space?.visibility ?? EMPTY_VALUE, }, spaceID: space?.parentSpace?.parentSpace?.id ?? @@ -836,9 +821,7 @@ export class SearchIngestService { return this.entityManager.count(Space, { loadEagerRelations: false, where: { - account: { - license: { visibility: Not(SpaceVisibility.ARCHIVED) }, - }, + visibility: Not(SpaceVisibility.ARCHIVED), }, }); } @@ -847,12 +830,9 @@ export class SearchIngestService { .find(Space, { loadEagerRelations: false, where: { - account: { - license: { visibility: Not(SpaceVisibility.ARCHIVED) }, - }, + visibility: Not(SpaceVisibility.ARCHIVED), }, relations: { - account: { license: true }, collaboration: { callouts: { contributions: { @@ -868,7 +848,7 @@ export class SearchIngestService { }, select: { id: true, - account: { id: true, license: { visibility: true } }, + visibility: true, collaboration: { id: true, callouts: { @@ -909,8 +889,7 @@ export class SearchIngestService { ...contribution.post, type: SearchEntityTypes.POST, license: { - visibility: - space?.account?.license?.visibility ?? EMPTY_VALUE, + visibility: space?.visibility ?? EMPTY_VALUE, }, spaceID: space.parentSpace?.parentSpace?.id ?? diff --git a/src/services/external/excalidraw-backend/excalidraw.server.ts b/src/services/external/excalidraw-backend/excalidraw.server.ts index d959f88cbd..5229516d85 100644 --- a/src/services/external/excalidraw-backend/excalidraw.server.ts +++ b/src/services/external/excalidraw-backend/excalidraw.server.ts @@ -341,8 +341,10 @@ export class ExcalidrawServer { const community = await this.communityResolver.getCommunityFromWhiteboardOrFail(roomId); - const spaceID = - await this.communityResolver.getRootSpaceIDFromCommunityOrFail(community); + const levelZeroSpaceID = + await this.communityResolver.getLevelZeroSpaceIdForCommunity( + community.id + ); const wb = await this.whiteboardService.getProfile(roomId); const sockets = await this.fetchSocketsSafe(roomId); @@ -355,7 +357,7 @@ export class ExcalidrawServer { { id: roomId, name: wb.displayName, - space: spaceID, + space: levelZeroSpaceID, }, { id: socket.data.agentInfo.userID, diff --git a/src/services/infrastructure/contributor-lookup/contributor.lookup.service.ts b/src/services/infrastructure/contributor-lookup/contributor.lookup.service.ts index 6c2142f4fb..97535b7fd7 100644 --- a/src/services/infrastructure/contributor-lookup/contributor.lookup.service.ts +++ b/src/services/infrastructure/contributor-lookup/contributor.lookup.service.ts @@ -1,10 +1,10 @@ import { EntityManager, FindOneOptions, In } from 'typeorm'; +import { isUUID } from 'class-validator'; import { InjectEntityManager } from '@nestjs/typeorm'; import { Inject, LoggerService } from '@nestjs/common'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { User } from '@domain/community/user/user.entity'; import { IUser } from '@domain/community/user/user.interface'; -import { UUID_LENGTH } from '@common/constants/entity.field.length.constants'; import { IContributor } from '@domain/community/contributor/contributor.interface'; import { EntityNotFoundException, @@ -15,6 +15,7 @@ import { Credential, CredentialsSearchInput, ICredential } from '@domain/agent'; import { VirtualContributor } from '@domain/community/virtual-contributor'; import { IOrganization, Organization } from '@domain/community/organization'; import { CommunityContributorType } from '@common/enums/community.contributor.type'; +import { InvalidUUID } from '@common/exceptions/invalid.uuid'; export class ContributorLookupService { constructor( @@ -28,19 +29,31 @@ export class ContributorLookupService { userID: string, options?: FindOneOptions | undefined ): Promise { - let user: IUser | null = null; + const user: IUser | null = await this.entityManager.findOne(User, { + where: { + id: userID, + }, + ...options, + }); - if (userID.length === UUID_LENGTH) { - { - user = await this.entityManager.findOne(User, { - where: { - id: userID, - }, - ...options, - }); - } + return user; + } + public async getUserByNameIdOrFail( + userNameID: string, + options?: FindOneOptions | undefined + ): Promise { + const user: IUser | null = await this.entityManager.findOne(User, { + where: { + nameID: userNameID, + }, + ...options, + }); + if (!user) { + throw new EntityNotFoundException( + `User with nameId ${userNameID} not found`, + LogContext.COMMUNITY + ); } - return user; } @@ -58,23 +71,37 @@ export class ContributorLookupService { return user; } - async getOrganization( + async getOrganizationByUUID( organizationID: string, options?: FindOneOptions ): Promise { - let organization: IOrganization | null; - if (organizationID.length === UUID_LENGTH) { - organization = await this.entityManager.findOne(Organization, { + const organization: IOrganization | null = await this.entityManager.findOne( + Organization, + { ...options, where: { ...options?.where, id: organizationID }, - }); - } else { - // look up based on nameID - organization = await this.entityManager.findOne(Organization, { + } + ); + + return organization; + } + + async getOrganizationByNameIdOrFail( + organizationNameID: string, + options?: FindOneOptions + ): Promise { + const organization: IOrganization | null = await this.entityManager.findOne( + Organization, + { ...options, - where: { ...options?.where, nameID: organizationID }, - }); - } + where: { ...options?.where, nameID: organizationNameID }, + } + ); + if (!organization) + throw new EntityNotFoundException( + `Unable to find Organization with NameID: ${organizationNameID}`, + LogContext.COMMUNITY + ); return organization; } @@ -82,7 +109,10 @@ export class ContributorLookupService { organizationID: string, options?: FindOneOptions ): Promise { - const organization = await this.getOrganization(organizationID, options); + const organization = await this.getOrganizationByUUID( + organizationID, + options + ); if (!organization) throw new EntityNotFoundException( `Unable to find Organization with ID: ${organizationID}`, @@ -257,55 +287,43 @@ export class ContributorLookupService { .concat(vcContributors); } - async getContributor( + async getContributorByUUID( contributorID: string, options?: FindOneOptions ): Promise { - let contributor: IContributor | null; - if (contributorID.length === UUID_LENGTH) { - contributor = await this.entityManager.findOne(User, { + if (!isUUID(contributorID)) { + throw new InvalidUUID('Invalid UUID provided!', LogContext.COMMUNITY, { + provided: contributorID, + }); + } + let contributor: IContributor | null = await this.entityManager.findOne( + User, + { ...options, where: { ...options?.where, id: contributorID }, - }); - if (!contributor) { - contributor = await this.entityManager.findOne(Organization, { - ...options, - where: { ...options?.where, id: contributorID }, - }); - } - if (!contributor) { - contributor = await this.entityManager.findOne(VirtualContributor, { - ...options, - where: { ...options?.where, id: contributorID }, - }); } - } else { - // look up based on nameID - contributor = await this.entityManager.findOne(User, { + ); + if (!contributor) { + contributor = await this.entityManager.findOne(Organization, { ...options, - where: { ...options?.where, nameID: contributorID }, + where: { ...options?.where, id: contributorID }, + }); + } + if (!contributor) { + contributor = await this.entityManager.findOne(VirtualContributor, { + ...options, + where: { ...options?.where, id: contributorID }, }); - if (!contributor) { - contributor = await this.entityManager.findOne(Organization, { - ...options, - where: { ...options?.where, nameID: contributorID }, - }); - } - if (!contributor) { - contributor = await this.entityManager.findOne(VirtualContributor, { - ...options, - where: { ...options?.where, nameID: contributorID }, - }); - } } + return contributor; } - async getContributorOrFail( + async getContributorByUuidOrFail( contributorID: string, options?: FindOneOptions ): Promise { - const contributor = await this.getContributor(contributorID, options); + const contributor = await this.getContributorByUUID(contributorID, options); if (!contributor) throw new EntityNotFoundException( `Unable to find Contributor with ID: ${contributorID}`, diff --git a/src/services/infrastructure/entity-resolver/community.resolver.service.ts b/src/services/infrastructure/entity-resolver/community.resolver.service.ts index fe1d6be181..4e3298512b 100644 --- a/src/services/infrastructure/entity-resolver/community.resolver.service.ts +++ b/src/services/infrastructure/entity-resolver/community.resolver.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; -import { EntityManager, Repository } from 'typeorm'; +import { EntityManager, FindOneOptions, Repository } from 'typeorm'; import { Community, ICommunity } from '@domain/community/community'; import { EntityNotFoundException } from '@common/exceptions'; import { LogContext } from '@common/enums'; @@ -22,19 +22,42 @@ export class CommunityResolverService { private entityManager: EntityManager ) {} - private async getRootSpaceFromCommunity(community: ICommunity) { - return this.entityManager.findOne(Space, { + public async getLevelZeroSpaceIdForCommunity( + communityID: string + ): Promise { + const space = await this.entityManager.findOne(Space, { where: { community: { - id: community.id, + id: communityID, }, }, - relations: { - account: { - space: true, + }); + if (!space) { + throw new EntityNotFoundException( + `Unable to find Space for given community id: ${communityID}`, + LogContext.COMMUNITY + ); + } + return space.levelZeroSpaceID; + } + + public async getLevelZeroSpaceIdForCollaboration( + collaborationID: string + ): Promise { + const space = await this.entityManager.findOne(Space, { + where: { + collaboration: { + id: collaborationID, }, }, }); + if (!space) { + throw new EntityNotFoundException( + `Unable to find Space for given collaboration id: ${collaborationID}`, + LogContext.COMMUNITY + ); + } + return space.levelZeroSpaceID; } public async isCommunityAccountMatchingVcAccount( @@ -80,54 +103,6 @@ export class CommunityResolverService { return virtualContributor.account.id === accountID; } - public async getRootSpaceIDFromCalloutOrFail( - calloutID: string - ): Promise { - const space = await this.entityManager.findOne(Space, { - where: { - collaboration: { - callouts: { - id: calloutID, - }, - }, - }, - relations: { - account: { - space: true, - }, - }, - }); - if (!space) { - throw new EntityNotFoundException( - `Unable to find space for callout: ${calloutID}`, - LogContext.COMMUNITY - ); - } - const rootSpace = space.account.space; - if (!rootSpace) { - throw new EntityNotFoundException( - `Unable to find rootSpace for Callout: ${calloutID} in space ${space.id}`, - LogContext.COMMUNITY - ); - } - return rootSpace.id; - } - - public async getRootSpaceIDFromCommunityOrFail( - community: ICommunity - ): Promise { - const space = await this.getRootSpaceFromCommunity(community); - - if (space && space.account && space.account.space) { - return space.account.space.id; - } - - throw new EntityNotFoundException( - `Unable to find Space for given community id: ${community.id}`, - LogContext.COLLABORATION - ); - } - public async getAccountAgentFromCommunityOrFail( community: ICommunity ): Promise { @@ -318,6 +293,30 @@ export class CommunityResolverService { return space; } + public async getSpaceForCollaborationOrFail( + collaborationID: string, + options?: FindOneOptions + ): Promise { + const space = await this.entityManager.findOne(Space, { + where: { + collaboration: { + id: collaborationID, + }, + }, + relations: { + ...options?.relations, + profile: true, + }, + }); + if (!space) { + throw new EntityNotFoundException( + `Unable to find space for collaboration: ${collaborationID}`, + LogContext.SPACES + ); + } + return space; + } + public async getDisplayNameForCommunityOrFail( communityId: string ): Promise { diff --git a/src/services/infrastructure/naming/naming.module.ts b/src/services/infrastructure/naming/naming.module.ts index 8afe890c69..0940c2c99a 100644 --- a/src/services/infrastructure/naming/naming.module.ts +++ b/src/services/infrastructure/naming/naming.module.ts @@ -1,14 +1,12 @@ import { Module } from '@nestjs/common'; import { NamingService } from './naming.service'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Callout } from '@domain/collaboration/callout/callout.entity'; import { CalendarEvent } from '@domain/timeline/event'; import { InnovationHub } from '@domain/innovation-hub/innovation.hub.entity'; import { Discussion } from '@platform/forum-discussion/discussion.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([Callout]), TypeOrmModule.forFeature([CalendarEvent]), TypeOrmModule.forFeature([Discussion]), TypeOrmModule.forFeature([InnovationHub]), diff --git a/src/services/infrastructure/naming/naming.service.ts b/src/services/infrastructure/naming/naming.service.ts index 91e85c8ebf..e1c47313f3 100644 --- a/src/services/infrastructure/naming/naming.service.ts +++ b/src/services/infrastructure/naming/naming.service.ts @@ -19,18 +19,16 @@ import { Space } from '@domain/space/space/space.entity'; import { ISpaceSettings } from '@domain/space/space.settings/space.settings.interface'; import { SpaceLevel } from '@common/enums/space.level'; import { User } from '@domain/community/user/user.entity'; -import { InnovationPack } from '@library/innovation-pack/innovation.pack.entity'; import { VirtualContributor } from '@domain/community/virtual-contributor'; import { Organization } from '@domain/community/organization'; import { Discussion } from '@platform/forum-discussion/discussion.entity'; import { IDiscussion } from '@platform/forum-discussion/discussion.interface'; +import { SpaceReservedName } from '@common/enums/space.reserved.name'; export class NamingService { replaceSpecialCharacters = require('replace-special-characters'); constructor( - @InjectRepository(Callout) - private calloutRepository: Repository, @InjectRepository(Discussion) private discussionRepository: Repository, @InjectRepository(InnovationHub) @@ -40,14 +38,12 @@ export class NamingService { @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} - public async getReservedNameIDsInAccount( - accountID: string + public async getReservedNameIDsInLevelZeroSpace( + levelZeroSpaceID: string ): Promise { const subspaces = await this.entityManager.find(Space, { where: { - account: { - id: accountID, - }, + levelZeroSpaceID: levelZeroSpaceID, level: Not(SpaceLevel.SPACE), }, select: { @@ -58,6 +54,21 @@ export class NamingService { return nameIDs; } + public async getReservedNameIDsLevelZeroSpaces(): Promise { + const levelZeroSpaces = await this.entityManager.find(Space, { + where: { + level: SpaceLevel.SPACE, + }, + select: { + nameID: true, + }, + }); + const nameIDs = levelZeroSpaces.map(space => space.nameID.toLowerCase()); + const reservedTopLevelSpaces = Object.values(SpaceReservedName) as string[]; + + return nameIDs.concat(reservedTopLevelSpaces); + } + public async getReservedNameIDsInForum(forumID: string): Promise { const discussions = await this.entityManager.find(Discussion, { where: { @@ -107,23 +118,6 @@ export class NamingService { return nameIDs; } - public async getReservedNameIDsInLibrary( - libraryID: string - ): Promise { - const innovationPacks = await this.entityManager.find(InnovationPack, { - where: { - library: { - id: libraryID, - }, - }, - select: { - nameID: true, - }, - }); - const nameIDs = innovationPacks?.map(pack => pack.nameID) || []; - return nameIDs; - } - public async getReservedNameIDsInHubs(): Promise { const hubs = await this.entityManager.find(InnovationHub, { select: { diff --git a/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts b/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts index 2fced468b5..07dbf0945e 100644 --- a/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts +++ b/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts @@ -15,6 +15,8 @@ import { StorageAggregatorNotFoundException } from '@common/exceptions/storage.a import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Space } from '@domain/space/space/space.entity'; import { SpaceLevel } from '@common/enums/space.level'; +import { isUUID } from 'class-validator'; +import { InvalidUUID } from '@common/exceptions/invalid.uuid'; @Injectable() export class StorageAggregatorResolverService { @@ -60,15 +62,6 @@ export class StorageAggregatorResolverService { return this.getStorageAggregatorOrFail(result.storageAggregatorId); } - public async getLibraryStorageAggregator(): Promise { - const query = `SELECT \`storageAggregatorId\` - FROM \`library\` LIMIT 1`; - const [result]: { - storageAggregatorId: string; - }[] = await this.entityManager.connection.query(query); - return this.getStorageAggregatorOrFail(result.storageAggregatorId); - } - public async getParentEntityInformation( storageAggregatorID: string ): Promise<{ @@ -105,29 +98,39 @@ export class StorageAggregatorResolverService { public async getStorageAggregatorForTemplatesSet( templatesSetId: string ): Promise { - const space = await this.entityManager.findOne(Space, { - where: { - account: { - library: { - id: templatesSetId, - }, - }, - }, - relations: { - storageAggregator: true, - }, - }); - - if (space && space.storageAggregator) { - return await this.getStorageAggregatorOrFail(space.storageAggregator.id); + // This query is a bit tricky because we have a TemplateSetId and we need to find the StorageAggregator + // associated to it's parent. + // TemplatesSets can be in a Space (the space's templates), or an InnovationPack (the templates of that IP). + // In practice it's just a ManyToMany relationship between Spaces/IPs and the templates associated to them. + + // The parent of Spaces and IPs is an Account, Spaces have a StorageAggregator but Account also has a StorageAggregator. + // So for templatesSets in spaces we return the Space's StoreAggregator, and for templatesSets in IPs we return the Account's StorageAggregator. + + if (!isUUID(templatesSetId)) { + throw new InvalidUUID( + 'Invalid UUID provided to find the StorageAggregator of a templateSet', + LogContext.COMMUNITY, + { provided: templatesSetId } + ); } - const query = `SELECT \`id\` FROM \`innovation_pack\` - WHERE \`innovation_pack\`.\`templatesSetId\`='${templatesSetId}'`; + // We are doing this UNION here, but only one of them will return a result. + const query = ` + SELECT account.storageAggregatorId FROM account + WHERE account.libraryId = '${templatesSetId}' + UNION + SELECT account.storageAggregatorId FROM innovation_pack + JOIN account ON innovation_pack.accountId = account.id + WHERE innovation_pack.templatesSetId = '${templatesSetId}'`; + + // If we want to get the storageAggregator of the space in the first case, we would do: + // SELECT space.storageAggregatorId FROM space + // JOIN account ON space.accountId = account.id + // WHERE space.level = 0 AND account.libraryId = '${templatesSetId}' + const [result] = await this.entityManager.connection.query(query); if (result) { - // use the library sorage aggregator - return await this.getLibraryStorageAggregator(); + return this.getStorageAggregatorOrFail(result.storageAggregatorId); } throw new StorageAggregatorNotFoundException( @@ -147,9 +150,8 @@ export class StorageAggregatorResolverService { public async getStorageAggregatorForCalendar( calendarID: string ): Promise { - const storageAggregatorId = await this.getStorageAggregatorIdForCalendar( - calendarID - ); + const storageAggregatorId = + await this.getStorageAggregatorIdForCalendar(calendarID); return await this.getStorageAggregatorOrFail(storageAggregatorId); } @@ -192,9 +194,8 @@ export class StorageAggregatorResolverService { public async getStorageAggregatorForCommunity( communityID: string ): Promise { - const storageAggregatorId = await this.getStorageAggregatorIdForCommunity( - communityID - ); + const storageAggregatorId = + await this.getStorageAggregatorIdForCommunity(communityID); return await this.getStorageAggregatorOrFail(storageAggregatorId); } @@ -223,9 +224,8 @@ export class StorageAggregatorResolverService { public async getStorageAggregatorForCallout( calloutID: string ): Promise { - const storageAggregatorId = await this.getStorageAggregatorIdForCallout( - calloutID - ); + const storageAggregatorId = + await this.getStorageAggregatorIdForCallout(calloutID); return await this.getStorageAggregatorOrFail(storageAggregatorId); } diff --git a/src/services/infrastructure/url-generator/url.generator.service.ts b/src/services/infrastructure/url-generator/url.generator.service.ts index 8331de3129..8a67e3770f 100644 --- a/src/services/infrastructure/url-generator/url.generator.service.ts +++ b/src/services/infrastructure/url-generator/url.generator.service.ts @@ -803,7 +803,6 @@ export class UrlGeneratorService { this.FIELD_PROFILE_ID, profileID ); - return `${this.endpoint_cluster}/${this.PATH_INNOVATION_PACKS}/${innovationPackInfo.entityNameID}`; } diff --git a/src/services/whiteboard-integration/whiteboard.integration.service.ts b/src/services/whiteboard-integration/whiteboard.integration.service.ts index a65efc2e4c..639d57655d 100644 --- a/src/services/whiteboard-integration/whiteboard.integration.service.ts +++ b/src/services/whiteboard-integration/whiteboard.integration.service.ts @@ -108,8 +108,10 @@ export class WhiteboardIntegrationService { await this.communityResolver.getCommunityFromWhiteboardOrFail( whiteboardId ); - const spaceID = - await this.communityResolver.getRootSpaceIDFromCommunityOrFail(community); + const levelZeroSpaceID = + await this.communityResolver.getLevelZeroSpaceIdForCommunity( + community.id + ); const wb = await this.whiteboardService.getProfile(whiteboardId); users.forEach(({ id, email }) => { @@ -117,7 +119,7 @@ export class WhiteboardIntegrationService { { id: whiteboardId, name: wb.displayName, - space: spaceID, + space: levelZeroSpaceID, }, { id, email } ); diff --git a/test/mocks/community.resolver.service.mock.ts b/test/mocks/community.resolver.service.mock.ts index ff1eb8b03f..139f0b7b50 100644 --- a/test/mocks/community.resolver.service.mock.ts +++ b/test/mocks/community.resolver.service.mock.ts @@ -6,6 +6,6 @@ export const MockCommunityResolverService: MockValueProvider> = { provide: SpaceService, useValue: { getSpaceOrFail: jest.fn(), - getSpaceForCommunityOrFail: jest.fn(), }, };