Common API - create common find one query (#14720)
closes https://github.com/twentyhq/core-team-issues/issues/1418 Tested : - findOne on Rest and Gql ### Vision #### Common - Common is kind of renamed Gql base resolver - Common handles args (filter, values, ...) validation - Common accepts depth or raw gql selected fields to compute selectedFields - Common is directly called by each CommonQueries (findOne, ...) service, which extend CommonBaseQuery service - Common sequence : | - Parse & Validate args (args-handlers, to create) | - Build query (query-parsers : currently in gql-query-parsers, to move) | - Execute query | - Fetch relation + format #### Rest - Simple parsing (without metadata validation) - Calling Common API - Simple rest response formatting #### Gql - Calling Common API
This commit is contained in:
+30
-8
@@ -12,10 +12,11 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
|
||||
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@@ -27,7 +28,10 @@ export class RestApiCoreController {
|
||||
constructor(private readonly restApiCoreService: RestApiCoreService) {}
|
||||
|
||||
@Post('batch/*')
|
||||
async handleApiPostBatch(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiPostBatch(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing BATCH request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -37,7 +41,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Post('*/duplicates')
|
||||
async handleApiFindDuplicates(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiFindDuplicates(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing DUPLICATES request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -47,7 +54,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Post('*')
|
||||
async handleApiPost(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiPost(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing POST request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -57,7 +67,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Get('*')
|
||||
async handleApiGet(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiGet(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing GET request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -67,7 +80,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Delete('*')
|
||||
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiDelete(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing DELETE request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -77,7 +93,10 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
@Patch('*')
|
||||
async handleApiPatch(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiPatch(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing PATCH request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
@@ -90,7 +109,10 @@ export class RestApiCoreController {
|
||||
// We keep it to avoid a breaking change since it initially used PUT instead
|
||||
// of PATCH, and because the PUT verb is often used as a PATCH.
|
||||
@Put('*')
|
||||
async handleApiPut(@Req() request: Request, @Res() res: Response) {
|
||||
async handleApiPut(
|
||||
@Req() request: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.log(
|
||||
`[REST API] Processing PUT request to ${request.path} on workspace ${request.workspaceId}`,
|
||||
);
|
||||
|
||||
+2
-2
@@ -4,17 +4,17 @@ import {
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
|
||||
+2
-2
@@ -4,17 +4,17 @@ import {
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
|
||||
+2
-3
@@ -1,15 +1,14 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiDeleteOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
|
||||
+2
-1
@@ -10,11 +10,12 @@ import {
|
||||
RestApiBaseHandler,
|
||||
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
this.validate(request);
|
||||
|
||||
const {
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiFindManyHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const {
|
||||
repository,
|
||||
objectMetadata,
|
||||
|
||||
+76
-2
@@ -1,15 +1,24 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
constructor(
|
||||
private readonly commonFindOneQueryRunnerService: CommonFindOneQueryRunnerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
@@ -46,4 +55,69 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||
data: record,
|
||||
});
|
||||
}
|
||||
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
const { filter, depth } = await this.parseRequestArgs(request);
|
||||
const {
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
} = await this.buildCommonOptions(request);
|
||||
|
||||
const selectedFieldsResult = await this.computeSelectedFields({
|
||||
depth,
|
||||
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
authContext,
|
||||
});
|
||||
|
||||
const record = await this.commonFindOneQueryRunnerService.run({
|
||||
args: { filter, selectedFieldsResult },
|
||||
authContext,
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
return this.formatRestResponse(
|
||||
record,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
private formatRestResponse(record: ObjectRecord, objectNameSingular: string) {
|
||||
return { data: { [objectNameSingular]: record } };
|
||||
}
|
||||
|
||||
private async parseRequestArgs(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
const filter = { id: { eq: recordId } };
|
||||
const depth = this.depthInputFactory.create(request);
|
||||
|
||||
return {
|
||||
filter,
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
private async buildCommonOptions(request: AuthenticatedRequest) {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
const { objectMetadataMaps, objectMetadataMapItem } =
|
||||
await this.coreQueryBuilderFactory.getObjectMetadata(
|
||||
request,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
const authContext = this.getAuthContextFromRequest(request);
|
||||
|
||||
return {
|
||||
authContext: authContext,
|
||||
objectMetadataItemWithFieldMaps: objectMetadataMapItem,
|
||||
objectMetadataMaps: objectMetadataMaps,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,18 +4,18 @@ import {
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
|
||||
+116
-52
@@ -10,6 +10,7 @@ import {
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { In, type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
import {
|
||||
type ObjectRecord,
|
||||
type ObjectRecordFilter,
|
||||
@@ -20,17 +21,18 @@ import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/
|
||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler';
|
||||
import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
import {
|
||||
type Depth,
|
||||
DepthInputFactory,
|
||||
MAX_DEPTH,
|
||||
type Depth,
|
||||
} from 'src/engine/api/rest/input-factories/depth-input.factory';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import {
|
||||
@@ -40,13 +42,16 @@ import {
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
export interface PageInfo {
|
||||
hasNextPage?: boolean;
|
||||
@@ -94,13 +99,21 @@ export abstract class RestApiBaseHandler {
|
||||
@Inject()
|
||||
protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService;
|
||||
@Inject()
|
||||
protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService;
|
||||
@Inject()
|
||||
protected readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService;
|
||||
@Inject()
|
||||
protected readonly apiKeyRoleService: ApiKeyRoleService;
|
||||
@Inject()
|
||||
protected readonly restToCommonSelectedFieldsHandler: RestToCommonSelectedFieldsHandler;
|
||||
@Inject()
|
||||
protected readonly userRoleService: UserRoleService;
|
||||
|
||||
protected abstract handle(
|
||||
request: Request,
|
||||
request: AuthenticatedRequest,
|
||||
): Promise<FormatResult | { data: FormatResult[] }>;
|
||||
|
||||
public async getRepositoryAndMetadataOrFail(request: Request) {
|
||||
public async getRepositoryAndMetadataOrFail(request: AuthenticatedRequest) {
|
||||
const { workspace, apiKey, userWorkspaceId } = request;
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
@@ -109,14 +122,14 @@ export abstract class RestApiBaseHandler {
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object metadata not found');
|
||||
}
|
||||
|
||||
if (!workspace?.id) {
|
||||
throw new BadRequestException('Workspace not found');
|
||||
}
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object metadata not found');
|
||||
}
|
||||
|
||||
const workspaceDataSource = await this.twentyORMManager.getDatasource();
|
||||
|
||||
const objectMetadataNameSingular =
|
||||
@@ -134,18 +147,22 @@ export abstract class RestApiBaseHandler {
|
||||
);
|
||||
}
|
||||
|
||||
let roleId: string | undefined = undefined;
|
||||
let shouldBypassPermissionChecks = false;
|
||||
let roleId: string;
|
||||
|
||||
if (isDefined(apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
apiKey.id,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(userWorkspaceId)) {
|
||||
roleId =
|
||||
if (!isDefined(roleId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.API_KEY_ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const userWorkspaceRoleId =
|
||||
await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
@@ -153,60 +170,53 @@ export abstract class RestApiBaseHandler {
|
||||
},
|
||||
);
|
||||
|
||||
if (!roleId) {
|
||||
if (!isDefined(userWorkspaceRoleId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(apiKey) && !isDefined(userWorkspaceId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT,
|
||||
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
|
||||
);
|
||||
roleId = userWorkspaceRoleId;
|
||||
}
|
||||
|
||||
const repository = workspaceDataSource.getRepository<ObjectRecord>(
|
||||
objectMetadataNameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
false,
|
||||
roleId,
|
||||
);
|
||||
|
||||
let restrictedFields: RestrictedFieldsPermissions = {};
|
||||
const objectMetadataPermissions =
|
||||
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
roleIds: roleId ? [roleId] : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
if (roleId) {
|
||||
const objectMetadataPermissions =
|
||||
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
roleIds: roleId ? [roleId] : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(
|
||||
objectMetadataPermissions?.[roleId]?.[
|
||||
objectMetadata.objectMetadataMapItem.id
|
||||
]?.restrictedFields,
|
||||
)
|
||||
) {
|
||||
throw new InternalServerError('Fields permissions not found for role');
|
||||
}
|
||||
|
||||
restrictedFields =
|
||||
objectMetadataPermissions[roleId][
|
||||
if (
|
||||
!isDefined(
|
||||
objectMetadataPermissions?.[roleId]?.[
|
||||
objectMetadata.objectMetadataMapItem.id
|
||||
].restrictedFields;
|
||||
]?.restrictedFields,
|
||||
)
|
||||
) {
|
||||
throw new InternalServerError('Fields permissions not found for role');
|
||||
}
|
||||
|
||||
const restrictedFields =
|
||||
objectMetadataPermissions[roleId][objectMetadata.objectMetadataMapItem.id]
|
||||
.restrictedFields;
|
||||
|
||||
return {
|
||||
objectMetadata,
|
||||
repository,
|
||||
workspaceDataSource,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
isExecutedByApiKey: isDefined(apiKey),
|
||||
authContext: this.getAuthContextFromRequest(request),
|
||||
objectsPermissions: objectMetadataPermissions[roleId],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -325,14 +335,10 @@ export abstract class RestApiBaseHandler {
|
||||
return orderedRecords;
|
||||
}
|
||||
|
||||
public getAuthContextFromRequest(request: Request): AuthContext {
|
||||
return {
|
||||
user: request.user,
|
||||
workspace: request.workspace,
|
||||
apiKey: request.apiKey,
|
||||
workspaceMemberId: request.workspaceMemberId,
|
||||
userWorkspaceId: request.userWorkspaceId,
|
||||
};
|
||||
public getAuthContextFromRequest(
|
||||
request: AuthenticatedRequest,
|
||||
): WorkspaceAuthContext {
|
||||
return request;
|
||||
}
|
||||
|
||||
public formatResult<T>({
|
||||
@@ -546,4 +552,62 @@ export abstract class RestApiBaseHandler {
|
||||
throw new BadRequestException(`Invalid cursor: ${cursor}`);
|
||||
}
|
||||
};
|
||||
|
||||
private getObjectsPermissions = async (authContext: WorkspaceAuthContext) => {
|
||||
let roleId: string;
|
||||
|
||||
if (isDefined(authContext.apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
authContext.apiKey.id,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
} else {
|
||||
const userWorkspaceRoleId =
|
||||
await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
|
||||
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: authContext.workspace.id,
|
||||
roleIds: [roleId],
|
||||
},
|
||||
);
|
||||
|
||||
return { objectsPermissions: objectMetadataPermissions[roleId] };
|
||||
};
|
||||
|
||||
async computeSelectedFields({
|
||||
authContext,
|
||||
depth,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
authContext: WorkspaceAuthContext;
|
||||
depth: Depth;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
}) {
|
||||
const { objectsPermissions } =
|
||||
await this.getObjectsPermissions(authContext);
|
||||
|
||||
return this.restToCommonSelectedFieldsHandler.computeFromDepth({
|
||||
objectsPermissions,
|
||||
objectMetadataMaps,
|
||||
objectMetadataMapItem,
|
||||
depth,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -2,19 +2,23 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AuthModule,
|
||||
ApiKeyModule,
|
||||
DomainManagerModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
providers: [...coreQueryBuilderFactories, CoreQueryBuilderFactory],
|
||||
exports: [CoreQueryBuilderFactory],
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
import { FindDuplicatesQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory';
|
||||
import { FindDuplicatesVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory';
|
||||
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
import { inputFactories } from 'src/engine/api/rest/input-factories/factories';
|
||||
|
||||
export const coreQueryBuilderFactories = [
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
|
||||
@@ -29,7 +30,7 @@ export class GetVariablesFactory {
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
): QueryVariables {
|
||||
if (id) {
|
||||
if (isDefined(id)) {
|
||||
return { filter: { id: { eq: id } } };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module';
|
||||
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
|
||||
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||
@@ -11,13 +12,17 @@ import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-ap
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||
import { restToCommonArgsHandlers } from 'src/engine/api/rest/core/rest-to-common-args-handlers/rest-to-common-args-handlers';
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
@@ -38,12 +43,16 @@ const restApiCoreResolvers = [
|
||||
WorkspaceCacheStorageModule,
|
||||
AuthModule,
|
||||
ApiKeyModule,
|
||||
UserRoleModule,
|
||||
HttpModule,
|
||||
TwentyORMModule,
|
||||
RecordTransformerModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
ActorModule,
|
||||
FeatureFlagModule,
|
||||
CoreCommonApiModule,
|
||||
DomainManagerModule,
|
||||
],
|
||||
controllers: [RestApiCoreController],
|
||||
providers: [
|
||||
@@ -51,6 +60,7 @@ const restApiCoreResolvers = [
|
||||
RestApiCoreService,
|
||||
...coreQueryBuilderFactories,
|
||||
...restApiCoreResolvers,
|
||||
...restToCommonArgsHandlers,
|
||||
],
|
||||
})
|
||||
export class RestApiCoreModule {}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler';
|
||||
|
||||
export const restToCommonArgsHandlers = [RestToCommonSelectedFieldsHandler];
|
||||
+140
@@ -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 RestToCommonSelectedFieldsHandler {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
||||
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
||||
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
||||
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
|
||||
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
||||
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCoreService {
|
||||
@@ -22,32 +24,42 @@ export class RestApiCoreService {
|
||||
private readonly restApiFindOneHandler: RestApiFindOneHandler,
|
||||
private readonly restApiFindManyHandler: RestApiFindManyHandler,
|
||||
private readonly restApiFindDuplicatesHandler: RestApiFindDuplicatesHandler,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async delete(request: Request) {
|
||||
async delete(request: AuthenticatedRequest) {
|
||||
return await this.restApiDeleteOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async createOne(request: Request) {
|
||||
async createOne(request: AuthenticatedRequest) {
|
||||
return await this.restApiCreateOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async createMany(request: Request) {
|
||||
async createMany(request: AuthenticatedRequest) {
|
||||
return await this.restApiCreateManyHandler.handle(request);
|
||||
}
|
||||
|
||||
async findDuplicates(request: Request) {
|
||||
async findDuplicates(request: AuthenticatedRequest) {
|
||||
return await this.restApiFindDuplicatesHandler.handle(request);
|
||||
}
|
||||
|
||||
async update(request: Request) {
|
||||
async update(request: AuthenticatedRequest) {
|
||||
return await this.restApiUpdateOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async get(request: Request) {
|
||||
async get(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (isDefined(recordId)) {
|
||||
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
request.workspace.id,
|
||||
);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
return await this.restApiFindOneHandler.commonHandle(request);
|
||||
}
|
||||
|
||||
return await this.restApiFindOneHandler.handle(request);
|
||||
} else {
|
||||
return await this.restApiFindManyHandler.handle(request);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
|
||||
export type AuthenticatedRequest = Request & WorkspaceAuthContext;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type QueryFailedError } from 'typeorm';
|
||||
|
||||
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
|
||||
import { commonQueryRunnerToRestApiExceptionHandler } from 'src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util';
|
||||
|
||||
interface QueryFailedErrorWithCode extends QueryFailedError {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export const workspaceQueryRunnerRestApiExceptionHandler = (
|
||||
error: QueryFailedErrorWithCode,
|
||||
): never => {
|
||||
switch (true) {
|
||||
case error instanceof CommonQueryRunnerException:
|
||||
return commonQueryRunnerToRestApiExceptionHandler(error);
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user