Skip to content

Commit

Permalink
Merge branch 'develop' into wingback
Browse files Browse the repository at this point in the history
  • Loading branch information
valentinyanakiev committed Dec 11, 2024
2 parents b846c35 + f67bd11 commit 0461e4e
Show file tree
Hide file tree
Showing 69 changed files with 1,947 additions and 871 deletions.
3 changes: 3 additions & 0 deletions alkemio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ storage:
# MySQL username.
username: 'root'

# The timezone in which typeorm will interpret the dates. Default is Z for UTC
timezone: ${TYPEORM_TIMEZONE}:Z

# MySQL password.
password: ${MYSQL_ROOT_PASSWORD}:toor

Expand Down
1,251 changes: 523 additions & 728 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "alkemio-server",
"version": "0.97.0",
"version": "0.98.0",
"description": "Alkemio server, responsible for managing the shared Alkemio platform",
"author": "Alkemio Foundation",
"private": false,
Expand Down Expand Up @@ -56,7 +56,7 @@
},
"dependencies": {
"@alkemio/matrix-adapter-lib": "^0.4.1",
"@alkemio/notifications-lib": "^0.9.7",
"@alkemio/notifications-lib": "0.10.0",
"@apollo/server": "^4.10.4",
"@elastic/elasticsearch": "^8.12.2",
"@golevelup/nestjs-rabbitmq": "^5.3.0",
Expand Down Expand Up @@ -87,7 +87,7 @@
"class-validator": "0.14.0",
"cookie-parser": "^1.4.6",
"cross-env": "^7.0.3",
"dataloader": "^2.2.2",
"dataloader": "^2.2.3",
"graphql": "^16.9.0",
"graphql-amqp-subscriptions": "^2.0.0",
"graphql-helix": "^1.13.0",
Expand Down
7 changes: 5 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ import {
UnhandledExceptionFilter,
} from '@core/error-handling';
import { MeModule } from '@services/api/me';
import { ExcalidrawServerModule } from '@services/external/excalidraw-backend';
import { ChatGuidanceModule } from '@services/api/chat-guidance/chat.guidance.module';
import { LookupModule } from '@services/api/lookup';
import { AuthResetSubscriberModule } from '@services/auth-reset/subscriber/auth-reset.subscriber.module';
Expand All @@ -85,6 +84,8 @@ import { TemplateApplierModule } from '@domain/template/template-applier/templat
import { LoaderCreatorModule } from '@core/dataloader/creators/loader.creator.module';
import { Cipher, EncryptionModule } from '@hedger/nestjs-encryption';
import { AdminUsersModule } from '@platform/admin/users/admin.users.module';
import { InAppNotificationReaderModule } from '@domain/in-app-notification-reader/in.app.notification.reader.module';
import { InAppNotificationReceiverModule } from '@domain/in-app-notification-receiver';
import { LicensingWingbackSubscriptionModule } from '@platform/licensing/wingback-subscription/licensing.wingback.subscription.module';
import { WingbackManagerModule } from '@services/external/wingback/wingback.manager.module';

Expand Down Expand Up @@ -141,6 +142,7 @@ import { WingbackManagerModule } from '@services/external/wingback/wingback.mana
entities: [join(__dirname, '**', '*.entity.{ts,js}')],
host: dbOptions.host,
port: dbOptions.port,
timezone: dbOptions.timezone,
charset: dbOptions.charset,
username: dbOptions.username,
password: dbOptions.password,
Expand Down Expand Up @@ -285,7 +287,6 @@ import { WingbackManagerModule } from '@services/external/wingback/wingback.mana
SsiCredentialFlowModule,
StorageAccessModule,
MeModule,
ExcalidrawServerModule,
ChatGuidanceModule,
VirtualContributorModule,
InputCreatorModule,
Expand All @@ -299,6 +300,8 @@ import { WingbackManagerModule } from '@services/external/wingback/wingback.mana
FileIntegrationModule,
PlatformSettingsModule,
TemplateApplierModule,
InAppNotificationReaderModule,
InAppNotificationReceiverModule,
],
controllers: [AppController, SsiCredentialFlowController],
providers: [
Expand Down
1 change: 1 addition & 0 deletions src/common/enums/logging.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum LogContext {
COLLABORATION = 'collaboration',
AGENT = 'agent',
ACTIVITY = 'activity',
IN_APP_NOTIFICATION = 'in-app-notification',
ACTIVITY_FEED = 'activity-feed',
API = 'api',
AUTH = 'auth',
Expand Down
1 change: 1 addition & 0 deletions src/common/enums/messaging.queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export enum MessagingQueue {
//
WHITEBOARDS = 'alkemio-whiteboards',
FILES = 'alkemio-files',
IN_APP_NOTIFICATIONS = 'alkemio-in-app-notifications',
}
5 changes: 3 additions & 2 deletions src/common/exceptions/forbidden.exception.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { AlkemioErrorStatus, LogContext } from '@common/enums';
import { BaseException } from './base.exception';
import { ExceptionDetails } from '@common/exceptions/exception.details';

export class ForbiddenException extends BaseException {
constructor(error: string, context: LogContext, code?: AlkemioErrorStatus) {
super(error, context, code ?? AlkemioErrorStatus.FORBIDDEN);
constructor(error: string, context: LogContext, details?: ExceptionDetails) {
super(error, context, AlkemioErrorStatus.FORBIDDEN, details);
}
}
25 changes: 25 additions & 0 deletions src/common/utils/compare.enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Compares two TypeScript enums for equality
* @param enum1 First enum to compare
* @param enum2 Second enum to compare
* @returns boolean indicating if enums are equal
*/
export const compareEnums = <T extends { [key: string]: string | number }>(
enum1: T,
enum2: T
): boolean => {
const keys1 = Object.keys(enum1);
const keys2 = Object.keys(enum2);

if (keys1.length !== keys2.length) {
return false;
}

for (const key of keys1) {
if (enum1[key] !== enum2[key]) {
return false;
}
}

return true;
};
1 change: 1 addition & 0 deletions src/common/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './path.resolve';
export * from './get-differences';
export * from './has-allowed-allowed-fields';
export * from './convert-to-entity';
export * from './compare.enums';
2 changes: 1 addition & 1 deletion src/core/bootstrap/bootstrap.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ export class BootstrapService {

const space = await this.accountService.createSpaceOnAccount(spaceInput);
const spaceAuthorizations =
await this.spaceAuthorizationService.applyAuthorizationPolicy(space);
await this.spaceAuthorizationService.applyAuthorizationPolicy(space.id);
await this.authorizationPolicyService.saveAll(spaceAuthorizations);

const accountEntitlements =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { EntityManager, In } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { DataLoaderCreator } from '@core/dataloader/creators/base';
import { createBatchLoader } from '@core/dataloader/utils';
import { ILoader } from '@core/dataloader/loader.interface';
import { Callout, ICallout } from '@domain/collaboration/callout';

@Injectable()
export class CalloutLoaderCreator implements DataLoaderCreator<ICallout> {
constructor(@InjectEntityManager() private manager: EntityManager) {}

public create(): ILoader<ICallout> {
return createBatchLoader(
this.constructor.name,
Callout.name,
this.calloutInBatch
);
}

private calloutInBatch = (
keys: ReadonlyArray<string>
): Promise<Callout[]> => {
return this.manager.findBy(Callout, { id: In(keys) });
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { EntityManager } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { CommunityContributorType } from '@common/enums/community.contributor.type';
import { DataLoaderCreator } from '@core/dataloader/creators/base';
import { createBatchLoader } from '@core/dataloader/utils';
import { User } from '@domain/community/user/user.entity';
import { Organization } from '@domain/community/organization';
import { VirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.entity';

@Injectable()
export class CommunityTypeLoaderCreator
implements DataLoaderCreator<{ id: string; type: CommunityContributorType }>
{
constructor(@InjectEntityManager() private manager: EntityManager) {}

create() {
return createBatchLoader(
this.constructor.name,
'CommunityContributorType',
this.communityTypeInBatch
);
}

private async communityTypeInBatch(
keys: ReadonlyArray<string>
): Promise<{ id: string; type: CommunityContributorType }[]> {
const result = await this.manager
.createQueryBuilder()
.select('user.id')
.from(User, 'user')
.addSelect('organization.id')
.addFrom(Organization, 'organization')
.addSelect('vc.id')
.addFrom(VirtualContributor, 'vc')
.where('user.id IN (:...ids)', { ids: keys })
.orWhere('organization.id IN (:...ids)', { ids: keys })
.orWhere('vc.id IN (:...ids)', { ids: keys })
.getRawMany<User | Organization | VirtualContributor>();

return result.map(item => {
if (item instanceof User) {
return { id: item.id, type: CommunityContributorType.USER };
}
if (item instanceof Organization) {
return { id: item.id, type: CommunityContributorType.ORGANIZATION };
}
return { id: item.id, type: CommunityContributorType.VIRTUAL };
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { EntityManager, In } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { DataLoaderCreator } from '@core/dataloader/creators/base';
import { createBatchLoader } from '@core/dataloader/utils';
import { ILoader } from '@core/dataloader/loader.interface';
import { IContributor } from '@domain/community/contributor/contributor.interface';
import { User } from '@domain/community/user/user.entity';
import { Organization } from '@domain/community/organization';
import { VirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.entity';
import { IContributorBase } from '@domain/community/contributor';

@Injectable()
export class ContributorLoaderCreator
implements DataLoaderCreator<IContributorBase>
{
constructor(@InjectEntityManager() private manager: EntityManager) {}

public create(): ILoader<IContributor> {
return createBatchLoader(
this.constructor.name,
'Contributor',
this.contributorsInBatch
);
}

private contributorsInBatch = async (
keys: ReadonlyArray<string>
): Promise<(User | Organization | VirtualContributor)[]> => {
// do when we actually need all contributors
// const a = await this.manager.query(`
// SELECT \`user\`.\`id\`, 'User' AS \`type\`
// FROM \`user\`
// WHERE \`user\`.\`id\` IN (${strKeys})
//
// UNION
//
// SELECT \`vc\`.\`id\`, 'VirtualContributor' AS \`type\`
// FROM \`virtual_contributor\` \`vc\`
// WHERE \`vc\`.\`id\` IN (${strKeys})
//
// UNION
//
// SELECT \`organization\`.\`id\`, 'Organization' AS \`type\`
// FROM \`organization\`
// WHERE \`organization\`.\`id\` IN (${strKeys});
// `);

return this.manager.findBy(User, { id: In(keys) });
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EntityManager, In } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { DataLoaderCreator } from '@core/dataloader/creators/base';
import { createBatchLoader } from '@core/dataloader/utils';
import { ILoader } from '@core/dataloader/loader.interface';
import { ISpace } from '@domain/space/space/space.interface';
import { Space } from '@domain/space/space/space.entity';

@Injectable()
export class SpaceLoaderCreator implements DataLoaderCreator<ISpace> {
constructor(@InjectEntityManager() private manager: EntityManager) {}

public create(): ILoader<ISpace> {
return createBatchLoader(
this.constructor.name,
Space.name,
this.spaceInBatch
);
}

private spaceInBatch = (keys: ReadonlyArray<string>): Promise<Space[]> => {
return this.manager.findBy(Space, { id: In(keys) });
};
}
4 changes: 4 additions & 0 deletions src/core/dataloader/creators/loader.creators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ export * from './collaboration/collaboration.timeline.loader.creator';
export * from './journey/journey.collaboration.loader.creator';
export * from './journey/journey.context.loader.creator';
export * from './journey/journey.community.loader.creator';

export * from './in-app-notification/contributor.loader.creator';
export * from './in-app-notification/callout.loader.creator';
export * from './in-app-notification/space.loader.creator';
Empty file.
Empty file.
48 changes: 48 additions & 0 deletions src/core/dataloader/utils/createTypedBatchLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import DataLoader from 'dataloader';
import { EntityNotFoundException } from '@common/exceptions';
import { LogContext } from '@common/enums';
import { ILoader } from '../loader.interface';
import { sorOutputByKeys } from '@core/dataloader/utils/sort.output.by.keys';

export const createBatchLoader = <TResult extends { id: string }>(
name: string, // for debugging purposes
loadedTypeName: string, // for debugging purposes
batchLoadFn: (keys: ReadonlyArray<string>) => Promise<TResult[]>
): ILoader<TResult> => {
// the data loader returns an array the MUST match the input length
// the provided batch function does not necessarily complete this requirement
// so we create a wrapper function that executes the batch function and ensure the output length
// by either returning the original output (if the length matches) or filling the missing values with errors
const loadAndEnsureOutputLengthAndOrder = async (keys: readonly string[]) => {
const unsortedOutput = await batchLoadFn(keys);
const sortedOutput = sorOutputByKeys(unsortedOutput, keys);
if (sortedOutput.length == keys.length) {
// length is ensured
return sortedOutput;
}
// maps each returned result to its id
const resultsById = new Map<string, TResult>(
sortedOutput.map<[string, TResult]>(result => [result.id, result])
);
// ensure the result length matches the input length
return keys.map(
key => resultsById.get(key) ?? resolveUnresolvedForKey(key)
);
};
// a function to resolve an unresolved entity for a given key (e.g. if not found, etc.)
const resolveUnresolvedForKey = (key: string) => {
return new EntityNotFoundException(
`Could not find ${loadedTypeName} for the given key`,
LogContext.DATA_LOADER,
{ id: key }
);
};

return new DataLoader<string, TResult>(
keys => loadAndEnsureOutputLengthAndOrder(keys),
{
cache: true,
name,
}
);
};
1 change: 1 addition & 0 deletions src/core/dataloader/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './findByBatchIds';
export * from './findByBatchIdsSimple';
export * from './createTypedRelationLoader';
export * from './createTypedSimpleLoader';
export * from './createTypedBatchLoader';
export * from './selectOptionsFromFields';
export * from './find.by.batch.options';
15 changes: 15 additions & 0 deletions src/core/dataloader/utils/sort.output.by.keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { sortBy } from 'lodash';

export const sorOutputByKeys = <T extends { id: string }>(
output: T[],
keys: readonly string[]
) => {
const orderMap = keys.reduce(
(acc, key, index) => {
acc[key] = index;
return acc;
},
{} as Record<string, number>
);
return sortBy(output, obj => orderMap[obj.id]);
};
Loading

0 comments on commit 0461e4e

Please sign in to comment.