Add local only cache to cache service and cache typeorm entity metadata (#16287)
## Problem buildEntityMetadatas in GlobalWorkspaceOrmManager is computationally expensive and was running on every executeInWorkspaceContext call. This method uses TypeORM's EntitySchemaTransformer and EntityMetadataBuilder to build metadata for all workspace entities (30-50+ objects with many fields each). The resulting EntityMetadata[] is not serialisable which means it cannot be cached in Redis because they contain: - Circular references - Functions/methods - References to the DataSource instance ## Solution Extended the workspace cache system to support local-only caching, then created a cache provider for entityMetadatas. ## Implementation details Updated @WorkspaceCache decorator (workspace-cache.decorator.ts) - Added localOnly?: boolean option to skip Redis storage for non-serializable data Created WorkspaceEntityMetadatasCacheService - Computes entity metadatas from DB to avoid race condition, this is acceptable Simplified GlobalWorkspaceOrmManager - Now fetches entityMetadatas from cache instead of rebuilding on every call Updated Workspace migration runner - the only entry point where metadata can change - Now invalidate the new 'entityMetadata' local cache when shouldIncrementMetadataGraphqlSchemaVersion is true (== field/object mutations)
This commit is contained in:
+27
-106
@@ -30,7 +30,6 @@ import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-q
|
||||
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
@@ -132,32 +131,15 @@ export abstract class CommonBaseQueryRunnerService<
|
||||
commonQueryParser,
|
||||
);
|
||||
|
||||
const isGlobalDatasourceEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
if (isGlobalDatasourceEnabled) {
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () =>
|
||||
this.executeQueryAndEnrichResults(
|
||||
processedArgs,
|
||||
authContext,
|
||||
queryRunnerContext,
|
||||
commonQueryParser,
|
||||
isGlobalDatasourceEnabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return this.executeQueryAndEnrichResults(
|
||||
processedArgs,
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
queryRunnerContext,
|
||||
commonQueryParser,
|
||||
isGlobalDatasourceEnabled,
|
||||
async () =>
|
||||
this.executeQueryAndEnrichResults(
|
||||
processedArgs,
|
||||
authContext,
|
||||
queryRunnerContext,
|
||||
commonQueryParser,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,17 +199,12 @@ export abstract class CommonBaseQueryRunnerService<
|
||||
authContext: WorkspaceAuthContext,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
commonQueryParser: GraphqlQueryParser,
|
||||
isGlobalDatasourceEnabled: boolean,
|
||||
): Promise<Output> {
|
||||
const extendedQueryRunnerContext = isGlobalDatasourceEnabled
|
||||
? await this.prepareExtendedQueryRunnerContextWithGlobalDatasource(
|
||||
authContext,
|
||||
queryRunnerContext,
|
||||
)
|
||||
: await this.prepareExtendedQueryRunnerContext(
|
||||
authContext,
|
||||
queryRunnerContext,
|
||||
);
|
||||
const extendedQueryRunnerContext =
|
||||
await this.prepareExtendedQueryRunnerContextWithGlobalDatasource(
|
||||
authContext,
|
||||
queryRunnerContext,
|
||||
);
|
||||
|
||||
const results = await this.run(processedArgs, {
|
||||
...extendedQueryRunnerContext,
|
||||
@@ -312,81 +289,28 @@ export abstract class CommonBaseQueryRunnerService<
|
||||
}
|
||||
}
|
||||
|
||||
private async getRoleIdAndObjectsPermissions(
|
||||
private async getRoleIdOrThrow(
|
||||
authContext: AuthContext,
|
||||
workspaceId: string,
|
||||
) {
|
||||
let roleId: string;
|
||||
|
||||
if (
|
||||
!isDefined(authContext.apiKey) &&
|
||||
!isDefined(authContext.userWorkspaceId)
|
||||
) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT,
|
||||
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
|
||||
);
|
||||
}
|
||||
|
||||
): Promise<string> {
|
||||
if (isDefined(authContext.apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
return this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
authContext.apiKey.id,
|
||||
workspaceId,
|
||||
);
|
||||
} else {
|
||||
const userWorkspaceRoleId =
|
||||
await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!isDefined(userWorkspaceRoleId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
);
|
||||
}
|
||||
|
||||
roleId = userWorkspaceRoleId;
|
||||
}
|
||||
|
||||
const { rolesPermissions } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'rolesPermissions',
|
||||
]);
|
||||
if (!isDefined(authContext.userWorkspaceId)) {
|
||||
throw new CommonQueryRunnerException(
|
||||
'Invalid auth context',
|
||||
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
|
||||
);
|
||||
}
|
||||
|
||||
return { roleId, objectsPermissions: rolesPermissions[roleId] };
|
||||
}
|
||||
|
||||
private async prepareExtendedQueryRunnerContext(
|
||||
authContext: WorkspaceAuthContext,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<Omit<CommonExtendedQueryRunnerContext, 'commonQueryParser'>> {
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
|
||||
const { roleId } = await this.getRoleIdAndObjectsPermissions(
|
||||
authContext,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
const rolePermissionConfig = { unionOf: [roleId] };
|
||||
|
||||
const repository = workspaceDataSource.getRepository(
|
||||
queryRunnerContext.flatObjectMetadata.nameSingular,
|
||||
rolePermissionConfig,
|
||||
authContext,
|
||||
);
|
||||
|
||||
return {
|
||||
...queryRunnerContext,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
rolePermissionConfig,
|
||||
repository,
|
||||
};
|
||||
return this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
private async prepareExtendedQueryRunnerContextWithGlobalDatasource(
|
||||
@@ -395,10 +319,7 @@ export abstract class CommonBaseQueryRunnerService<
|
||||
): Promise<Omit<CommonExtendedQueryRunnerContext, 'commonQueryParser'>> {
|
||||
const workspaceId = authContext.workspace.id;
|
||||
|
||||
const { roleId } = await this.getRoleIdAndObjectsPermissions(
|
||||
authContext,
|
||||
workspaceId,
|
||||
);
|
||||
const roleId = await this.getRoleIdOrThrow(authContext, workspaceId);
|
||||
|
||||
const rolePermissionConfig = { unionOf: [roleId] };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user