Remove connected account feature flag (#19286)
Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
-18
@@ -1,18 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([CalendarChannelEntity]),
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
],
|
||||
providers: [CalendarChannelDataAccessService],
|
||||
exports: [CalendarChannelDataAccessService],
|
||||
})
|
||||
export class CalendarChannelDataAccessModule {}
|
||||
-311
@@ -1,311 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import {
|
||||
type FindManyOptions,
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarChannelDataAccessService {
|
||||
private readonly logger = new Logger(CalendarChannelDataAccessService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly coreRepository: Repository<CalendarChannelEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
private async isMigrated(workspaceId: string): Promise<boolean> {
|
||||
return this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async toCoreWhere(
|
||||
workspaceId: string,
|
||||
where: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const coreWhere: Record<string, unknown> = { ...where, workspaceId };
|
||||
|
||||
if (
|
||||
coreWhere.connectedAccount &&
|
||||
typeof coreWhere.connectedAccount === 'object'
|
||||
) {
|
||||
const connectedAccountWhere = {
|
||||
...(coreWhere.connectedAccount as Record<string, unknown>),
|
||||
};
|
||||
|
||||
if ('accountOwnerId' in connectedAccountWhere) {
|
||||
const { accountOwnerId, ...restConnectedAccount } =
|
||||
connectedAccountWhere;
|
||||
|
||||
const resolvedConnectedAccounts =
|
||||
await this.connectedAccountDataAccessService.find(workspaceId, {
|
||||
accountOwnerId,
|
||||
} as never);
|
||||
|
||||
if (resolvedConnectedAccounts.length > 0) {
|
||||
coreWhere.connectedAccountId = resolvedConnectedAccounts[0].id;
|
||||
} else {
|
||||
coreWhere.connectedAccountId = '00000000-0000-0000-0000-000000000000';
|
||||
}
|
||||
|
||||
if (Object.keys(restConnectedAccount).length > 0) {
|
||||
coreWhere.connectedAccount = restConnectedAccount;
|
||||
} else {
|
||||
delete coreWhere.connectedAccount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return coreWhere;
|
||||
}
|
||||
|
||||
async getWorkspaceRepository(workspaceId: string) {
|
||||
return this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<CalendarChannelWorkspaceEntity>,
|
||||
): Promise<CalendarChannelWorkspaceEntity | null> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const where = options.where as Record<string, unknown>;
|
||||
const coreWhere = Array.isArray(where)
|
||||
? await Promise.all(
|
||||
where.map((whereItem: Record<string, unknown>) =>
|
||||
this.toCoreWhere(workspaceId, whereItem),
|
||||
),
|
||||
)
|
||||
: await this.toCoreWhere(workspaceId, where);
|
||||
|
||||
const requestedRelations =
|
||||
(options.relations as string[] | undefined)?.slice() ?? [];
|
||||
|
||||
const needsConnectedAccount =
|
||||
requestedRelations.includes('connectedAccount');
|
||||
|
||||
const coreRelations = requestedRelations.filter(
|
||||
(r) => r !== 'connectedAccount',
|
||||
);
|
||||
|
||||
const result = await this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
relations: coreRelations,
|
||||
} as FindOneOptions<CalendarChannelEntity>);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceResult =
|
||||
result as unknown as CalendarChannelWorkspaceEntity;
|
||||
|
||||
if (needsConnectedAccount) {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: result.connectedAccountId },
|
||||
});
|
||||
|
||||
if (connectedAccount) {
|
||||
workspaceResult.connectedAccount = connectedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceResult;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.findOne(options);
|
||||
}
|
||||
|
||||
async find(
|
||||
workspaceId: string,
|
||||
whereOrOptions?:
|
||||
| FindOptionsWhere<CalendarChannelWorkspaceEntity>
|
||||
| FindManyOptions<CalendarChannelWorkspaceEntity>,
|
||||
): Promise<CalendarChannelWorkspaceEntity[]> {
|
||||
if (
|
||||
whereOrOptions !== undefined &&
|
||||
typeof whereOrOptions === 'object' &&
|
||||
whereOrOptions !== null &&
|
||||
!Array.isArray(whereOrOptions) &&
|
||||
'where' in whereOrOptions
|
||||
) {
|
||||
const options =
|
||||
whereOrOptions as FindManyOptions<CalendarChannelWorkspaceEntity>;
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const { where } = options;
|
||||
|
||||
const coreWhere = Array.isArray(where)
|
||||
? await Promise.all(
|
||||
where.map((whereItem) =>
|
||||
this.toCoreWhere(
|
||||
workspaceId,
|
||||
whereItem as Record<string, unknown>,
|
||||
),
|
||||
),
|
||||
)
|
||||
: await this.toCoreWhere(
|
||||
workspaceId,
|
||||
where as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return this.coreRepository.find({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindManyOptions<CalendarChannelEntity>) as unknown as Promise<
|
||||
CalendarChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
const workspaceRepository =
|
||||
await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find(options);
|
||||
}
|
||||
|
||||
const where = whereOrOptions as
|
||||
| FindOptionsWhere<CalendarChannelWorkspaceEntity>
|
||||
| undefined;
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const coreWhere = where
|
||||
? await this.toCoreWhere(workspaceId, where as Record<string, unknown>)
|
||||
: { workspaceId };
|
||||
|
||||
return this.coreRepository.find({
|
||||
where: coreWhere,
|
||||
} as FindManyOptions<CalendarChannelEntity>) as unknown as Promise<
|
||||
CalendarChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find({ where });
|
||||
}
|
||||
|
||||
async save(
|
||||
workspaceId: string,
|
||||
data: Partial<CalendarChannelWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.save(data, {}, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.save({
|
||||
...data,
|
||||
workspaceId,
|
||||
} as unknown as CalendarChannelEntity);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write calendarChannel to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<CalendarChannelWorkspaceEntity>,
|
||||
data: Partial<CalendarChannelWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.update(where, data);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.update(
|
||||
{ ...where, workspaceId } as FindOptionsWhere<CalendarChannelEntity>,
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write calendarChannel update to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async increment(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<CalendarChannelWorkspaceEntity>,
|
||||
propertyPath: string,
|
||||
value: number,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.increment(where, propertyPath, value, undefined, [
|
||||
propertyPath,
|
||||
'id',
|
||||
]);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.increment(
|
||||
{
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<CalendarChannelEntity>,
|
||||
propertyPath,
|
||||
value,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write calendarChannel increment to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<CalendarChannelWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.delete(where);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.delete({
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<CalendarChannelEntity>);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write calendarChannel delete to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -1,3 +1,5 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -19,6 +21,19 @@ import {
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
registerEnumType(CalendarChannelVisibility, {
|
||||
name: 'CalendarChannelVisibility',
|
||||
});
|
||||
registerEnumType(CalendarChannelSyncStatus, {
|
||||
name: 'CalendarChannelSyncStatus',
|
||||
});
|
||||
registerEnumType(CalendarChannelSyncStage, {
|
||||
name: 'CalendarChannelSyncStage',
|
||||
});
|
||||
registerEnumType(CalendarChannelContactAutoCreationPolicy, {
|
||||
name: 'CalendarChannelContactAutoCreationPolicy',
|
||||
});
|
||||
|
||||
@Entity({ name: 'calendarChannel', schema: 'core' })
|
||||
export class CalendarChannelEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
||||
+1
-9
@@ -1,17 +1,11 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CalendarChannelMetadataService } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service';
|
||||
@@ -19,7 +13,7 @@ import { CalendarChannelDTO } from 'src/engine/metadata-modules/calendar-channel
|
||||
import { UpdateCalendarChannelInput } from 'src/engine/metadata-modules/calendar-channel/dtos/update-calendar-channel.input';
|
||||
import { CalendarChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(CalendarChannelGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => CalendarChannelDTO)
|
||||
export class CalendarChannelResolver {
|
||||
@@ -29,7 +23,6 @@ export class CalendarChannelResolver {
|
||||
|
||||
@Query(() => [CalendarChannelDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async myCalendarChannels(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@@ -57,7 +50,6 @@ export class CalendarChannelResolver {
|
||||
|
||||
@Mutation(() => CalendarChannelDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateCalendarChannel(
|
||||
@Args('input') input: UpdateCalendarChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
|
||||
+10
-8
@@ -2,25 +2,27 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor';
|
||||
import { ConnectedAccountResolver } from 'src/engine/metadata-modules/connected-account/resolvers/connected-account.resolver';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ConnectedAccountEntity]),
|
||||
CalendarChannelDataAccessModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
TypeOrmModule.forFeature([
|
||||
ConnectedAccountEntity,
|
||||
CalendarChannelEntity,
|
||||
MessageChannelEntity,
|
||||
]),
|
||||
FeatureFlagModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
MessageChannelDataAccessModule,
|
||||
PermissionsModule,
|
||||
WorkspaceEventEmitterModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [
|
||||
ConnectedAccountMetadataService,
|
||||
|
||||
+17
-16
@@ -11,11 +11,10 @@ import {
|
||||
} from 'src/engine/metadata-modules/connected-account/connected-account.exception';
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
@@ -27,9 +26,10 @@ export class ConnectedAccountMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly repository: Repository<ConnectedAccountEntity>,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
@@ -164,15 +164,15 @@ export class ConnectedAccountMetadataService {
|
||||
const [messageChannels, calendarChannels] = await Promise.all([
|
||||
this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () =>
|
||||
this.messageChannelDataAccessService.find(workspaceId, {
|
||||
connectedAccountId: id,
|
||||
this.messageChannelRepository.find({
|
||||
where: { connectedAccountId: id, workspaceId },
|
||||
}),
|
||||
authContext,
|
||||
),
|
||||
this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () =>
|
||||
this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
connectedAccountId: id,
|
||||
this.calendarChannelRepository.find({
|
||||
where: { connectedAccountId: id, workspaceId },
|
||||
}),
|
||||
authContext,
|
||||
),
|
||||
@@ -182,6 +182,13 @@ export class ConnectedAccountMetadataService {
|
||||
`WorkspaceId: ${workspaceId} Deleting connected account ${id} with ${messageChannels.length} message channel(s) and ${calendarChannels.length} calendar channel(s)`,
|
||||
);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
await this.repository.delete({
|
||||
id,
|
||||
workspaceId,
|
||||
});
|
||||
}, authContext);
|
||||
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -234,12 +241,6 @@ export class ConnectedAccountMetadataService {
|
||||
});
|
||||
}
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
await this.connectedAccountDataAccessService.delete(workspaceId, {
|
||||
id,
|
||||
});
|
||||
}, authContext);
|
||||
|
||||
return connectedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ConnectedAccountEntity, UserWorkspaceEntity]),
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [ConnectedAccountDataAccessService],
|
||||
exports: [ConnectedAccountDataAccessService],
|
||||
})
|
||||
export class ConnectedAccountDataAccessModule {}
|
||||
-352
@@ -1,352 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
In,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectedAccountDataAccessService {
|
||||
private readonly logger = new Logger(ConnectedAccountDataAccessService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly coreRepository: Repository<ConnectedAccountEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
private async isMigrated(workspaceId: string): Promise<boolean> {
|
||||
return this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveUserWorkspaceId(
|
||||
workspaceId: string,
|
||||
workspaceMemberId: string,
|
||||
): Promise<string | null> {
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: { id: workspaceMemberId },
|
||||
});
|
||||
|
||||
if (!workspaceMember) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: { userId: workspaceMember.userId, workspaceId },
|
||||
});
|
||||
|
||||
return userWorkspace?.id ?? null;
|
||||
}
|
||||
|
||||
private async toCore(
|
||||
workspaceId: string,
|
||||
data: Partial<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<Partial<ConnectedAccountEntity>> {
|
||||
const {
|
||||
handleAliases,
|
||||
lastSyncHistoryId: _lastSyncHistoryId,
|
||||
accountOwnerId,
|
||||
...rest
|
||||
} = data as Record<string, unknown>;
|
||||
|
||||
const coreData: Record<string, unknown> = { ...rest };
|
||||
|
||||
if (handleAliases !== undefined) {
|
||||
if (Array.isArray(handleAliases)) {
|
||||
coreData.handleAliases = handleAliases;
|
||||
} else if (isNonEmptyString(handleAliases)) {
|
||||
coreData.handleAliases = handleAliases
|
||||
.split(',')
|
||||
.map((alias: string) => alias.trim());
|
||||
} else {
|
||||
coreData.handleAliases = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (accountOwnerId !== undefined) {
|
||||
const userWorkspaceId = await this.resolveUserWorkspaceId(
|
||||
workspaceId,
|
||||
accountOwnerId as string,
|
||||
);
|
||||
|
||||
if (!userWorkspaceId) {
|
||||
this.logger.warn(
|
||||
`Could not resolve userWorkspaceId for workspaceMember ${accountOwnerId}`,
|
||||
);
|
||||
}
|
||||
|
||||
coreData.userWorkspaceId = userWorkspaceId;
|
||||
}
|
||||
|
||||
return coreData as Partial<ConnectedAccountEntity>;
|
||||
}
|
||||
|
||||
private async toCoreWhere(
|
||||
workspaceId: string,
|
||||
where: Record<string, unknown>,
|
||||
): Promise<FindOptionsWhere<ConnectedAccountEntity>> {
|
||||
const { accountOwnerId, ...rest } = where;
|
||||
const coreWhere: Record<string, unknown> = { ...rest, workspaceId };
|
||||
|
||||
if (accountOwnerId !== undefined) {
|
||||
const userWorkspaceId = await this.resolveUserWorkspaceId(
|
||||
workspaceId,
|
||||
accountOwnerId as string,
|
||||
);
|
||||
|
||||
if (userWorkspaceId) {
|
||||
coreWhere.userWorkspaceId = userWorkspaceId;
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`toCoreWhere: could not resolve userWorkspaceId for workspaceMember ${accountOwnerId}, returning empty result`,
|
||||
);
|
||||
coreWhere.id = '00000000-0000-0000-0000-000000000000';
|
||||
}
|
||||
}
|
||||
|
||||
return coreWhere as FindOptionsWhere<ConnectedAccountEntity>;
|
||||
}
|
||||
|
||||
private async fromCoreEntities(
|
||||
workspaceId: string,
|
||||
entities: ConnectedAccountEntity[],
|
||||
): Promise<ConnectedAccountWorkspaceEntity[]> {
|
||||
if (entities.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userWorkspaceIds = entities
|
||||
.map((entity) => entity.userWorkspaceId)
|
||||
.filter(isDefined);
|
||||
|
||||
const userWorkspaces =
|
||||
userWorkspaceIds.length > 0
|
||||
? await this.userWorkspaceRepository.find({
|
||||
where: { id: In(userWorkspaceIds) },
|
||||
select: ['id', 'userId'],
|
||||
})
|
||||
: [];
|
||||
|
||||
const userIdByUserWorkspaceId = new Map(
|
||||
userWorkspaces.map((userWorkspace) => [
|
||||
userWorkspace.id,
|
||||
userWorkspace.userId,
|
||||
]),
|
||||
);
|
||||
|
||||
const uniqueUserIds = [
|
||||
...new Set(userWorkspaces.map((userWorkspace) => userWorkspace.userId)),
|
||||
];
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMembers =
|
||||
uniqueUserIds.length > 0
|
||||
? await workspaceMemberRepository.find({
|
||||
where: { userId: In(uniqueUserIds) },
|
||||
})
|
||||
: [];
|
||||
|
||||
const workspaceMemberIdByUserId = new Map(
|
||||
workspaceMembers.map((workspaceMember) => [
|
||||
workspaceMember.userId,
|
||||
workspaceMember.id,
|
||||
]),
|
||||
);
|
||||
|
||||
return entities.map((entity) => {
|
||||
const userId = entity.userWorkspaceId
|
||||
? userIdByUserWorkspaceId.get(entity.userWorkspaceId)
|
||||
: undefined;
|
||||
|
||||
const accountOwnerId = userId
|
||||
? workspaceMemberIdByUserId.get(userId)
|
||||
: undefined;
|
||||
|
||||
const handleAliases = Array.isArray(entity.handleAliases)
|
||||
? entity.handleAliases.join(',')
|
||||
: (entity.handleAliases ?? '');
|
||||
|
||||
return {
|
||||
...entity,
|
||||
handleAliases,
|
||||
accountOwnerId: accountOwnerId ?? null,
|
||||
} as unknown as ConnectedAccountWorkspaceEntity;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceRepository(workspaceId: string) {
|
||||
return this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<ConnectedAccountWorkspaceEntity | null> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const where = options.where as Record<string, unknown>;
|
||||
const coreWhere = Array.isArray(where)
|
||||
? await Promise.all(
|
||||
where.map((whereItem: Record<string, unknown>) =>
|
||||
this.toCoreWhere(workspaceId, whereItem),
|
||||
),
|
||||
)
|
||||
: await this.toCoreWhere(workspaceId, where);
|
||||
|
||||
const coreResult = await this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindOneOptions<ConnectedAccountEntity>);
|
||||
|
||||
if (!coreResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [transformed] = await this.fromCoreEntities(workspaceId, [
|
||||
coreResult,
|
||||
]);
|
||||
|
||||
return transformed ?? null;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.findOne(options);
|
||||
}
|
||||
|
||||
async find(
|
||||
workspaceId: string,
|
||||
where?: FindOptionsWhere<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<ConnectedAccountWorkspaceEntity[]> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const coreWhere = where
|
||||
? await this.toCoreWhere(workspaceId, where as Record<string, unknown>)
|
||||
: { workspaceId };
|
||||
|
||||
const coreResults = await this.coreRepository.find({
|
||||
where: coreWhere,
|
||||
});
|
||||
|
||||
return this.fromCoreEntities(workspaceId, coreResults);
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find({ where });
|
||||
}
|
||||
|
||||
async save(
|
||||
workspaceId: string,
|
||||
data: Partial<ConnectedAccountWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.save(data, {}, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreData = await this.toCore(workspaceId, data);
|
||||
|
||||
await this.coreRepository.save({
|
||||
...coreData,
|
||||
workspaceId,
|
||||
} as ConnectedAccountEntity);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write connectedAccount to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<ConnectedAccountWorkspaceEntity>,
|
||||
data: Partial<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.update(where, data);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreData = await this.toCore(workspaceId, data);
|
||||
const coreWhere = await this.toCoreWhere(
|
||||
workspaceId,
|
||||
where as Record<string, unknown>,
|
||||
);
|
||||
|
||||
await this.coreRepository.update(
|
||||
coreWhere,
|
||||
coreData as Record<string, unknown>,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write connectedAccount update to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.delete(where);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreWhere = await this.toCoreWhere(
|
||||
workspaceId,
|
||||
where as Record<string, unknown>,
|
||||
);
|
||||
|
||||
await this.coreRepository.delete(coreWhere);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write connectedAccount delete to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -8,6 +8,9 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
import { type ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
|
||||
import { type CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
@@ -21,7 +24,7 @@ export class ConnectedAccountEntity extends WorkspaceRelatedEntity {
|
||||
handle: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
provider: string;
|
||||
provider: ConnectedAccountProvider;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
accessToken: string | null;
|
||||
@@ -42,7 +45,7 @@ export class ConnectedAccountEntity extends WorkspaceRelatedEntity {
|
||||
scopes: string[] | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
connectionParameters: Record<string, unknown> | null;
|
||||
connectionParameters: ImapSmtpCaldavParams | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
lastSignedInAt: Date | null;
|
||||
|
||||
+1
-9
@@ -2,17 +2,12 @@ import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
@@ -20,7 +15,7 @@ import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/con
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { ConnectedAccountGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(ConnectedAccountGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => ConnectedAccountDTO)
|
||||
export class ConnectedAccountResolver {
|
||||
@@ -30,7 +25,6 @@ export class ConnectedAccountResolver {
|
||||
|
||||
@Query(() => [ConnectedAccountDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async myConnectedAccounts(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@@ -43,7 +37,6 @@ export class ConnectedAccountResolver {
|
||||
|
||||
@Query(() => [ConnectedAccountDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async connectedAccounts(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO[]> {
|
||||
@@ -52,7 +45,6 @@ export class ConnectedAccountResolver {
|
||||
|
||||
@Mutation(() => ConnectedAccountDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteConnectedAccount(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([MessageChannelEntity]),
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
MessageFolderDataAccessModule,
|
||||
],
|
||||
providers: [MessageChannelDataAccessService],
|
||||
exports: [MessageChannelDataAccessService],
|
||||
})
|
||||
export class MessageChannelDataAccessModule {}
|
||||
-327
@@ -1,327 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import {
|
||||
type FindManyOptions,
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageChannelDataAccessService {
|
||||
private readonly logger = new Logger(MessageChannelDataAccessService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly coreRepository: Repository<MessageChannelEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
|
||||
) {}
|
||||
|
||||
private async isMigrated(workspaceId: string): Promise<boolean> {
|
||||
return this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async toCoreWhere(
|
||||
workspaceId: string,
|
||||
where: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const coreWhere: Record<string, unknown> = { ...where, workspaceId };
|
||||
|
||||
if (
|
||||
coreWhere.connectedAccount &&
|
||||
typeof coreWhere.connectedAccount === 'object'
|
||||
) {
|
||||
const connectedAccountWhere = {
|
||||
...(coreWhere.connectedAccount as Record<string, unknown>),
|
||||
};
|
||||
|
||||
if ('accountOwnerId' in connectedAccountWhere) {
|
||||
const { accountOwnerId, ...restConnectedAccount } =
|
||||
connectedAccountWhere;
|
||||
|
||||
const resolvedConnectedAccounts =
|
||||
await this.connectedAccountDataAccessService.find(workspaceId, {
|
||||
accountOwnerId,
|
||||
} as never);
|
||||
|
||||
if (resolvedConnectedAccounts.length > 0) {
|
||||
coreWhere.connectedAccountId = resolvedConnectedAccounts[0].id;
|
||||
} else {
|
||||
coreWhere.connectedAccountId = '00000000-0000-0000-0000-000000000000';
|
||||
}
|
||||
|
||||
if (Object.keys(restConnectedAccount).length > 0) {
|
||||
coreWhere.connectedAccount = restConnectedAccount;
|
||||
} else {
|
||||
delete coreWhere.connectedAccount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return coreWhere;
|
||||
}
|
||||
|
||||
async getWorkspaceRepository(workspaceId: string) {
|
||||
return this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<MessageChannelWorkspaceEntity>,
|
||||
): Promise<MessageChannelWorkspaceEntity | null> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const where = options.where as Record<string, unknown>;
|
||||
const coreWhere = Array.isArray(where)
|
||||
? await Promise.all(
|
||||
where.map((whereItem: Record<string, unknown>) =>
|
||||
this.toCoreWhere(workspaceId, whereItem),
|
||||
),
|
||||
)
|
||||
: await this.toCoreWhere(workspaceId, where);
|
||||
|
||||
const requestedRelations =
|
||||
(options.relations as string[] | undefined)?.slice() ?? [];
|
||||
|
||||
const needsConnectedAccount =
|
||||
requestedRelations.includes('connectedAccount');
|
||||
|
||||
const needsMessageFolders = requestedRelations.includes('messageFolders');
|
||||
|
||||
const coreRelations = requestedRelations.filter(
|
||||
(r) => r !== 'connectedAccount' && r !== 'messageFolders',
|
||||
);
|
||||
|
||||
const result = await this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
relations: coreRelations,
|
||||
} as FindOneOptions<MessageChannelEntity>);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceResult =
|
||||
result as unknown as MessageChannelWorkspaceEntity;
|
||||
|
||||
if (needsConnectedAccount) {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: result.connectedAccountId },
|
||||
});
|
||||
|
||||
if (connectedAccount) {
|
||||
workspaceResult.connectedAccount = connectedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMessageFolders) {
|
||||
const messageFolders = await this.messageFolderDataAccessService.find(
|
||||
workspaceId,
|
||||
{
|
||||
messageChannelId: result.id,
|
||||
},
|
||||
);
|
||||
|
||||
workspaceResult.messageFolders =
|
||||
messageFolders as unknown as MessageChannelWorkspaceEntity['messageFolders'];
|
||||
}
|
||||
|
||||
return workspaceResult;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.findOne(options);
|
||||
}
|
||||
|
||||
async find(
|
||||
workspaceId: string,
|
||||
where?: FindOptionsWhere<MessageChannelWorkspaceEntity>,
|
||||
): Promise<MessageChannelWorkspaceEntity[]> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
return this.coreRepository.find({
|
||||
where: {
|
||||
...(where as Record<string, unknown>),
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageChannelEntity>,
|
||||
}) as unknown as Promise<MessageChannelWorkspaceEntity[]>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find({ where });
|
||||
}
|
||||
|
||||
async findMany(
|
||||
workspaceId: string,
|
||||
options: FindManyOptions<MessageChannelWorkspaceEntity>,
|
||||
): Promise<MessageChannelWorkspaceEntity[]> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const baseWhere = options.where;
|
||||
|
||||
if (!baseWhere) {
|
||||
return this.coreRepository.find({
|
||||
...options,
|
||||
where: { workspaceId },
|
||||
} as FindManyOptions<MessageChannelEntity>) as unknown as Promise<
|
||||
MessageChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
if (Array.isArray(baseWhere)) {
|
||||
const coreWhereArray = await Promise.all(
|
||||
baseWhere.map((whereItem) =>
|
||||
this.toCoreWhere(workspaceId, whereItem as Record<string, unknown>),
|
||||
),
|
||||
);
|
||||
|
||||
return this.coreRepository.find({
|
||||
...options,
|
||||
where: coreWhereArray,
|
||||
} as FindManyOptions<MessageChannelEntity>) as unknown as Promise<
|
||||
MessageChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
const coreWhere = await this.toCoreWhere(
|
||||
workspaceId,
|
||||
baseWhere as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return this.coreRepository.find({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindManyOptions<MessageChannelEntity>) as unknown as Promise<
|
||||
MessageChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find(options);
|
||||
}
|
||||
|
||||
async save(
|
||||
workspaceId: string,
|
||||
data: Partial<MessageChannelWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.save(data, {}, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.save({
|
||||
...data,
|
||||
workspaceId,
|
||||
} as unknown as MessageChannelEntity);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageChannel to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageChannelWorkspaceEntity>,
|
||||
data: Partial<MessageChannelWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.update(where, data, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.update(
|
||||
{ ...where, workspaceId } as FindOptionsWhere<MessageChannelEntity>,
|
||||
data as never,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageChannel update to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async increment(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageChannelWorkspaceEntity>,
|
||||
propertyPath: string,
|
||||
value: number,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.increment(where, propertyPath, value, undefined, [
|
||||
propertyPath,
|
||||
'id',
|
||||
]);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.increment(
|
||||
{
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageChannelEntity>,
|
||||
propertyPath,
|
||||
value,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageChannel increment to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageChannelWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.delete(where);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.delete({
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageChannelEntity>);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageChannel delete to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -1,3 +1,5 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -24,6 +26,26 @@ import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-ac
|
||||
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
registerEnumType(MessageChannelVisibility, {
|
||||
name: 'MessageChannelVisibility',
|
||||
});
|
||||
registerEnumType(MessageChannelSyncStatus, {
|
||||
name: 'MessageChannelSyncStatus',
|
||||
});
|
||||
registerEnumType(MessageChannelSyncStage, {
|
||||
name: 'MessageChannelSyncStage',
|
||||
});
|
||||
registerEnumType(MessageChannelType, { name: 'MessageChannelType' });
|
||||
registerEnumType(MessageChannelContactAutoCreationPolicy, {
|
||||
name: 'MessageChannelContactAutoCreationPolicy',
|
||||
});
|
||||
registerEnumType(MessageFolderImportPolicy, {
|
||||
name: 'MessageFolderImportPolicy',
|
||||
});
|
||||
registerEnumType(MessageChannelPendingGroupEmailsAction, {
|
||||
name: 'MessageChannelPendingGroupEmailsAction',
|
||||
});
|
||||
|
||||
@Entity({ name: 'messageChannel', schema: 'core' })
|
||||
export class MessageChannelEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
||||
+2
-5
@@ -1,23 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
|
||||
import { MessageChannelResolver } from 'src/engine/metadata-modules/message-channel/resolvers/message-channel.resolver';
|
||||
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([MessageChannelEntity]),
|
||||
TypeOrmModule.forFeature([MessageChannelEntity, MessageFolderEntity]),
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountMetadataModule,
|
||||
MessageFolderDataAccessModule,
|
||||
MessagingImportManagerModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
+19
-21
@@ -1,18 +1,22 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
MessageFolderPendingSyncAction,
|
||||
} from 'twenty-shared/types';
|
||||
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
|
||||
@@ -23,29 +27,22 @@ import {
|
||||
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
|
||||
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
|
||||
import {
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
|
||||
import { Not } from 'typeorm';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(MessageChannelGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => MessageChannelDTO)
|
||||
export class MessageChannelResolver {
|
||||
constructor(
|
||||
private readonly messageChannelMetadataService: MessageChannelMetadataService,
|
||||
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
|
||||
@InjectRepository(MessageFolderEntity)
|
||||
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
|
||||
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
|
||||
) {}
|
||||
|
||||
@Query(() => [MessageChannelDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async myMessageChannels(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@@ -73,7 +70,6 @@ export class MessageChannelResolver {
|
||||
|
||||
@Mutation(() => MessageChannelDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageChannel(
|
||||
@Args('input') input: UpdateMessageChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -90,11 +86,13 @@ export class MessageChannelResolver {
|
||||
messageChannel.syncStage ===
|
||||
MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING;
|
||||
|
||||
const foldersWithPendingAction =
|
||||
await this.messageFolderDataAccessService.find(workspace.id, {
|
||||
const foldersWithPendingAction = await this.messageFolderRepository.find({
|
||||
where: {
|
||||
messageChannelId: messageChannel.id,
|
||||
pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE),
|
||||
});
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
const hasPendingGroupEmailsAction =
|
||||
messageChannel.pendingGroupEmailsAction !==
|
||||
@@ -118,7 +116,7 @@ export class MessageChannelResolver {
|
||||
) {
|
||||
// Service expects WorkspaceEntity type but only reads .id
|
||||
await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction(
|
||||
messageChannel as unknown as MessageChannelWorkspaceEntity,
|
||||
messageChannel as unknown as MessageChannelEntity,
|
||||
workspace.id,
|
||||
input.update.excludeGroupEmails
|
||||
? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([MessageFolderEntity]), FeatureFlagModule],
|
||||
providers: [MessageFolderDataAccessService],
|
||||
exports: [MessageFolderDataAccessService],
|
||||
})
|
||||
export class MessageFolderDataAccessModule {}
|
||||
-216
@@ -1,216 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageFolderDataAccessService {
|
||||
private readonly logger = new Logger(MessageFolderDataAccessService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(MessageFolderEntity)
|
||||
private readonly coreRepository: Repository<MessageFolderEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
private async isMigrated(workspaceId: string): Promise<boolean> {
|
||||
return this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveParentFolderIdForCore(
|
||||
workspaceId: string,
|
||||
parentFolderId: string | null,
|
||||
messageChannelId: string | undefined,
|
||||
): Promise<string | null> {
|
||||
if (!isNonEmptyString(parentFolderId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uuidValidate(parentFolderId)) {
|
||||
return parentFolderId;
|
||||
}
|
||||
|
||||
if (!isDefined(messageChannelId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentFolder = await this.coreRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
messageChannelId,
|
||||
externalId: parentFolderId,
|
||||
},
|
||||
select: ['id'],
|
||||
});
|
||||
|
||||
return parentFolder?.id ?? null;
|
||||
}
|
||||
|
||||
private async toCore(
|
||||
workspaceId: string,
|
||||
data: Partial<MessageFolderWorkspaceEntity>,
|
||||
messageChannelId?: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const coreData: Record<string, unknown> = { ...data, workspaceId };
|
||||
|
||||
const channelId = (coreData.messageChannelId as string) ?? messageChannelId;
|
||||
|
||||
if ('parentFolderId' in coreData) {
|
||||
coreData.parentFolderId = await this.resolveParentFolderIdForCore(
|
||||
workspaceId,
|
||||
coreData.parentFolderId as string | null,
|
||||
channelId,
|
||||
);
|
||||
}
|
||||
|
||||
return coreData;
|
||||
}
|
||||
|
||||
async getWorkspaceRepository(workspaceId: string) {
|
||||
return this.globalWorkspaceOrmManager.getRepository<MessageFolderWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageFolder',
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<MessageFolderWorkspaceEntity>,
|
||||
): Promise<MessageFolderWorkspaceEntity | null> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const where = options.where as Record<string, unknown>;
|
||||
const coreWhere = Array.isArray(where)
|
||||
? where.map((whereItem) => ({
|
||||
...(whereItem as Record<string, unknown>),
|
||||
workspaceId,
|
||||
}))
|
||||
: {
|
||||
...(where as Record<string, unknown>),
|
||||
workspaceId,
|
||||
};
|
||||
|
||||
return this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindOneOptions<MessageFolderEntity>) as unknown as Promise<MessageFolderWorkspaceEntity | null>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.findOne(options);
|
||||
}
|
||||
|
||||
async find(
|
||||
workspaceId: string,
|
||||
where?: FindOptionsWhere<MessageFolderWorkspaceEntity>,
|
||||
): Promise<MessageFolderWorkspaceEntity[]> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
return this.coreRepository.find({
|
||||
where: {
|
||||
...(where as Record<string, unknown>),
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageFolderEntity>,
|
||||
}) as unknown as Promise<MessageFolderWorkspaceEntity[]>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find({ where });
|
||||
}
|
||||
|
||||
async save(
|
||||
workspaceId: string,
|
||||
data: Partial<MessageFolderWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
const savedData = await workspaceRepository.save(data);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreData = await this.toCore(workspaceId, savedData);
|
||||
|
||||
await this.coreRepository.save(
|
||||
coreData as unknown as MessageFolderEntity,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageFolder to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageFolderWorkspaceEntity>,
|
||||
data: Partial<MessageFolderWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.update(where, data, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreData = await this.toCore(
|
||||
workspaceId,
|
||||
data,
|
||||
(where as Record<string, unknown>).messageChannelId as string,
|
||||
);
|
||||
|
||||
await this.coreRepository.update(
|
||||
{ ...where, workspaceId } as FindOptionsWhere<MessageFolderEntity>,
|
||||
coreData,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageFolder update to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageFolderWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.delete(where);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.delete({
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageFolderEntity>);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageFolder delete to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -49,8 +49,8 @@ export class CreateMessageFolderInput {
|
||||
@Field(() => UUIDScalarType)
|
||||
messageChannelId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@Field(() => String, { nullable: true })
|
||||
parentFolderId?: string;
|
||||
}
|
||||
|
||||
+2
-2
@@ -36,9 +36,9 @@ export class MessageFolderDTO {
|
||||
@Field()
|
||||
isSynced: boolean;
|
||||
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@Field(() => String, { nullable: true })
|
||||
parentFolderId: string | null;
|
||||
|
||||
@IsString()
|
||||
|
||||
+1
-10
@@ -1,17 +1,11 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto';
|
||||
@@ -22,7 +16,7 @@ import {
|
||||
import { MessageFolderGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-folder/interceptors/message-folder-graphql-api-exception.interceptor';
|
||||
import { MessageFolderMetadataService } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.service';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(MessageFolderGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => MessageFolderDTO)
|
||||
export class MessageFolderResolver {
|
||||
@@ -32,7 +26,6 @@ export class MessageFolderResolver {
|
||||
|
||||
@Query(() => [MessageFolderDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async myMessageFolders(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@@ -58,7 +51,6 @@ export class MessageFolderResolver {
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageFolder(
|
||||
@Args('input') input: UpdateMessageFolderInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -79,7 +71,6 @@ export class MessageFolderResolver {
|
||||
|
||||
@Mutation(() => [MessageFolderDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageFolders(
|
||||
@Args('input') input: UpdateMessageFoldersInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
|
||||
import { TOOL_PERMISSION_FLAGS } from 'src/engine/metadata-modules/permissions/constants/tool-permission-flags';
|
||||
import {
|
||||
PermissionsException,
|
||||
|
||||
Reference in New Issue
Block a user