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:
+43
@@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
+52
@@ -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;
|
||||
}
|
||||
}
|
||||
+6
@@ -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>>;
|
||||
};
|
||||
+5
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user