5622 add a syncemail onboarding step (#5689)

- add sync email onboarding step
- refactor calendar and email visibility enums
- add a new table `keyValuePair` in `core` schema
- add a new resolved boolean field `skipSyncEmail` in current user




https://github.com/twentyhq/twenty/assets/29927851/de791475-5bfe-47f9-8e90-76c349fba56f
This commit is contained in:
martmull
2024-06-05 18:16:53 +02:00
committed by GitHub
parent fda0d2a170
commit 9f6a6c3282
92 changed files with 2707 additions and 1246 deletions

View File

@ -27,6 +27,7 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { UserStateModule } from 'src/engine/core-modules/user-state/user-state.module';
import { AuthResolver } from './auth.resolver';
@ -63,6 +64,7 @@ const jwtModule = JwtModule.registerAsync({
]),
HttpModule,
UserWorkspaceModule,
UserStateModule,
],
controllers: [
GoogleAuthController,

View File

@ -15,6 +15,10 @@ import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/strategies/googl
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
@Controller('auth/google-apis')
export class GoogleAPIsAuthController {
@ -22,6 +26,9 @@ export class GoogleAPIsAuthController {
private readonly googleAPIsService: GoogleAPIsService,
private readonly tokenService: TokenService,
private readonly environmentService: EnvironmentService,
private readonly userStateService: UserStateService,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberService: WorkspaceMemberRepository,
) {}
@Get()
@ -39,7 +46,15 @@ export class GoogleAPIsAuthController {
) {
const { user } = req;
const { email, accessToken, refreshToken, transientToken } = user;
const {
email,
accessToken,
refreshToken,
transientToken,
redirectLocation,
calendarVisibility,
messageVisibility,
} = user;
const { workspaceMemberId, workspaceId } =
await this.tokenService.verifyTransientToken(transientToken);
@ -62,10 +77,25 @@ export class GoogleAPIsAuthController {
workspaceId: workspaceId,
accessToken,
refreshToken,
calendarVisibility,
messageVisibility,
});
const userId = (
await this.workspaceMemberService.find(workspaceMemberId, workspaceId)
)?.userId;
if (userId) {
await this.userStateService.skipSyncEmailOnboardingStep(
userId,
workspaceId,
);
}
return res.redirect(
`${this.environmentService.get('FRONT_BASE_URL')}/settings/accounts`,
`${this.environmentService.get('FRONT_BASE_URL')}${
redirectLocation || '/settings/accounts'
}`,
);
}
}

View File

@ -12,10 +12,26 @@ export class GoogleAPIsOauthGuard extends AuthGuard('google-apis') {
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const transientToken = request.query.transientToken;
const redirectLocation = request.query.redirectLocation;
const calendarVisibility = request.query.calendarVisibility;
const messageVisibility = request.query.messageVisibility;
if (transientToken && typeof transientToken === 'string') {
request.params.transientToken = transientToken;
}
if (redirectLocation && typeof redirectLocation === 'string') {
request.params.redirectLocation = redirectLocation;
}
if (calendarVisibility && typeof calendarVisibility === 'string') {
request.params.calendarVisibility = calendarVisibility;
}
if (messageVisibility && typeof messageVisibility === 'string') {
request.params.messageVisibility = messageVisibility;
}
const activate = (await super.canActivate(context)) as boolean;
return activate;

View File

@ -58,8 +58,16 @@ export class GoogleAPIsService {
workspaceId: string;
accessToken: string;
refreshToken: string;
calendarVisibility: CalendarChannelVisibility | undefined;
messageVisibility: MessageChannelVisibility | undefined;
}) {
const { handle, workspaceId, workspaceMemberId } = input;
const {
handle,
workspaceId,
workspaceMemberId,
calendarVisibility,
messageVisibility,
} = input;
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
@ -104,7 +112,8 @@ export class GoogleAPIsService {
connectedAccountId: newOrExistingConnectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
},
workspaceId,
manager,
@ -116,7 +125,9 @@ export class GoogleAPIsService {
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
handle,
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
visibility:
calendarVisibility ||
CalendarChannelVisibility.SHARE_EVERYTHING,
},
workspaceId,
manager,

View File

@ -5,6 +5,8 @@ import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Request } from 'express';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { CalendarChannelVisibility } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
export type GoogleAPIsRequest = Omit<
Request,
@ -19,6 +21,9 @@ export type GoogleAPIsRequest = Omit<
accessToken: string;
refreshToken: string;
transientToken: string;
redirectLocation?: string;
calendarVisibility?: CalendarChannelVisibility;
messageVisibility?: MessageChannelVisibility;
};
};
@ -64,6 +69,9 @@ export class GoogleAPIsStrategy extends PassportStrategy(
prompt: 'consent',
state: JSON.stringify({
transientToken: req.params.transientToken,
redirectLocation: req.params.redirectLocation,
calendarVisibility: req.params.calendarVisibility,
messageVisibility: req.params.messageVisibility,
}),
};
@ -92,6 +100,9 @@ export class GoogleAPIsStrategy extends PassportStrategy(
accessToken,
refreshToken,
transientToken: state.transientToken,
redirectLocation: state.redirectLocation,
calendarVisibility: state.calendarVisibility,
messageVisibility: state.messageVisibility,
};
done(null, user);

View File

@ -2,15 +2,11 @@ import { ObjectType, Field, registerEnumType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { TimelineCalendarEventParticipant } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto';
import { CalendarChannelVisibility } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
export enum TimelineCalendarEventVisibility {
METADATA = 'METADATA',
SHARE_EVERYTHING = 'SHARE_EVERYTHING',
}
registerEnumType(TimelineCalendarEventVisibility, {
name: 'TimelineCalendarEventVisibility',
description: 'Visibility of the calendar event',
registerEnumType(CalendarChannelVisibility, {
name: 'CalendarChannelVisibility',
description: 'Visibility of the calendar channel',
});
@ObjectType('LinkMetadata')
@ -57,6 +53,6 @@ export class TimelineCalendarEvent {
@Field(() => [TimelineCalendarEventParticipant])
participants: TimelineCalendarEventParticipant[];
@Field(() => TimelineCalendarEventVisibility)
visibility: TimelineCalendarEventVisibility;
@Field(() => CalendarChannelVisibility)
visibility: CalendarChannelVisibility;
}

View File

@ -4,12 +4,12 @@ import { Any } from 'typeorm';
import omit from 'lodash.omit';
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { TimelineCalendarEventVisibility } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event.dto';
import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { CalendarChannelVisibility } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
@Injectable()
export class TimelineCalendarEventService {
@ -107,8 +107,8 @@ export class TimelineCalendarEventService {
const visibility = event.calendarChannelEventAssociations.some(
(association) => association.calendarChannel.visibility === 'METADATA',
)
? TimelineCalendarEventVisibility.METADATA
: TimelineCalendarEventVisibility.SHARE_EVERYTHING;
? CalendarChannelVisibility.METADATA
: CalendarChannelVisibility.SHARE_EVERYTHING;
return {
...omit(event, [

View File

@ -0,0 +1,62 @@
import { Field, ObjectType } from '@nestjs/graphql';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
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';
@Entity({ name: 'keyValuePair', schema: 'core' })
@ObjectType('KeyValuePair')
@Unique('IndexOnKeyUserIdWorkspaceIdUnique', ['key', 'userId', 'workspaceId'])
export class KeyValuePair {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => User, (user) => user.keyValuePairs, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
user: Relation<User>;
@Column({ nullable: true })
userId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.keyValuePairs, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@Column({ nullable: true })
workspaceId: string;
@Field(() => String)
@Column({ nullable: false, type: 'text' })
key: string;
@Field(() => String, { nullable: true })
@Column({ nullable: true, type: 'text' })
value: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@Column({ nullable: true, type: 'timestamptz' })
deletedAt: Date | null;
}

View File

@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryTypeOrmModule.forFeature([KeyValuePair], 'core'),
TypeORMModule,
],
}),
],
exports: [KeyValuePairService],
providers: [KeyValuePairService],
})
export class KeyValuePairModule {}

View File

@ -0,0 +1,55 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { UserStates } from 'src/engine/core-modules/user-state/enums/user-states.enum';
import { UserStateEmailSyncValues } from 'src/engine/core-modules/user-state/enums/user-state-email-sync-values.enum';
export enum KeyValueTypes {
USER_STATE = 'USER_STATE',
}
type KeyValuePairs = {
[KeyValueTypes.USER_STATE]: {
[UserStates.SYNC_EMAIL_ONBOARDING_STEP]: UserStateEmailSyncValues;
};
};
export class KeyValuePairService<TYPE extends keyof KeyValuePairs> {
constructor(
@InjectRepository(KeyValuePair, 'core')
private readonly keyValuePairRepository: Repository<KeyValuePair>,
) {}
async get<K extends keyof KeyValuePairs[TYPE]>(
userId: string,
workspaceId: string,
key: K,
) {
return await this.keyValuePairRepository.findOne({
where: {
userId,
workspaceId,
key: key as string,
},
});
}
async set<K extends keyof KeyValuePairs[TYPE]>(
userId: string,
workspaceId: string,
key: K,
value: KeyValuePairs[TYPE][K],
) {
await this.keyValuePairRepository.upsert(
{
userId,
workspaceId,
key: key as string,
value: value as string,
},
{ conflictPaths: ['userId', 'workspaceId', 'key'] },
);
}
}

View File

@ -1,7 +1,13 @@
import { ObjectType, Field } from '@nestjs/graphql';
import { ObjectType, Field, registerEnumType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
registerEnumType(MessageChannelVisibility, {
name: 'MessageChannelVisibility',
description: 'Visibility of the message channel',
});
@ObjectType('TimelineThread')
export class TimelineThread {
@ -11,8 +17,8 @@ export class TimelineThread {
@Field()
read: boolean;
@Field()
visibility: string;
@Field(() => MessageChannelVisibility)
visibility: MessageChannelVisibility;
@Field()
firstParticipant: TimelineThreadParticipant;

View File

@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants';
import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
type TimelineThreadParticipant = {
personId: string;
@ -352,7 +353,7 @@ export class TimelineMessagingService {
const threadVisibility:
| {
id: string;
visibility: 'metadata' | 'subject' | 'share_everything';
visibility: MessageChannelVisibility;
}[]
| undefined = await this.workspaceDataSourceService.executeRawQuery(
`
@ -372,11 +373,11 @@ export class TimelineMessagingService {
workspaceId,
);
const visibilityValues = ['metadata', 'subject', 'share_everything'];
const visibilityValues = Object.values(MessageChannelVisibility);
const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants:
| {
[key: string]: 'metadata' | 'subject' | 'share_everything';
[key: string]: MessageChannelVisibility;
}
| undefined = threadVisibility?.reduce(
(threadVisibilityAcc, threadVisibility) => {
@ -385,7 +386,8 @@ export class TimelineMessagingService {
Math.max(
visibilityValues.indexOf(threadVisibility.visibility),
visibilityValues.indexOf(
threadVisibilityAcc[threadVisibility.id] ?? 'metadata',
threadVisibilityAcc[threadVisibility.id] ??
MessageChannelVisibility.METADATA,
),
)
];
@ -396,7 +398,7 @@ export class TimelineMessagingService {
);
const threadVisibilityByThreadId: {
[key: string]: 'metadata' | 'subject' | 'share_everything';
[key: string]: MessageChannelVisibility;
} = messageThreadIds.reduce((threadVisibilityAcc, messageThreadId) => {
// If the workspace member is not in the participants of the thread, use the visibility value from the query
threadVisibilityAcc[messageThreadId] =
@ -405,8 +407,8 @@ export class TimelineMessagingService {
)
? threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants?.[
messageThreadId
] ?? 'metadata'
: 'share_everything';
] ?? MessageChannelVisibility.METADATA
: MessageChannelVisibility.SHARE_EVERYTHING;
return threadVisibilityAcc;
}, {});
@ -461,7 +463,9 @@ export class TimelineMessagingService {
lastTwoParticipants,
lastMessageReceivedAt: thread.lastMessageReceivedAt,
lastMessageBody: thread.lastMessageBody,
visibility: threadVisibilityByThreadId?.[messageThreadId] ?? 'metadata',
visibility:
threadVisibilityByThreadId?.[messageThreadId] ??
MessageChannelVisibility.METADATA,
subject: threadSubject,
numberOfMessagesInThread: numberOfMessages,
participantCount: threadActiveParticipants.length,

View File

@ -0,0 +1,5 @@
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
export const DEFAULT_USER_STATE: UserState = {
skipSyncEmailOnboardingStep: true,
};

View File

@ -0,0 +1,9 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class SkipSyncEmailOnboardingStep {
@Field(() => Boolean, {
description: 'Boolean that confirms query was dispatched',
})
success: boolean;
}

View File

@ -0,0 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType('UserState')
export class UserState {
@Field(() => Boolean, { nullable: true })
skipSyncEmailOnboardingStep: boolean | null;
}

View File

@ -0,0 +1,3 @@
export enum UserStateEmailSyncValues {
SKIPPED = 'SKIPPED',
}

View File

@ -0,0 +1,3 @@
export enum UserStates {
SYNC_EMAIL_ONBOARDING_STEP = 'SYNC_EMAIL_ONBOARDING_STEP',
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
import { UserStateResolver } from 'src/engine/core-modules/user-state/user-state.resolver';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
@Module({
imports: [DataSourceModule, KeyValuePairModule],
exports: [UserStateService],
providers: [UserStateService, UserStateResolver],
})
export class UserStateModule {}

View File

@ -0,0 +1,28 @@
import { UseGuards } from '@nestjs/common';
import { Mutation, Resolver } from '@nestjs/graphql';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { User } from 'src/engine/core-modules/user/user.entity';
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { SkipSyncEmailOnboardingStep } from 'src/engine/core-modules/user-state/dtos/skip-sync-email.entity-onboarding-step';
@UseGuards(JwtAuthGuard)
@Resolver(() => UserState)
export class UserStateResolver {
constructor(private readonly userStateService: UserStateService) {}
@Mutation(() => SkipSyncEmailOnboardingStep)
async skipSyncEmailOnboardingStep(
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<SkipSyncEmailOnboardingStep> {
return await this.userStateService.skipSyncEmailOnboardingStep(
user.id,
workspace.id,
);
}
}

View File

@ -0,0 +1,64 @@
import { Injectable } from '@nestjs/common';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
import {
KeyValuePairService,
KeyValueTypes,
} from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
import { UserStates } from 'src/engine/core-modules/user-state/enums/user-states.enum';
import { UserStateEmailSyncValues } from 'src/engine/core-modules/user-state/enums/user-state-email-sync-values.enum';
import { SkipSyncEmailOnboardingStep } from 'src/engine/core-modules/user-state/dtos/skip-sync-email.entity-onboarding-step';
@Injectable()
export class UserStateService {
constructor(
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
private readonly keyValuePairService: KeyValuePairService<KeyValueTypes.USER_STATE>,
) {}
async getUserState(user: User, workspace: Workspace): Promise<UserState> {
const connectedAccounts =
await this.connectedAccountRepository.getAllByUserId(
user.id,
workspace.id,
);
if (connectedAccounts?.length) {
return {
skipSyncEmailOnboardingStep: true,
};
}
const skipSyncEmail = await this.keyValuePairService.get(
user.id,
workspace.id,
UserStates.SYNC_EMAIL_ONBOARDING_STEP,
);
return {
skipSyncEmailOnboardingStep:
!!skipSyncEmail &&
skipSyncEmail.value === UserStateEmailSyncValues.SKIPPED,
};
}
async skipSyncEmailOnboardingStep(
userId: string,
workspaceId: string,
): Promise<SkipSyncEmailOnboardingStep> {
await this.keyValuePairService.set(
userId,
workspaceId,
UserStates.SYNC_EMAIL_ONBOARDING_STEP,
UserStateEmailSyncValues.SKIPPED,
);
return { success: true };
}
}

View File

@ -2,14 +2,13 @@ import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;

View File

@ -17,6 +17,8 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
@Entity({ name: 'user', schema: 'core' })
@ObjectType('User')
@ -100,10 +102,18 @@ export class User {
})
appTokens: Relation<AppToken[]>;
@OneToMany(() => KeyValuePair, (keyValuePair) => keyValuePair.user, {
cascade: true,
})
keyValuePairs: Relation<KeyValuePair[]>;
@Field(() => WorkspaceMember, { nullable: true })
workspaceMember: Relation<WorkspaceMember>;
@Field(() => [UserWorkspace])
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user)
workspaces: Relation<UserWorkspace[]>;
@Field(() => UserState, { nullable: false })
state: UserState;
}

View File

@ -11,6 +11,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
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 { UserStateModule } from 'src/engine/core-modules/user-state/user-state.module';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
@ -27,6 +28,7 @@ import { UserService } from './services/user.service';
}),
DataSourceModule,
FileUploadModule,
UserStateModule,
WorkspaceModule,
],
exports: [UserService],

View File

@ -1,10 +1,10 @@
import {
Resolver,
Query,
Args,
Parent,
ResolveField,
Mutation,
Parent,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
@ -17,6 +17,7 @@ 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 { 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';
@ -26,8 +27,11 @@ 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 { UserService } from './services/user.service';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
import { DEFAULT_USER_STATE } from 'src/engine/core-modules/user-state/constants/default-user-state';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
@ -43,6 +47,7 @@ export class UserResolver {
constructor(
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
private readonly userStateService: UserStateService,
private readonly userService: UserService,
private readonly environmentService: EnvironmentService,
private readonly fileUploadService: FileUploadService,
@ -113,4 +118,16 @@ export class UserResolver {
// Proceed with user deletion
return this.userService.deleteUser(userId);
}
@ResolveField(() => UserState)
async state(
@Parent() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<UserState> {
if (!user || !workspace) {
return DEFAULT_USER_STATE;
}
return this.userStateService.getUserState(user, workspace);
}
}

View File

@ -18,6 +18,7 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
@Entity({ name: 'workspace', schema: 'core' })
@ObjectType('Workspace')
@ -63,6 +64,11 @@ export class Workspace {
})
appTokens: Relation<AppToken[]>;
@OneToMany(() => KeyValuePair, (keyValuePair) => keyValuePair.workspace, {
cascade: true,
})
keyValuePairs: Relation<KeyValuePair[]>;
@OneToMany(() => User, (user) => user.defaultWorkspace)
users: Relation<User[]>;