Cache WorkspaceMember (#17247)

## Context
Due to RLS feature, workspaceMember entity and its properties is
becoming necessary in different places of the backend. To avoid querying
many times, this PR adds a map of workspace members per workspaceId,
leveraging WorkspaceCache pattern for WorkspaceEntity (in opposition
with CoreEntity)

Due to its content (mostly PII), I prefer to cache it locally only (node
process VS Redis) using localDataOnly.

TODO: invalidation logic when the workspaceMember object is updated
(more precisely, when its field map is updated). There is no easy way
today to update that list so it can be done in another PR imho
This commit is contained in:
Weiko
2026-01-19 18:13:10 +01:00
committed by GitHub
parent b15c042e77
commit 1f1ccb281e
10 changed files with 220 additions and 93 deletions
@@ -23,7 +23,7 @@ describe('JwtAuthStrategy', () => {
let applicationRepository: any;
let jwtWrapperService: any;
let permissionsService: any;
let globalWorkspaceOrmManager: any;
let workspaceCacheService: any;
let workspaceMemberRepository: any;
const jwt = {
@@ -67,11 +67,26 @@ describe('JwtAuthStrategy', () => {
id: 'workspace-member-id',
});
globalWorkspaceOrmManager = {
executeInWorkspaceContext: jest.fn(async (_authContext, callback) => {
return await callback();
workspaceCacheService = {
getOrRecompute: jest.fn(async (_workspaceId, _cacheKeys) => {
return {
flatWorkspaceMemberMaps: {
byId: {
'workspace-member-id': {
id: 'workspace-member-id',
userId: 'valid-user-id',
workspaceId: 'workspace-id',
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
},
},
idByUserId: {
'valid-user-id': 'workspace-member-id',
},
},
};
}),
getRepository: jest.fn(async () => workspaceMemberRepository),
};
});
@@ -96,7 +111,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -128,7 +143,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -163,7 +178,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -198,7 +213,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
const result = await strategy.validate(payload as JwtPayload);
@@ -217,7 +232,7 @@ describe('JwtAuthStrategy', () => {
describe('ACCESS token validation', () => {
it('should throw AuthExceptionCode if type is ACCESS, no jti, and user not found', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
@@ -240,7 +255,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -257,7 +272,7 @@ describe('JwtAuthStrategy', () => {
});
it('should throw AuthExceptionCode if type is ACCESS, no jti, and userWorkspace not found', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
@@ -282,7 +297,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -299,7 +314,7 @@ describe('JwtAuthStrategy', () => {
});
it('should not throw if type is ACCESS, no jti, and user and userWorkspace exist', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
@@ -312,7 +327,10 @@ describe('JwtAuthStrategy', () => {
workspaceRepository.findOneBy.mockResolvedValue(new WorkspaceEntity());
userRepository.findOne.mockResolvedValue({ lastName: 'lastNameDefault' });
userRepository.findOne.mockResolvedValue({
id: validUserId,
lastName: 'lastNameDefault',
});
userWorkspaceRepository.findOne.mockResolvedValue({
id: validUserWorkspaceId,
@@ -328,7 +346,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
const user = await strategy.validate(payload as JwtPayload);
@@ -362,7 +380,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -381,7 +399,7 @@ describe('JwtAuthStrategy', () => {
describe('Impersonation validation', () => {
it('should throw AuthException if impersonation token has missing impersonatorUserWorkspaceId', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
@@ -416,7 +434,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -428,7 +446,7 @@ describe('JwtAuthStrategy', () => {
});
it('should throw AuthException if impersonation token has missing impersonatedUserWorkspaceId', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
const impersonatorUserWorkspaceId = randomUUID();
@@ -463,7 +481,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -475,7 +493,7 @@ describe('JwtAuthStrategy', () => {
});
it('should throw AuthException if user tries to impersonate themselves', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
@@ -512,7 +530,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -524,7 +542,7 @@ describe('JwtAuthStrategy', () => {
});
it('should throw AuthException if impersonator user workspace not found', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
const impersonatorUserWorkspaceId = randomUUID();
@@ -560,7 +578,7 @@ describe('JwtAuthStrategy', () => {
.mockResolvedValueOnce({
// For impersonatedUserWorkspace lookup
id: validUserWorkspaceId,
user: { id: randomUUID() },
user: { id: 'valid-user-id' },
workspace: mockWorkspace,
});
@@ -572,7 +590,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -585,7 +603,7 @@ describe('JwtAuthStrategy', () => {
});
it('should throw AuthException if impersonated user workspace not found', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
const impersonatorUserWorkspaceId = randomUUID();
@@ -627,7 +645,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -639,7 +657,7 @@ describe('JwtAuthStrategy', () => {
});
it('should throw AuthException for server level impersonation without permission', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
const impersonatorUserWorkspaceId = randomUUID();
@@ -670,13 +688,13 @@ describe('JwtAuthStrategy', () => {
const mockImpersonatorUserWorkspace = {
id: impersonatorUserWorkspaceId,
user: { id: randomUUID(), canImpersonate: false }, // No server level permission
user: { id: 'valid-user-id', canImpersonate: false }, // No server level permission
workspace: { id: differentWorkspaceId }, // Different workspace
};
const mockImpersonatedUserWorkspace = {
id: validUserWorkspaceId,
user: { id: randomUUID() },
user: { id: 'valid-user-id' },
workspace: mockWorkspace,
};
@@ -699,7 +717,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -711,7 +729,7 @@ describe('JwtAuthStrategy', () => {
});
it('should throw AuthException when no impersonation permissions are granted', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
const impersonatorUserWorkspaceId = randomUUID();
@@ -741,13 +759,13 @@ describe('JwtAuthStrategy', () => {
const mockImpersonatorUserWorkspace = {
id: impersonatorUserWorkspaceId,
user: { id: randomUUID(), canImpersonate: false },
user: { id: 'valid-user-id', canImpersonate: false },
workspace: mockWorkspace, // Same workspace
};
const mockImpersonatedUserWorkspace = {
id: validUserWorkspaceId,
user: { id: randomUUID() },
user: { id: 'valid-user-id' },
workspace: mockWorkspace,
};
@@ -770,7 +788,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -782,7 +800,7 @@ describe('JwtAuthStrategy', () => {
});
it('should throw AuthException when impersonatedUserWorkspaceId does not match userWorkspaceId', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
const impersonatorUserWorkspaceId = randomUUID();
@@ -813,13 +831,13 @@ describe('JwtAuthStrategy', () => {
const mockImpersonatorUserWorkspace = {
id: impersonatorUserWorkspaceId,
user: { id: randomUUID(), canImpersonate: true },
user: { id: 'valid-user-id', canImpersonate: true },
workspace: mockWorkspace,
};
const mockImpersonatedUserWorkspace = {
id: impersonatedUserWorkspaceId,
user: { id: randomUUID() },
user: { id: 'valid-user-id' },
workspace: mockWorkspace,
};
@@ -842,7 +860,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
@@ -854,7 +872,7 @@ describe('JwtAuthStrategy', () => {
});
it('should successfully validate workspace level impersonation with permission', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
const impersonatorUserWorkspaceId = randomUUID();
@@ -884,7 +902,7 @@ describe('JwtAuthStrategy', () => {
const mockImpersonatorUserWorkspace = {
id: impersonatorUserWorkspaceId,
user: { id: randomUUID(), canImpersonate: false },
user: { id: 'valid-user-id', canImpersonate: false },
workspace: mockWorkspace, // Same workspace
};
@@ -907,7 +925,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
const result = await strategy.validate(payload as JwtPayload);
@@ -924,7 +942,7 @@ describe('JwtAuthStrategy', () => {
});
it('should successfully validate server level impersonation with permission', async () => {
const validUserId = randomUUID();
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
const validWorkspaceId = randomUUID();
const impersonatorUserWorkspaceId = randomUUID();
@@ -949,7 +967,7 @@ describe('JwtAuthStrategy', () => {
const mockImpersonatorUserWorkspace = {
id: impersonatorUserWorkspaceId,
user: { id: randomUUID(), canImpersonate: true }, // Server level permission
user: { id: 'valid-user-id', canImpersonate: true }, // Server level permission
workspace: { id: differentWorkspaceId }, // Different workspace
};
@@ -974,7 +992,7 @@ describe('JwtAuthStrategy', () => {
userWorkspaceRepository,
apiKeyRepository,
permissionsService,
globalWorkspaceOrmManager,
workspaceCacheService,
);
const result = await strategy.validate(payload as JwtPayload);
@@ -31,9 +31,7 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@Injectable()
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
@@ -50,7 +48,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
@InjectRepository(ApiKeyEntity)
private readonly apiKeyRepository: Repository<ApiKeyEntity>,
private readonly permissionsService: PermissionsService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
const jwtFromRequestFunction = jwtWrapperService.extractJwtFromRequest();
// @ts-expect-error legacy noImplicitAny
@@ -199,37 +197,27 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
return context;
}
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
buildSystemAuthContext(workspace.id),
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const { flatWorkspaceMemberMaps } =
await this.workspaceCacheService.getOrRecompute(workspace.id, [
'flatWorkspaceMemberMaps',
]);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
userId: user.id,
},
});
const workspaceMemberId = flatWorkspaceMemberMaps.idByUserId[user.id];
assertIsDefinedOrThrow(
workspaceMember,
new AuthException(
'User is not a member of the workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
{
userFriendlyMessage: msg`User is not a member of the workspace.`,
},
),
);
const workspaceMember = isDefined(workspaceMemberId)
? flatWorkspaceMemberMaps.byId[workspaceMemberId]
: undefined;
return workspaceMember;
assertIsDefinedOrThrow(
workspaceMember,
new AuthException(
'User is not a member of the workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
{
userFriendlyMessage: msg`User is not a member of the workspace.`,
},
);
),
);
return {
...context,
@@ -4,8 +4,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { JwtAuthStrategy } from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { RenewTokenService } from 'src/engine/core-modules/auth/token/services/renew-token.service';
@@ -16,8 +18,7 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [
@@ -33,6 +34,7 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
TypeORMModule,
DataSourceModule,
PermissionsModule,
WorkspaceCacheModule,
],
providers: [
RenewTokenService,
@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import {
ObjectRecordCreateEvent,
ObjectRecordRestoreEvent,
ObjectRecordUpdateEvent,
ObjectRecordUpsertEvent,
type ObjectRecordDeleteEvent,
type ObjectRecordDestroyEvent,
} from 'twenty-shared/database-events';
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class GlobalWorkspaceMemberListener {
constructor(private readonly workspaceCacheService: WorkspaceCacheService) {}
@OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.CREATED)
@OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.UPDATED)
@OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.DELETED)
@OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.DESTROYED)
@OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.RESTORED)
@OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.UPSERTED)
async handleWorkspaceMemberEvent(
payload: WorkspaceEventBatch<
| ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity>
| ObjectRecordUpdateEvent<WorkspaceMemberWorkspaceEntity>
| ObjectRecordDeleteEvent<WorkspaceMemberWorkspaceEntity>
| ObjectRecordDestroyEvent<WorkspaceMemberWorkspaceEntity>
| ObjectRecordRestoreEvent<WorkspaceMemberWorkspaceEntity>
| ObjectRecordUpsertEvent<WorkspaceMemberWorkspaceEntity>
>,
) {
await this.workspaceCacheService.invalidateAndRecompute(
payload.workspaceId,
['flatWorkspaceMemberMaps'],
);
}
}
@@ -0,0 +1,52 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { FlatWorkspaceMemberMaps } from 'src/engine/core-modules/user/types/flat-workspace-member-maps.type';
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 { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
@WorkspaceCache('flatWorkspaceMemberMaps', { localDataOnly: true })
export class WorkspaceFlatWorkspaceMemberMapCacheService extends WorkspaceCacheProvider<FlatWorkspaceMemberMaps> {
constructor(
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatWorkspaceMemberMaps> {
const flatWorkspaceMemberMaps =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
buildSystemAuthContext(workspaceId),
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const flatWorkspaceMemberMaps: FlatWorkspaceMemberMaps = {
byId: {},
idByUserId: {},
};
const workspaceMembers = await workspaceMemberRepository.find({
withDeleted: true,
});
for (const workspaceMember of workspaceMembers) {
flatWorkspaceMemberMaps.byId[workspaceMember.id] = workspaceMember;
flatWorkspaceMemberMaps.idByUserId[workspaceMember.userId] =
workspaceMember.id;
}
return flatWorkspaceMemberMaps;
},
);
return flatWorkspaceMemberMaps;
}
}
@@ -0,0 +1,6 @@
import { type FlatWorkspaceMember } from 'src/engine/core-modules/user/types/flat-workspace-member.type';
export type FlatWorkspaceMemberMaps = {
byId: Partial<Record<string, FlatWorkspaceMember>>;
idByUserId: Partial<Record<string, string>>;
};
@@ -0,0 +1,5 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type FlatWorkspaceMember =
FlatEntityFrom<WorkspaceMemberWorkspaceEntity>;
@@ -6,6 +6,8 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { EmailVerificationModule } from 'src/engine/core-modules/email-verification/email-verification.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
@@ -13,6 +15,7 @@ import { KeyValuePairEntity } from 'src/engine/core-modules/key-value-pair/key-v
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { WorkspaceFlatWorkspaceMemberMapCacheService } from 'src/engine/core-modules/user/services/workspace-flat-workspace-member-map-cache.service';
import { WorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/workspace-member-transpiler.service';
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
@@ -22,8 +25,8 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { EmailVerificationModule } from 'src/engine/core-modules/email-verification/email-verification.module';
import { GlobalWorkspaceMemberListener } from 'src/engine/core-modules/user/services/global-workspace-member.listener';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
@@ -53,8 +56,15 @@ import { UserService } from './services/user.service';
PermissionsModule,
EmailVerificationModule,
WorkspaceDomainsModule,
WorkspaceCacheModule,
],
exports: [UserService, WorkspaceMemberTranspiler],
providers: [UserService, UserResolver, WorkspaceMemberTranspiler],
providers: [
UserService,
UserResolver,
WorkspaceMemberTranspiler,
WorkspaceFlatWorkspaceMemberMapCacheService,
GlobalWorkspaceMemberListener,
],
})
export class UserModule {}
@@ -3,6 +3,7 @@ import { type EntityMetadata } from 'typeorm';
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type FlatWorkspaceMemberMaps } from 'src/engine/core-modules/user/types/flat-workspace-member-maps.type';
import { type FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type UserWorkspaceRoleMap } from 'src/engine/metadata-modules/role-target/services/workspace-user-workspace-role-map-cache.service';
@@ -42,6 +43,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
flatRowLevelPermissionPredicateGroupMaps:
'flat-maps:row-level-permission-predicate-group',
flatFrontComponentMaps: 'flat-maps:front-component',
flatWorkspaceMemberMaps: 'flat-maps:workspace-member',
} as const satisfies Record<WorkspaceCacheKeyName, string>;
export type AdditionalCacheDataMaps = {
@@ -54,6 +56,7 @@ export type AdditionalCacheDataMaps = {
flatRoleTargetByAgentIdMaps: FlatRoleTargetByAgentIdMaps;
flatRowLevelPermissionPredicateMaps: FlatRowLevelPermissionPredicateMaps;
flatRowLevelPermissionPredicateGroupMaps: FlatRowLevelPermissionPredicateGroupMaps;
flatWorkspaceMemberMaps: FlatWorkspaceMemberMaps;
};
export type WorkspaceCacheDataMap = AllFlatEntityMaps & AdditionalCacheDataMaps;
@@ -2,10 +2,10 @@ import { registerEnumType } from '@nestjs/graphql';
import { type APP_LOCALES } from 'twenty-shared/translations';
import { FieldMetadataType, type FullNameMetadata } from 'twenty-shared/types';
import { type Relation } from 'typeorm';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
import { type CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
@@ -74,18 +74,18 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
timeZone: string;
dateFormat: string;
timeFormat: string;
assignedTasks: EntityRelation<TaskWorkspaceEntity[]>;
favorites: EntityRelation<FavoriteWorkspaceEntity[]>;
accountOwnerForCompanies: EntityRelation<CompanyWorkspaceEntity[]>;
authoredAttachments: EntityRelation<AttachmentWorkspaceEntity[]>;
connectedAccounts: EntityRelation<ConnectedAccountWorkspaceEntity[]>;
messageParticipants: EntityRelation<MessageParticipantWorkspaceEntity[]>;
blocklist: EntityRelation<BlocklistWorkspaceEntity[]>;
calendarEventParticipants: EntityRelation<
assignedTasks: Relation<TaskWorkspaceEntity[]>;
favorites: Relation<FavoriteWorkspaceEntity[]>;
accountOwnerForCompanies: Relation<CompanyWorkspaceEntity[]>;
authoredAttachments: Relation<AttachmentWorkspaceEntity[]>;
connectedAccounts: Relation<ConnectedAccountWorkspaceEntity[]>;
messageParticipants: Relation<MessageParticipantWorkspaceEntity[]>;
blocklist: Relation<BlocklistWorkspaceEntity[]>;
calendarEventParticipants: Relation<
CalendarEventParticipantWorkspaceEntity[]
>;
timelineActivities: EntityRelation<TimelineActivityWorkspaceEntity[]>;
ownedOpportunities: EntityRelation<OpportunityWorkspaceEntity[]>;
timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>;
ownedOpportunities: Relation<OpportunityWorkspaceEntity[]>;
searchVector: string;
numberFormat: string;
}