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:
+1
-1
@@ -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 {
|
||||
|
||||
+12
-1
@@ -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],
|
||||
})
|
||||
|
||||
+90
-26
@@ -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,
|
||||
|
||||
+102
-22
@@ -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, [
|
||||
|
||||
Reference in New Issue
Block a user