5899 display a banner to alert users which need to reconnect their account (#6301)

Closes #5899

<img width="1280" alt="Index - banner"
src="https://github.com/twentyhq/twenty/assets/71827178/313cf20d-eb34-496a-8c7c-7589fbd55954">

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
bosiraphael
2024-07-27 18:34:52 +02:00
committed by GitHub
parent d0db3b765f
commit 6728e40256
48 changed files with 910 additions and 147 deletions

View File

@ -1,41 +1,42 @@
/* eslint-disable no-restricted-imports */
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { HttpModule } from '@nestjs/axios';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { GoogleAPIsAuthController } from 'src/engine/core-modules/auth/controllers/google-apis-auth.controller';
import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller';
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
import { VerifyAuthController } from 'src/engine/core-modules/auth/controllers/verify-auth.controller';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { User } from 'src/engine/core-modules/user/user.entity';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { AuthResolver } from './auth.resolver';
import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
import { AuthService } from './services/auth.service';
import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
const jwtModule = JwtModule.registerAsync({
useFactory: async (environmentService: EnvironmentService) => {
return {
@ -70,6 +71,7 @@ const jwtModule = JwtModule.registerAsync({
OnboardingModule,
TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]),
WorkspaceDataSourceModule,
ConnectedAccountModule,
],
controllers: [
GoogleAuthController,

View File

@ -18,6 +18,7 @@ import {
CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import {
ConnectedAccountProvider,
ConnectedAccountWorkspaceEntity,
@ -33,6 +34,7 @@ import {
MessagingMessageListFetchJob,
MessagingMessageListFetchJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class GoogleAPIsService {
@ -47,6 +49,7 @@ export class GoogleAPIsService {
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelRepository: MessageChannelRepository,
private readonly accountsToReconnectService: AccountsToReconnectService,
) {}
async refreshGoogleRefreshToken(input: {
@ -139,6 +142,23 @@ export class GoogleAPIsService {
manager,
);
const workspaceMemberRepository =
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
'workspaceMember',
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: { id: workspaceMemberId },
});
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
await this.messageChannelRepository.resetSync(
newOrExistingConnectedAccountId,
workspaceId,

View File

@ -1,5 +1,6 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
@ -12,12 +13,17 @@ import {
Unique,
UpdateDateColumn,
} from 'typeorm';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
export enum KeyValuePairType {
USER_VAR = 'USER_VAR',
FEATURE_FLAG = 'FEATURE_FLAG',
SYSTEM_VAR = 'SYSTEM_VAR',
}
@Entity({ name: 'keyValuePair', schema: 'core' })
@ObjectType('KeyValuePair')
@Unique('IndexOnKeyUserIdWorkspaceIdUnique', ['key', 'userId', 'workspaceId'])
@ -41,7 +47,7 @@ export class KeyValuePair {
user: Relation<User>;
@Column({ nullable: true })
userId: string;
userId: string | null;
@ManyToOne(() => Workspace, (workspace) => workspace.keyValuePairs, {
onDelete: 'CASCADE',
@ -50,15 +56,28 @@ export class KeyValuePair {
workspace: Relation<Workspace>;
@Column({ nullable: true })
workspaceId: string;
workspaceId: string | null;
@Field(() => String)
@Column({ nullable: false, type: 'text' })
key: string;
@Field(() => String, { nullable: true })
@Column({ nullable: true, type: 'text' })
value: string;
@Field(() => JSON, { nullable: true })
@Column('jsonb', { nullable: true })
value: JSON;
@Field(() => String)
@Column({ nullable: false, type: 'text' })
textValueDeprecated: string;
@Field(() => KeyValuePairType)
@Column({
type: 'enum',
enum: Object.values(KeyValuePairType),
nullable: false,
default: KeyValuePairType.USER_VAR,
})
type: KeyValuePairType;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;

View File

@ -1,59 +1,79 @@
import { InjectRepository } from '@nestjs/typeorm';
import { BadRequestException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { IsNull, Repository } from 'typeorm';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import {
KeyValuePair,
KeyValuePairType,
} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
export class KeyValuePairService<TYPE> {
export class KeyValuePairService<
KeyValueTypesMap extends Record<string, any> = Record<string, any>,
> {
constructor(
@InjectRepository(KeyValuePair, 'core')
private readonly keyValuePairRepository: Repository<KeyValuePair>,
) {}
async get<K extends keyof TYPE>({
async get<K extends keyof KeyValueTypesMap>({
userId,
workspaceId,
type,
key,
}: {
userId?: string;
workspaceId?: string;
key: K;
}): Promise<TYPE[K] | undefined> {
return (
await this.keyValuePairRepository.findOne({
where: {
userId,
workspaceId,
key: key as string,
},
})
)?.value as TYPE[K] | undefined;
userId?: string | null;
workspaceId?: string | null;
type: KeyValuePairType;
key?: Extract<K, string>;
}): Promise<Array<KeyValueTypesMap[K]>> {
const keyValuePairs = (await this.keyValuePairRepository.find({
where: {
...(userId === undefined
? {}
: userId === null
? { userId: IsNull() }
: { userId }),
...(workspaceId === undefined
? {}
: workspaceId === null
? { workspaceId: IsNull() }
: { workspaceId }),
...(key === undefined ? {} : { key }),
type,
},
})) as Array<KeyValueTypesMap[K]>;
return keyValuePairs.map((keyValuePair) => ({
...keyValuePair,
value: keyValuePair.value ?? keyValuePair.textValueDeprecated,
}));
}
async set<K extends keyof TYPE>({
async set<K extends keyof KeyValueTypesMap>({
userId,
workspaceId,
key,
value,
type,
}: {
userId?: string;
workspaceId?: string;
key: K;
value: TYPE[K];
userId?: string | null;
workspaceId?: string | null;
key: Extract<K, string>;
value: KeyValueTypesMap[K];
type: KeyValuePairType;
}) {
if (!userId && !workspaceId) {
throw new BadRequestException('userId and workspaceId are undefined');
}
const upsertData = {
userId,
workspaceId,
key: key as string,
value: value as string,
key,
value,
type,
};
const conflictPaths = Object.keys(upsertData).filter(
(key) => key !== 'value' && upsertData[key] !== undefined,
(key) =>
['userId', 'workspaceId', 'key'].includes(key) &&
upsertData[key] !== undefined,
);
const indexPredicate = !userId
@ -67,4 +87,31 @@ export class KeyValuePairService<TYPE> {
indexPredicate,
});
}
async delete({
userId,
workspaceId,
type,
key,
}: {
userId?: string | null;
workspaceId?: string | null;
type: KeyValuePairType;
key: Extract<keyof KeyValueTypesMap, string>;
}) {
await this.keyValuePairRepository.delete({
...(userId === undefined
? {}
: userId === null
? { userId: IsNull() }
: { userId }),
...(workspaceId === undefined
? {}
: workspaceId === null
? { workspaceId: IsNull() }
: { workspaceId }),
type,
key,
});
}
}

View File

@ -1,22 +1,22 @@
import { Module } from '@nestjs/common';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboarding.resolver';
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({
imports: [
DataSourceModule,
WorkspaceManagerModule,
UserWorkspaceModule,
KeyValuePairModule,
EnvironmentModule,
BillingModule,
UserVarsModule,
],
exports: [OnboardingService],
providers: [OnboardingService, OnboardingResolver],

View File

@ -3,9 +3,9 @@ import { Injectable } from '@nestjs/common';
import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing.workspace-service';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
@ -25,7 +25,7 @@ enum OnboardingStepKeys {
INVITE_TEAM_ONBOARDING_STEP = 'INVITE_TEAM_ONBOARDING_STEP',
}
type OnboardingKeyValueType = {
type OnboardingKeyValueTypeMap = {
[OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP]: OnboardingStepValues;
[OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP]: OnboardingStepValues;
};
@ -37,7 +37,7 @@ export class OnboardingService {
private readonly billingWorkspaceService: BillingWorkspaceService,
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly keyValuePairService: KeyValuePairService<OnboardingKeyValueType>,
private readonly userVarsService: UserVarsService<OnboardingKeyValueTypeMap>,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
) {}
@ -86,7 +86,7 @@ export class OnboardingService {
}
private async isSyncEmailOnboardingStatus(user: User) {
const syncEmailValue = await this.keyValuePairService.get({
const syncEmailValue = await this.userVarsService.get({
userId: user.id,
workspaceId: user.defaultWorkspaceId,
key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP,
@ -102,7 +102,7 @@ export class OnboardingService {
}
private async isInviteTeamOnboardingStatus(workspace: Workspace) {
const inviteTeamValue = await this.keyValuePairService.get({
const inviteTeamValue = await this.userVarsService.get({
workspaceId: workspace.id,
key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP,
});
@ -143,7 +143,7 @@ export class OnboardingService {
}
async skipInviteTeamOnboardingStep(workspaceId: string) {
await this.keyValuePairService.set({
await this.userVarsService.set({
workspaceId,
key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP,
value: OnboardingStepValues.SKIPPED,
@ -151,7 +151,7 @@ export class OnboardingService {
}
async skipSyncEmailOnboardingStep(userId: string, workspaceId: string) {
await this.keyValuePairService.set({
await this.userVarsService.set({
userId,
workspaceId,
key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP,

View File

@ -1,18 +1,18 @@
import { InjectRepository } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { assert } from 'src/utils/assert';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { assert } from 'src/utils/assert';
export class UserService extends TypeOrmQueryService<User> {
constructor(

View File

@ -0,0 +1,110 @@
import { Injectable } from '@nestjs/common';
import { KeyValuePairType } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
import { mergeUserVars } from 'src/engine/core-modules/user/user-vars/utils/merge-user-vars.util';
@Injectable()
export class UserVarsService<
KeyValueTypesMap extends Record<string, any> = Record<string, any>,
> {
constructor(private readonly keyValuePairService: KeyValuePairService) {}
public async get<K extends keyof KeyValueTypesMap>({
userId,
workspaceId,
key,
}: {
userId?: string;
workspaceId?: string;
key: Extract<K, string>;
}): Promise<KeyValueTypesMap[K]> {
const userVarWorkspaceLevel = await this.keyValuePairService.get({
type: KeyValuePairType.USER_VAR,
userId: null,
workspaceId,
key,
});
if (userVarWorkspaceLevel.length > 1) {
throw new Error(
`Multiple values found for key ${key} at workspace level`,
);
}
const userVarUserLevel = await this.keyValuePairService.get({
type: KeyValuePairType.USER_VAR,
userId,
key,
});
if (userVarUserLevel.length > 1) {
throw new Error(`Multiple values found for key ${key} at user level`);
}
return mergeUserVars([...userVarUserLevel, ...userVarWorkspaceLevel]).get(
key,
) as KeyValueTypesMap[K];
}
public async getAll({
userId,
workspaceId,
}: {
userId?: string;
workspaceId?: string;
}): Promise<Map<Extract<keyof KeyValueTypesMap, string>, any>> {
const userVarsWorkspaceLevel = await this.keyValuePairService.get({
type: KeyValuePairType.USER_VAR,
userId: null,
workspaceId,
});
const userVarsUserLevel = await this.keyValuePairService.get({
type: KeyValuePairType.USER_VAR,
userId,
});
return mergeUserVars<Extract<keyof KeyValueTypesMap, string>>([
...userVarsWorkspaceLevel,
...userVarsUserLevel,
]);
}
set<K extends keyof KeyValueTypesMap>({
userId,
workspaceId,
key,
value,
}: {
userId?: string;
workspaceId?: string;
key: Extract<K, string>;
value: KeyValueTypesMap[K];
}) {
return this.keyValuePairService.set({
userId,
workspaceId,
key: key,
value,
type: KeyValuePairType.USER_VAR,
});
}
async delete({
userId,
workspaceId,
key,
}: {
userId?: string;
workspaceId?: string;
key: Extract<keyof KeyValueTypesMap, string>;
}) {
return this.keyValuePairService.delete({
userId,
workspaceId,
key,
type: KeyValuePairType.USER_VAR,
});
}
}

View File

@ -0,0 +1,12 @@
/* eslint-disable no-restricted-imports */
import { Module } from '@nestjs/common';
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
@Module({
imports: [KeyValuePairModule],
exports: [UserVarsService],
providers: [UserVarsService],
})
export class UserVarsModule {}

View File

@ -0,0 +1,128 @@
import { mergeUserVars } from 'src/engine/core-modules/user/user-vars/utils/merge-user-vars.util';
describe('mergeUserVars', () => {
it('should merge user vars correctly', () => {
const userVars = [
{
key: 'key1',
value: JSON.parse('"value1"'),
userId: 'userId1',
workspaceId: 'workspaceId1',
},
{
key: 'key2',
value: JSON.parse('"value2"'),
userId: 'userId1',
workspaceId: null,
},
{
key: 'key3',
value: JSON.parse('"value3"'),
userId: null,
workspaceId: 'workspaceId1',
},
];
const mergedUserVars = mergeUserVars(userVars);
expect(mergedUserVars).toEqual(
new Map([
['key1', JSON.parse('"value1"')],
['key2', JSON.parse('"value2"')],
['key3', JSON.parse('"value3"')],
]),
);
});
it('should merge user vars correctly when user vars are empty', () => {
const userVars = [];
const mergedUserVars = mergeUserVars(userVars);
expect(mergedUserVars).toEqual(new Map());
});
it('should overwrite user vars correctly', () => {
const userVars1 = [
{
key: 'key',
value: JSON.parse('"value1"'),
userId: 'userId',
workspaceId: 'workspaceId',
},
{
key: 'key',
value: JSON.parse('"value2"'),
userId: 'userId',
workspaceId: null,
},
{
key: 'key',
value: JSON.parse('"value3"'),
userId: null,
workspaceId: 'workspaceId',
},
];
const mergedUserVars1 = mergeUserVars(userVars1);
const userVars2 = [
{
key: 'key',
value: JSON.parse('"value1"'),
userId: 'userId',
workspaceId: 'workspaceId',
},
{
key: 'key',
value: JSON.parse('"value2"'),
userId: 'userId',
workspaceId: null,
},
];
const mergedUserVars2 = mergeUserVars(userVars2);
const userVars3 = [
{
key: 'key',
value: JSON.parse('"value2"'),
userId: 'userId',
workspaceId: null,
},
{
key: 'key',
value: JSON.parse('"value3"'),
userId: null,
workspaceId: 'workspaceId',
},
];
const mergedUserVars3 = mergeUserVars(userVars3);
const userVars4 = [
{
key: 'key',
value: JSON.parse('"value1"'),
userId: 'userId',
workspaceId: 'workspaceId',
},
{
key: 'key',
value: JSON.parse('"value3"'),
userId: null,
workspaceId: 'workspaceId',
},
];
const mergedUserVars4 = mergeUserVars(userVars4);
expect(mergedUserVars1).toEqual(new Map([['key', JSON.parse('"value1"')]]));
expect(mergedUserVars2).toEqual(new Map([['key', JSON.parse('"value1"')]]));
expect(mergedUserVars3).toEqual(new Map([['key', JSON.parse('"value2"')]]));
expect(mergedUserVars4).toEqual(new Map([['key', JSON.parse('"value1"')]]));
});
});

View File

@ -0,0 +1,31 @@
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
export const mergeUserVars = <T>(
userVars: Pick<KeyValuePair, 'key' | 'value' | 'userId' | 'workspaceId'>[],
): Map<T, JSON> => {
const workspaceUserVarMap = new Map<T, JSON>();
const userUserVarMap = new Map<T, JSON>();
const userWorkspaceUserVarMap = new Map<T, JSON>();
for (const { key, value, userId, workspaceId } of userVars) {
if (!userId && workspaceId) {
workspaceUserVarMap.set(key as T, value);
}
if (userId && !workspaceId) {
userUserVarMap.set(key as T, value);
}
if (userId && workspaceId) {
userWorkspaceUserVarMap.set(key as T, value);
}
}
const mergedUserVars = new Map<T, JSON>([
...workspaceUserVarMap,
...userUserVarMap,
...userWorkspaceUserVarMap,
]);
return mergedUserVars;
};

View File

@ -1,17 +1,20 @@
/* eslint-disable no-restricted-imports */
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
import { User } from 'src/engine/core-modules/user/user.entity';
import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
@ -30,6 +33,8 @@ import { UserService } from './services/user.service';
FileUploadModule,
WorkspaceModule,
OnboardingModule,
TypeOrmModule.forFeature([KeyValuePair], 'core'),
UserVarsModule,
],
exports: [UserService],
providers: [UserService, UserResolver, TypeORMService],

View File

@ -1,3 +1,4 @@
import { UseGuards } from '@nestjs/common';
import {
Args,
Mutation,
@ -6,30 +7,33 @@ import {
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import crypto from 'crypto';
import { GraphQLJSONObject } from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { Repository } from 'typeorm';
import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { assert } from 'src/utils/assert';
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
import { assert } from 'src/utils/assert';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
@ -50,13 +54,14 @@ export class UserResolver {
private readonly fileUploadService: FileUploadService,
private readonly onboardingService: OnboardingService,
private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext,
private readonly userVarService: UserVarsService,
) {}
@Query(() => User)
async currentUser(@AuthUser() { id }: User): Promise<User> {
async currentUser(@AuthUser() { id: userId }: User): Promise<User> {
const user = await this.userRepository.findOne({
where: {
id,
id: userId,
},
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
});
@ -66,6 +71,28 @@ export class UserResolver {
return user;
}
@ResolveField(() => GraphQLJSONObject)
async userVars(
@Parent() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<Record<string, any>> {
const userVars = await this.userVarService.getAll({
userId: user.id,
workspaceId: workspace?.id ?? user.defaultWorkspaceId,
});
const userVarAllowList = [
'SYNC_EMAIL_ONBOARDING_STEP',
'ACCOUNTS_TO_RECONNECT',
];
const filteredMap = new Map(
[...userVars].filter(([key]) => userVarAllowList.includes(key)),
);
return Object.fromEntries(filteredMap);
}
@ResolveField(() => WorkspaceMember, {
nullable: true,
})