diff --git a/packages/twenty-server/src/app.module.ts b/packages/twenty-server/src/app.module.ts index 85e91f98e..3021f9d1b 100644 --- a/packages/twenty-server/src/app.module.ts +++ b/packages/twenty-server/src/app.module.ts @@ -37,7 +37,6 @@ const MIGRATED_REST_METHODS = [ RequestMethod.POST, RequestMethod.PATCH, RequestMethod.PUT, - RequestMethod.GET, ]; @Module({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts index 2b7949613..36fa98e7e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts @@ -6,6 +6,7 @@ import { } from 'typeorm'; import { ObjectRecordFilter } 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 { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; @@ -15,10 +16,14 @@ export class GraphqlQueryFilterConditionParser { private fieldMetadataMapByName: FieldMetadataMap; private queryFilterFieldParser: GraphqlQueryFilterFieldParser; - constructor(fieldMetadataMapByName: FieldMetadataMap) { + constructor( + fieldMetadataMapByName: FieldMetadataMap, + featureFlagsMap: FeatureFlagMap, + ) { this.fieldMetadataMapByName = fieldMetadataMapByName; this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( this.fieldMetadataMapByName, + featureFlagsMap, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts index e013fc794..5eb4b6940 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -1,6 +1,7 @@ import { WhereExpressionBuilder } from 'typeorm'; import { capitalize } from 'twenty-shared/utils'; +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 { @@ -17,9 +18,14 @@ const ARRAY_OPERATORS = ['in', 'contains', 'notContains']; export class GraphqlQueryFilterFieldParser { private fieldMetadataMapByName: FieldMetadataMap; + private featureFlagsMap: FeatureFlagMap; - constructor(fieldMetadataMapByName: FieldMetadataMap) { + constructor( + fieldMetadataMapByName: FieldMetadataMap, + featureFlagsMap: FeatureFlagMap, + ) { this.fieldMetadataMapByName = fieldMetadataMapByName; + this.featureFlagsMap = featureFlagsMap; } public parse( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts index a19fa7fab..40eda0e71 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts @@ -4,6 +4,7 @@ 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 { @@ -17,9 +18,14 @@ import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspac export class GraphqlQueryOrderFieldParser { private fieldMetadataMapByName: FieldMetadataMap; + private featureFlagsMap: FeatureFlagMap; - constructor(fieldMetadataMapByName: FieldMetadataMap) { + constructor( + fieldMetadataMapByName: FieldMetadataMap, + featureFlagsMap: FeatureFlagMap, + ) { this.fieldMetadataMapByName = fieldMetadataMapByName; + this.featureFlagsMap = featureFlagsMap; } parse( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index 7cbcb307d..f15acd743 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -39,9 +39,11 @@ export class GraphqlQueryParser { this.featureFlagsMap = featureFlagsMap; this.filterConditionParser = new GraphqlQueryFilterConditionParser( this.fieldMetadataMapByName, + featureFlagsMap, ); this.orderFieldParser = new GraphqlQueryOrderFieldParser( this.fieldMetadataMapByName, + featureFlagsMap, ); } diff --git a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts index 0258ca9a8..b06928937 100644 --- a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts +++ b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts @@ -22,7 +22,6 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; @Controller('rest/*') @UseGuards(JwtAuthGuard, WorkspaceAuthGuard) -@UseFilters(RestApiExceptionFilter) export class RestApiCoreController { constructor( private readonly restApiCoreService: RestApiCoreService, @@ -30,6 +29,7 @@ export class RestApiCoreController { ) {} @Post('/duplicates') + @UseFilters(RestApiExceptionFilter) async handleApiFindDuplicates(@Req() request: Request, @Res() res: Response) { const result = await this.restApiCoreService.findDuplicates(request); @@ -37,13 +37,17 @@ export class RestApiCoreController { } @Get() + @UseFilters(RestApiExceptionFilter) async handleApiGet(@Req() request: Request, @Res() res: Response) { - const result = await this.restApiCoreServiceV2.get(request); + const result = await this.restApiCoreService.get(request); - res.status(200).send(result); + res.status(200).send(cleanGraphQLResponse(result.data.data)); } @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); @@ -51,6 +55,7 @@ export class RestApiCoreController { } @Post() + @UseFilters(RestApiExceptionFilter) async handleApiPost(@Req() request: Request, @Res() res: Response) { const result = await this.restApiCoreServiceV2.createOne(request); @@ -69,6 +74,7 @@ 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); diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts index 53a09f770..e0d71e2f5 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts @@ -2,54 +2,21 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { Request } from 'express'; import { capitalize } from 'twenty-shared/utils'; -import { ObjectLiteral, OrderByCondition, SelectQueryBuilder } from 'typeorm'; import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser'; -import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; 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 { FieldValue } from 'src/engine/api/rest/core/types/field-value.type'; -import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory'; -import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; -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 { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; -import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util'; import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -interface FindManyMeta { - hasNextPage: boolean; - hasPreviousPage: boolean; - startCursor: string | null; - endCursor: string | null; - totalCount: number; -} - -interface FormatResultParams { - operation: 'delete' | 'create' | 'update' | 'findOne' | 'findMany'; - objectNameSingular?: string; - objectNamePlural?: string; - data: T; - meta?: FindManyMeta; -} @Injectable() export class RestApiCoreServiceV2 { constructor( private readonly coreQueryBuilderFactory: CoreQueryBuilderFactory, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly limitInputFactory: LimitInputFactory, - private readonly filterInputFactory: FilterInputFactory, - private readonly orderByInputFactory: OrderByInputFactory, - private readonly startingAfterInputFactory: StartingAfterInputFactory, - private readonly endingBeforeInputFactory: EndingBeforeInputFactory, + protected readonly apiEventEmitterService: ApiEventEmitterService, ) {} @@ -74,12 +41,8 @@ export class RestApiCoreServiceV2 { objectMetadata.objectMetadataMapItem, ); - return this.formatResult({ - operation: 'delete', - objectNameSingular: objectMetadataNameSingular, - data: { - id: recordToDelete.id, - }, + return this.formatResult('delete', objectMetadataNameSingular, { + id: recordToDelete.id, }); } @@ -96,11 +59,11 @@ export class RestApiCoreServiceV2 { objectMetadata.objectMetadataMapItem, ); - return this.formatResult({ - operation: 'create', - objectNameSingular: objectMetadataNameSingular, - data: createdRecord, - }); + return this.formatResult( + 'create', + objectMetadataNameSingular, + createdRecord, + ); } async update(request: Request) { @@ -130,310 +93,24 @@ export class RestApiCoreServiceV2 { objectMetadata.objectMetadataMapItem, ); - return this.formatResult({ - operation: 'update', - objectNameSingular: objectMetadataNameSingular, - data: updatedRecord, - }); - } - - async get(request: Request) { - const { id: recordId } = parseCorePath(request); - const { + return this.formatResult( + 'update', objectMetadataNameSingular, - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - } = await this.getRepositoryAndMetadataOrFail(request); - - if (recordId) { - return await this.findOne( - repository, - recordId, - objectMetadataNameSingular, - ); - } else { - return await this.findMany( - request, - repository, - objectMetadata, - objectMetadataNameSingular, - objectMetadataItemWithFieldsMaps, - ); - } - } - - private async findOne( - repository: any, - recordId: string, - objectMetadataNameSingular: string, - ) { - const record = await repository.findOne({ - where: { id: recordId }, - }); - - return this.formatResult({ - operation: 'findOne', - objectNameSingular: objectMetadataNameSingular, - data: record, - }); - } - - private async findMany( - request: Request, - repository: WorkspaceRepository, - objectMetadata: any, - objectMetadataNameSingular: string, - objectMetadataItemWithFieldsMaps: - | ObjectMetadataItemWithFieldMaps - | undefined, - ) { - // Get input parameters - const inputs = this.getPaginationInputs(request, objectMetadata); - - // Create query builder - const qb = repository.createQueryBuilder(objectMetadataNameSingular); - - // Apply filters with cursor - const { finalQuery } = await this.applyFiltersWithCursor( - qb, - objectMetadataNameSingular, - objectMetadataItemWithFieldsMaps, - inputs, - ); - - // Get total count - const totalCount = await this.getTotalCount(finalQuery); - - // Get records with pagination - const { finalRecords, hasMoreRecords } = - await this.getRecordsWithPagination( - finalQuery, - objectMetadataNameSingular, - objectMetadataItemWithFieldsMaps, - inputs, - ); - - // Format and return result - return this.formatPaginatedResult( - finalRecords, - objectMetadataNameSingular, - objectMetadataItemWithFieldsMaps, - objectMetadata, - inputs.isForwardPagination, - hasMoreRecords, - totalCount, + updatedRecord, ); } - private getPaginationInputs(request: Request, objectMetadata: any) { - const limit = this.limitInputFactory.create(request); - const filter = this.filterInputFactory.create(request, objectMetadata); - const orderBy = this.orderByInputFactory.create(request, objectMetadata); - const endingBefore = this.endingBeforeInputFactory.create(request); - const startingAfter = this.startingAfterInputFactory.create(request); - const isForwardPagination = !endingBefore; - - return { - limit, - filter, - orderBy, - endingBefore, - startingAfter, - isForwardPagination, - }; - } - - private async applyFiltersWithCursor( - qb: SelectQueryBuilder, - objectMetadataNameSingular: string, - objectMetadataItemWithFieldsMaps: - | ObjectMetadataItemWithFieldMaps - | undefined, - inputs: { - filter: Record; - orderBy: any; - startingAfter: string | undefined; - endingBefore: string | undefined; - isForwardPagination: boolean; - }, + private formatResult( + operation: 'delete' | 'create' | 'update' | 'find', + objectNameSingular: string, + data: T, ) { - const fieldMetadataMapByName = - objectMetadataItemWithFieldsMaps?.fieldsByName || {}; - - let appliedFilters = inputs.filter; - - // Handle cursor-based filtering - if (inputs.startingAfter || inputs.endingBefore) { - const cursor = inputs.startingAfter || inputs.endingBefore; - - try { - const cursorData = JSON.parse( - Buffer.from(cursor ?? '', 'base64').toString(), - ); - - // We always include ID in the ordering to ensure consistent pagination results - // Even if two records have identical values for the user-specified sort fields, their IDs ensures a deterministic order - const orderByWithIdCondition = [ - ...(inputs.orderBy || []), - { id: 'ASC' }, - ]; - - const cursorFilter = await this.computeCursorFilter( - cursorData, - orderByWithIdCondition, - fieldMetadataMapByName, - inputs.isForwardPagination, - ); - - // Combine cursor filter with any user-provided filters - appliedFilters = inputs.filter - ? { and: [inputs.filter, cursorFilter] } - : cursorFilter; - } catch (error) { - throw new BadRequestException(`Invalid cursor: ${cursor}`); - } - } - - // Apply filters to query builder - const finalQuery = new GraphqlQueryFilterConditionParser( - fieldMetadataMapByName, - ).parse(qb, objectMetadataNameSingular, appliedFilters); - - return { finalQuery, appliedFilters }; - } - - private async getTotalCount( - query: SelectQueryBuilder, - ): Promise { - // Clone the query to avoid modifying the original query that will fetch records - const countQuery = query.clone(); - - return await countQuery.getCount(); - } - - private async getRecordsWithPagination( - query: SelectQueryBuilder, - objectMetadataNameSingular: string, - objectMetadataItemWithFieldsMaps: - | ObjectMetadataItemWithFieldMaps - | undefined, - inputs: { - orderBy: any; - limit: number; - isForwardPagination: boolean; - }, - ) { - const fieldMetadataMapByName = - objectMetadataItemWithFieldsMaps?.fieldsByName || {}; - - // Get parsed order by - const parsedOrderBy = new GraphqlQueryOrderFieldParser( - fieldMetadataMapByName, - ).parse(inputs.orderBy, objectMetadataNameSingular); - - // For backward pagination (endingBefore), we need to reverse the sort order - const finalOrderBy = inputs.isForwardPagination - ? parsedOrderBy - : Object.entries(parsedOrderBy).reduce((acc, [key, direction]) => { - acc[key] = direction === 'ASC' ? 'DESC' : 'ASC'; - - return acc; - }, {}); - - // Fetch one extra record beyond the requested limit - // We'll remove it from the results before returning to the client - const records = await query - .orderBy(finalOrderBy as OrderByCondition) - .take(inputs.limit + 1) - .getMany(); - - // If we got more records than the limit, it means there are more pages - const hasMoreRecords = records.length > inputs.limit; - - // Remove the extra record if we fetched more than requested - if (hasMoreRecords) { - records.pop(); - } - - // For backward pagination, we reversed the order to get the correct records - // Now we need to reverse them back to maintain the expected order for the client - const finalRecords = !inputs.isForwardPagination - ? records.reverse() - : records; - - return { finalRecords, hasMoreRecords }; - } - - private formatPaginatedResult( - finalRecords: any[], - objectMetadataNameSingular: string, - objectMetadataItemWithFieldsMaps: any, - objectMetadata: any, - isForwardPagination: boolean, - hasMoreRecords: boolean, - totalCount: number, - ) { - return this.formatResult({ - operation: 'findMany', - objectNamePlural: objectMetadataNameSingular, - data: formatGetManyData( - finalRecords, - objectMetadataItemWithFieldsMaps as any, - objectMetadata.objectMetadataMaps, - ), - meta: { - hasNextPage: isForwardPagination && hasMoreRecords, - hasPreviousPage: !isForwardPagination && hasMoreRecords, - 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, - }, - }); - } - - private formatResult({ - operation, - objectNameSingular, - objectNamePlural, - data, - meta, - }: FormatResultParams) { - let prefix: string; - - if (operation === 'findOne') { - prefix = objectNameSingular || ''; - } else if (operation === 'findMany') { - prefix = objectNamePlural || ''; - } else { - prefix = operation + capitalize(objectNameSingular || ''); - } const result = { data: { - [prefix]: data, + [operation + capitalize(objectNameSingular)]: data, }, }; - if (meta) { - const { totalCount, ...rest } = meta; - - (result.data as any).pageInfo = { ...rest }; - - (result.data as any).totalCount = totalCount; - } - return result; } @@ -456,37 +133,13 @@ export class RestApiCoreServiceV2 { const objectMetadataNameSingular = objectMetadata.objectMetadataMapItem.nameSingular; - - const objectMetadataItemWithFieldsMaps = - getObjectMetadataMapItemByNameSingular( - objectMetadata.objectMetadataMaps, - objectMetadataNameSingular, - ); - const repository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspace.id, objectMetadataNameSingular, ); - return { - objectMetadataNameSingular, - objectMetadata, - repository, - objectMetadataItemWithFieldsMaps, - }; - } - - // Helper method to compute cursor filter - private async computeCursorFilter( - cursorData: Record, - orderByWithIdCondition: any[], - fieldMetadataMapByName: FieldMetadataMap, - isForwardPagination: boolean, - ): Promise { - return { - id: isForwardPagination ? { gt: cursorData.id } : { lt: cursorData.id }, - }; + return { objectMetadataNameSingular, objectMetadata, repository }; } private getAuthContextFromRequest(request: Request): AuthContext { diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts index fd5a0a4e0..d61fc5bcb 100644 --- a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts @@ -6,10 +6,7 @@ import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; describe('OrderByInputFactory', () => { - const objectMetadata = { - objectMetadataItem: objectMetadataItemMock, - objectMetadataMapItem: objectMetadataItemMock, - }; + const objectMetadata = { objectMetadataItem: objectMetadataItemMock }; let service: OrderByInputFactory; diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts b/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts index 63d61059c..25a3700e0 100644 --- a/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts @@ -72,7 +72,7 @@ export class OrderByInputFactory { result = [...result, ...resultFields]; } - checkArrayFields(objectMetadata.objectMetadataMapItem, result); + checkArrayFields(objectMetadata.objectMetadataItem, result); return result; } diff --git a/packages/twenty-server/src/engine/api/rest/rest-api.module.ts b/packages/twenty-server/src/engine/api/rest/rest-api.module.ts index 8334b9802..065b6417b 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api.module.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api.module.ts @@ -7,9 +7,7 @@ import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/c 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 { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; 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 { 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'; @@ -42,8 +40,6 @@ import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-run StartingAfterInputFactory, EndingBeforeInputFactory, LimitInputFactory, - FilterInputFactory, - OrderByInputFactory, ApiEventEmitterService, ], exports: [RestApiMetadataService], diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-many.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-many.integration-spec.ts deleted file mode 100644 index da09de97c..000000000 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-many.integration-spec.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { - PERSON_1_ID, - PERSON_2_ID, - PERSON_3_ID, -} from 'test/integration/constants/mock-person-ids.constants'; -import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; - -describe.skip('Core REST API Find Many endpoint', () => { - const testPersonIds = [PERSON_1_ID, PERSON_2_ID, PERSON_3_ID]; - const testPersonCities: Record = {}; - - beforeAll(async () => { - // Create test people with different cities for testing - for (const personId of testPersonIds) { - const city = generateRecordName(personId); - - testPersonCities[personId] = city; - - await makeRestAPIRequest({ - method: 'post', - path: '/people', - body: { - id: personId, - city: city, - // Add different jobTitles to test filtering - jobTitle: `Job ${personId.slice(0, 4)}`, - }, - }).expect(201); - } - }); - - afterAll(async () => { - // Clean up test people - for (const personId of testPersonIds) { - await makeRestAPIRequest({ - method: 'delete', - path: `/people/${personId}`, - }).expect(200); - } - }); - - it('5.a. should retrieve all people with pagination metadata', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: '/people', - }).expect(200); - - const people = response.body.data.people; - const pageInfo = response.body.data.pageInfo; - const totalCount = response.body.data.totalCount; - - expect(people).toBeDefined(); - expect(Array.isArray(people)).toBe(true); - expect(people.length).toBeGreaterThanOrEqual(testPersonIds.length); - - // Check that our test people are included in the results - for (const personId of testPersonIds) { - const person = people.find((p) => p.id === personId); - - expect(person).toBeDefined(); - expect(person.city).toBe(testPersonCities[personId]); - } - - // Check pagination metadata - expect(pageInfo).toBeDefined(); - expect(pageInfo.startCursor).toBeDefined(); - expect(pageInfo.endCursor).toBeDefined(); - expect(typeof totalCount).toBe('number'); - expect(totalCount).toBeGreaterThanOrEqual(testPersonIds.length); - }); - - it('5.b. should limit results based on the limit parameter', async () => { - const limit = 2; - const response = await makeRestAPIRequest({ - method: 'get', - path: `/people?limit=${limit}`, - }).expect(200); - - const people = response.body.data.people; - - expect(people).toBeDefined(); - expect(Array.isArray(people)).toBe(true); - expect(people.length).toBeLessThanOrEqual(limit); - }); - - it('5.c. should filter results based on filter parameters', async () => { - // Filter by jobTitle starting with "Job" - const response = await makeRestAPIRequest({ - method: 'get', - path: '/people?filter[jobTitle][contains]=Job', - }).expect(200); - - const people = response.body.data.people; - - expect(people).toBeDefined(); - expect(Array.isArray(people)).toBe(true); - expect(people.length).toBeGreaterThanOrEqual(testPersonIds.length); - - // All returned people should have jobTitle containing "Job" - for (const person of people) { - expect(person.jobTitle).toContain('Job'); - } - }); - - it('5.d. should support cursor-based pagination with startingAfter', async () => { - // First, get initial results to get a cursor - const initialResponse = await makeRestAPIRequest({ - method: 'get', - path: '/people?limit=1', - }).expect(200); - - const firstPerson = initialResponse.body.data.people[0]; - const endCursor = initialResponse.body.data.pageInfo.endCursor; - - expect(firstPerson).toBeDefined(); - expect(endCursor).toBeDefined(); - - // Now use the cursor to get the next page - const nextPageResponse = await makeRestAPIRequest({ - method: 'get', - path: `/people?startingAfter=${endCursor}&limit=1`, - }).expect(200); - - const nextPagePeople = nextPageResponse.body.data.people; - - expect(nextPagePeople).toBeDefined(); - expect(nextPagePeople.length).toBe(1); - expect(nextPagePeople[0].id).not.toBe(firstPerson.id); - }); - - it('5.e. should support cursor-based pagination with endingBefore', async () => { - // First, get results to get a cursor from the second page - const initialResponse = await makeRestAPIRequest({ - method: 'get', - path: '/people?limit=2', - }).expect(200); - - const people = initialResponse.body.data.people; - const startCursor = initialResponse.body.data.pageInfo.startCursor; - - expect(people.length).toBe(2); - expect(startCursor).toBeDefined(); - - // Now use the cursor to get the previous page (which should be empty in this case) - const prevPageResponse = await makeRestAPIRequest({ - method: 'get', - path: `/people?endingBefore=${startCursor}&limit=1`, - }).expect(200); - - const prevPagePeople = prevPageResponse.body.data.people; - const pageInfo = prevPageResponse.body.data.pageInfo; - - // Since we're at the beginning, there might not be previous results - expect(prevPagePeople).toBeDefined(); - expect(pageInfo.hasPreviousPage).toBeDefined(); - }); - - it('5.f. should support ordering of results', async () => { - // Order by city in ascending order - const ascResponse = await makeRestAPIRequest({ - method: 'get', - path: '/people?orderBy[city]=ASC', - }).expect(200); - - const ascPeople = ascResponse.body.data.people; - - // Check that cities are in ascending order - for (let i = 1; i < ascPeople.length; i++) { - if (ascPeople[i - 1].city && ascPeople[i].city) { - expect(ascPeople[i - 1].city <= ascPeople[i].city).toBe(true); - } - } - - // Order by city in descending order - const descResponse = await makeRestAPIRequest({ - method: 'get', - path: '/people?orderBy[city]=DESC', - }).expect(200); - - const descPeople = descResponse.body.data.people; - - // Check that cities are in descending order - for (let i = 1; i < descPeople.length; i++) { - if (descPeople[i - 1].city && descPeople[i].city) { - expect(descPeople[i - 1].city >= descPeople[i].city).toBe(true); - } - } - }); - - it('5.g. should return an UnauthorizedException when no token is provided', async () => { - await makeRestAPIRequest({ - method: 'get', - path: '/people', - headers: { authorization: '' }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - }); - }); - - it('5.h. should return an UnauthorizedException when an invalid token is provided', async () => { - await makeRestAPIRequest({ - method: 'get', - path: '/people', - headers: { authorization: 'Bearer invalid-token' }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - }); - }); - - it('5.i. should handle invalid cursor gracefully', async () => { - await makeRestAPIRequest({ - method: 'get', - path: '/people?startingAfter=invalid-cursor', - }) - .expect(400) - .expect((res) => { - expect(res.body.error).toBe('BadRequestException'); - expect(res.body.messages[0]).toContain('Invalid cursor'); - }); - }); - - it('5.j. should combine filtering, ordering, and pagination', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: '/people?filter[jobTitle][contains]=Job&orderBy[city]=ASC&limit=2', - }).expect(200); - - const people = response.body.data.people; - const pageInfo = response.body.data.pageInfo; - - expect(people).toBeDefined(); - expect(people.length).toBeLessThanOrEqual(2); - expect(pageInfo).toBeDefined(); - - // Check that all returned people match the filter - for (const person of people) { - expect(person.jobTitle).toContain('Job'); - } - - // Check that cities are in ascending order - for (let i = 1; i < people.length; i++) { - if (people[i - 1].city && people[i].city) { - expect(people[i - 1].city <= people[i].city).toBe(true); - } - } - }); -}); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-one.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-one.integration-spec.ts deleted file mode 100644 index b3bc17df6..000000000 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-one.integration-spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - FAKE_PERSON_ID, - PERSON_1_ID, -} from 'test/integration/constants/mock-person-ids.constants'; -import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; - -describe.skip('Core REST API Find One endpoint', () => { - let personCity: string; - - beforeAll(async () => { - personCity = generateRecordName(PERSON_1_ID); - - // Create a test person to retrieve - await makeRestAPIRequest({ - method: 'post', - path: '/people', - body: { - id: PERSON_1_ID, - city: personCity, - }, - }).expect(201); - }); - - afterAll(async () => { - // Clean up the test person - await makeRestAPIRequest({ - method: 'delete', - path: `/people/${PERSON_1_ID}`, - }).expect(200); - }); - - it('4.a. should retrieve a person by ID', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: `/people/${PERSON_1_ID}`, - }).expect(200); - - const person = response.body.data.person; - - expect(person).toBeDefined(); - expect(person.id).toBe(PERSON_1_ID); - expect(person.city).toBe(personCity); - }); - - it('4.b. should return null when trying to retrieve a non-existing person', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: `/people/${FAKE_PERSON_ID}`, - }).expect(200); - - const person = response.body.data.person; - - expect(person).toBeNull(); - }); - - it('4.c. should return an UnauthorizedException when no token is provided', async () => { - await makeRestAPIRequest({ - method: 'get', - path: `/people/${PERSON_1_ID}`, - headers: { authorization: '' }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - }); - }); - - it('4.d. should return an UnauthorizedException when an invalid token is provided', async () => { - await makeRestAPIRequest({ - method: 'get', - path: `/people/${PERSON_1_ID}`, - headers: { authorization: 'Bearer invalid-token' }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - }); - }); - - it('4.e. should return an UnauthorizedException when an expired token is provided', async () => { - await makeRestAPIRequest({ - method: 'get', - path: `/people/${PERSON_1_ID}`, - headers: { authorization: `Bearer ${EXPIRED_ACCESS_TOKEN}` }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - expect(res.body.messages[0]).toBe('Token has expired.'); - }); - }); -});