[permissions] Implement object-records permissions in query builders (#11458)
In this PR we are - (if permissionsV2 is enabled) executing permission checks at query builder level. To do so we want to override the query builders methods that are performing db calls (.execute(), .getMany(), ... etc.) For now I have just overriden some of the query builders methods for the poc. To do so I created custom query builder classes that extend typeorm's query builder (selectQueryBuilder and updateQueryBuilder, for now and later I will tackle softDeleteQueryBuilder, etc.). - adding a notion of roles permissions version and roles permissions object to datasources. We will now use one datasource per roleId and rolePermissionVersion. Both rolesPermissionsVersion and rolesPermissions objects are stored in redis and recomputed at role update or if queried and found empty. Unlike for metadata version we don't need to store a version in the db that stands for the source of truth. We also don't need to destroy and recreate the datasource if the rolesPermissions version changes, but only to update the value for rolesPermissions and rolesPermissionsVersions on the existing datasource. What this PR misses - computing of roles permissions should take into account objectPermissions table (for now it only looks at what's on the roles table) - pursue extension of query builder classes and overriding of their db calling-methods - what should the behaviour be for calls from twentyOrmGlobalManager that don't have a roleId?
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import {
|
||||
DataSource,
|
||||
DataSourceOptions,
|
||||
@ -6,6 +7,7 @@ import {
|
||||
QueryRunner,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/entity.manager';
|
||||
@ -14,20 +16,37 @@ import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.
|
||||
export class WorkspaceDataSource extends DataSource {
|
||||
readonly internalContext: WorkspaceInternalContext;
|
||||
readonly manager: WorkspaceEntityManager;
|
||||
featureFlagMapVersion: string;
|
||||
featureFlagMap: FeatureFlagMap;
|
||||
rolesPermissionsVersion?: string;
|
||||
permissionsPerRoleId?: ObjectRecordsPermissionsByRoleId;
|
||||
|
||||
constructor(
|
||||
internalContext: WorkspaceInternalContext,
|
||||
options: DataSourceOptions,
|
||||
featureFlagMapVersion: string,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
rolesPermissionsVersion?: string,
|
||||
permissionsPerRoleId?: ObjectRecordsPermissionsByRoleId,
|
||||
) {
|
||||
super(options);
|
||||
this.internalContext = internalContext;
|
||||
// Recreate manager after internalContext has been initialized
|
||||
this.manager = this.createEntityManager();
|
||||
this.featureFlagMap = featureFlagMap;
|
||||
this.featureFlagMapVersion = featureFlagMapVersion;
|
||||
this.rolesPermissionsVersion = rolesPermissionsVersion;
|
||||
this.permissionsPerRoleId = permissionsPerRoleId;
|
||||
}
|
||||
|
||||
override getRepository<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
roleId?: string,
|
||||
): WorkspaceRepository<Entity> {
|
||||
if (roleId) {
|
||||
return this.manager.getRepository(target, roleId);
|
||||
}
|
||||
|
||||
return this.manager.getRepository(target);
|
||||
}
|
||||
|
||||
@ -36,4 +55,20 @@ export class WorkspaceDataSource extends DataSource {
|
||||
): WorkspaceEntityManager {
|
||||
return new WorkspaceEntityManager(this.internalContext, this, queryRunner);
|
||||
}
|
||||
|
||||
setRolesPermissionsVersion(rolesPermissionsVersion: string) {
|
||||
this.rolesPermissionsVersion = rolesPermissionsVersion;
|
||||
}
|
||||
|
||||
setRolesPermissions(permissionsPerRoleId: ObjectRecordsPermissionsByRoleId) {
|
||||
this.permissionsPerRoleId = permissionsPerRoleId;
|
||||
}
|
||||
|
||||
setFeatureFlagMap(featureFlagMap: FeatureFlagMap) {
|
||||
this.featureFlagMap = featureFlagMap;
|
||||
}
|
||||
|
||||
setFeatureFlagMapVersion(featureFlagMapVersion: string) {
|
||||
this.featureFlagMapVersion = featureFlagMapVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,17 @@ import {
|
||||
EntityTarget,
|
||||
ObjectLiteral,
|
||||
QueryRunner,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
|
||||
export class WorkspaceEntityManager extends EntityManager {
|
||||
private readonly internalContext: WorkspaceInternalContext;
|
||||
readonly repositories: Map<string, Repository<any>>;
|
||||
|
||||
constructor(
|
||||
internalContext: WorkspaceInternalContext,
|
||||
@ -20,27 +23,39 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
) {
|
||||
super(connection, queryRunner);
|
||||
this.internalContext = internalContext;
|
||||
this.repositories = new Map();
|
||||
}
|
||||
|
||||
override getRepository<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
roleId?: string,
|
||||
): WorkspaceRepository<Entity> {
|
||||
// find already created repository instance and return it if found
|
||||
|
||||
const repoFromMap = this.repositories.get(target);
|
||||
const dataSource = this.connection as WorkspaceDataSource;
|
||||
const repositoryKey = `${dataSource.getMetadata(target).name}_${roleId ?? 'default'}${dataSource.rolesPermissionsVersion ? `_${dataSource.rolesPermissionsVersion}` : ''}${dataSource.featureFlagMapVersion ? `_${dataSource.featureFlagMapVersion}` : ''}`;
|
||||
const repoFromMap = this.repositories.get(repositoryKey);
|
||||
|
||||
if (repoFromMap) {
|
||||
return repoFromMap as WorkspaceRepository<Entity>;
|
||||
}
|
||||
|
||||
let objectPermissions = {};
|
||||
|
||||
if (roleId) {
|
||||
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId;
|
||||
|
||||
objectPermissions = objectPermissionsByRoleId?.[roleId] ?? {};
|
||||
}
|
||||
|
||||
const newRepository = new WorkspaceRepository<Entity>(
|
||||
this.internalContext,
|
||||
target,
|
||||
this,
|
||||
dataSource.featureFlagMap,
|
||||
this.queryRunner,
|
||||
objectPermissions,
|
||||
);
|
||||
|
||||
this.repositories.set(target, newRepository);
|
||||
this.repositories.set(repositoryKey, newRepository);
|
||||
|
||||
return newRepository;
|
||||
}
|
||||
|
||||
@ -11,4 +11,6 @@ export enum TwentyORMExceptionCode {
|
||||
METADATA_VERSION_MISMATCH = 'METADATA_VERSION_MISMATCH',
|
||||
METADATA_COLLECTION_NOT_FOUND = 'METADATA_COLLECTION_NOT_FOUND',
|
||||
WORKSPACE_SCHEMA_NOT_FOUND = 'WORKSPACE_SCHEMA_NOT_FOUND',
|
||||
ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND',
|
||||
FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND',
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ export class ScopedWorkspaceContextFactory {
|
||||
public create(): {
|
||||
workspaceId: string | null;
|
||||
workspaceMetadataVersion: number | null;
|
||||
userWorkspaceId: string | null;
|
||||
} {
|
||||
const workspaceId: string | undefined =
|
||||
this.request?.['req']?.['workspaceId'] ||
|
||||
@ -22,6 +23,7 @@ export class ScopedWorkspaceContextFactory {
|
||||
return {
|
||||
workspaceId: workspaceId ?? null,
|
||||
workspaceMetadataVersion: workspaceMetadataVersion ?? null,
|
||||
userWorkspaceId: this.request?.['req']?.['userWorkspaceId'] ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { EntitySchema } from 'typeorm';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceFeatureFlagMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flag-map-cache.service.ts/workspace-feature-flag-map-cache.service';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service';
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import {
|
||||
TwentyORMException,
|
||||
@ -17,6 +23,11 @@ import { PromiseMemoizer } from 'src/engine/twenty-orm/storage/promise-memoizer.
|
||||
import { CacheKey } from 'src/engine/twenty-orm/storage/types/cache-key.type';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
type CacheResult<T, U> = {
|
||||
version: T;
|
||||
data: U;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceDatasourceFactory {
|
||||
private readonly logger = new Logger(WorkspaceDatasourceFactory.name);
|
||||
@ -28,19 +39,35 @@ export class WorkspaceDatasourceFactory {
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly entitySchemaFactory: EntitySchemaFactory,
|
||||
private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService,
|
||||
private readonly workspaceFeatureFlagMapCacheService: WorkspaceFeatureFlagMapCacheService,
|
||||
) {}
|
||||
|
||||
public async create(
|
||||
workspaceId: string,
|
||||
workspaceMetadataVersion: number | null,
|
||||
failOnMetadataCacheMiss = true,
|
||||
shouldFailIfMetadataNotFound = true,
|
||||
): Promise<WorkspaceDataSource> {
|
||||
const cachedWorkspaceMetadataVersion =
|
||||
await this.getWorkspaceMetadataVersionFromCache(
|
||||
workspaceId,
|
||||
failOnMetadataCacheMiss,
|
||||
shouldFailIfMetadataNotFound,
|
||||
);
|
||||
|
||||
const { data: cachedFeatureFlagMap, version: cachedFeatureFlagMapVersion } =
|
||||
await this.getFeatureFlagMapFromCache({ workspaceId });
|
||||
|
||||
const isPermissionsV2Enabled =
|
||||
cachedFeatureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled];
|
||||
|
||||
const {
|
||||
data: cachedRolesPermissions,
|
||||
version: cachedRolesPermissionsVersion,
|
||||
} = await this.getRolesPermissionsFromCache({
|
||||
workspaceId,
|
||||
isPermissionsV2Enabled,
|
||||
});
|
||||
|
||||
if (
|
||||
workspaceMetadataVersion !== null &&
|
||||
cachedWorkspaceMetadataVersion !== workspaceMetadataVersion
|
||||
@ -139,6 +166,10 @@ export class WorkspaceDatasourceFactory {
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
cachedFeatureFlagMapVersion,
|
||||
cachedFeatureFlagMap,
|
||||
cachedRolesPermissionsVersion,
|
||||
cachedRolesPermissions,
|
||||
);
|
||||
|
||||
await workspaceDataSource.initialize();
|
||||
@ -162,28 +193,206 @@ export class WorkspaceDatasourceFactory {
|
||||
throw new Error(`Failed to create WorkspaceDataSource for ${cacheKey}`);
|
||||
}
|
||||
|
||||
if (isPermissionsV2Enabled) {
|
||||
await this.updateWorkspaceDataSourceRolesPermissionsIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedRolesPermissionsVersion,
|
||||
cachedRolesPermissions,
|
||||
});
|
||||
}
|
||||
|
||||
await this.updateWorkspaceDataSourceFeatureFlagMapIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedFeatureFlagMapVersion,
|
||||
cachedFeatureFlagMap,
|
||||
});
|
||||
|
||||
return workspaceDataSource;
|
||||
}
|
||||
|
||||
private async getFromCacheWithRecompute<T, U>({
|
||||
workspaceId,
|
||||
getCacheData,
|
||||
getCacheVersion,
|
||||
recomputeCache,
|
||||
cachedEntityName,
|
||||
exceptionCode,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
getCacheData: (workspaceId: string) => Promise<U | undefined>;
|
||||
getCacheVersion: (workspaceId: string) => Promise<T | undefined>;
|
||||
recomputeCache: (params: { workspaceId: string }) => Promise<void>;
|
||||
cachedEntityName: string;
|
||||
exceptionCode: TwentyORMExceptionCode;
|
||||
}): Promise<CacheResult<T, U>> {
|
||||
let cachedVersion: T | undefined;
|
||||
let cachedData: U | undefined;
|
||||
|
||||
cachedVersion = await getCacheVersion(workspaceId);
|
||||
cachedData = await getCacheData(workspaceId);
|
||||
|
||||
if (!isDefined(cachedData) || !isDefined(cachedVersion)) {
|
||||
await recomputeCache({ workspaceId });
|
||||
|
||||
cachedData = await getCacheData(workspaceId);
|
||||
cachedVersion = await getCacheVersion(workspaceId);
|
||||
|
||||
if (!isDefined(cachedData) || !isDefined(cachedVersion)) {
|
||||
throw new TwentyORMException(
|
||||
`${cachedEntityName} not found after recompute for workspace ${workspaceId}`,
|
||||
exceptionCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: cachedVersion,
|
||||
data: cachedData,
|
||||
};
|
||||
}
|
||||
|
||||
private async getRolesPermissionsFromCache({
|
||||
workspaceId,
|
||||
isPermissionsV2Enabled,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
isPermissionsV2Enabled?: boolean;
|
||||
}): Promise<
|
||||
CacheResult<
|
||||
string | undefined,
|
||||
ObjectRecordsPermissionsByRoleId | undefined
|
||||
>
|
||||
> {
|
||||
if (!isPermissionsV2Enabled) {
|
||||
return { version: undefined, data: undefined };
|
||||
}
|
||||
|
||||
return this.getFromCacheWithRecompute<
|
||||
string | undefined,
|
||||
ObjectRecordsPermissionsByRoleId | undefined
|
||||
>({
|
||||
workspaceId,
|
||||
getCacheData: () =>
|
||||
this.workspaceCacheStorageService.getRolesPermissions(workspaceId),
|
||||
getCacheVersion: () =>
|
||||
this.workspaceCacheStorageService.getRolesPermissionsVersionFromCache(
|
||||
workspaceId,
|
||||
),
|
||||
recomputeCache: (params) =>
|
||||
this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
params,
|
||||
),
|
||||
cachedEntityName: 'Roles permissions',
|
||||
exceptionCode: TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
private async getFeatureFlagMapFromCache({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<CacheResult<string, FeatureFlagMap>> {
|
||||
return this.getFromCacheWithRecompute<string, FeatureFlagMap>({
|
||||
workspaceId,
|
||||
getCacheData: () =>
|
||||
this.workspaceCacheStorageService.getFeatureFlagMap(workspaceId),
|
||||
getCacheVersion: () =>
|
||||
this.workspaceCacheStorageService.getFeatureFlagMapVersionFromCache(
|
||||
workspaceId,
|
||||
),
|
||||
recomputeCache: (params) =>
|
||||
this.workspaceFeatureFlagMapCacheService.recomputeFeatureFlagMapCache(
|
||||
params,
|
||||
),
|
||||
cachedEntityName: 'Feature flag map',
|
||||
exceptionCode: TwentyORMExceptionCode.FEATURE_FLAG_MAP_VERSION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
private updateWorkspaceDataSourceIfNeeded<T>({
|
||||
workspaceDataSource,
|
||||
currentVersion,
|
||||
newVersion,
|
||||
newData,
|
||||
setData,
|
||||
setVersion,
|
||||
}: {
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
currentVersion: string | undefined;
|
||||
newVersion: string | undefined;
|
||||
newData: T | undefined;
|
||||
setData: (data: T) => void;
|
||||
setVersion: (version: string) => void;
|
||||
}): void {
|
||||
if (
|
||||
isDefined(newVersion) &&
|
||||
isDefined(newData) &&
|
||||
currentVersion !== newVersion
|
||||
) {
|
||||
workspaceDataSource.manager.repositories.clear();
|
||||
setData(newData);
|
||||
setVersion(newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateWorkspaceDataSourceRolesPermissionsIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedRolesPermissionsVersion,
|
||||
cachedRolesPermissions,
|
||||
}: {
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
cachedRolesPermissionsVersion: string | undefined;
|
||||
cachedRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined;
|
||||
}): Promise<void> {
|
||||
this.updateWorkspaceDataSourceIfNeeded({
|
||||
workspaceDataSource,
|
||||
currentVersion: workspaceDataSource.rolesPermissionsVersion,
|
||||
newVersion: cachedRolesPermissionsVersion,
|
||||
newData: cachedRolesPermissions,
|
||||
setData: (data) => workspaceDataSource.setRolesPermissions(data),
|
||||
setVersion: (version) =>
|
||||
workspaceDataSource.setRolesPermissionsVersion(version),
|
||||
});
|
||||
}
|
||||
|
||||
private async updateWorkspaceDataSourceFeatureFlagMapIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedFeatureFlagMapVersion,
|
||||
cachedFeatureFlagMap,
|
||||
}: {
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
cachedFeatureFlagMapVersion: string | undefined;
|
||||
cachedFeatureFlagMap: FeatureFlagMap | undefined;
|
||||
}): Promise<void> {
|
||||
this.updateWorkspaceDataSourceIfNeeded({
|
||||
workspaceDataSource,
|
||||
currentVersion: workspaceDataSource.featureFlagMapVersion,
|
||||
newVersion: cachedFeatureFlagMapVersion,
|
||||
newData: cachedFeatureFlagMap,
|
||||
setData: (data) => workspaceDataSource.setFeatureFlagMap(data),
|
||||
setVersion: (version) =>
|
||||
workspaceDataSource.setFeatureFlagMapVersion(version),
|
||||
});
|
||||
}
|
||||
|
||||
private async getWorkspaceMetadataVersionFromCache(
|
||||
workspaceId: string,
|
||||
failOnMetadataCacheMiss = true,
|
||||
shouldFailIfMetadataNotFound = true,
|
||||
): Promise<number> {
|
||||
let latestWorkspaceMetadataVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
|
||||
|
||||
if (latestWorkspaceMetadataVersion === undefined) {
|
||||
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
||||
workspaceId,
|
||||
ignoreLock: !failOnMetadataCacheMiss,
|
||||
});
|
||||
|
||||
if (failOnMetadataCacheMiss) {
|
||||
if (shouldFailIfMetadataNotFound) {
|
||||
throw new TwentyORMException(
|
||||
`Metadata version not found for workspace ${workspaceId}`,
|
||||
TwentyORMExceptionCode.METADATA_VERSION_NOT_FOUND,
|
||||
);
|
||||
} else {
|
||||
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
||||
workspaceId,
|
||||
ignoreLock: !shouldFailIfMetadataNotFound,
|
||||
});
|
||||
latestWorkspaceMetadataVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(
|
||||
workspaceId,
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
||||
import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap';
|
||||
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => {
|
||||
const mainEntity = expressionMap.aliases[0].metadata.name;
|
||||
const operationType = expressionMap.queryType;
|
||||
|
||||
return {
|
||||
mainEntity,
|
||||
operationType,
|
||||
};
|
||||
};
|
||||
|
||||
export const validateQueryIsPermittedOrThrow = (
|
||||
expressionMap: QueryExpressionMap,
|
||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||
) => {
|
||||
const { mainEntity, operationType } =
|
||||
getTargetEntityAndOperationType(expressionMap);
|
||||
|
||||
const permissionsForEntity = objectRecordsPermissions[mainEntity];
|
||||
|
||||
switch (operationType) {
|
||||
case 'select':
|
||||
if (!permissionsForEntity?.canRead) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'insert':
|
||||
case 'update':
|
||||
if (!permissionsForEntity?.canUpdate) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (!permissionsForEntity?.canDestroy) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'soft-delete':
|
||||
if (!permissionsForEntity?.canSoftDelete) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.UNKNOWN_OPERATION_NAME,
|
||||
PermissionsExceptionCode.UNKNOWN_OPERATION_NAME,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
||||
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
|
||||
export class WorkspaceQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends WorkspaceSelectQueryBuilder<T> {
|
||||
constructor(
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||
) {
|
||||
super(queryBuilder, objectRecordsPermissions);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
}
|
||||
|
||||
override clone(): this {
|
||||
const clonedQueryBuilder = super.clone();
|
||||
|
||||
return new WorkspaceQueryBuilder(
|
||||
clonedQueryBuilder,
|
||||
this.objectRecordsPermissions,
|
||||
) as this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
||||
import { ObjectLiteral, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.util';
|
||||
import { WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
|
||||
export class WorkspaceSelectQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends SelectQueryBuilder<T> {
|
||||
objectRecordsPermissions: ObjectRecordsPermissions;
|
||||
constructor(
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||
) {
|
||||
super(queryBuilder);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
}
|
||||
|
||||
override update(): WorkspaceUpdateQueryBuilder<T>;
|
||||
|
||||
override update(
|
||||
updateSet: QueryDeepPartialEntity<T>,
|
||||
): WorkspaceUpdateQueryBuilder<T>;
|
||||
|
||||
override update(
|
||||
updateSet?: QueryDeepPartialEntity<T>,
|
||||
): UpdateQueryBuilder<T> {
|
||||
const updateQueryBuilder = updateSet
|
||||
? super.update(updateSet)
|
||||
: super.update();
|
||||
|
||||
return new WorkspaceUpdateQueryBuilder<T>(
|
||||
updateQueryBuilder,
|
||||
this.objectRecordsPermissions,
|
||||
);
|
||||
}
|
||||
|
||||
override execute(): Promise<T[]> {
|
||||
validateQueryIsPermittedOrThrow(
|
||||
this.expressionMap,
|
||||
this.objectRecordsPermissions,
|
||||
);
|
||||
|
||||
return super.execute();
|
||||
}
|
||||
|
||||
override getMany(): Promise<T[]> {
|
||||
validateQueryIsPermittedOrThrow(
|
||||
this.expressionMap,
|
||||
this.objectRecordsPermissions,
|
||||
);
|
||||
|
||||
return super.getMany();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
||||
import { ObjectLiteral, UpdateQueryBuilder, UpdateResult } from 'typeorm';
|
||||
|
||||
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.util';
|
||||
|
||||
export class WorkspaceUpdateQueryBuilder<
|
||||
Entity extends ObjectLiteral,
|
||||
> extends UpdateQueryBuilder<Entity> {
|
||||
private objectRecordsPermissions: ObjectRecordsPermissions;
|
||||
constructor(
|
||||
queryBuilder: UpdateQueryBuilder<Entity>,
|
||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||
) {
|
||||
super(queryBuilder);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
}
|
||||
|
||||
override execute(): Promise<UpdateResult> {
|
||||
validateQueryIsPermittedOrThrow(
|
||||
this.expressionMap,
|
||||
this.objectRecordsPermissions,
|
||||
);
|
||||
|
||||
return super.execute();
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
DeepPartial,
|
||||
DeleteResult,
|
||||
@ -20,36 +21,70 @@ import { PickKeysByType } from 'typeorm/common/PickKeysByType';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import { UpsertOptions } from 'typeorm/repository/UpsertOptions';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-query-builder';
|
||||
import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
export class WorkspaceRepository<
|
||||
Entity extends ObjectLiteral,
|
||||
> extends Repository<Entity> {
|
||||
T extends ObjectLiteral,
|
||||
> extends Repository<T> {
|
||||
private readonly internalContext: WorkspaceInternalContext;
|
||||
private featureFlagMap: FeatureFlagMap;
|
||||
private objectRecordsPermissions?: ObjectRecordsPermissions;
|
||||
|
||||
constructor(
|
||||
internalContext: WorkspaceInternalContext,
|
||||
target: EntityTarget<Entity>,
|
||||
target: EntityTarget<T>,
|
||||
manager: EntityManager,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
queryRunner?: QueryRunner,
|
||||
objectRecordsPermissions?: ObjectRecordsPermissions,
|
||||
) {
|
||||
super(target, manager, queryRunner);
|
||||
this.internalContext = internalContext;
|
||||
this.featureFlagMap = featureFlagMap;
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
}
|
||||
|
||||
override createQueryBuilder<U extends T>(
|
||||
alias?: string,
|
||||
queryRunner?: QueryRunner,
|
||||
): WorkspaceQueryBuilder<U> {
|
||||
const queryBuilder = super.createQueryBuilder(
|
||||
alias,
|
||||
queryRunner,
|
||||
) as unknown as WorkspaceQueryBuilder<U>;
|
||||
const isPermissionsV2Enabled =
|
||||
this.featureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled];
|
||||
|
||||
if (!isPermissionsV2Enabled) {
|
||||
return queryBuilder;
|
||||
} else {
|
||||
if (!this.objectRecordsPermissions) {
|
||||
throw new Error('Object records permissions are required');
|
||||
}
|
||||
|
||||
return new WorkspaceQueryBuilder(
|
||||
queryBuilder,
|
||||
this.objectRecordsPermissions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIND METHODS
|
||||
*/
|
||||
override async find(
|
||||
options?: FindManyOptions<Entity>,
|
||||
options?: FindManyOptions<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<Entity[]> {
|
||||
): Promise<T[]> {
|
||||
const manager = entityManager || this.manager;
|
||||
const computedOptions = await this.transformOptions(options);
|
||||
const result = await manager.find(this.target, computedOptions);
|
||||
@ -59,9 +94,9 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async findBy(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
where: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<Entity[]> {
|
||||
): Promise<T[]> {
|
||||
const manager = entityManager || this.manager;
|
||||
const computedOptions = await this.transformOptions({ where });
|
||||
const result = await manager.findBy(this.target, computedOptions.where);
|
||||
@ -71,9 +106,9 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async findAndCount(
|
||||
options?: FindManyOptions<Entity>,
|
||||
options?: FindManyOptions<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<[Entity[], number]> {
|
||||
): Promise<[T[], number]> {
|
||||
const manager = entityManager || this.manager;
|
||||
const computedOptions = await this.transformOptions(options);
|
||||
const result = await manager.findAndCount(this.target, computedOptions);
|
||||
@ -83,9 +118,9 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async findAndCountBy(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
where: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<[Entity[], number]> {
|
||||
): Promise<[T[], number]> {
|
||||
const manager = entityManager || this.manager;
|
||||
const computedOptions = await this.transformOptions({ where });
|
||||
const result = await manager.findAndCountBy(
|
||||
@ -98,9 +133,9 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async findOne(
|
||||
options: FindOneOptions<Entity>,
|
||||
options: FindOneOptions<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<Entity | null> {
|
||||
): Promise<T | null> {
|
||||
const manager = entityManager || this.manager;
|
||||
const computedOptions = await this.transformOptions(options);
|
||||
const result = await manager.findOne(this.target, computedOptions);
|
||||
@ -110,9 +145,9 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async findOneBy(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
where: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<Entity | null> {
|
||||
): Promise<T | null> {
|
||||
const manager = entityManager || this.manager;
|
||||
const computedOptions = await this.transformOptions({ where });
|
||||
const result = await manager.findOneBy(this.target, computedOptions.where);
|
||||
@ -122,9 +157,9 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async findOneOrFail(
|
||||
options: FindOneOptions<Entity>,
|
||||
options: FindOneOptions<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<Entity> {
|
||||
): Promise<T> {
|
||||
const manager = entityManager || this.manager;
|
||||
const computedOptions = await this.transformOptions(options);
|
||||
const result = await manager.findOneOrFail(this.target, computedOptions);
|
||||
@ -134,9 +169,9 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async findOneByOrFail(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
where: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<Entity> {
|
||||
): Promise<T> {
|
||||
const manager = entityManager || this.manager;
|
||||
const computedOptions = await this.transformOptions({ where });
|
||||
const result = await manager.findOneByOrFail(
|
||||
@ -151,38 +186,38 @@ export class WorkspaceRepository<
|
||||
/**
|
||||
* SAVE METHODS
|
||||
*/
|
||||
override save<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
override save<U extends DeepPartial<T>>(
|
||||
entities: U[],
|
||||
options: SaveOptions & { reload: false },
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T[]>;
|
||||
|
||||
override save<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
override save<U extends DeepPartial<T>>(
|
||||
entities: U[],
|
||||
options?: SaveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<(T & Entity)[]>;
|
||||
): Promise<(U & T)[]>;
|
||||
|
||||
override save<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
override save<U extends DeepPartial<T>>(
|
||||
entity: U,
|
||||
options: SaveOptions & { reload: false },
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T>;
|
||||
|
||||
override save<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
override save<U extends DeepPartial<T>>(
|
||||
entity: U,
|
||||
options?: SaveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T & Entity>;
|
||||
): Promise<U & T>;
|
||||
|
||||
override async save<T extends DeepPartial<Entity>>(
|
||||
entityOrEntities: T | T[],
|
||||
override async save<U extends DeepPartial<T>>(
|
||||
entityOrEntities: U | U[],
|
||||
options?: SaveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T | T[]> {
|
||||
): Promise<U | U[]> {
|
||||
const manager = entityManager || this.manager;
|
||||
const formattedEntityOrEntities = await this.formatData(entityOrEntities);
|
||||
let result: T | T[];
|
||||
let result: U | U[];
|
||||
|
||||
// Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion
|
||||
if (Array.isArray(formattedEntityOrEntities)) {
|
||||
@ -208,22 +243,22 @@ export class WorkspaceRepository<
|
||||
* REMOVE METHODS
|
||||
*/
|
||||
override remove(
|
||||
entities: Entity[],
|
||||
entities: T[],
|
||||
options?: RemoveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<Entity[]>;
|
||||
): Promise<T[]>;
|
||||
|
||||
override remove(
|
||||
entity: Entity,
|
||||
entity: T,
|
||||
options?: RemoveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<Entity>;
|
||||
): Promise<T>;
|
||||
|
||||
override async remove(
|
||||
entityOrEntities: Entity | Entity[],
|
||||
entityOrEntities: T | T[],
|
||||
options?: RemoveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<Entity | Entity[]> {
|
||||
): Promise<T | T[]> {
|
||||
const manager = entityManager || this.manager;
|
||||
const formattedEntityOrEntities = await this.formatData(entityOrEntities);
|
||||
const result = await manager.remove(
|
||||
@ -247,7 +282,7 @@ export class WorkspaceRepository<
|
||||
| Date[]
|
||||
| ObjectId
|
||||
| ObjectId[]
|
||||
| FindOptionsWhere<Entity>,
|
||||
| FindOptionsWhere<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<DeleteResult> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -259,38 +294,38 @@ export class WorkspaceRepository<
|
||||
return manager.delete(this.target, criteria);
|
||||
}
|
||||
|
||||
override softRemove<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
override softRemove<U extends DeepPartial<T>>(
|
||||
entities: U[],
|
||||
options: SaveOptions & { reload: false },
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T[]>;
|
||||
|
||||
override softRemove<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
override softRemove<U extends DeepPartial<T>>(
|
||||
entities: U[],
|
||||
options?: SaveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<(T & Entity)[]>;
|
||||
): Promise<(U & T)[]>;
|
||||
|
||||
override softRemove<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
override softRemove<U extends DeepPartial<T>>(
|
||||
entity: U,
|
||||
options: SaveOptions & { reload: false },
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T>;
|
||||
): Promise<U>;
|
||||
|
||||
override softRemove<T extends DeepPartial<Entity>>(
|
||||
override softRemove<U extends DeepPartial<T>>(
|
||||
entity: T,
|
||||
options?: SaveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T & Entity>;
|
||||
): Promise<U & T>;
|
||||
|
||||
override async softRemove<T extends DeepPartial<Entity>>(
|
||||
entityOrEntities: T | T[],
|
||||
override async softRemove<U extends DeepPartial<T>>(
|
||||
entityOrEntities: U | U[],
|
||||
options?: SaveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T | T[]> {
|
||||
): Promise<U | U[]> {
|
||||
const manager = entityManager || this.manager;
|
||||
const formattedEntityOrEntities = await this.formatData(entityOrEntities);
|
||||
let result: T | T[];
|
||||
let result: U | U[];
|
||||
|
||||
// Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion
|
||||
if (Array.isArray(formattedEntityOrEntities)) {
|
||||
@ -322,7 +357,7 @@ export class WorkspaceRepository<
|
||||
| Date[]
|
||||
| ObjectId
|
||||
| ObjectId[]
|
||||
| FindOptionsWhere<Entity>,
|
||||
| FindOptionsWhere<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<UpdateResult> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -337,38 +372,38 @@ export class WorkspaceRepository<
|
||||
/**
|
||||
* RECOVERY METHODS
|
||||
*/
|
||||
override recover<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
override recover<U extends DeepPartial<T>>(
|
||||
entities: U,
|
||||
options: SaveOptions & { reload: false },
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T[]>;
|
||||
): Promise<U>;
|
||||
|
||||
override recover<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
override recover<U extends DeepPartial<T>>(
|
||||
entities: U,
|
||||
options?: SaveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<(T & Entity)[]>;
|
||||
): Promise<(U & T)[]>;
|
||||
|
||||
override recover<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
override recover<U extends DeepPartial<T>>(
|
||||
entity: U,
|
||||
options: SaveOptions & { reload: false },
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T>;
|
||||
): Promise<U>;
|
||||
|
||||
override recover<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
override recover<U extends DeepPartial<T>>(
|
||||
entity: U,
|
||||
options?: SaveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T & Entity>;
|
||||
): Promise<U & T>;
|
||||
|
||||
override async recover<T extends DeepPartial<Entity>>(
|
||||
entityOrEntities: T | T[],
|
||||
override async recover<U extends DeepPartial<T>>(
|
||||
entityOrEntities: U | U[],
|
||||
options?: SaveOptions,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<T | T[]> {
|
||||
): Promise<U | U[]> {
|
||||
const manager = entityManager || this.manager;
|
||||
const formattedEntityOrEntities = await this.formatData(entityOrEntities);
|
||||
let result: T | T[];
|
||||
let result: U | U[];
|
||||
|
||||
// Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion
|
||||
if (Array.isArray(formattedEntityOrEntities)) {
|
||||
@ -400,7 +435,7 @@ export class WorkspaceRepository<
|
||||
| Date[]
|
||||
| ObjectId
|
||||
| ObjectId[]
|
||||
| FindOptionsWhere<Entity>,
|
||||
| FindOptionsWhere<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<UpdateResult> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -416,7 +451,7 @@ export class WorkspaceRepository<
|
||||
* INSERT METHODS
|
||||
*/
|
||||
override async insert(
|
||||
entity: QueryDeepPartialEntity<Entity> | QueryDeepPartialEntity<Entity>[],
|
||||
entity: QueryDeepPartialEntity<T> | QueryDeepPartialEntity<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<InsertResult> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -445,8 +480,8 @@ export class WorkspaceRepository<
|
||||
| Date[]
|
||||
| ObjectId
|
||||
| ObjectId[]
|
||||
| FindOptionsWhere<Entity>,
|
||||
partialEntity: QueryDeepPartialEntity<Entity>,
|
||||
| FindOptionsWhere<T>,
|
||||
partialEntity: QueryDeepPartialEntity<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<UpdateResult> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -459,10 +494,8 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async upsert(
|
||||
entityOrEntities:
|
||||
| QueryDeepPartialEntity<Entity>
|
||||
| QueryDeepPartialEntity<Entity>[],
|
||||
conflictPathsOrOptions: string[] | UpsertOptions<Entity>,
|
||||
entityOrEntities: QueryDeepPartialEntity<T> | QueryDeepPartialEntity<T>[],
|
||||
conflictPathsOrOptions: string[] | UpsertOptions<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<InsertResult> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -488,7 +521,7 @@ export class WorkspaceRepository<
|
||||
* EXIST METHODS
|
||||
*/
|
||||
override async exists(
|
||||
options?: FindManyOptions<Entity>,
|
||||
options?: FindManyOptions<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<boolean> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -498,7 +531,7 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async existsBy(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
where: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<boolean> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -511,7 +544,7 @@ export class WorkspaceRepository<
|
||||
* COUNT METHODS
|
||||
*/
|
||||
override async count(
|
||||
options?: FindManyOptions<Entity>,
|
||||
options?: FindManyOptions<T>,
|
||||
entityManager?: EntityManager,
|
||||
): Promise<number> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -521,7 +554,7 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async countBy(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
where: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<number> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -534,8 +567,8 @@ export class WorkspaceRepository<
|
||||
* MATH METHODS
|
||||
*/
|
||||
override async sum(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
columnName: PickKeysByType<T, number>,
|
||||
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<number | null> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -545,8 +578,8 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async average(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
columnName: PickKeysByType<T, number>,
|
||||
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<number | null> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -556,8 +589,8 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async minimum(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
columnName: PickKeysByType<T, number>,
|
||||
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<number | null> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -567,8 +600,8 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async maximum(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
columnName: PickKeysByType<T, number>,
|
||||
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[],
|
||||
entityManager?: EntityManager,
|
||||
): Promise<number | null> {
|
||||
const manager = entityManager || this.manager;
|
||||
@ -578,7 +611,7 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async increment(
|
||||
conditions: FindOptionsWhere<Entity>,
|
||||
conditions: FindOptionsWhere<T>,
|
||||
propertyPath: string,
|
||||
value: number | string,
|
||||
entityManager?: EntityManager,
|
||||
@ -597,7 +630,7 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
override async decrement(
|
||||
conditions: FindOptionsWhere<Entity>,
|
||||
conditions: FindOptionsWhere<T>,
|
||||
propertyPath: string,
|
||||
value: number | string,
|
||||
entityManager?: EntityManager,
|
||||
@ -652,8 +685,8 @@ export class WorkspaceRepository<
|
||||
}
|
||||
|
||||
private async transformOptions<
|
||||
T extends FindManyOptions<Entity> | FindOneOptions<Entity> | undefined,
|
||||
>(options: T): Promise<T> {
|
||||
U extends FindManyOptions<T> | FindOneOptions<T> | undefined,
|
||||
>(options: U): Promise<U> {
|
||||
if (!options) {
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -15,19 +15,19 @@ export class TwentyORMGlobalManager {
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntity: Type<T>,
|
||||
failOnMetadataCacheMiss?: boolean,
|
||||
shouldFailIfMetadataNotFound?: boolean,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
objectMetadataName: string,
|
||||
failOnMetadataCacheMiss?: boolean,
|
||||
shouldFailIfMetadataNotFound?: boolean,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntityOrobjectMetadataName: Type<T> | string,
|
||||
failOnMetadataCacheMiss = true,
|
||||
shouldFailIfMetadataNotFound = true,
|
||||
): Promise<WorkspaceRepository<T>> {
|
||||
let objectMetadataName: string;
|
||||
|
||||
@ -42,7 +42,7 @@ export class TwentyORMGlobalManager {
|
||||
const workspaceDataSource = await this.workspaceDataSourceFactory.create(
|
||||
workspaceId,
|
||||
null,
|
||||
failOnMetadataCacheMiss,
|
||||
shouldFailIfMetadataNotFound,
|
||||
);
|
||||
|
||||
const repository = workspaceDataSource.getRepository<T>(objectMetadataName);
|
||||
@ -52,12 +52,12 @@ export class TwentyORMGlobalManager {
|
||||
|
||||
async getDataSourceForWorkspace(
|
||||
workspaceId: string,
|
||||
failOnMetadataCacheMiss = true,
|
||||
shouldFailIfMetadataNotFound = true,
|
||||
) {
|
||||
return await this.workspaceDataSourceFactory.create(
|
||||
workspaceId,
|
||||
null,
|
||||
failOnMetadataCacheMiss,
|
||||
shouldFailIfMetadataNotFound,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { Injectable, Type } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ObjectLiteral, Repository } from 'typeorm';
|
||||
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
@ -10,6 +13,8 @@ import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manag
|
||||
@Injectable()
|
||||
export class TwentyORMManager {
|
||||
constructor(
|
||||
@InjectRepository(UserWorkspaceRoleEntity, 'metadata')
|
||||
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
|
||||
private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
) {}
|
||||
@ -25,7 +30,7 @@ export class TwentyORMManager {
|
||||
async getRepository<T extends ObjectLiteral>(
|
||||
workspaceEntityOrobjectMetadataName: Type<T> | string,
|
||||
): Promise<WorkspaceRepository<T>> {
|
||||
const { workspaceId, workspaceMetadataVersion } =
|
||||
const { workspaceId, workspaceMetadataVersion, userWorkspaceId } =
|
||||
this.scopedWorkspaceContextFactory.create();
|
||||
|
||||
let objectMetadataName: string;
|
||||
@ -47,7 +52,20 @@ export class TwentyORMManager {
|
||||
workspaceMetadataVersion,
|
||||
);
|
||||
|
||||
return workspaceDataSource.getRepository<T>(objectMetadataName);
|
||||
let roleId: string | undefined;
|
||||
|
||||
if (isDefined(userWorkspaceId)) {
|
||||
const userWorkspaceRole = await this.userWorkspaceRoleRepository.findOne({
|
||||
where: {
|
||||
userWorkspaceId,
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
roleId = userWorkspaceRole?.roleId;
|
||||
}
|
||||
|
||||
return workspaceDataSource.getRepository<T>(objectMetadataName, roleId);
|
||||
}
|
||||
|
||||
async getDatasource() {
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { WorkspaceFeatureFlagMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flag-map-cache.service.ts/workspace-roles-feature-flag-map-cache.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module';
|
||||
import { entitySchemaFactories } from 'src/engine/twenty-orm/factories';
|
||||
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
@ -13,10 +18,17 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, UserWorkspaceRoleEntity],
|
||||
'metadata',
|
||||
),
|
||||
DataSourceModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
PermissionsModule,
|
||||
WorkspaceRolesPermissionsCacheModule,
|
||||
WorkspaceFeatureFlagMapCacheModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
...entitySchemaFactories,
|
||||
|
||||
Reference in New Issue
Block a user