Revert "Revert "[4/n]: migrate the RESTAPI GET /rest/* to use TwentyORM direc…" (#11349)

This commit is contained in:
martmull
2025-05-12 10:32:04 +02:00
committed by GitHub
parent 1f4d4c5265
commit 650f8f5963
50 changed files with 1532 additions and 698 deletions

View File

@ -9,6 +9,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
describe('WorkspaceSchemaFactory', () => {
let service: WorkspaceSchemaFactory;
@ -49,6 +50,10 @@ describe('WorkspaceSchemaFactory', () => {
provide: FeatureFlagService,
useValue: {},
},
{
provide: TwentyConfigService,
useValue: {},
},
],
}).compile();

View File

@ -1,8 +1,8 @@
import { capitalize } from 'twenty-shared/utils';
import { WhereExpressionBuilder } from 'typeorm';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import {
GraphqlQueryRunnerException,

View File

@ -4,7 +4,6 @@ import {
ObjectRecordOrderBy,
OrderByDirection,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import {
@ -18,14 +17,9 @@ import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspac
export class GraphqlQueryOrderFieldParser {
private fieldMetadataMapByName: FieldMetadataMap;
private featureFlagsMap: FeatureFlagMap;
constructor(
fieldMetadataMapByName: FieldMetadataMap,
featureFlagsMap: FeatureFlagMap,
) {
constructor(fieldMetadataMapByName: FieldMetadataMap) {
this.fieldMetadataMapByName = fieldMetadataMapByName;
this.featureFlagsMap = featureFlagsMap;
}
parse(

View File

@ -21,6 +21,10 @@ import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metada
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 { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
export class GraphqlQueryParser {
private fieldMetadataMapByName: FieldMetadataMap;
@ -47,7 +51,6 @@ export class GraphqlQueryParser {
);
this.orderFieldParser = new GraphqlQueryOrderFieldParser(
this.fieldMetadataMapByName,
featureFlagsMap,
);
}
@ -125,8 +128,9 @@ export class GraphqlQueryParser {
)?.fieldsByName;
if (!parentFields) {
throw new Error(
throw new GraphqlQueryRunnerException(
`Could not find object metadata for ${parentObjectMetadata.nameSingular}`,
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}

View File

@ -6,6 +6,8 @@ import { GraphQLSchema, printSchema } from 'graphql';
import { gql } from 'graphql-tag';
import { isDefined } from 'twenty-shared/utils';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
@ -21,6 +23,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class WorkspaceSchemaFactory {
@ -32,6 +35,7 @@ export class WorkspaceSchemaFactory {
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly featureFlagService: FeatureFlagService,
private readonly twentyConfigService: TwentyConfigService,
) {}
async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> {
@ -44,7 +48,10 @@ export class WorkspaceSchemaFactory {
authContext.workspace.id,
);
if (isNewRelationEnabled) {
if (
isNewRelationEnabled &&
this.twentyConfigService.get('NODE_ENV') !== NodeEnvironment.test
) {
// eslint-disable-next-line no-console
console.log(
chalk.yellow('🚧 New relation schema generation is enabled 🚧'),

View File

@ -22,6 +22,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/*')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseFilters(RestApiExceptionFilter)
export class RestApiCoreController {
constructor(
private readonly restApiCoreService: RestApiCoreService,
@ -38,15 +39,12 @@ export class RestApiCoreController {
@Get()
async handleApiGet(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.get(request);
const result = await this.restApiCoreServiceV2.get(request);
res.status(200).send(cleanGraphQLResponse(result.data.data));
res.status(200).send(result);
}
@Delete()
// We should move this exception filter to RestApiCoreController class level
// when all endpoints are migrated to v2
@UseFilters(RestApiExceptionFilter)
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreServiceV2.delete(request);
@ -54,7 +52,6 @@ export class RestApiCoreController {
}
@Post()
@UseFilters(RestApiExceptionFilter)
async handleApiPost(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreServiceV2.createOne(request);
@ -73,7 +70,6 @@ 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()
@UseFilters(RestApiExceptionFilter)
async handleApiPut(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreServiceV2.update(request);

View File

@ -0,0 +1,58 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
@Injectable()
export class RestApiCreateOneHandler extends RestApiBaseHandler {
async handle(request: Request) {
const { objectMetadataNameSingular, objectMetadata, repository } =
await this.getRepositoryAndMetadataOrFail(request);
const overriddenBody = await this.recordInputTransformerService.process({
recordInput: request.body,
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
});
const recordExists =
isDefined(overriddenBody.id) &&
(await repository.exists({
where: {
id: overriddenBody.id,
},
}));
if (recordExists) {
throw new BadRequestException('Record already exists');
}
const createdRecord = await repository.save(overriddenBody);
this.apiEventEmitterService.emitCreateEvents(
[createdRecord],
this.getAuthContextFromRequest(request),
objectMetadata.objectMetadataMapItem,
);
const records = await this.getRecord({
recordIds: [createdRecord.id],
repository,
objectMetadata,
depth: this.depthInputFactory.create(request),
});
const record = records[0];
if (!isDefined(record)) {
throw new Error('Created record not found');
}
return this.formatResult({
operation: 'create',
objectNameSingular: objectMetadataNameSingular,
data: record,
});
}
}

View File

@ -0,0 +1,40 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { 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';
@Injectable()
export class RestApiDeleteOneHandler extends RestApiBaseHandler {
async handle(request: Request) {
const { id: recordId } = parseCorePath(request);
if (!recordId) {
throw new BadRequestException('Record ID not found');
}
const { objectMetadataNameSingular, objectMetadata, repository } =
await this.getRepositoryAndMetadataOrFail(request);
const recordToDelete = await repository.findOneOrFail({
where: { id: recordId },
});
await repository.delete(recordId);
this.apiEventEmitterService.emitDestroyEvents(
[recordToDelete],
this.getAuthContextFromRequest(request),
objectMetadata.objectMetadataMapItem,
);
return this.formatResult({
operation: 'delete',
objectNameSingular: objectMetadataNameSingular,
data: {
id: recordToDelete.id,
},
});
}
}

View File

@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
@Injectable()
export class RestApiGetManyHandler extends RestApiBaseHandler {
async handle(request: Request) {
const {
objectMetadataNameSingular,
objectMetadataNamePlural,
repository,
dataSource,
objectMetadata,
objectMetadataItemWithFieldsMaps,
} = await this.getRepositoryAndMetadataOrFail(request);
const { records, isForwardPagination, hasMoreRecords, totalCount } =
await this.findRecords({
request,
repository,
dataSource,
objectMetadata,
objectMetadataNameSingular,
objectMetadataItemWithFieldsMaps,
});
return this.formatPaginatedResult(
records,
objectMetadataNamePlural,
isForwardPagination,
hasMoreRecords,
totalCount,
);
}
}

View File

@ -0,0 +1,51 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
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';
@Injectable()
export class RestApiGetOneHandler extends RestApiBaseHandler {
async handle(request: Request) {
const { id: recordId } = parseCorePath(request);
if (!isDefined(recordId)) {
throw new BadRequestException(
'No recordId provided in rest api get one query',
);
}
const {
objectMetadataNameSingular,
repository,
dataSource,
objectMetadata,
objectMetadataItemWithFieldsMaps,
} = await this.getRepositoryAndMetadataOrFail(request);
const { records } = await this.findRecords({
request,
recordId,
repository,
dataSource,
objectMetadata,
objectMetadataNameSingular,
objectMetadataItemWithFieldsMaps,
});
const record = records?.[0];
if (!isDefined(record)) {
throw new BadRequestException('Record not found');
}
return this.formatResult({
operation: 'findOne',
objectNameSingular: objectMetadataNameSingular,
data: record,
});
}
}

View File

@ -0,0 +1,63 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
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';
@Injectable()
export class RestApiUpdateOneHandler extends RestApiBaseHandler {
async handle(request: Request) {
const { id: recordId } = parseCorePath(request);
if (!recordId) {
throw new BadRequestException('Record ID not found');
}
const { objectMetadataNameSingular, objectMetadata, repository } =
await this.getRepositoryAndMetadataOrFail(request);
const recordToUpdate = await repository.findOneOrFail({
where: { id: recordId },
});
const overriddenBody = await this.recordInputTransformerService.process({
recordInput: request.body,
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
});
const updatedRecord = await repository.save({
...recordToUpdate,
...overriddenBody,
});
this.apiEventEmitterService.emitUpdateEvents(
[recordToUpdate],
[updatedRecord],
Object.keys(request.body),
this.getAuthContextFromRequest(request),
objectMetadata.objectMetadataMapItem,
);
const records = await this.getRecord({
recordIds: [updatedRecord.id],
repository,
objectMetadata,
depth: this.depthInputFactory.create(request),
});
const record = records[0];
if (!isDefined(record)) {
throw new Error('Updated record not found');
}
return this.formatResult({
operation: 'update',
objectNameSingular: objectMetadataNameSingular,
data: record,
});
}
}

View File

@ -0,0 +1,416 @@
import { BadRequestException, Inject } from '@nestjs/common';
import { Request } from 'express';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { In, ObjectLiteral, SelectQueryBuilder } from 'typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import {
ObjectRecord,
OrderByDirection,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
import {
Depth,
DepthInputFactory,
MAX_DEPTH,
} from 'src/engine/api/rest/input-factories/depth-input.factory';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
export interface PageInfo {
hasNextPage?: boolean;
hasPreviousPage?: boolean;
startCursor: string | null;
endCursor: string | null;
}
interface FormatResultParams<T> {
operation: 'delete' | 'create' | 'update' | 'findOne' | 'findMany';
objectNameSingular?: string;
objectNamePlural?: string;
data: T;
pageInfo?: PageInfo;
totalCount?: number;
}
export interface FormatResult {
data: {
[operation: string]: object;
};
pageInfo?: PageInfo;
totalCount?: number;
}
export abstract class RestApiBaseHandler {
@Inject()
protected readonly recordInputTransformerService: RecordInputTransformerService;
@Inject()
protected readonly coreQueryBuilderFactory: CoreQueryBuilderFactory;
@Inject()
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
@Inject()
protected readonly getVariablesFactory: GetVariablesFactory;
@Inject()
protected readonly depthInputFactory: DepthInputFactory;
@Inject()
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
@Inject()
protected readonly apiEventEmitterService: ApiEventEmitterService;
protected abstract handle(request: Request): Promise<FormatResult>;
public async getRepositoryAndMetadataOrFail(request: Request) {
const { workspace, apiKey, userWorkspaceId } = request;
const { object: parsedObject } = parseCorePath(request);
const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata(
request,
parsedObject,
);
if (!objectMetadata) {
throw new BadRequestException('Object metadata not found');
}
if (!workspace?.id) {
throw new BadRequestException('Workspace not found');
}
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: workspace.id,
shouldFailIfMetadataNotFound: false,
});
const objectMetadataNameSingular =
objectMetadata.objectMetadataMapItem.nameSingular;
const objectMetadataItemWithFieldsMaps =
getObjectMetadataMapItemByNameSingular(
objectMetadata.objectMetadataMaps,
objectMetadataNameSingular,
);
const shouldBypassPermissionChecks = !!apiKey;
const roleId =
await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId({
workspaceId: workspace.id,
userWorkspaceId,
});
const repository = dataSource.getRepository<ObjectRecord>(
objectMetadataNameSingular,
shouldBypassPermissionChecks,
roleId,
);
return {
objectMetadataNameSingular,
objectMetadataNamePlural: objectMetadata.objectMetadataMapItem.namePlural,
objectMetadata,
repository,
dataSource,
objectMetadataItemWithFieldsMaps,
};
}
getRelations({
objectMetadata,
depth,
}: {
objectMetadata: {
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
};
depth: Depth | undefined;
}) {
if (!isDefined(depth) || depth === 0) {
return [];
}
const relations: string[] = [];
objectMetadata.objectMetadataMapItem.fields.forEach((field) => {
if (field.type === FieldMetadataType.RELATION) {
if (
depth === MAX_DEPTH &&
isDefined(field.relationTargetObjectMetadataId)
) {
const relationTargetObjectMetadata =
objectMetadata.objectMetadataMaps.byId[
field.relationTargetObjectMetadataId
];
const depth2Relations = this.getRelations({
objectMetadata: {
objectMetadataMaps: objectMetadata.objectMetadataMaps,
objectMetadataMapItem: relationTargetObjectMetadata,
},
depth: 1,
});
depth2Relations.forEach((depth2Relation) => {
relations.push(`${field.name}.${depth2Relation}`);
});
} else {
relations.push(`${field.name}`);
}
}
});
return relations;
}
async getRecord({
recordIds,
repository,
objectMetadata,
depth,
}: {
recordIds: string[];
repository: WorkspaceRepository<ObjectLiteral>;
objectMetadata: {
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
};
depth: Depth | undefined;
}) {
const relations = this.getRelations({
objectMetadata,
depth: depth,
});
const unorderedRecords = await repository.find({
where: { id: In(recordIds) },
relations,
});
const recordMap = new Map(unorderedRecords.map((r) => [r.id, r]));
const orderedRecords = recordIds.map((id) => recordMap.get(id));
return orderedRecords;
}
public getAuthContextFromRequest(request: Request): AuthContext {
return {
user: request.user,
workspace: request.workspace,
apiKey: request.apiKey,
workspaceMemberId: request.workspaceMemberId,
userWorkspaceId: request.userWorkspaceId,
};
}
public formatResult<T>({
operation,
objectNameSingular,
objectNamePlural,
data,
pageInfo,
totalCount,
}: FormatResultParams<T>) {
let prefix: string;
if (operation === 'findOne') {
prefix = objectNameSingular || '';
} else if (operation === 'findMany') {
prefix = objectNamePlural || '';
} else {
prefix = operation + capitalize(objectNameSingular || '');
}
return {
data: {
[prefix]: data,
},
...(isDefined(pageInfo) ? { pageInfo } : {}),
...(isDefined(totalCount) ? { totalCount } : {}),
};
}
formatPaginatedResult(
finalRecords: any[],
objectMetadataNamePlural: string,
isForwardPagination: boolean,
hasMoreRecords: boolean,
totalCount: number,
) {
const hasPreviousPage = !isForwardPagination && hasMoreRecords;
return this.formatResult({
operation: 'findMany',
objectNamePlural: objectMetadataNamePlural,
data: isForwardPagination ? finalRecords : finalRecords.reverse(),
pageInfo: {
hasNextPage: isForwardPagination && hasMoreRecords,
...(hasPreviousPage ? { hasPreviousPage } : {}),
startCursor:
finalRecords.length > 0
? Buffer.from(JSON.stringify({ id: finalRecords[0].id })).toString(
'base64',
)
: null,
endCursor:
finalRecords.length > 0
? Buffer.from(
JSON.stringify({
id: finalRecords[finalRecords.length - 1].id,
}),
).toString('base64')
: null,
},
totalCount,
});
}
async findRecords({
request,
recordId,
repository,
dataSource,
objectMetadata,
objectMetadataNameSingular,
objectMetadataItemWithFieldsMaps,
}: {
request: Request;
recordId?: string;
repository: WorkspaceRepository<ObjectLiteral>;
dataSource: WorkspaceDataSource;
objectMetadata: any;
objectMetadataNameSingular: string;
objectMetadataItemWithFieldsMaps:
| ObjectMetadataItemWithFieldMaps
| undefined;
}) {
const qb = repository.createQueryBuilder(objectMetadataNameSingular);
const inputs = this.getVariablesFactory.create(
recordId,
request,
objectMetadata,
);
const fieldMetadataMapByName =
objectMetadataItemWithFieldsMaps?.fieldsByName || {};
const fieldMetadataMapByJoinColumnName =
objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName || {};
const graphqlQueryParser = new GraphqlQueryParser(
fieldMetadataMapByName,
fieldMetadataMapByJoinColumnName,
objectMetadata.objectMetadataMaps,
dataSource.featureFlagMap,
);
const filters = this.computeFilters(inputs);
let selectQueryBuilder = isDefined(filters)
? graphqlQueryParser.applyFilterToBuilder(
qb,
objectMetadataNameSingular,
filters,
)
: qb;
const totalCount = await this.getTotalCount(selectQueryBuilder);
const isForwardPagination = !inputs.endingBefore;
selectQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
selectQueryBuilder,
[...(inputs.orderBy || []), { id: OrderByDirection.AscNullsFirst }],
objectMetadataNameSingular,
isForwardPagination,
);
if (inputs.first) {
selectQueryBuilder = selectQueryBuilder.limit(inputs.first);
}
if (inputs.last) {
selectQueryBuilder = selectQueryBuilder.limit(inputs.last);
}
const recordIds = await selectQueryBuilder
.select(`${objectMetadataNameSingular}.id`)
.getMany();
const records = await this.getRecord({
recordIds: recordIds.map((record) => record.id),
repository,
objectMetadata,
depth: this.depthInputFactory.create(request),
});
const hasMoreRecords = records.length < totalCount;
return {
records: formatGetManyData<ObjectLiteral[]>(
records,
objectMetadataItemWithFieldsMaps as any,
objectMetadata.objectMetadataMaps,
dataSource.featureFlagMap[FeatureFlagKey.IsNewRelationEnabled],
),
totalCount,
hasMoreRecords,
isForwardPagination,
};
}
async getTotalCount(
query: SelectQueryBuilder<ObjectLiteral>,
): Promise<number> {
const countQuery = query.clone();
return await countQuery.getCount();
}
computeFilters(inputs: QueryVariables) {
let appliedFilters = inputs.filter;
if (inputs.startingAfter) {
appliedFilters = {
and: [
appliedFilters || {},
{ id: { gt: this.parseCursor(inputs.startingAfter).id } },
],
};
}
if (inputs.endingBefore) {
appliedFilters = {
and: [
appliedFilters || {},
{ id: { lt: this.parseCursor(inputs.endingBefore).id } },
],
};
}
return appliedFilters;
}
private parseCursor = (cursor: string) => {
try {
return JSON.parse(Buffer.from(cursor ?? '', 'base64').toString());
} catch (error) {
throw new BadRequestException(`Invalid cursor: ${cursor}`);
}
};
}

View File

@ -3,17 +3,8 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory';
import { CreateOneQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-one-query.factory';
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
import { DeleteQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/delete-query.factory';
import { DeleteVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/delete-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 { FindManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-many-query.factory';
import { FindOneQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-one-query.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
import { UpdateQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/update-query.factory';
import { UpdateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/update-variables.factory';
import { computeDepth } from 'src/engine/api/rest/core/query-builder/utils/compute-depth.utils';
import { parseCoreBatchPath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
@ -26,21 +17,14 @@ import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-module
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 { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
@Injectable()
export class CoreQueryBuilderFactory {
constructor(
private readonly deleteQueryFactory: DeleteQueryFactory,
private readonly createOneQueryFactory: CreateOneQueryFactory,
private readonly createManyQueryFactory: CreateManyQueryFactory,
private readonly updateQueryFactory: UpdateQueryFactory,
private readonly findOneQueryFactory: FindOneQueryFactory,
private readonly findManyQueryFactory: FindManyQueryFactory,
private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory,
private readonly deleteVariablesFactory: DeleteVariablesFactory,
private readonly createVariablesFactory: CreateVariablesFactory,
private readonly updateVariablesFactory: UpdateVariablesFactory,
private readonly getVariablesFactory: GetVariablesFactory,
private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory,
private readonly accessTokenService: AccessTokenService,
private readonly domainManagerService: DomainManagerService,
@ -113,38 +97,6 @@ export class CoreQueryBuilderFactory {
};
}
async delete(request: Request): Promise<Query> {
const { object: parsedObject } = parseCorePath(request);
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
const { id } = parseCorePath(request);
if (!id) {
throw new BadRequestException(
`delete ${objectMetadata.objectMetadataMapItem.nameSingular} query invalid. Id missing. eg: /rest/${objectMetadata.objectMetadataMapItem.namePlural}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
);
}
return {
query: this.deleteQueryFactory.create(
objectMetadata.objectMetadataMapItem,
),
variables: this.deleteVariablesFactory.create(id),
};
}
async createOne(request: Request): Promise<Query> {
const { object: parsedObject } = parseCorePath(request);
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
const depth = computeDepth(request);
return {
query: this.createOneQueryFactory.create(objectMetadata, depth),
variables: this.createVariablesFactory.create(request),
};
}
async createMany(request: Request): Promise<Query> {
const { object: parsedObject } = parseCoreBatchPath(request);
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
@ -156,42 +108,6 @@ export class CoreQueryBuilderFactory {
};
}
async update(request: Request): Promise<Query> {
const { object: parsedObject } = parseCorePath(request);
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
const depth = computeDepth(request);
const { id } = parseCorePath(request);
if (!id) {
throw new BadRequestException(
`update ${objectMetadata.objectMetadataMapItem.nameSingular} query invalid. Id missing. eg: /rest/${objectMetadata.objectMetadataMapItem.namePlural}/0d4389ef-ea9c-4ae8-ada1-1cddc440fb56`,
);
}
return {
query: this.updateQueryFactory.create(objectMetadata, depth),
variables: this.updateVariablesFactory.create(id, request),
};
}
async get(request: Request): Promise<Query> {
const { object: parsedObject } = parseCorePath(request);
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
const depth = computeDepth(request);
const { id } = parseCorePath(request);
return {
query: id
? this.findOneQueryFactory.create(objectMetadata, depth)
: this.findManyQueryFactory.create(objectMetadata, depth),
variables: this.getVariablesFactory.create(id, request, objectMetadata),
};
}
async findDuplicates(request: Request): Promise<Query> {
const { object: parsedObject } = parseCorePath(request);
const objectMetadata = await this.getObjectMetadata(request, parsedObject);

View File

@ -1,39 +0,0 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'twenty-shared/utils';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.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';
@Injectable()
export class CreateOneQueryFactory {
create(
objectMetadata: {
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
},
depth?: number,
): string {
const objectNameSingular = capitalize(
objectMetadata.objectMetadataMapItem.nameSingular,
);
return `
mutation Create${objectNameSingular}($data: ${objectNameSingular}CreateInput!) {
create${objectNameSingular}(data: $data) {
id
${objectMetadata.objectMetadataMapItem.fields
.map((field) =>
mapFieldMetadataToGraphqlQuery(
objectMetadata.objectMetadataMaps,
field,
depth,
),
)
.join('\n')}
}
}
`;
}
}

View File

@ -1,20 +0,0 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'twenty-shared/utils';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
@Injectable()
export class DeleteQueryFactory {
create(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps): string {
const objectNameSingular = capitalize(objectMetadataMapItem.nameSingular);
return `
mutation Delete${objectNameSingular}($id: ID!) {
delete${objectNameSingular}(id: $id) {
id
}
}
`;
}
}

View File

@ -1,12 +0,0 @@
import { Injectable } from '@nestjs/common';
import { QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
@Injectable()
export class DeleteVariablesFactory {
create(id: string): QueryVariables {
return {
id,
};
}
}

View File

@ -1,28 +1,14 @@
import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory';
import { CreateOneQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-one-query.factory';
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
import { DeleteQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/delete-query.factory';
import { DeleteVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/delete-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 { FindManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-many-query.factory';
import { FindOneQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-one-query.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
import { UpdateQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/update-query.factory';
import { UpdateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/update-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 = [
DeleteQueryFactory,
CreateOneQueryFactory,
CreateManyQueryFactory,
UpdateQueryFactory,
FindOneQueryFactory,
FindManyQueryFactory,
FindDuplicatesQueryFactory,
DeleteVariablesFactory,
CreateVariablesFactory,
UpdateVariablesFactory,
GetVariablesFactory,
FindDuplicatesVariablesFactory,
...inputFactories,

View File

@ -1,65 +0,0 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'twenty-shared/utils';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.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';
@Injectable()
export class FindManyQueryFactory {
create(
objectMetadata: {
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
},
depth?: number,
): string {
const objectNameSingular = capitalize(
objectMetadata.objectMetadataMapItem.nameSingular,
);
const objectNamePlural = objectMetadata.objectMetadataMapItem.namePlural;
return `
query FindMany${capitalize(objectNamePlural)}(
$filter: ${objectNameSingular}FilterInput,
$orderBy: [${objectNameSingular}OrderByInput],
$startingAfter: String,
$endingBefore: String,
$first: Int,
$last: Int
) {
${objectNamePlural}(
filter: $filter,
orderBy: $orderBy,
first: $first,
last: $last,
after: $startingAfter,
before: $endingBefore
) {
edges {
node {
id
${objectMetadata.objectMetadataMapItem.fields
.map((field) =>
mapFieldMetadataToGraphqlQuery(
objectMetadata.objectMetadataMaps,
field,
depth,
),
)
.join('\n')}
}
cursor
}
pageInfo {
hasNextPage
startCursor
endCursor
}
totalCount
}
}
`;
}
}

View File

@ -1,40 +0,0 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'twenty-shared/utils';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.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';
@Injectable()
export class FindOneQueryFactory {
create(
objectMetadata: {
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
},
depth?: number,
): string {
const objectNameSingular =
objectMetadata.objectMetadataMapItem.nameSingular;
return `
query FindOne${capitalize(objectNameSingular)}(
$filter: ${capitalize(objectNameSingular)}FilterInput!,
) {
${objectNameSingular}(filter: $filter) {
id
${objectMetadata.objectMetadataMapItem.fields
.map((field) =>
mapFieldMetadataToGraphqlQuery(
objectMetadata.objectMetadataMaps,
field,
depth,
),
)
.join('\n')}
}
}
`;
}
}

View File

@ -33,13 +33,15 @@ export class GetVariablesFactory {
return { filter: { id: { eq: id } } };
}
const filter = this.filterInputFactory.create(request, objectMetadata);
const limit = this.limitInputFactory.create(request);
const orderBy = this.orderByInputFactory.create(request, objectMetadata);
const endingBefore = this.endingBeforeInputFactory.create(request);
const startingAfter = this.startingAfterInputFactory.create(request);
return {
filter: this.filterInputFactory.create(request, objectMetadata),
orderBy: this.orderByInputFactory.create(request, objectMetadata),
filter,
orderBy,
first: !endingBefore ? limit : undefined,
last: endingBefore ? limit : undefined,
startingAfter,

View File

@ -1,40 +0,0 @@
import { Injectable } from '@nestjs/common';
import { capitalize } from 'twenty-shared/utils';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.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';
@Injectable()
export class UpdateQueryFactory {
create(
objectMetadata: {
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
},
depth?: number,
): string {
const objectNameSingular =
objectMetadata.objectMetadataMapItem.nameSingular;
return `
mutation Update${capitalize(
objectNameSingular,
)}($id: ID!, $data: ${capitalize(objectNameSingular)}UpdateInput!) {
update${capitalize(objectNameSingular)}(id: $id, data: $data) {
id
${Object.values(objectMetadata.objectMetadataMapItem.fieldsById)
.map((field) =>
mapFieldMetadataToGraphqlQuery(
objectMetadata.objectMetadataMaps,
field,
depth,
),
)
.join('\n')}
}
}
`;
}
}

View File

@ -1,15 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
@Injectable()
export class UpdateVariablesFactory {
create(id: string, request: Request): QueryVariables {
return {
id,
data: request.body,
};
}
}

View File

@ -1,197 +1,44 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.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';
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 { RestApiGetOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-one.handler';
import { RestApiGetManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-many.handler';
@Injectable()
export class RestApiCoreServiceV2 {
constructor(
private readonly coreQueryBuilderFactory: CoreQueryBuilderFactory,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly recordInputTransformerService: RecordInputTransformerService,
protected readonly apiEventEmitterService: ApiEventEmitterService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly restApiDeleteOneHandler: RestApiDeleteOneHandler,
private readonly restApiCreateOneHandler: RestApiCreateOneHandler,
private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler,
private readonly restApiGetOneHandler: RestApiGetOneHandler,
private readonly restApiGetManyHandler: RestApiGetManyHandler,
) {}
async delete(request: Request) {
const { id: recordId } = parseCorePath(request);
if (!recordId) {
throw new BadRequestException('Record ID not found');
}
const { objectMetadataNameSingular, objectMetadata, repository } =
await this.getRepositoryAndMetadataOrFail(request);
const recordToDelete = await repository.findOneOrFail({
where: { id: recordId },
});
await repository.delete(recordId);
this.apiEventEmitterService.emitDestroyEvents(
[recordToDelete],
this.getAuthContextFromRequest(request),
objectMetadata.objectMetadataMapItem,
);
return this.formatResult('delete', objectMetadataNameSingular, {
id: recordToDelete.id,
});
return await this.restApiDeleteOneHandler.handle(request);
}
async createOne(request: Request) {
const { objectMetadataNameSingular, objectMetadata, repository } =
await this.getRepositoryAndMetadataOrFail(request);
const overriddenBody = await this.recordInputTransformerService.process({
recordInput: request.body,
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
});
const recordExists =
isDefined(overriddenBody.id) &&
(await repository.exists({
where: {
id: overriddenBody.id,
},
}));
if (recordExists) {
throw new BadRequestException('Record already exists');
}
const createdRecord = await repository.save(overriddenBody);
this.apiEventEmitterService.emitCreateEvents(
[createdRecord],
this.getAuthContextFromRequest(request),
objectMetadata.objectMetadataMapItem,
);
return this.formatResult(
'create',
objectMetadataNameSingular,
createdRecord,
);
return await this.restApiCreateOneHandler.handle(request);
}
async update(request: Request) {
return await this.restApiUpdateOneHandler.handle(request);
}
async get(request: Request) {
const { id: recordId } = parseCorePath(request);
if (!recordId) {
throw new BadRequestException('Record ID not found');
if (isDefined(recordId)) {
return await this.restApiGetOneHandler.handle(request);
} else {
return await this.restApiGetManyHandler.handle(request);
}
const { objectMetadataNameSingular, objectMetadata, repository } =
await this.getRepositoryAndMetadataOrFail(request);
const recordToUpdate = await repository.findOneOrFail({
where: { id: recordId },
});
const overriddenBody = await this.recordInputTransformerService.process({
recordInput: request.body,
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
});
const updatedRecord = await repository.save({
...recordToUpdate,
...overriddenBody,
});
this.apiEventEmitterService.emitUpdateEvents(
[recordToUpdate],
[updatedRecord],
Object.keys(request.body),
this.getAuthContextFromRequest(request),
objectMetadata.objectMetadataMapItem,
);
return this.formatResult(
'update',
objectMetadataNameSingular,
updatedRecord,
);
}
private formatResult<T>(
operation: 'delete' | 'create' | 'update' | 'find',
objectNameSingular: string,
data: T,
) {
const result = {
data: {
[operation + capitalize(objectNameSingular)]: data,
},
};
return result;
}
private async getRepositoryAndMetadataOrFail(request: Request) {
const { workspace, apiKey, userWorkspaceId } = request;
const { object: parsedObject } = parseCorePath(request);
const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata(
request,
parsedObject,
);
if (!objectMetadata) {
throw new BadRequestException('Object metadata not found');
}
if (!workspace?.id) {
throw new BadRequestException('Workspace not found');
}
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: workspace.id,
shouldFailIfMetadataNotFound: false,
});
const objectMetadataNameSingular =
objectMetadata.objectMetadataMapItem.nameSingular;
const shouldBypassPermissionChecks = !!apiKey;
const roleId =
await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId({
workspaceId: workspace.id,
userWorkspaceId,
});
const repository = dataSource.getRepository<ObjectRecord>(
objectMetadataNameSingular,
shouldBypassPermissionChecks,
roleId,
);
return {
objectMetadataNameSingular,
objectMetadata,
repository,
};
}
private getAuthContextFromRequest(request: Request): AuthContext {
return {
user: request.user,
workspace: request.workspace,
apiKey: request.apiKey,
workspaceMemberId: request.workspaceMemberId,
userWorkspaceId: request.userWorkspaceId,
};
}
}

View File

@ -0,0 +1,51 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
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 { RestApiGetOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-one.handler';
import { RestApiGetManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-many.handler';
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller';
import { RestApiCoreServiceV2 } from 'src/engine/api/rest/core/rest-api-core-v2.service';
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
const restApiCoreResolvers = [
RestApiDeleteOneHandler,
RestApiCreateOneHandler,
RestApiUpdateOneHandler,
RestApiGetOneHandler,
RestApiGetManyHandler,
];
@Module({
imports: [
CoreQueryBuilderModule,
WorkspaceCacheStorageModule,
AuthModule,
HttpModule,
TwentyORMModule,
RecordTransformerModule,
WorkspacePermissionsCacheModule,
],
controllers: [RestApiCoreController, RestApiCoreBatchController],
providers: [
RestApiService,
RestApiCoreService,
RestApiCoreServiceV2,
ApiEventEmitterService,
...coreQueryBuilderFactories,
...restApiCoreResolvers,
],
})
export class RestApiCoreModule {}

View File

@ -15,36 +15,12 @@ export class RestApiCoreService {
private readonly restApiService: RestApiService,
) {}
async get(request: Request) {
const data = await this.coreQueryBuilderFactory.get(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
async delete(request: Request) {
const data = await this.coreQueryBuilderFactory.delete(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
async createOne(request: Request) {
const data = await this.coreQueryBuilderFactory.createOne(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
async createMany(request: Request) {
const data = await this.coreQueryBuilderFactory.createMany(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
async update(request: Request) {
const data = await this.coreQueryBuilderFactory.update(request);
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
}
async findDuplicates(request: Request) {
const data = await this.coreQueryBuilderFactory.findDuplicates(request);

View File

@ -1,9 +1,11 @@
import { ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
export type QueryVariables = {
id?: string;
ids?: string[];
data?: object | null;
filter?: object;
orderBy?: object;
orderBy?: ObjectRecordOrderBy;
last?: number;
first?: number;
startingAfter?: string;

View File

@ -0,0 +1,32 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
export const MAX_DEPTH = 2;
export type Depth = 0 | 1 | 2;
const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1, MAX_DEPTH];
@Injectable()
export class DepthInputFactory {
create(request: Request): Depth {
if (!request.query.depth) {
return 0;
}
const depth = +request.query.depth as Depth;
if (isNaN(depth) || !ALLOWED_DEPTH_VALUES.includes(depth)) {
throw new BadRequestException(
`'depth=${
request.query.depth
}' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join(
', ',
)}`,
);
}
return depth;
}
}

View File

@ -3,11 +3,13 @@ import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/en
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
import { DepthInputFactory } from 'src/engine/api/rest/input-factories/depth-input.factory';
export const inputFactories = [
StartingAfterInputFactory,
DepthInputFactory,
EndingBeforeInputFactory,
FilterInputFactory,
LimitInputFactory,
OrderByInputFactory,
FilterInputFactory,
StartingAfterInputFactory,
];

View File

@ -1,51 +1,23 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller';
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
import { RestApiCoreServiceV2 } from 'src/engine/api/rest/core/rest-api-core-v2.service';
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
import { MetadataQueryBuilderModule } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.module';
import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api-metadata.controller';
import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.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';
import { RestApiCoreModule } from 'src/engine/api/rest/core/rest-api-core.module';
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api-metadata.controller';
@Module({
imports: [
CoreQueryBuilderModule,
MetadataQueryBuilderModule,
WorkspaceCacheStorageModule,
AuthModule,
HttpModule,
TwentyORMModule,
RecordTransformerModule,
WorkspacePermissionsCacheModule,
RestApiCoreModule,
],
controllers: [
RestApiMetadataController,
RestApiCoreBatchController,
RestApiCoreController,
],
providers: [
RestApiMetadataService,
RestApiCoreService,
RestApiCoreServiceV2,
RestApiService,
StartingAfterInputFactory,
EndingBeforeInputFactory,
LimitInputFactory,
ApiEventEmitterService,
],
exports: [RestApiMetadataService],
controllers: [RestApiMetadataController],
providers: [RestApiService, RestApiMetadataService],
})
export class RestApiModule {}