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:
neo773
2026-04-06 13:43:41 +05:30
committed by GitHub
parent 3a1e112b86
commit d3f0162cf5
246 changed files with 5216 additions and 6424 deletions
@@ -1,8 +1,8 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { CalendarChannelVisibility } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { TimelineCalendarEventParticipantDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
@ObjectType('LinkMetadata')
class LinkMetadataDTO {
@@ -1,11 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TimelineCalendarEventResolver } from 'src/engine/core-modules/calendar/timeline-calendar-event.resolver';
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { UserModule } from 'src/engine/core-modules/user/user.module';
@Module({
imports: [UserModule],
imports: [
UserModule,
TypeOrmModule.forFeature([
CalendarChannelEntity,
ConnectedAccountEntity,
UserWorkspaceEntity,
]),
],
exports: [],
providers: [TimelineCalendarEventResolver, TimelineCalendarEventService],
})
@@ -1,10 +1,14 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { CalendarChannelVisibility } from 'twenty-shared/types';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { TimelineCalendarEventService } from './timeline-calendar-event.service';
@@ -19,6 +23,10 @@ type MockWorkspaceRepository = Partial<
describe('TimelineCalendarEventService', () => {
let service: TimelineCalendarEventService;
let mockCalendarEventRepository: MockWorkspaceRepository;
let mockCalendarChannelCoreRepository: { find: jest.Mock };
let mockConnectedAccountRepository: { find: jest.Mock };
let mockUserWorkspaceRepository: { findOne: jest.Mock };
let mockWorkspaceMemberRepository: { findOne: jest.Mock };
const mockCalendarEvent: Partial<CalendarEventWorkspaceEntity> = {
id: '1',
@@ -36,8 +44,32 @@ describe('TimelineCalendarEventService', () => {
findAndCount: jest.fn(),
};
mockConnectedAccountRepository = {
find: jest.fn().mockResolvedValue([]),
};
mockCalendarChannelCoreRepository = {
find: jest.fn().mockResolvedValue([]),
};
mockUserWorkspaceRepository = {
findOne: jest.fn().mockResolvedValue(null),
};
mockWorkspaceMemberRepository = {
findOne: jest.fn().mockResolvedValue(null),
};
const mockGlobalWorkspaceOrmManager = {
getRepository: jest.fn().mockResolvedValue(mockCalendarEventRepository),
getRepository: jest
.fn()
.mockImplementation((_workspaceId, entityName) => {
if (entityName === 'workspaceMember') {
return Promise.resolve(mockWorkspaceMemberRepository);
}
return Promise.resolve(mockCalendarEventRepository);
}),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
@@ -50,6 +82,18 @@ describe('TimelineCalendarEventService', () => {
provide: GlobalWorkspaceOrmManager,
useValue: mockGlobalWorkspaceOrmManager,
},
{
provide: getRepositoryToken(CalendarChannelEntity),
useValue: mockCalendarChannelCoreRepository,
},
{
provide: getRepositoryToken(ConnectedAccountEntity),
useValue: mockConnectedAccountRepository,
},
{
provide: getRepositoryToken(UserWorkspaceEntity),
useValue: mockUserWorkspaceRepository,
},
],
}).compile();
@@ -70,19 +114,21 @@ describe('TimelineCalendarEventService', () => {
{
...mockCalendarEvent,
calendarChannelEventAssociations: [
{
calendarChannel: {
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
connectedAccount: {
accountOwnerId: 'other-workspace-member-id',
},
},
},
{ calendarChannelId: 'channel-1' },
],
},
],
1,
]);
mockCalendarChannelCoreRepository.find.mockResolvedValue([
{
id: 'channel-1',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
connectedAccountId: 'connected-account-1',
},
]);
// Ownership doesn't matter for SHARE_EVERYTHING
mockWorkspaceMemberRepository.findOne.mockResolvedValue(null);
const result = await service.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
@@ -110,19 +156,27 @@ describe('TimelineCalendarEventService', () => {
{
...mockCalendarEvent,
calendarChannelEventAssociations: [
{
calendarChannel: {
visibility: CalendarChannelVisibility.METADATA,
connectedAccount: {
accountOwnerId: 'other-workspace-member-id',
},
},
},
{ calendarChannelId: 'channel-1' },
],
},
],
1,
]);
mockCalendarChannelCoreRepository.find.mockResolvedValue([
{
id: 'channel-1',
visibility: CalendarChannelVisibility.METADATA,
connectedAccountId: 'connected-account-1',
},
]);
// Current user resolves but doesn't own the account
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
userId: 'current-user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'current-uw-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
@@ -152,19 +206,29 @@ describe('TimelineCalendarEventService', () => {
{
...mockCalendarEvent,
calendarChannelEventAssociations: [
{
calendarChannel: {
visibility: CalendarChannelVisibility.METADATA,
connectedAccount: {
accountOwnerId: 'current-workspace-member-id',
},
},
},
{ calendarChannelId: 'channel-1' },
],
},
],
1,
]);
mockCalendarChannelCoreRepository.find.mockResolvedValue([
{
id: 'channel-1',
visibility: CalendarChannelVisibility.METADATA,
connectedAccountId: 'connected-account-1',
},
]);
// Current user resolves and owns the account
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
userId: 'current-user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'current-uw-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([
{ id: 'connected-account-1' },
]);
const result = await service.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
@@ -1,22 +1,33 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import omit from 'lodash.omit';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { Any } from 'typeorm';
import { Any, In, type Repository } from 'typeorm';
import { CalendarChannelVisibility } from 'twenty-shared/types';
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { type TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
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 { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { type OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class TimelineCalendarEventService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
async getCalendarEventsFromPersonIds({
@@ -79,16 +90,83 @@ export class TimelineCalendarEventService {
person: true,
workspaceMember: true,
},
calendarChannelEventAssociations: {
calendarChannel: {
connectedAccount: {
accountOwner: true,
},
},
},
calendarChannelEventAssociations: true,
},
});
const allCalendarChannelIds = [
...new Set(
events.flatMap((event) =>
event.calendarChannelEventAssociations.map(
(association) => association.calendarChannelId,
),
),
),
];
const calendarChannels =
allCalendarChannelIds.length > 0
? await this.calendarChannelRepository.find({
where: { id: In(allCalendarChannelIds), workspaceId },
})
: [];
// Resolve current user's userWorkspaceId (workspaceMember → userId → userWorkspace)
const workspaceMemberRepo =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const currentMember = await workspaceMemberRepo.findOne({
where: { id: currentWorkspaceMemberId },
select: { userId: true },
});
const currentUserWorkspaceId = currentMember
? ((
await this.userWorkspaceRepository.findOne({
where: { userId: currentMember.userId, workspaceId },
select: { id: true },
})
)?.id ?? null)
: null;
// Find which connected accounts the current user owns (1 query)
const connectedAccountIds = [
...new Set(
calendarChannels.map((channel) => channel.connectedAccountId),
),
];
const ownedAccountIds =
connectedAccountIds.length > 0 && currentUserWorkspaceId
? new Set(
(
await this.connectedAccountRepository.find({
where: {
id: In(connectedAccountIds),
userWorkspaceId: currentUserWorkspaceId,
},
select: { id: true },
})
).map((a) => a.id),
)
: new Set<string>();
const calendarChannelMap = new Map(
calendarChannels.map((channel) => [
channel.id,
{
visibility: channel.visibility,
isOwnedByCurrentUser: ownedAccountIds.has(
channel.connectedAccountId,
),
},
]),
);
const orderedEvents = events.sort(
(a, b) => ids.indexOf(a.id) - ids.indexOf(b.id),
);
@@ -123,20 +201,22 @@ export class TimelineCalendarEventService {
}),
);
const isCalendarEventImportedByCurrentWorkspaceMember =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.connectedAccount.accountOwnerId ===
currentWorkspaceMemberId,
);
const hasFullAccess = event.calendarChannelEventAssociations.some(
(association) => {
const channel = calendarChannelMap.get(
association.calendarChannelId,
);
const visibility =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.visibility === 'SHARE_EVERYTHING',
) || isCalendarEventImportedByCurrentWorkspaceMember
? CalendarChannelVisibility.SHARE_EVERYTHING
: CalendarChannelVisibility.METADATA;
return (
channel?.visibility === 'SHARE_EVERYTHING' ||
channel?.isOwnedByCurrentUser
);
},
);
const visibility = hasFullAccess
? CalendarChannelVisibility.SHARE_EVERYTHING
: CalendarChannelVisibility.METADATA;
return {
...omit(event, [