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:
Etienne
2025-10-08 12:18:42 +02:00
committed by GitHub
parent 8914ba9fec
commit c693c4d9cf
49 changed files with 1230 additions and 110 deletions
@@ -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}`,
);
@@ -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);
@@ -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);
@@ -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) {
@@ -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 {
@@ -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,
@@ -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,
};
}
}
@@ -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) {
@@ -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,
});
}
}
@@ -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,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 = [
@@ -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 {}
@@ -0,0 +1,3 @@
import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler';
export const restToCommonArgsHandlers = [RestToCommonSelectedFieldsHandler];
@@ -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;
@@ -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;
}
};