Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';

export const CommonArgsHandlers = [CommonSelectedFieldsHandler];
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { BadRequestException, Injectable } from '@nestjs/common';

import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';

import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import {
Depth,
MAX_DEPTH,
} from 'src/engine/api/rest/input-factories/depth-input.factory';
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';

@Injectable()
export class CommonSelectedFieldsHandler {
computeFromDepth = ({
objectsPermissions,
objectMetadataMaps,
objectMetadataMapItem,
depth,
}: {
objectsPermissions: ObjectsPermissions;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
depth: Depth | undefined;
}): CommonSelectedFieldsResult => {
const restrictedFields =
objectsPermissions[objectMetadataMapItem.id].restrictedFields;

const { relations, relationsSelectFields } =
this.getRelationsAndRelationsSelectFields({
objectMetadataMaps,
objectMetadataMapItem,
objectsPermissions,
depth,
});

const selectableFields = getAllSelectableFields({
restrictedFields,
objectMetadata: {
objectMetadataMapItem,
},
});

return {
select: {
...selectableFields,
...relationsSelectFields,
},
relations,
aggregate: {},
};
};

private getRelationsAndRelationsSelectFields({
objectMetadataMaps,
objectMetadataMapItem,
objectsPermissions,
depth,
}: {
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
objectsPermissions: ObjectsPermissions;
depth: Depth | undefined;
}) {
if (!isDefined(depth) || depth === 0) {
return {
relations: {},
relationsSelectFields: {},
};
}

let relations: { [key: string]: boolean | { [key: string]: boolean } } = {};

let relationsSelectFields: {
[key: string]:
| boolean
| { [key: string]: boolean | { [key: string]: boolean } };
} = {};

for (const field of Object.values(objectMetadataMapItem.fieldsById)) {
if (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION))
continue;

const relationTargetObjectMetadata =
objectMetadataMaps.byId[field.relationTargetObjectMetadataId];

if (!isDefined(relationTargetObjectMetadata)) {
throw new BadRequestException(
`Object metadata relation target not found for relation creation payload`,
);
}
const relationFieldSelectFields = getAllSelectableFields({
restrictedFields:
objectsPermissions[relationTargetObjectMetadata.id].restrictedFields,
objectMetadata: {
objectMetadataMapItem: relationTargetObjectMetadata,
},
});

if (Object.keys(relationFieldSelectFields).length === 0) continue;

if (
depth === MAX_DEPTH &&
isDefined(field.relationTargetObjectMetadataId)
) {
const {
relations: depth2Relations,
relationsSelectFields: depth2RelationsSelectFields,
} = this.getRelationsAndRelationsSelectFields({
objectMetadataMaps,
objectMetadataMapItem: relationTargetObjectMetadata,
objectsPermissions,
depth: 1,
}) as {
relations: { [key: string]: boolean };
relationsSelectFields: {
[key: string]: boolean;
};
};

relations[field.name] = depth2Relations as {
[key: string]: boolean;
};

relationsSelectFields[field.name] = {
...relationFieldSelectFields,
...depth2RelationsSelectFields,
};
} else {
relations[field.name] = true;
relationsSelectFields[field.name] = relationFieldSelectFields;
}
}

return { relations, relationsSelectFields };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { Inject, Injectable } from '@nestjs/common';

import { ObjectsPermissions } from 'twenty-shared/types';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';

import { CommonQueryRunnerOptions } from 'src/engine/api/common/interfaces/common-query-runner-options.interface';
import { type ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';

import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';
import {
CommonQueryNames,
RawSelectedFields,
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
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/api-key-role.service';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';

@Injectable()
export abstract class CommonBaseQueryRunnerService<
Response extends
| ObjectRecord
| ObjectRecord[]
| IConnection<ObjectRecord, IEdge<ObjectRecord>>
| IConnection<ObjectRecord, IEdge<ObjectRecord>>[],
> {
@Inject()
protected readonly workspaceQueryHookService: WorkspaceQueryHookService;
@Inject()
protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory;
@Inject()
protected readonly queryResultGettersFactory: QueryResultGettersFactory;
@Inject()
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
@Inject()
protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper;
@Inject()
protected readonly permissionsService: PermissionsService;
@Inject()
protected readonly userRoleService: UserRoleService;
@Inject()
protected readonly apiKeyRoleService: ApiKeyRoleService;
@Inject()
protected readonly selectedFieldsHandler: CommonSelectedFieldsHandler;
@Inject()
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;

public async prepareQueryRunnerContext(options: CommonQueryRunnerOptions) {
const { authContext, objectMetadataItemWithFieldMaps } = options;

if (objectMetadataItemWithFieldMaps.isSystem === true) {
await this.validateSettingsPermissionsOnObjectOrThrow(options);
}

assertIsDefinedOrThrow(authContext.workspace);

const workspace = authContext.workspace;

const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: workspace.id,
});

const { roleId, objectsPermissions } =
await this.getRoleIdAndObjectsPermissions(authContext, workspace.id);

const repository = workspaceDataSource.getRepository(
objectMetadataItemWithFieldMaps.nameSingular,
false,
roleId,
authContext,
);

return {
options,
workspaceDataSource,
repository,
isExecutedByApiKey: isDefined(authContext.apiKey),
roleId,
shouldBypassPermissionChecks: false,
objectsPermissions,
};
}

public async computeSelectedFields(
rawSelectedFields: RawSelectedFields,
options: CommonQueryRunnerOptions,
objectsPermissions: ObjectsPermissions,
): Promise<CommonSelectedFieldsResult> {
const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = options;

if (isDefined(rawSelectedFields.graphqlSelectedFields)) {
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);

//TODO : Refacto-common - QueryParser should be moved to selected fields handler
return graphqlQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
rawSelectedFields.graphqlSelectedFields,
objectMetadataMaps,
);
}

return this.selectedFieldsHandler.computeFromDepth({
objectsPermissions,
objectMetadataMaps,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
depth: rawSelectedFields.depth,
});
}

public async enrichResultsWithGettersAndHooks(
results: Response,
options: CommonQueryRunnerOptions,
operationName: CommonQueryNames,
) {
assertIsDefinedOrThrow(options.authContext.workspace);

const resultWithGetters = await this.queryResultGettersFactory.create(
results,
options.objectMetadataItemWithFieldMaps,
options.authContext.workspace.id,
options.objectMetadataMaps,
);

await this.workspaceQueryHookService.executePostQueryHooks(
options.authContext,
options.objectMetadataItemWithFieldMaps.nameSingular,
operationName,
resultWithGetters,
);

return resultWithGetters;
}

private async validateSettingsPermissionsOnObjectOrThrow(
options: CommonQueryRunnerOptions,
) {
const { authContext, objectMetadataItemWithFieldMaps } = options;

const workspace = authContext.workspace;

assertIsDefinedOrThrow(workspace);

if (
Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes(
objectMetadataItemWithFieldMaps.nameSingular,
)
) {
const permissionRequired: PermissionFlagType =
OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[
objectMetadataItemWithFieldMaps.nameSingular as keyof typeof OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS
];

const userHasPermission =
await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId: authContext.userWorkspaceId,
setting: permissionRequired,
workspaceId: workspace.id,
apiKeyId: authContext.apiKey?.id,
});

if (!userHasPermission) {
throw new PermissionsException(
PermissionsExceptionMessage.PERMISSION_DENIED,
PermissionsExceptionCode.PERMISSION_DENIED,
);
}
}
}

private async getRoleIdAndObjectsPermissions(
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,
);
}

if (isDefined(authContext.apiKey)) {
roleId = await 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 objectMetadataPermissions =
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
{
workspaceId: workspaceId,
roleIds: roleId ? [roleId] : undefined,
},
);

if (!isDefined(objectMetadataPermissions?.[roleId])) {
throw new InternalServerError('Permissions not found for role');
}

return { roleId, objectsPermissions: objectMetadataPermissions?.[roleId] };
}
}
Loading