feat: optimize hot database queries with multi-layer caching (#19068)
## Summary Introduces multi-layer caching for the 5 most frequent database queries identified in production (Sentry data), targeting the JWT authentication hot path and cron job logic. ### Problem Our database is under heavy load from uncached queries on the auth hot path: - `WorkspaceEntity` lookups: **638 queries/min** - `ApiKeyEntity` lookups: **491 queries/min** - `UserEntity` lookups: **147 queries/min** - `UserWorkspaceEntity` lookups: **143 queries/min** - `LogicFunctionEntity` lookups: **1800 queries/min** (cron job) ### Solution **1. New `CoreEntityCacheService`** for non-workspace-scoped entities (Workspace, User, UserWorkspace): - Mirrors `WorkspaceCacheService` architecture (in-process Map + Redis with hash validation) - Provider pattern with `@CoreEntityCache` decorator - Keyed by entity primary key (not workspaceId) - 100ms local TTL, Redis-backed hash validation for cross-instance consistency - Three providers: `WorkspaceEntityCacheProviderService`, `UserEntityCacheProviderService`, `UserWorkspaceEntityCacheProviderService` **2. New `apiKeyMap` WorkspaceCache** for workspace-scoped API key lookups: - `WorkspaceApiKeyMapCacheService` loads all API keys for a workspace into a map by ID - Leverages existing `WorkspaceCacheService` infrastructure - Cache invalidation on API key create/update/revoke **3. `CronTriggerCronJob` refactored** to use existing `flatLogicFunctionMaps` workspace cache: - Eliminates per-workspace `LogicFunctionEntity` repository queries (~1800/min) - Filters cached data in-memory instead **4. `JwtAuthStrategy` refactored** to use caches for all entity lookups: - Workspace, User, UserWorkspace → `CoreEntityCacheService` - ApiKey → `WorkspaceCacheService` (`apiKeyMap`) - Impersonation queries kept as direct DB queries (rare path, requires relations) **5. Cache invalidation** wired into mutation paths: - `WorkspaceService` → invalidates `workspaceEntity` on save/update/delete - `ApiKeyService` → invalidates `apiKeyMap` on create/update/revoke ### Architecture ``` Request → JwtAuthStrategy ├── Workspace lookup → CoreEntityCacheService (in-process → Redis → DB) ├── User lookup → CoreEntityCacheService (in-process → Redis → DB) ├── UserWorkspace lookup → CoreEntityCacheService (in-process → Redis → DB) └── ApiKey lookup → WorkspaceCacheService (in-process → Redis → DB) CronTriggerCronJob └── LogicFunction lookup → WorkspaceCacheService (flatLogicFunctionMaps) ``` ### Expected Impact | Query | Before | After | |-------|--------|-------| | WorkspaceEntity | 638/min | ~0 (cached) | | ApiKeyEntity | 491/min | ~0 (cached) | | UserEntity | 147/min | ~0 (cached) | | UserWorkspaceEntity | 143/min | ~0 (cached) | | LogicFunctionEntity | 1800/min | ~0 (cached) | ### Not included (ongoing separately) - DataSourceEntity query optimization (IS_DATASOURCE_MIGRATED migration) - ObjectMetadataEntity query optimization (already partially cached)
This commit is contained in:
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { type ActorMetadata, FieldActorSource } from 'twenty-shared/types';
|
||||
|
||||
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
|
||||
|
||||
type BuildCreatedByFromApiKeyArgs = {
|
||||
apiKey: ApiKeyEntity;
|
||||
apiKey: FlatApiKey;
|
||||
};
|
||||
export const buildCreatedByFromApiKey = ({
|
||||
apiKey,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ApiKeyResolver } from 'src/engine/core-modules/api-key/api-key.resolver
|
||||
import { GenerateApiKeyCommand } from 'src/engine/core-modules/api-key/commands/generate-api-key.command';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { ApiKeyService } from 'src/engine/core-modules/api-key/services/api-key.service';
|
||||
import { WorkspaceApiKeyMapCacheService } from 'src/engine/core-modules/api-key/services/workspace-api-key-map-cache.service';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
@@ -39,6 +40,7 @@ import { ApiKeyController } from './controllers/api-key.controller';
|
||||
ApiKeyService,
|
||||
ApiKeyResolver,
|
||||
ApiKeyRoleService,
|
||||
WorkspaceApiKeyMapCacheService,
|
||||
GenerateApiKeyCommand,
|
||||
],
|
||||
controllers: [ApiKeyController],
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
|
||||
export const API_KEY_ENTITY_NON_CACHED_PROPERTIES = [
|
||||
'workspace',
|
||||
] as const satisfies ReadonlyArray<keyof ApiKeyEntity>;
|
||||
+7
@@ -14,6 +14,7 @@ import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-contex
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
describe('ApiKeyService', () => {
|
||||
let service: ApiKeyService;
|
||||
@@ -111,6 +112,12 @@ describe('ApiKeyService', () => {
|
||||
provide: getDataSourceToken(),
|
||||
useValue: mockDataSource,
|
||||
},
|
||||
{
|
||||
provide: WorkspaceCacheService,
|
||||
useValue: {
|
||||
invalidateAndRecompute: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { type ApiKeyToken } from 'src/engine/core-modules/auth/dto/api-key-token
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyService {
|
||||
@@ -22,6 +23,7 @@ export class ApiKeyService {
|
||||
private readonly apiKeyRepository: Repository<ApiKeyEntity>,
|
||||
private readonly jwtWrapperService: JwtWrapperService,
|
||||
private readonly roleTargetService: RoleTargetService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
@@ -44,6 +46,8 @@ export class ApiKeyService {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.invalidateApiKeyCache(savedApiKey.workspaceId);
|
||||
|
||||
return savedApiKey;
|
||||
}
|
||||
|
||||
@@ -88,6 +92,7 @@ export class ApiKeyService {
|
||||
}
|
||||
|
||||
await this.apiKeyRepository.update(id, updateData);
|
||||
await this.invalidateApiKeyCache(workspaceId);
|
||||
|
||||
return this.findById(id, workspaceId);
|
||||
}
|
||||
@@ -184,4 +189,10 @@ export class ApiKeyService {
|
||||
isActive(apiKey: ApiKeyEntity): boolean {
|
||||
return !this.isRevoked(apiKey) && !this.isExpired(apiKey);
|
||||
}
|
||||
|
||||
private async invalidateApiKeyCache(workspaceId: string): Promise<void> {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'apiKeyMap',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
|
||||
import { fromApiKeyEntityToFlat } from 'src/engine/core-modules/api-key/utils/from-api-key-entity-to-flat.util';
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('apiKeyMap')
|
||||
export class WorkspaceApiKeyMapCacheService extends WorkspaceCacheProvider<
|
||||
Record<string, FlatApiKey>
|
||||
> {
|
||||
constructor(
|
||||
@InjectRepository(ApiKeyEntity)
|
||||
private readonly apiKeyRepository: Repository<ApiKeyEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<Record<string, FlatApiKey>> {
|
||||
const apiKeys = await this.apiKeyRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
return apiKeys.reduce(
|
||||
(map, apiKey) => {
|
||||
map[apiKey.id] = fromApiKeyEntityToFlat(apiKey);
|
||||
|
||||
return map;
|
||||
},
|
||||
{} as Record<string, FlatApiKey>,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
|
||||
import { type API_KEY_ENTITY_NON_CACHED_PROPERTIES } from 'src/engine/core-modules/api-key/constants/api-key-entity-non-cached-properties.constant';
|
||||
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
|
||||
type ApiKeyEntityNonCachedProperties =
|
||||
(typeof API_KEY_ENTITY_NON_CACHED_PROPERTIES)[number];
|
||||
|
||||
type ApiKeyCachedFields = Omit<ApiKeyEntity, ApiKeyEntityNonCachedProperties>;
|
||||
|
||||
export type FlatApiKey = Omit<
|
||||
ApiKeyCachedFields,
|
||||
keyof CastRecordTypeOrmDatePropertiesToString<ApiKeyCachedFields>
|
||||
> &
|
||||
CastRecordTypeOrmDatePropertiesToString<ApiKeyCachedFields>;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
|
||||
|
||||
export const fromApiKeyEntityToFlat = (entity: ApiKeyEntity): FlatApiKey => ({
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
workspaceId: entity.workspaceId,
|
||||
expiresAt: entity.expiresAt.toISOString(),
|
||||
revokedAt: entity.revokedAt?.toISOString() ?? null,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
});
|
||||
@@ -57,6 +57,7 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
@@ -117,6 +118,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
ApplicationRegistrationModule,
|
||||
ApplicationModule,
|
||||
WorkspaceCacheModule,
|
||||
CoreEntityCacheModule,
|
||||
SecureHttpClientModule,
|
||||
EnterpriseModule,
|
||||
FileModule,
|
||||
|
||||
@@ -512,8 +512,10 @@ export class AuthResolver {
|
||||
@AuthUser() currentUser: AuthContextUser,
|
||||
@AuthProvider() authProvider: AuthProviderEnum,
|
||||
): Promise<SignUpDTO> {
|
||||
const fullUser = await this.userService.findUserByIdOrThrow(currentUser.id);
|
||||
|
||||
const { user, workspace } = await this.signInUpService.signUpOnNewWorkspace(
|
||||
{ type: 'existingUser', existingUser: currentUser },
|
||||
{ type: 'existingUser', existingUser: fullUser },
|
||||
);
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
compareHash,
|
||||
hashPassword,
|
||||
} from 'src/engine/core-modules/auth/auth.util';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import {
|
||||
type AuthProviderWithPasswordType,
|
||||
type ExistingUserOrPartialUserWithPicture,
|
||||
@@ -314,7 +313,7 @@ export class SignInUpService {
|
||||
workspace,
|
||||
shouldShowConnectAccountStep,
|
||||
}: {
|
||||
user: AuthContextUser;
|
||||
user: Pick<UserEntity, 'id' | 'firstName' | 'lastName'>;
|
||||
workspace: WorkspaceEntity;
|
||||
shouldShowConnectAccountStep: boolean;
|
||||
},
|
||||
@@ -577,7 +576,11 @@ export class SignInUpService {
|
||||
);
|
||||
|
||||
await this.activateOnboardingForUser(
|
||||
{ user, workspace, shouldShowConnectAccountStep: true },
|
||||
{
|
||||
user,
|
||||
workspace,
|
||||
shouldShowConnectAccountStep: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
|
||||
+328
-407
@@ -16,38 +16,31 @@ import { JwtAuthStrategy } from './jwt.auth.strategy';
|
||||
|
||||
describe('JwtAuthStrategy', () => {
|
||||
let strategy: JwtAuthStrategy;
|
||||
let workspaceRepository: any;
|
||||
let userWorkspaceRepository: any;
|
||||
let userRepository: any;
|
||||
let apiKeyRepository: any;
|
||||
let applicationRepository: any;
|
||||
let jwtWrapperService: any;
|
||||
let permissionsService: any;
|
||||
let workspaceCacheService: any;
|
||||
let workspaceMemberRepository: any;
|
||||
let coreEntityCacheService: any;
|
||||
|
||||
const jwt = {
|
||||
sub: 'sub-default',
|
||||
jti: 'jti-default',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceRepository = {
|
||||
findOneBy: jest.fn(),
|
||||
};
|
||||
let workspaceStore: Record<string, any>;
|
||||
let userStore: Record<string, any>;
|
||||
let apiKeyStore: Record<string, Record<string, any>>;
|
||||
|
||||
userRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
beforeEach(() => {
|
||||
workspaceStore = {};
|
||||
userStore = {};
|
||||
apiKeyStore = {};
|
||||
|
||||
userWorkspaceRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
apiKeyRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
applicationRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
@@ -60,33 +53,55 @@ describe('JwtAuthStrategy', () => {
|
||||
userHasWorkspaceSettingPermission: jest.fn(),
|
||||
};
|
||||
|
||||
workspaceMemberRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
workspaceMemberRepository.findOne.mockResolvedValue({
|
||||
id: 'workspace-member-id',
|
||||
});
|
||||
|
||||
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,
|
||||
getOrRecompute: jest.fn(
|
||||
async (workspaceId: string, cacheKeys: string[]) => {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
if (cacheKeys.includes('flatWorkspaceMemberMaps')) {
|
||||
result.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',
|
||||
},
|
||||
},
|
||||
};
|
||||
idByUserId: {
|
||||
'valid-user-id': 'workspace-member-id',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (cacheKeys.includes('apiKeyMap')) {
|
||||
result.apiKeyMap = apiKeyStore[workspaceId] ?? {};
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
coreEntityCacheService = {
|
||||
get: jest.fn(async (keyName: string, entityId: string) => {
|
||||
if (keyName === 'workspaceEntity') {
|
||||
return workspaceStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'user') {
|
||||
return userStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'userWorkspaceEntity') {
|
||||
return userWorkspaceRepository.findOne({ where: { id: entityId } });
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
invalidate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -94,6 +109,16 @@ describe('JwtAuthStrategy', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const createStrategy = () =>
|
||||
new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
applicationRepository,
|
||||
userWorkspaceRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
coreEntityCacheService,
|
||||
);
|
||||
|
||||
describe('API_KEY validation', () => {
|
||||
it('should throw AuthException if type is API_KEY and workspace is not found', async () => {
|
||||
const payload = {
|
||||
@@ -101,18 +126,7 @@ describe('JwtAuthStrategy', () => {
|
||||
type: JwtTokenTypeEnum.API_KEY,
|
||||
};
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(null);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -131,20 +145,10 @@ describe('JwtAuthStrategy', () => {
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
|
||||
mockWorkspace.id = 'workspace-id';
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
workspaceStore[payload.sub] = mockWorkspace;
|
||||
apiKeyStore['workspace-id'] = {};
|
||||
|
||||
apiKeyRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -163,23 +167,15 @@ describe('JwtAuthStrategy', () => {
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
|
||||
mockWorkspace.id = 'workspace-id';
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
workspaceStore[payload.sub] = mockWorkspace;
|
||||
apiKeyStore['workspace-id'] = {
|
||||
[payload.jti]: {
|
||||
id: 'api-key-id',
|
||||
revokedAt: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
apiKeyRepository.findOne.mockResolvedValue({
|
||||
id: 'api-key-id',
|
||||
revokedAt: new Date(),
|
||||
});
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -198,35 +194,20 @@ describe('JwtAuthStrategy', () => {
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
|
||||
mockWorkspace.id = 'workspace-id';
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
workspaceStore[payload.sub] = mockWorkspace;
|
||||
apiKeyStore['workspace-id'] = {
|
||||
[payload.jti]: {
|
||||
id: 'api-key-id',
|
||||
revokedAt: null,
|
||||
},
|
||||
};
|
||||
|
||||
apiKeyRepository.findOne.mockResolvedValue({
|
||||
id: 'api-key-id',
|
||||
revokedAt: null,
|
||||
});
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
const result = await strategy.validate(payload as JwtPayload);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.apiKey?.id).toBe('api-key-id');
|
||||
|
||||
expect(apiKeyRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: payload.jti,
|
||||
workspaceId: mockWorkspace.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -243,20 +224,9 @@ describe('JwtAuthStrategy', () => {
|
||||
workspaceId: validWorkspaceId,
|
||||
};
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(new WorkspaceEntity());
|
||||
workspaceStore[validWorkspaceId] = new WorkspaceEntity();
|
||||
|
||||
userRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -287,22 +257,12 @@ describe('JwtAuthStrategy', () => {
|
||||
workspaceId: validWorkspaceId,
|
||||
};
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(new WorkspaceEntity());
|
||||
|
||||
userRepository.findOne.mockResolvedValue({ lastName: 'lastNameDefault' });
|
||||
workspaceStore[validWorkspaceId] = new WorkspaceEntity();
|
||||
userStore[validUserId] = { lastName: 'lastNameDefault' };
|
||||
|
||||
userWorkspaceRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -333,30 +293,36 @@ describe('JwtAuthStrategy', () => {
|
||||
workspaceId: validWorkspaceId,
|
||||
};
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(new WorkspaceEntity());
|
||||
|
||||
userRepository.findOne.mockResolvedValue({
|
||||
workspaceStore[validWorkspaceId] = new WorkspaceEntity();
|
||||
userStore[validUserId] = {
|
||||
id: validUserId,
|
||||
lastName: 'lastNameDefault',
|
||||
});
|
||||
};
|
||||
|
||||
userWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: validUserId, lastName: 'lastNameDefault' },
|
||||
workspace: { id: validWorkspaceId },
|
||||
});
|
||||
coreEntityCacheService.get.mockImplementation(
|
||||
async (keyName: string, entityId: string) => {
|
||||
if (keyName === 'workspaceEntity') {
|
||||
return workspaceStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
if (keyName === 'user') {
|
||||
return userStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'userWorkspaceEntity') {
|
||||
return {
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: validUserId, lastName: 'lastNameDefault' },
|
||||
workspace: { id: validWorkspaceId },
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
strategy = createStrategy();
|
||||
|
||||
const user = await strategy.validate(payload as JwtPayload);
|
||||
|
||||
expect(user.user?.lastName).toBe('lastNameDefault');
|
||||
@@ -376,20 +342,11 @@ describe('JwtAuthStrategy', () => {
|
||||
workspaceId: validWorkspaceId,
|
||||
};
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(new WorkspaceEntity());
|
||||
workspaceStore[validWorkspaceId] = new WorkspaceEntity();
|
||||
|
||||
applicationRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException('Application not found', expect.any(String), {
|
||||
@@ -418,32 +375,20 @@ describe('JwtAuthStrategy', () => {
|
||||
workspaceId: validWorkspaceId,
|
||||
isImpersonating: true,
|
||||
impersonatedUserWorkspaceId: validUserWorkspaceId,
|
||||
// Missing impersonatorUserWorkspaceId
|
||||
};
|
||||
|
||||
const mockUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: validUserId, lastName: 'lastNameDefault' },
|
||||
workspace: { id: validWorkspaceId },
|
||||
};
|
||||
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
|
||||
mockWorkspace.id = validWorkspaceId;
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
|
||||
userWorkspaceRepository.findOne.mockResolvedValue(mockUserWorkspace);
|
||||
userWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: validUserId, lastName: 'lastNameDefault' },
|
||||
workspace: { id: validWorkspaceId },
|
||||
});
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -466,31 +411,20 @@ describe('JwtAuthStrategy', () => {
|
||||
workspaceId: validWorkspaceId,
|
||||
isImpersonating: true,
|
||||
impersonatorUserWorkspaceId,
|
||||
// Missing impersonatedUserWorkspaceId
|
||||
};
|
||||
|
||||
const mockUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: validUserId, lastName: 'lastNameDefault' },
|
||||
workspace: { id: validWorkspaceId },
|
||||
};
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
|
||||
mockWorkspace.id = validWorkspaceId;
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
|
||||
userWorkspaceRepository.findOne.mockResolvedValue(mockUserWorkspace);
|
||||
userWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: validUserId, lastName: 'lastNameDefault' },
|
||||
workspace: { id: validWorkspaceId },
|
||||
});
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -512,34 +446,23 @@ describe('JwtAuthStrategy', () => {
|
||||
workspaceId: validWorkspaceId,
|
||||
isImpersonating: true,
|
||||
impersonatorUserWorkspaceId: validUserWorkspaceId,
|
||||
impersonatedUserWorkspaceId: validUserWorkspaceId, // Same as impersonator
|
||||
};
|
||||
|
||||
const mockUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: validUserId, lastName: 'lastNameDefault' },
|
||||
workspace: { id: validWorkspaceId },
|
||||
impersonatedUserWorkspaceId: validUserWorkspaceId,
|
||||
};
|
||||
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
|
||||
mockWorkspace.id = validWorkspaceId;
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
userWorkspaceRepository.findOne.mockResolvedValue(mockUserWorkspace);
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
userWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: validUserId, lastName: 'lastNameDefault' },
|
||||
workspace: { id: validWorkspaceId },
|
||||
});
|
||||
permissionsService.userHasWorkspaceSettingPermission.mockResolvedValue(
|
||||
true,
|
||||
);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -572,34 +495,40 @@ describe('JwtAuthStrategy', () => {
|
||||
|
||||
const mockUser = { id: validUserId, lastName: 'lastNameDefault' };
|
||||
|
||||
const mockUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
userStore[validUserId] = mockUser;
|
||||
|
||||
coreEntityCacheService.get.mockImplementation(
|
||||
async (keyName: string, entityId: string) => {
|
||||
if (keyName === 'workspaceEntity') {
|
||||
return workspaceStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'user') {
|
||||
return userStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'userWorkspaceEntity') {
|
||||
return {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
userRepository.findOne.mockResolvedValue(mockUser);
|
||||
userWorkspaceRepository.findOne
|
||||
.mockResolvedValueOnce(mockUserWorkspace) // For the main userWorkspace lookup
|
||||
.mockResolvedValueOnce(null) // For impersonatorUserWorkspace lookup
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce({
|
||||
// For impersonatedUserWorkspace lookup
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: 'valid-user-id' },
|
||||
workspace: mockWorkspace,
|
||||
});
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -633,29 +562,35 @@ describe('JwtAuthStrategy', () => {
|
||||
|
||||
const mockUser = { id: validUserId, lastName: 'lastNameDefault' };
|
||||
|
||||
const mockUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
userStore[validUserId] = mockUser;
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
userRepository.findOne.mockResolvedValue(mockUser);
|
||||
userWorkspaceRepository.findOne
|
||||
.mockResolvedValueOnce(mockUserWorkspace) // For the main userWorkspace lookup
|
||||
.mockResolvedValueOnce(null); // For impersonatedUserWorkspace lookup
|
||||
coreEntityCacheService.get.mockImplementation(
|
||||
async (keyName: string, entityId: string) => {
|
||||
if (keyName === 'workspaceEntity') {
|
||||
return workspaceStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
if (keyName === 'user') {
|
||||
return userStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'userWorkspaceEntity') {
|
||||
return {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
userWorkspaceRepository.findOne.mockResolvedValueOnce(null);
|
||||
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
'Invalid impersonation token, cannot find impersonator or impersonated user workspace',
|
||||
@@ -684,49 +619,52 @@ describe('JwtAuthStrategy', () => {
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
|
||||
mockWorkspace.id = validWorkspaceId;
|
||||
mockWorkspace.allowImpersonation = false; // Disabled
|
||||
mockWorkspace.allowImpersonation = false;
|
||||
|
||||
const mockUser = { id: validUserId, lastName: 'lastNameDefault' };
|
||||
|
||||
const mockUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
userStore[validUserId] = mockUser;
|
||||
|
||||
const mockImpersonatorUserWorkspace = {
|
||||
id: impersonatorUserWorkspaceId,
|
||||
user: { id: 'valid-user-id', canImpersonate: false }, // No server level permission
|
||||
workspace: { id: differentWorkspaceId }, // Different workspace
|
||||
};
|
||||
coreEntityCacheService.get.mockImplementation(
|
||||
async (keyName: string, entityId: string) => {
|
||||
if (keyName === 'workspaceEntity') {
|
||||
return workspaceStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
const mockImpersonatedUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: 'valid-user-id' },
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
if (keyName === 'user') {
|
||||
return userStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'userWorkspaceEntity') {
|
||||
return {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
userRepository.findOne.mockResolvedValue(mockUser);
|
||||
userWorkspaceRepository.findOne
|
||||
.mockResolvedValueOnce(mockUserWorkspace) // For the main userWorkspace lookup
|
||||
.mockResolvedValueOnce(mockImpersonatorUserWorkspace) // For impersonatorUserWorkspace lookup
|
||||
.mockResolvedValueOnce(mockImpersonatedUserWorkspace); // For impersonatedUserWorkspace lookup
|
||||
.mockResolvedValueOnce({
|
||||
id: impersonatorUserWorkspaceId,
|
||||
user: { id: 'valid-user-id', canImpersonate: false },
|
||||
workspace: { id: differentWorkspaceId },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: 'valid-user-id' },
|
||||
workspace: mockWorkspace,
|
||||
});
|
||||
|
||||
permissionsService.userHasWorkspaceSettingPermission.mockResolvedValue(
|
||||
false,
|
||||
);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -759,45 +697,48 @@ describe('JwtAuthStrategy', () => {
|
||||
|
||||
const mockUser = { id: validUserId, lastName: 'lastNameDefault' };
|
||||
|
||||
const mockUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
userStore[validUserId] = mockUser;
|
||||
|
||||
const mockImpersonatorUserWorkspace = {
|
||||
id: impersonatorUserWorkspaceId,
|
||||
user: { id: 'valid-user-id', canImpersonate: false },
|
||||
workspace: mockWorkspace, // Same workspace
|
||||
};
|
||||
coreEntityCacheService.get.mockImplementation(
|
||||
async (keyName: string, entityId: string) => {
|
||||
if (keyName === 'workspaceEntity') {
|
||||
return workspaceStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
const mockImpersonatedUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: 'valid-user-id' },
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
if (keyName === 'user') {
|
||||
return userStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'userWorkspaceEntity') {
|
||||
return {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
userRepository.findOne.mockResolvedValue(mockUser);
|
||||
userWorkspaceRepository.findOne
|
||||
.mockResolvedValueOnce(mockUserWorkspace) // For the main userWorkspace lookup
|
||||
.mockResolvedValueOnce(mockImpersonatorUserWorkspace) // For impersonatorUserWorkspace lookup
|
||||
.mockResolvedValueOnce(mockImpersonatedUserWorkspace); // For impersonatedUserWorkspace lookup
|
||||
.mockResolvedValueOnce({
|
||||
id: impersonatorUserWorkspaceId,
|
||||
user: { id: 'valid-user-id', canImpersonate: false },
|
||||
workspace: mockWorkspace,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: validUserWorkspaceId,
|
||||
user: { id: 'valid-user-id' },
|
||||
workspace: mockWorkspace,
|
||||
});
|
||||
|
||||
permissionsService.userHasWorkspaceSettingPermission.mockResolvedValue(
|
||||
false,
|
||||
);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -821,7 +762,7 @@ describe('JwtAuthStrategy', () => {
|
||||
workspaceId: validWorkspaceId,
|
||||
isImpersonating: true,
|
||||
impersonatorUserWorkspaceId,
|
||||
impersonatedUserWorkspaceId, // Different from userWorkspaceId
|
||||
impersonatedUserWorkspaceId,
|
||||
};
|
||||
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
@@ -829,47 +770,9 @@ describe('JwtAuthStrategy', () => {
|
||||
mockWorkspace.id = validWorkspaceId;
|
||||
mockWorkspace.allowImpersonation = true;
|
||||
|
||||
const mockUser = { id: validUserId, lastName: 'lastNameDefault' };
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
|
||||
const mockUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
|
||||
const mockImpersonatorUserWorkspace = {
|
||||
id: impersonatorUserWorkspaceId,
|
||||
user: { id: 'valid-user-id', canImpersonate: true },
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
|
||||
const mockImpersonatedUserWorkspace = {
|
||||
id: impersonatedUserWorkspaceId,
|
||||
user: { id: 'valid-user-id' },
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
userRepository.findOne.mockResolvedValue(mockUser);
|
||||
userWorkspaceRepository.findOne
|
||||
.mockResolvedValueOnce(mockUserWorkspace) // For the main userWorkspace lookup
|
||||
.mockResolvedValueOnce(mockImpersonatorUserWorkspace) // For impersonatorUserWorkspace lookup
|
||||
.mockResolvedValueOnce(mockImpersonatedUserWorkspace); // For impersonatedUserWorkspace lookup
|
||||
|
||||
permissionsService.userHasWorkspaceSettingPermission.mockResolvedValue(
|
||||
true,
|
||||
);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException(
|
||||
@@ -898,43 +801,52 @@ describe('JwtAuthStrategy', () => {
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
|
||||
mockWorkspace.id = validWorkspaceId;
|
||||
mockWorkspace.allowImpersonation = false; // Server level disabled
|
||||
mockWorkspace.allowImpersonation = false;
|
||||
|
||||
const mockUser = { id: validUserId, lastName: 'lastNameDefault' };
|
||||
|
||||
const mockUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
userStore[validUserId] = mockUser;
|
||||
|
||||
const mockImpersonatorUserWorkspace = {
|
||||
id: impersonatorUserWorkspaceId,
|
||||
user: { id: 'valid-user-id', canImpersonate: false },
|
||||
workspace: mockWorkspace, // Same workspace
|
||||
};
|
||||
coreEntityCacheService.get.mockImplementation(
|
||||
async (keyName: string, entityId: string) => {
|
||||
if (keyName === 'workspaceEntity') {
|
||||
return workspaceStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'user') {
|
||||
return userStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
if (keyName === 'userWorkspaceEntity') {
|
||||
return {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
userRepository.findOne.mockResolvedValue(mockUser);
|
||||
userWorkspaceRepository.findOne
|
||||
.mockResolvedValueOnce(mockUserWorkspace) // For the main userWorkspace lookup
|
||||
.mockResolvedValueOnce(mockImpersonatorUserWorkspace) // For impersonatorUserWorkspace lookup
|
||||
.mockResolvedValueOnce(mockUserWorkspace); // For impersonatedUserWorkspace lookup (same as main)
|
||||
.mockResolvedValueOnce({
|
||||
id: impersonatorUserWorkspaceId,
|
||||
user: { id: 'valid-user-id', canImpersonate: false },
|
||||
workspace: mockWorkspace,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
});
|
||||
|
||||
permissionsService.userHasWorkspaceSettingPermission.mockResolvedValue(
|
||||
true,
|
||||
);
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
);
|
||||
strategy = createStrategy();
|
||||
|
||||
const result = await strategy.validate(payload as JwtPayload);
|
||||
|
||||
@@ -969,40 +881,49 @@ describe('JwtAuthStrategy', () => {
|
||||
const mockWorkspace = new WorkspaceEntity();
|
||||
|
||||
mockWorkspace.id = validWorkspaceId;
|
||||
mockWorkspace.allowImpersonation = true; // Server level enabled
|
||||
mockWorkspace.allowImpersonation = true;
|
||||
|
||||
const mockUser = { id: validUserId, lastName: 'lastNameDefault' };
|
||||
|
||||
const mockImpersonatorUserWorkspace = {
|
||||
id: impersonatorUserWorkspaceId,
|
||||
user: { id: 'valid-user-id', canImpersonate: true }, // Server level permission
|
||||
workspace: { id: differentWorkspaceId }, // Different workspace
|
||||
};
|
||||
workspaceStore[validWorkspaceId] = mockWorkspace;
|
||||
userStore[validUserId] = mockUser;
|
||||
|
||||
const mockImpersonatedUserWorkspace = {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
coreEntityCacheService.get.mockImplementation(
|
||||
async (keyName: string, entityId: string) => {
|
||||
if (keyName === 'workspaceEntity') {
|
||||
return workspaceStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
workspaceRepository.findOneBy.mockResolvedValue(mockWorkspace);
|
||||
userRepository.findOne.mockResolvedValue(mockUser);
|
||||
userWorkspaceRepository.findOne
|
||||
.mockResolvedValueOnce(mockImpersonatorUserWorkspace) // For impersonatorUserWorkspace lookup
|
||||
.mockResolvedValueOnce(mockImpersonatedUserWorkspace) // For impersonatedUserWorkspace lookup
|
||||
.mockResolvedValueOnce(mockImpersonatedUserWorkspace); // For access token lookup
|
||||
if (keyName === 'user') {
|
||||
return userStore[entityId] ?? null;
|
||||
}
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
workspaceRepository,
|
||||
applicationRepository,
|
||||
userRepository,
|
||||
userWorkspaceRepository,
|
||||
apiKeyRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
if (keyName === 'userWorkspaceEntity') {
|
||||
return {
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
userWorkspaceRepository.findOne
|
||||
.mockResolvedValueOnce({
|
||||
id: impersonatorUserWorkspaceId,
|
||||
user: { id: 'valid-user-id', canImpersonate: true },
|
||||
workspace: { id: differentWorkspaceId },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: validUserWorkspaceId,
|
||||
user: mockUser,
|
||||
workspace: mockWorkspace,
|
||||
});
|
||||
|
||||
strategy = createStrategy();
|
||||
|
||||
const result = await strategy.validate(payload as JwtPayload);
|
||||
|
||||
expect(result.user?.lastName).toBe('lastNameDefault');
|
||||
|
||||
+37
-40
@@ -9,7 +9,6 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import {
|
||||
AuthException,
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
type AccessTokenJwtPayload,
|
||||
type ApiKeyTokenJwtPayload,
|
||||
ApplicationAccessTokenJwtPayload,
|
||||
AUTH_CONTEXT_USER_SELECT_FIELDS,
|
||||
type AuthContext,
|
||||
type AuthContextUser,
|
||||
FileTokenJwtPayloadLegacy,
|
||||
@@ -27,11 +25,10 @@ import {
|
||||
JwtTokenTypeEnum,
|
||||
type WorkspaceAgnosticTokenJwtPayload,
|
||||
} from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { type FlatUserWorkspace } from 'src/engine/core-modules/user-workspace/types/flat-user-workspace.type';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
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 { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@@ -39,18 +36,13 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works
|
||||
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
constructor(
|
||||
private readonly jwtWrapperService: JwtWrapperService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userRepository: Repository<UserEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
@InjectRepository(ApiKeyEntity)
|
||||
private readonly apiKeyRepository: Repository<ApiKeyEntity>,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly coreEntityCacheService: CoreEntityCacheService,
|
||||
) {
|
||||
const jwtFromRequestFunction = jwtWrapperService.extractJwtFromRequest();
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
@@ -88,9 +80,10 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
private async validateAPIKey(
|
||||
payload: ApiKeyTokenJwtPayload,
|
||||
): Promise<AuthContext> {
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: payload.sub,
|
||||
});
|
||||
const workspace = await this.coreEntityCacheService.get(
|
||||
'workspaceEntity',
|
||||
payload.sub,
|
||||
);
|
||||
|
||||
assertIsDefinedOrThrow(
|
||||
workspace,
|
||||
@@ -100,12 +93,12 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
),
|
||||
);
|
||||
|
||||
const apiKey = await this.apiKeyRepository.findOne({
|
||||
where: {
|
||||
id: payload.jti,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
const { apiKeyMap } = await this.workspaceCacheService.getOrRecompute(
|
||||
workspace.id,
|
||||
['apiKeyMap'],
|
||||
);
|
||||
|
||||
const apiKey = payload.jti ? apiKeyMap[payload.jti] : undefined;
|
||||
|
||||
if (!apiKey || apiKey.revokedAt) {
|
||||
throw new AuthException(
|
||||
@@ -114,6 +107,13 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
);
|
||||
}
|
||||
|
||||
if (new Date(apiKey.expiresAt) < new Date()) {
|
||||
throw new AuthException(
|
||||
'This API Key is expired',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
return { apiKey, workspace, workspaceMemberId: payload.workspaceMemberId };
|
||||
}
|
||||
|
||||
@@ -123,9 +123,10 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
let user: AuthContextUser | null = null;
|
||||
let context: AuthContext = {};
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: payload.workspaceId,
|
||||
});
|
||||
const workspace = await this.coreEntityCacheService.get(
|
||||
'workspaceEntity',
|
||||
payload.workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(workspace)) {
|
||||
throw new AuthException(
|
||||
@@ -224,20 +225,18 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
expectedWorkspaceId?: string;
|
||||
}): Promise<{
|
||||
user: AuthContextUser;
|
||||
userWorkspace: UserWorkspaceEntity;
|
||||
userWorkspace: FlatUserWorkspace;
|
||||
} | null> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: params.userId },
|
||||
select: [...AUTH_CONTEXT_USER_SELECT_FIELDS],
|
||||
});
|
||||
const user = await this.coreEntityCacheService.get('user', params.userId);
|
||||
|
||||
if (!isDefined(user)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: { id: params.userWorkspaceId },
|
||||
});
|
||||
const userWorkspace = await this.coreEntityCacheService.get(
|
||||
'userWorkspaceEntity',
|
||||
params.userWorkspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(userWorkspace)) {
|
||||
return null;
|
||||
@@ -254,7 +253,6 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
}
|
||||
|
||||
private async validateImpersonation(payload: AccessTokenJwtPayload) {
|
||||
// Validate required impersonation fields
|
||||
if (
|
||||
!payload.impersonatorUserWorkspaceId ||
|
||||
!payload.impersonatedUserWorkspaceId
|
||||
@@ -282,6 +280,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
);
|
||||
}
|
||||
|
||||
// Impersonation validation requires relations -- not cached
|
||||
const impersonatorUserWorkspace =
|
||||
await this.userWorkspaceRepository.findOne({
|
||||
where: { id: payload.impersonatorUserWorkspaceId },
|
||||
@@ -348,12 +347,9 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
private async validateWorkspaceAgnosticToken(
|
||||
payload: WorkspaceAgnosticTokenJwtPayload,
|
||||
): Promise<AuthContext> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: payload.sub },
|
||||
select: [...AUTH_CONTEXT_USER_SELECT_FIELDS],
|
||||
});
|
||||
const user = await this.coreEntityCacheService.get('user', payload.sub);
|
||||
|
||||
userValidator.assertIsDefinedOrThrow(
|
||||
assertIsDefinedOrThrow(
|
||||
user,
|
||||
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
||||
);
|
||||
@@ -364,9 +360,10 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
private async validateApplicationToken(
|
||||
payload: ApplicationAccessTokenJwtPayload,
|
||||
): Promise<AuthContext> {
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: payload.workspaceId,
|
||||
});
|
||||
const workspace = await this.coreEntityCacheService.get(
|
||||
'workspaceEntity',
|
||||
payload.workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(workspace)) {
|
||||
throw new AuthException(
|
||||
|
||||
+8
-3
@@ -135,7 +135,12 @@ describe('WorkspaceAgnosticToken', () => {
|
||||
userId: userId,
|
||||
type: JwtTokenTypeEnum.WORKSPACE_AGNOSTIC,
|
||||
};
|
||||
const mockUser = { id: userId };
|
||||
const mockUser = {
|
||||
id: userId,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
deletedAt: null,
|
||||
} as unknown as UserEntity;
|
||||
|
||||
jest.spyOn(jwtWrapperService, 'decode').mockReturnValue(mockPayload);
|
||||
jest.spyOn(jwtWrapperService, 'verify').mockReturnValue({});
|
||||
@@ -145,8 +150,8 @@ describe('WorkspaceAgnosticToken', () => {
|
||||
|
||||
const result = await service.validateToken(mockToken);
|
||||
|
||||
expect(result).toEqual({
|
||||
user: mockUser,
|
||||
expect(result.user).toMatchObject({
|
||||
id: userId,
|
||||
});
|
||||
expect(jwtWrapperService.decode).toHaveBeenCalledWith(mockToken);
|
||||
expect(jwtWrapperService.verify).toHaveBeenCalledWith(
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { fromUserEntityToFlat } from 'src/engine/core-modules/user/utils/from-user-entity-to-flat.util';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
@@ -88,7 +89,7 @@ export class WorkspaceAgnosticTokenService {
|
||||
|
||||
userValidator.assertIsDefinedOrThrow(user);
|
||||
|
||||
return { user };
|
||||
return { user: fromUserEntityToFlat(user) };
|
||||
} catch (error) {
|
||||
if (error instanceof AuthException) {
|
||||
throw error;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
|
||||
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';
|
||||
@@ -16,6 +15,7 @@ import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
@@ -28,13 +28,13 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
AppTokenEntity,
|
||||
WorkspaceEntity,
|
||||
UserWorkspaceEntity,
|
||||
ApiKeyEntity,
|
||||
ApplicationEntity,
|
||||
]),
|
||||
TypeORMModule,
|
||||
DataSourceModule,
|
||||
PermissionsModule,
|
||||
WorkspaceCacheModule,
|
||||
CoreEntityCacheModule,
|
||||
],
|
||||
providers: [
|
||||
RenewTokenService,
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context-user.type';
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { type AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
|
||||
import { type FlatAuthContextUser } from 'src/engine/core-modules/auth/types/flat-auth-context-user.type';
|
||||
import { type FlatUserWorkspace } from 'src/engine/core-modules/user-workspace/types/flat-user-workspace.type';
|
||||
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
export { AUTH_CONTEXT_USER_SELECT_FIELDS } from 'src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants';
|
||||
export { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context-user.type';
|
||||
export { type FlatAuthContextUser as AuthContextUser } from 'src/engine/core-modules/auth/types/flat-auth-context-user.type';
|
||||
|
||||
export type RawAuthContext = {
|
||||
user?: AuthContextUser | null | undefined;
|
||||
apiKey?: ApiKeyEntity | null | undefined;
|
||||
user?: FlatAuthContextUser | null | undefined;
|
||||
apiKey?: FlatApiKey | null | undefined;
|
||||
workspaceMemberId?: string;
|
||||
workspaceMember?: WorkspaceMemberWorkspaceEntity;
|
||||
workspace?: WorkspaceEntity;
|
||||
workspace?: FlatWorkspace;
|
||||
application?: ApplicationEntity | null | undefined;
|
||||
userWorkspaceId?: string;
|
||||
userWorkspace?: UserWorkspaceEntity;
|
||||
userWorkspace?: FlatUserWorkspace;
|
||||
authProvider?: AuthProviderEnum;
|
||||
impersonationContext?: {
|
||||
impersonatorUserWorkspaceId?: string;
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type AUTH_CONTEXT_USER_SELECT_FIELDS } from 'src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants';
|
||||
import { type FlatUser } from 'src/engine/core-modules/user/types/flat-user.type';
|
||||
|
||||
export type FlatAuthContextUser = Pick<
|
||||
FlatUser,
|
||||
(typeof AUTH_CONTEXT_USER_SELECT_FIELDS)[number]
|
||||
>;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type APP_LOCALES } from 'twenty-shared/translations';
|
||||
|
||||
import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { type AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -42,7 +41,7 @@ export type ExistingUserOrNewUser = {
|
||||
|
||||
export type ExistingUserOrPartialUserWithPicture = {
|
||||
userData:
|
||||
| { type: 'existingUser'; existingUser: AuthContextUser }
|
||||
| { type: 'existingUser'; existingUser: UserEntity }
|
||||
| {
|
||||
type: 'newUserWithPicture';
|
||||
newUserWithPicture: PartialUserWithPicture;
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ export enum CacheStorageNamespace {
|
||||
ModuleCalendar = 'module:calendar',
|
||||
ModuleWorkflow = 'module:workflow',
|
||||
EngineWorkspace = 'engine:workspace',
|
||||
EngineCoreEntity = 'engine:core-entity',
|
||||
EngineLock = 'engine:lock',
|
||||
EngineHealth = 'engine:health',
|
||||
EngineMetrics = 'engine:metrics',
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ export const useGraphQLErrorHandlerHook = <
|
||||
return {
|
||||
id: req.workspace.id,
|
||||
displayName: req.workspace.displayName,
|
||||
createdAt: req.workspace.createdAt?.toISOString() ?? null,
|
||||
createdAt: req.workspace.createdAt ?? null,
|
||||
activationStatus: req.workspace.activationStatus,
|
||||
};
|
||||
}
|
||||
|
||||
+21
-14
@@ -2,7 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
LogicFunctionTriggerJob,
|
||||
LogicFunctionTriggerJobData,
|
||||
} from 'src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job';
|
||||
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { shouldRunNow } from 'src/utils/should-run-now.utils';
|
||||
|
||||
export const CRON_TRIGGER_CRON_PATTERN = '* * * * *';
|
||||
@@ -27,8 +27,7 @@ export class CronTriggerCronJob {
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(LogicFunctionEntity)
|
||||
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
@Process(CronTriggerCronJob.name)
|
||||
@@ -44,22 +43,30 @@ export class CronTriggerCronJob {
|
||||
const now = new Date();
|
||||
|
||||
for (const activeWorkspace of activeWorkspaces) {
|
||||
const logicFunctionsWithCronTrigger =
|
||||
await this.logicFunctionRepository.find({
|
||||
where: {
|
||||
workspaceId: activeWorkspace.id,
|
||||
cronTriggerSettings: Not(IsNull()),
|
||||
},
|
||||
select: ['id', 'cronTriggerSettings', 'workspaceId'],
|
||||
});
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(activeWorkspace.id, [
|
||||
'flatLogicFunctionMaps',
|
||||
]);
|
||||
|
||||
const logicFunctions = Object.values(
|
||||
flatLogicFunctionMaps.byUniversalIdentifier,
|
||||
);
|
||||
|
||||
for (const logicFunction of logicFunctions) {
|
||||
if (!isDefined(logicFunction)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const logicFunction of logicFunctionsWithCronTrigger) {
|
||||
const cronSettings = logicFunction.cronTriggerSettings;
|
||||
|
||||
if (!isDefined(cronSettings?.pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDefined(logicFunction.deletedAt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!shouldRunNow(cronSettings.pattern, now)) {
|
||||
continue;
|
||||
}
|
||||
@@ -69,7 +76,7 @@ export class CronTriggerCronJob {
|
||||
[
|
||||
{
|
||||
logicFunctionId: logicFunction.id,
|
||||
workspaceId: logicFunction.workspaceId,
|
||||
workspaceId: activeWorkspace.id,
|
||||
payload: {},
|
||||
},
|
||||
],
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ import {
|
||||
SdkClientException,
|
||||
SdkClientExceptionCode,
|
||||
} from 'src/engine/core-modules/sdk-client/exceptions/sdk-client.exception';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
const SDK_CLIENT_ARCHIVE_NAME = 'twenty-client-sdk.zip';
|
||||
@@ -46,7 +46,7 @@ export class SdkClientGenerationService {
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<Buffer> {
|
||||
const graphqlSchema = await this.workspaceSchemaFactory.createGraphQLSchema(
|
||||
{ id: workspaceId } as WorkspaceEntity,
|
||||
{ id: workspaceId } as FlatWorkspace,
|
||||
applicationId,
|
||||
);
|
||||
|
||||
|
||||
+4
-3
@@ -5,6 +5,8 @@ import { type ToolSet } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
import { fromUserEntityToFlat } from 'src/engine/core-modules/user/utils/from-user-entity-to-flat.util';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import {
|
||||
@@ -28,7 +30,6 @@ import {
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { stripLoadingMessage } from 'src/engine/core-modules/tool/utils/wrap-tool-for-execution.util';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
// Handler for individually registered static tools (e.g., action tools)
|
||||
@@ -303,9 +304,9 @@ export class ToolExecutorService {
|
||||
}
|
||||
|
||||
return buildUserAuthContext({
|
||||
workspace: { id: context.workspaceId } as WorkspaceEntity,
|
||||
workspace: { id: context.workspaceId } as FlatWorkspace,
|
||||
userWorkspaceId: context.userWorkspaceId,
|
||||
user,
|
||||
user: fromUserEntityToFlat(user),
|
||||
workspaceMemberId,
|
||||
workspaceMember,
|
||||
});
|
||||
|
||||
+19
-3
@@ -7,6 +7,7 @@ import {
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { type FlatAuthContextUser } from 'src/engine/core-modules/auth/types/flat-auth-context-user.type';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -53,10 +54,23 @@ describe('TwoFactorAuthenticationResolver', () => {
|
||||
>;
|
||||
let repository: ReturnType<typeof createMockRepository>;
|
||||
|
||||
const mockUser: UserEntity = {
|
||||
const MOCK_USER_ISO = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: 'test@example.com',
|
||||
} as UserEntity;
|
||||
defaultAvatarUrl: '',
|
||||
isEmailVerified: true,
|
||||
disabled: false,
|
||||
canImpersonate: false,
|
||||
canAccessFullAdminPanel: false,
|
||||
createdAt: MOCK_USER_ISO,
|
||||
updatedAt: MOCK_USER_ISO,
|
||||
deletedAt: null,
|
||||
locale: 'en',
|
||||
} as unknown as FlatAuthContextUser;
|
||||
|
||||
const mockWorkspace: WorkspaceEntity = {
|
||||
id: 'workspace-123',
|
||||
@@ -132,7 +146,9 @@ describe('TwoFactorAuthenticationResolver', () => {
|
||||
workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace.mockResolvedValue(
|
||||
mockWorkspace,
|
||||
);
|
||||
userService.findUserByEmailOrThrow.mockResolvedValue(mockUser);
|
||||
userService.findUserByEmailOrThrow.mockResolvedValue(
|
||||
mockUser as unknown as UserEntity,
|
||||
);
|
||||
twoFactorAuthenticationService.initiateStrategyConfiguration.mockResolvedValue(
|
||||
'otpauth://totp/Twenty:test@example.com?secret=SECRETKEY&issuer=Twenty',
|
||||
);
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
|
||||
export const USER_WORKSPACE_ENTITY_NON_CACHED_PROPERTIES = [
|
||||
'workspace',
|
||||
'user',
|
||||
'twoFactorAuthenticationMethods',
|
||||
'permissionFlags',
|
||||
'objectPermissions',
|
||||
'objectsPermissions',
|
||||
'twoFactorAuthenticationMethodSummary',
|
||||
] as const satisfies ReadonlyArray<keyof UserWorkspaceEntity>;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { CoreEntityCache } from 'src/engine/core-entity-cache/decorators/core-entity-cache.decorator';
|
||||
import { CoreEntityCacheProvider } from 'src/engine/core-entity-cache/interfaces/core-entity-cache-provider.service';
|
||||
import { type FlatUserWorkspace } from 'src/engine/core-modules/user-workspace/types/flat-user-workspace.type';
|
||||
import { fromUserWorkspaceEntityToFlat } from 'src/engine/core-modules/user-workspace/utils/from-user-workspace-entity-to-flat.util';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
@CoreEntityCache('userWorkspaceEntity')
|
||||
export class UserWorkspaceEntityCacheProviderService extends CoreEntityCacheProvider<FlatUserWorkspace> {
|
||||
constructor(
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(entityId: string): Promise<FlatUserWorkspace | null> {
|
||||
const entity = await this.userWorkspaceRepository.findOne({
|
||||
where: { id: entityId },
|
||||
});
|
||||
|
||||
if (entity === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fromUserWorkspaceEntityToFlat(entity);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
|
||||
import { type USER_WORKSPACE_ENTITY_NON_CACHED_PROPERTIES } from 'src/engine/core-modules/user-workspace/constants/user-workspace-entity-non-cached-properties.constant';
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
|
||||
type UserWorkspaceEntityNonCachedProperties =
|
||||
(typeof USER_WORKSPACE_ENTITY_NON_CACHED_PROPERTIES)[number];
|
||||
|
||||
type UserWorkspaceCachedFields = Omit<
|
||||
UserWorkspaceEntity,
|
||||
UserWorkspaceEntityNonCachedProperties
|
||||
>;
|
||||
|
||||
export type FlatUserWorkspace = Omit<
|
||||
UserWorkspaceCachedFields,
|
||||
keyof CastRecordTypeOrmDatePropertiesToString<UserWorkspaceCachedFields>
|
||||
> &
|
||||
CastRecordTypeOrmDatePropertiesToString<UserWorkspaceCachedFields>;
|
||||
+6
-1
@@ -12,6 +12,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { UploadProfilePicturePermissionGuard } from 'src/engine/core-modules/user-workspace/guards/upload-profile-picture-permission.guard';
|
||||
import { UserWorkspaceEntityCacheProviderService } from 'src/engine/core-modules/user-workspace/services/user-workspace-entity-cache-provider.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
@@ -57,6 +58,10 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
}),
|
||||
],
|
||||
exports: [UserWorkspaceService],
|
||||
providers: [UserWorkspaceService, UploadProfilePicturePermissionGuard],
|
||||
providers: [
|
||||
UserWorkspaceService,
|
||||
UserWorkspaceEntityCacheProviderService,
|
||||
UploadProfilePicturePermissionGuard,
|
||||
],
|
||||
})
|
||||
export class UserWorkspaceModule {}
|
||||
|
||||
+22
-5
@@ -6,6 +6,7 @@ import { type DataSource, type Repository } from 'typeorm';
|
||||
import { type ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
|
||||
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
@@ -249,7 +250,14 @@ describe('UserWorkspaceService', () => {
|
||||
lastName: 'Doe',
|
||||
defaultAvatarUrl: 'avatar-url',
|
||||
locale: 'en',
|
||||
} as UserEntity;
|
||||
isEmailVerified: false,
|
||||
disabled: false,
|
||||
canImpersonate: false,
|
||||
canAccessFullAdminPanel: false,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
} as unknown as AuthContextUser;
|
||||
const mainDataSource = {
|
||||
query: jest.fn(),
|
||||
} as unknown as DataSource;
|
||||
@@ -300,7 +308,10 @@ describe('UserWorkspaceService', () => {
|
||||
const user = {
|
||||
id: 'user-id',
|
||||
email: 'test@example.com',
|
||||
} as UserEntity;
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
deletedAt: null,
|
||||
} as unknown as UserEntity;
|
||||
const workspace = {
|
||||
id: 'workspace-id',
|
||||
defaultRoleId: 'default-role-id',
|
||||
@@ -335,7 +346,7 @@ describe('UserWorkspaceService', () => {
|
||||
});
|
||||
expect(service.createWorkspaceMember).toHaveBeenCalledWith(
|
||||
workspace.id,
|
||||
user,
|
||||
expect.objectContaining({ id: user.id, email: user.email }),
|
||||
);
|
||||
expect(
|
||||
userRoleService.assignRoleToManyUserWorkspace,
|
||||
@@ -361,7 +372,10 @@ describe('UserWorkspaceService', () => {
|
||||
const user = {
|
||||
id: 'user-id',
|
||||
email: 'test@example.com',
|
||||
} as UserEntity;
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
deletedAt: null,
|
||||
} as unknown as UserEntity;
|
||||
const workspace = {
|
||||
id: 'workspace-id',
|
||||
defaultRoleId: 'default-role-id',
|
||||
@@ -392,7 +406,10 @@ describe('UserWorkspaceService', () => {
|
||||
const user = {
|
||||
id: 'user-id',
|
||||
email: 'test@example.com',
|
||||
} as UserEntity;
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
deletedAt: null,
|
||||
} as unknown as UserEntity;
|
||||
const workspace = {
|
||||
id: 'workspace-id',
|
||||
defaultRoleId: undefined,
|
||||
|
||||
+8
-3
@@ -23,7 +23,6 @@ import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/u
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
@@ -103,7 +102,13 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
: this.userWorkspaceRepository.save(userWorkspace);
|
||||
}
|
||||
|
||||
async createWorkspaceMember(workspaceId: string, user: AuthContextUser) {
|
||||
async createWorkspaceMember(
|
||||
workspaceId: string,
|
||||
user: Pick<
|
||||
UserEntity,
|
||||
'id' | 'firstName' | 'lastName' | 'email' | 'locale'
|
||||
>,
|
||||
) {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
@@ -536,7 +541,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
appToken?: AppTokenEntity;
|
||||
}>;
|
||||
},
|
||||
user: AuthContextUser,
|
||||
user: Pick<UserEntity, 'email'>,
|
||||
authProvider: AuthProviderEnum,
|
||||
) {
|
||||
return {
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { type FlatUserWorkspace } from 'src/engine/core-modules/user-workspace/types/flat-user-workspace.type';
|
||||
|
||||
export const fromUserWorkspaceEntityToFlat = (
|
||||
entity: UserWorkspaceEntity,
|
||||
): FlatUserWorkspace => ({
|
||||
id: entity.id,
|
||||
workspaceId: entity.workspaceId,
|
||||
userId: entity.userId,
|
||||
defaultAvatarUrl: entity.defaultAvatarUrl,
|
||||
locale: entity.locale,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
deletedAt: entity.deletedAt?.toISOString() ?? null,
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
export const USER_ENTITY_NON_CACHED_PROPERTIES = [
|
||||
'formatEmail',
|
||||
'passwordHash',
|
||||
'appTokens',
|
||||
'keyValuePairs',
|
||||
'workspaceMember',
|
||||
'userWorkspaces',
|
||||
'onboardingStatus',
|
||||
'currentWorkspace',
|
||||
'currentUserWorkspace',
|
||||
] as const satisfies ReadonlyArray<keyof UserEntity>;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { CoreEntityCache } from 'src/engine/core-entity-cache/decorators/core-entity-cache.decorator';
|
||||
import { CoreEntityCacheProvider } from 'src/engine/core-entity-cache/interfaces/core-entity-cache-provider.service';
|
||||
import { type FlatUser } from 'src/engine/core-modules/user/types/flat-user.type';
|
||||
import { fromUserEntityToFlat } from 'src/engine/core-modules/user/utils/from-user-entity-to-flat.util';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Injectable()
|
||||
@CoreEntityCache('user')
|
||||
export class UserEntityCacheProviderService extends CoreEntityCacheProvider<FlatUser> {
|
||||
constructor(
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userRepository: Repository<UserEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(entityId: string): Promise<FlatUser | null> {
|
||||
const entity = await this.userRepository.findOne({
|
||||
where: { id: entityId },
|
||||
});
|
||||
|
||||
if (entity === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fromUserEntityToFlat(entity);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { type Repository, type UpdateResult } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
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';
|
||||
@@ -95,6 +97,12 @@ describe('UserService', () => {
|
||||
provide: ApplicationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: CoreEntityCacheService,
|
||||
useValue: {
|
||||
invalidate: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -114,7 +122,7 @@ describe('UserService', () => {
|
||||
// isWorkspaceActiveOrSuspendedSpy.mockReturnValue(false);
|
||||
|
||||
const res = await service.loadWorkspaceMember(
|
||||
{ id: 'u1' } as UserEntity,
|
||||
{ id: 'u1' } as Pick<AuthContextUser, 'id'>,
|
||||
{ id: 'w1' } as WorkspaceEntity,
|
||||
);
|
||||
|
||||
@@ -133,7 +141,7 @@ describe('UserService', () => {
|
||||
.mockResolvedValue(mockWorkspaceMemberRepo);
|
||||
|
||||
const res = await service.loadWorkspaceMember(
|
||||
{ id: 'u1' } as UserEntity,
|
||||
{ id: 'u1' } as Pick<AuthContextUser, 'id'>,
|
||||
{
|
||||
id: 'w1',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.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';
|
||||
@@ -55,11 +56,15 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
@InjectMessageQueue(MessageQueue.workspaceQueue)
|
||||
private readonly workspaceQueueService: MessageQueueService,
|
||||
private readonly coreEntityCacheService: CoreEntityCacheService,
|
||||
) {
|
||||
super(userRepository);
|
||||
}
|
||||
|
||||
async loadWorkspaceMember(user: AuthContextUser, workspace: WorkspaceEntity) {
|
||||
async loadWorkspaceMember(
|
||||
user: Pick<AuthContextUser, 'id'>,
|
||||
workspace: Pick<WorkspaceEntity, 'id' | 'activationStatus'>,
|
||||
) {
|
||||
if (!isWorkspaceActiveOrSuspended(workspace)) {
|
||||
return null;
|
||||
}
|
||||
@@ -85,7 +90,10 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
|
||||
);
|
||||
}
|
||||
|
||||
async loadWorkspaceMembers(workspace: WorkspaceEntity, withDeleted = false) {
|
||||
async loadWorkspaceMembers(
|
||||
workspace: Pick<WorkspaceEntity, 'id' | 'activationStatus'>,
|
||||
withDeleted = false,
|
||||
) {
|
||||
if (!isWorkspaceActiveOrSuspended(workspace)) {
|
||||
return [];
|
||||
}
|
||||
@@ -109,7 +117,9 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
|
||||
);
|
||||
}
|
||||
|
||||
async loadDeletedWorkspaceMembersOnly(workspace: WorkspaceEntity) {
|
||||
async loadDeletedWorkspaceMembersOnly(
|
||||
workspace: Pick<WorkspaceEntity, 'id' | 'activationStatus'>,
|
||||
) {
|
||||
if (!isWorkspaceActiveOrSuspended(workspace)) {
|
||||
return [];
|
||||
}
|
||||
@@ -151,6 +161,7 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
|
||||
}
|
||||
|
||||
await this.userRepository.softDelete({ id: userId });
|
||||
await this.coreEntityCacheService.invalidate('user', userId);
|
||||
|
||||
return await this.userRepository.findOne({
|
||||
where: {
|
||||
@@ -190,6 +201,7 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
|
||||
|
||||
if (user.userWorkspaces.length === 1) {
|
||||
await this.userRepository.softDelete(userId);
|
||||
await this.coreEntityCacheService.invalidate('user', userId);
|
||||
}
|
||||
|
||||
return userWorkspace;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
|
||||
import { type USER_ENTITY_NON_CACHED_PROPERTIES } from 'src/engine/core-modules/user/constants/user-entity-non-cached-properties.constant';
|
||||
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
type UserEntityNonCachedProperties =
|
||||
(typeof USER_ENTITY_NON_CACHED_PROPERTIES)[number];
|
||||
|
||||
type UserCachedFields = Omit<UserEntity, UserEntityNonCachedProperties>;
|
||||
|
||||
export type FlatUser = Omit<
|
||||
UserCachedFields,
|
||||
keyof CastRecordTypeOrmDatePropertiesToString<UserCachedFields>
|
||||
> &
|
||||
CastRecordTypeOrmDatePropertiesToString<UserCachedFields>;
|
||||
@@ -15,6 +15,7 @@ import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.
|
||||
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 { GlobalWorkspaceMemberListener } from 'src/engine/core-modules/user/services/global-workspace-member.listener';
|
||||
import { UserEntityCacheProviderService } from 'src/engine/core-modules/user/services/user-entity-cache-provider.service';
|
||||
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';
|
||||
@@ -25,6 +26,7 @@ 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 { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
||||
@@ -55,11 +57,13 @@ import { UserService } from './services/user.service';
|
||||
EmailVerificationModule,
|
||||
WorkspaceDomainsModule,
|
||||
WorkspaceCacheModule,
|
||||
CoreEntityCacheModule,
|
||||
],
|
||||
exports: [UserService, WorkspaceMemberTranspiler],
|
||||
providers: [
|
||||
UserService,
|
||||
UserResolver,
|
||||
UserEntityCacheProviderService,
|
||||
WorkspaceMemberTranspiler,
|
||||
WorkspaceFlatWorkspaceMemberMapCacheService,
|
||||
GlobalWorkspaceMemberListener,
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { type FlatUser } from 'src/engine/core-modules/user/types/flat-user.type';
|
||||
|
||||
export const fromUserEntityToFlat = (entity: UserEntity): FlatUser => ({
|
||||
id: entity.id,
|
||||
firstName: entity.firstName,
|
||||
lastName: entity.lastName,
|
||||
email: entity.email,
|
||||
defaultAvatarUrl: entity.defaultAvatarUrl,
|
||||
isEmailVerified: entity.isEmailVerified,
|
||||
disabled: entity.disabled,
|
||||
canImpersonate: entity.canImpersonate,
|
||||
canAccessFullAdminPanel: entity.canAccessFullAdminPanel,
|
||||
locale: entity.locale,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
deletedAt: entity.deletedAt?.toISOString(),
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
export const WORKSPACE_ENTITY_NON_CACHED_PROPERTIES = [
|
||||
'logoFile',
|
||||
'appTokens',
|
||||
'keyValuePairs',
|
||||
'workspaceUsers',
|
||||
'featureFlags',
|
||||
'approvedAccessDomains',
|
||||
'emailingDomains',
|
||||
'publicDomains',
|
||||
'workspaceMembersCount',
|
||||
'allPostgresCredentials',
|
||||
'workspaceSSOIdentityProviders',
|
||||
'agents',
|
||||
'webhooks',
|
||||
'apiKeys',
|
||||
'views',
|
||||
'viewFields',
|
||||
'viewFilters',
|
||||
'viewFilterGroups',
|
||||
'viewGroups',
|
||||
'viewSorts',
|
||||
'defaultRole',
|
||||
'workspaceCustomApplication',
|
||||
'applications',
|
||||
] as const satisfies ReadonlyArray<keyof WorkspaceEntity>;
|
||||
+7
@@ -31,6 +31,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
@@ -158,6 +159,12 @@ describe('WorkspaceService', () => {
|
||||
add: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CoreEntityCacheService,
|
||||
useValue: {
|
||||
invalidate: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getDataSourceToken(),
|
||||
useValue: {
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { CoreEntityCache } from 'src/engine/core-entity-cache/decorators/core-entity-cache.decorator';
|
||||
import { CoreEntityCacheProvider } from 'src/engine/core-entity-cache/interfaces/core-entity-cache-provider.service';
|
||||
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
import { fromWorkspaceEntityToFlat } from 'src/engine/core-modules/workspace/utils/from-workspace-entity-to-flat.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
@CoreEntityCache('workspaceEntity')
|
||||
export class WorkspaceEntityCacheProviderService extends CoreEntityCacheProvider<FlatWorkspace> {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(entityId: string): Promise<FlatWorkspace | null> {
|
||||
const entity = await this.workspaceRepository.findOneBy({ id: entityId });
|
||||
|
||||
if (entity === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fromWorkspaceEntityToFlat(entity);
|
||||
}
|
||||
}
|
||||
+24
@@ -38,6 +38,7 @@ import {
|
||||
WorkspaceExceptionCode,
|
||||
WorkspaceNotFoundDefaultError,
|
||||
} from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { isModelAllowedByWorkspace } from 'src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@@ -124,6 +125,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
private readonly coreEntityCacheService: CoreEntityCacheService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
@@ -290,6 +292,11 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.coreEntityCacheService.invalidate(
|
||||
'workspaceEntity',
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (payload.logo === null && isDefined(workspace.logoFileId)) {
|
||||
await this.fileCorePictureService.deleteCorePicture({
|
||||
fileId: workspace.logoFileId,
|
||||
@@ -325,6 +332,11 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
activationStatus: WorkspaceActivationStatus.ONGOING_CREATION,
|
||||
});
|
||||
|
||||
await this.coreEntityCacheService.invalidate(
|
||||
'workspaceEntity',
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
DEFAULT_FEATURE_FLAGS,
|
||||
workspace.id,
|
||||
@@ -350,6 +362,11 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
version: extractVersionMajorMinorPatch(appVersion),
|
||||
});
|
||||
|
||||
await this.coreEntityCacheService.invalidate(
|
||||
'workspaceEntity',
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return await this.workspaceRepository.findOneBy({
|
||||
id: workspace.id,
|
||||
});
|
||||
@@ -387,6 +404,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
|
||||
if (softDelete) {
|
||||
await this.workspaceRepository.softDelete({ id });
|
||||
await this.coreEntityCacheService.invalidate('workspaceEntity', id);
|
||||
|
||||
this.logger.log(`workspace ${id} soft deleted`);
|
||||
|
||||
@@ -418,6 +436,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
}
|
||||
|
||||
await this.workspaceRepository.delete(id);
|
||||
await this.coreEntityCacheService.invalidate('workspaceEntity', id);
|
||||
|
||||
this.logger.log(`workspace ${id} hard deleted`);
|
||||
|
||||
@@ -571,6 +590,10 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
userWorkspaceId: userWorkspaceOfRemovedWorkspaceMember.id,
|
||||
softDelete,
|
||||
});
|
||||
await this.coreEntityCacheService.invalidate(
|
||||
'userWorkspaceEntity',
|
||||
userWorkspaceOfRemovedWorkspaceMember.id,
|
||||
);
|
||||
}
|
||||
|
||||
const hasOtherUserWorkspaces = isDefined(
|
||||
@@ -581,6 +604,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
|
||||
if (!hasOtherUserWorkspaces) {
|
||||
await this.userRepository.softDelete(userId);
|
||||
await this.coreEntityCacheService.invalidate('user', userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
|
||||
import { type WORKSPACE_ENTITY_NON_CACHED_PROPERTIES } from 'src/engine/core-modules/workspace/constants/workspace-entity-non-cached-properties.constant';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
type WorkspaceEntityNonCachedProperties =
|
||||
(typeof WORKSPACE_ENTITY_NON_CACHED_PROPERTIES)[number];
|
||||
|
||||
type WorkspaceCachedFields = Omit<
|
||||
WorkspaceEntity,
|
||||
WorkspaceEntityNonCachedProperties
|
||||
>;
|
||||
|
||||
export type FlatWorkspace = Omit<
|
||||
WorkspaceCachedFields,
|
||||
keyof CastRecordTypeOrmDatePropertiesToString<WorkspaceCachedFields>
|
||||
> &
|
||||
CastRecordTypeOrmDatePropertiesToString<WorkspaceCachedFields>;
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
|
||||
export const fromWorkspaceEntityToFlat = (
|
||||
entity: WorkspaceEntity,
|
||||
): FlatWorkspace => ({
|
||||
id: entity.id,
|
||||
displayName: entity.displayName,
|
||||
logo: entity.logo,
|
||||
logoFileId: entity.logoFileId,
|
||||
inviteHash: entity.inviteHash,
|
||||
allowImpersonation: entity.allowImpersonation,
|
||||
isPublicInviteLinkEnabled: entity.isPublicInviteLinkEnabled,
|
||||
trashRetentionDays: entity.trashRetentionDays,
|
||||
eventLogRetentionDays: entity.eventLogRetentionDays,
|
||||
activationStatus: entity.activationStatus,
|
||||
metadataVersion: entity.metadataVersion,
|
||||
databaseSchema: entity.databaseSchema,
|
||||
subdomain: entity.subdomain,
|
||||
customDomain: entity.customDomain,
|
||||
isGoogleAuthEnabled: entity.isGoogleAuthEnabled,
|
||||
isGoogleAuthBypassEnabled: entity.isGoogleAuthBypassEnabled,
|
||||
isTwoFactorAuthenticationEnforced: entity.isTwoFactorAuthenticationEnforced,
|
||||
isPasswordAuthEnabled: entity.isPasswordAuthEnabled,
|
||||
isPasswordAuthBypassEnabled: entity.isPasswordAuthBypassEnabled,
|
||||
isMicrosoftAuthEnabled: entity.isMicrosoftAuthEnabled,
|
||||
isMicrosoftAuthBypassEnabled: entity.isMicrosoftAuthBypassEnabled,
|
||||
isCustomDomainEnabled: entity.isCustomDomainEnabled,
|
||||
editableProfileFields: entity.editableProfileFields,
|
||||
defaultRoleId: entity.defaultRoleId,
|
||||
version: entity.version,
|
||||
fastModel: entity.fastModel,
|
||||
smartModel: entity.smartModel,
|
||||
aiAdditionalInstructions: entity.aiAdditionalInstructions,
|
||||
enabledAiModelIds: entity.enabledAiModelIds,
|
||||
useRecommendedModels: entity.useRecommendedModels,
|
||||
workspaceCustomApplicationId: entity.workspaceCustomApplicationId,
|
||||
routerModel: entity.routerModel,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
deletedAt: entity.deletedAt?.toISOString(),
|
||||
suspendedAt: entity.suspendedAt?.toISOString() ?? null,
|
||||
});
|
||||
@@ -25,6 +25,8 @@ import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { CheckCustomDomainValidRecordsCronCommand } from 'src/engine/core-modules/workspace/crons/commands/check-custom-domain-valid-records.cron.command';
|
||||
import { CheckCustomDomainValidRecordsCronJob } from 'src/engine/core-modules/workspace/crons/jobs/check-custom-domain-valid-records.cron.job';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { WorkspaceEntityCacheProviderService } from 'src/engine/core-modules/workspace/services/workspace-entity-cache-provider.service';
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import { WorkspaceGaugeService } from 'src/engine/core-modules/workspace/workspace-gauge.service';
|
||||
import { workspaceAutoResolverOpts } from 'src/engine/core-modules/workspace/workspace.auto-resolver-opts';
|
||||
@@ -82,6 +84,7 @@ import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/stand
|
||||
ApplicationModule,
|
||||
EnterpriseModule,
|
||||
StandardObjectsPrefillModule,
|
||||
CoreEntityCacheModule,
|
||||
],
|
||||
services: [WorkspaceService],
|
||||
resolvers: workspaceAutoResolverOpts,
|
||||
@@ -92,6 +95,7 @@ import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/stand
|
||||
WorkspaceResolver,
|
||||
WorkspaceService,
|
||||
WorkspaceGaugeService,
|
||||
WorkspaceEntityCacheProviderService,
|
||||
BillingDisabledGuard,
|
||||
CheckCustomDomainValidRecordsCronCommand,
|
||||
CheckCustomDomainValidRecordsCronJob,
|
||||
|
||||
Reference in New Issue
Block a user