Common api - findMany query (#15004)
Done in this PR : - simplify rest input parsing by removing metadata validation (for filter and orderBy - to add in common) (1st commit) - simplify result getter handlers signature (array of objectRecord only for input) closes https://github.com/twentyhq/core-team-issues/issues/1614 closes https://github.com/twentyhq/core-team-issues/issues/1615 closes https://github.com/twentyhq/core-team-issues/issues/1616
This commit is contained in:
+2
-4
@@ -4,10 +4,8 @@ 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 { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant';
|
||||
import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type';
|
||||
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';
|
||||
|
||||
+14
-18
@@ -3,11 +3,10 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
import { type ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';
|
||||
import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service';
|
||||
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
|
||||
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
|
||||
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
|
||||
@@ -30,13 +29,7 @@ import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/wo
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export abstract class CommonBaseQueryRunnerService<
|
||||
Response extends
|
||||
| ObjectRecord
|
||||
| ObjectRecord[]
|
||||
| IConnection<ObjectRecord, IEdge<ObjectRecord>>
|
||||
| IConnection<ObjectRecord, IEdge<ObjectRecord>>[],
|
||||
> {
|
||||
export abstract class CommonBaseQueryRunnerService {
|
||||
@Inject()
|
||||
protected readonly workspaceQueryHookService: WorkspaceQueryHookService;
|
||||
@Inject()
|
||||
@@ -57,6 +50,8 @@ export abstract class CommonBaseQueryRunnerService<
|
||||
protected readonly selectedFieldsHandler: CommonSelectedFieldsHandler;
|
||||
@Inject()
|
||||
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
|
||||
@Inject()
|
||||
protected readonly commonResultGettersService: CommonResultGettersService;
|
||||
|
||||
public async prepareQueryRunnerContext({
|
||||
authContext,
|
||||
@@ -107,18 +102,19 @@ export abstract class CommonBaseQueryRunnerService<
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
results: Response;
|
||||
results: ObjectRecord[];
|
||||
operationName: CommonQueryNames;
|
||||
authContext: WorkspaceAuthContext;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
}) {
|
||||
const resultWithGetters = await this.queryResultGettersFactory.create(
|
||||
results,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
authContext.workspace.id,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
}): Promise<ObjectRecord[]> {
|
||||
const resultWithGetters =
|
||||
await this.commonResultGettersService.processQueryResult(
|
||||
results,
|
||||
objectMetadataItemWithFieldMaps.id,
|
||||
objectMetadataMaps,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
await this.workspaceQueryHookService.executePostQueryHooks(
|
||||
authContext,
|
||||
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
import {
|
||||
ObjectRecord,
|
||||
ObjectRecordFilter,
|
||||
ObjectRecordOrderBy,
|
||||
OrderByDirection,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
|
||||
import {
|
||||
CommonQueryRunnerException,
|
||||
CommonQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
|
||||
import { CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type';
|
||||
import {
|
||||
CommonQueryNames,
|
||||
FindManyQueryArgs,
|
||||
} from 'src/engine/api/common/types/common-query-args.type';
|
||||
import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util';
|
||||
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
||||
import { getCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
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 CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService {
|
||||
async run({
|
||||
args,
|
||||
authContext: toValidateAuthContext,
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
}: {
|
||||
args: FindManyQueryArgs;
|
||||
authContext: AuthContext;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
}): Promise<{
|
||||
records: ObjectRecord[];
|
||||
aggregatedValues: Record<string, number>;
|
||||
totalCount: number;
|
||||
pageInfo: CommonPageInfo;
|
||||
}> {
|
||||
this.validate(args);
|
||||
const authContext = toValidateAuthContext;
|
||||
|
||||
if (!isWorkspaceAuthContext(authContext)) {
|
||||
throw new CommonQueryRunnerException(
|
||||
'Invalid auth context',
|
||||
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
workspaceDataSource,
|
||||
repository,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
} = await this.prepareQueryRunnerContext({
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
const processedArgs = await this.processQueryArgs({
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
args,
|
||||
});
|
||||
|
||||
const queryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
const aggregateQueryBuilder = queryBuilder.clone();
|
||||
|
||||
let appliedFilters = processedArgs.filter ?? ({} as ObjectRecordFilter);
|
||||
|
||||
const commonQueryParser = new GraphqlQueryParser(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
|
||||
commonQueryParser.applyFilterToBuilder(
|
||||
aggregateQueryBuilder,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
commonQueryParser.applyDeletedAtToBuilder(
|
||||
aggregateQueryBuilder,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
const orderByWithIdCondition = [
|
||||
...(processedArgs.orderBy ?? []),
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
] as ObjectRecordOrderBy;
|
||||
|
||||
const isForwardPagination = !isDefined(processedArgs.before);
|
||||
|
||||
const cursor = getCursor(processedArgs);
|
||||
|
||||
if (cursor) {
|
||||
const cursorArgFilter = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderByWithIdCondition,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
appliedFilters = (processedArgs.filter
|
||||
? {
|
||||
and: [processedArgs.filter, { or: cursorArgFilter }],
|
||||
}
|
||||
: { or: cursorArgFilter }) as unknown as ObjectRecordFilter;
|
||||
}
|
||||
|
||||
commonQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
commonQueryParser.applyOrderToBuilder(
|
||||
queryBuilder,
|
||||
orderByWithIdCondition,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
|
||||
|
||||
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
|
||||
selectedAggregatedFields: processedArgs.selectedFieldsResult.aggregate,
|
||||
queryBuilder: aggregateQueryBuilder,
|
||||
objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
});
|
||||
|
||||
const limit =
|
||||
processedArgs.first ?? processedArgs.last ?? QUERY_MAX_RECORDS;
|
||||
|
||||
const columnsToSelect = buildColumnsToSelect({
|
||||
select: processedArgs.selectedFieldsResult.select,
|
||||
relations: processedArgs.selectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const objectRecords = (await queryBuilder
|
||||
.setFindOptions({
|
||||
select: columnsToSelect,
|
||||
})
|
||||
.take(limit + 1)
|
||||
.getMany()) as ObjectRecord[];
|
||||
|
||||
const pageInfo = getPageInfo(
|
||||
objectRecords,
|
||||
orderByWithIdCondition,
|
||||
limit,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
if (objectRecords.length > limit) {
|
||||
objectRecords.pop();
|
||||
}
|
||||
|
||||
if (!isForwardPagination) {
|
||||
objectRecords.reverse();
|
||||
}
|
||||
|
||||
const parentObjectRecordsAggregatedValues =
|
||||
await aggregateQueryBuilder.getRawOne();
|
||||
|
||||
if (processedArgs.selectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords: objectRecords,
|
||||
parentObjectRecordsAggregatedValues,
|
||||
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
|
||||
relations: processedArgs.selectedFieldsResult.relations as Record<
|
||||
string,
|
||||
FindOptionsRelations<ObjectLiteral>
|
||||
>,
|
||||
aggregate: processedArgs.selectedFieldsResult.aggregate,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
selectedFields: processedArgs.selectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const enrichedRecords = await this.enrichResultsWithGettersAndHooks({
|
||||
results: objectRecords,
|
||||
operationName: CommonQueryNames.findMany,
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
records: enrichedRecords,
|
||||
aggregatedValues: parentObjectRecordsAggregatedValues,
|
||||
totalCount: parentObjectRecordsAggregatedValues?.totalCount,
|
||||
pageInfo,
|
||||
};
|
||||
}
|
||||
|
||||
validate(args: FindManyQueryArgs) {
|
||||
if (args.first && args.last) {
|
||||
throw new CommonQueryRunnerException(
|
||||
'Cannot provide both first and last',
|
||||
CommonQueryRunnerExceptionCode.ARGS_CONFLICT,
|
||||
);
|
||||
}
|
||||
if (args.before && args.after) {
|
||||
throw new CommonQueryRunnerException(
|
||||
'Cannot provide both before and after',
|
||||
CommonQueryRunnerExceptionCode.ARGS_CONFLICT,
|
||||
);
|
||||
}
|
||||
if (args.before && args.first) {
|
||||
throw new CommonQueryRunnerException(
|
||||
'Cannot provide both before and first',
|
||||
CommonQueryRunnerExceptionCode.ARGS_CONFLICT,
|
||||
);
|
||||
}
|
||||
if (args.after && args.last) {
|
||||
throw new CommonQueryRunnerException(
|
||||
'Cannot provide both after and last',
|
||||
CommonQueryRunnerExceptionCode.ARGS_CONFLICT,
|
||||
);
|
||||
}
|
||||
if (args.first !== undefined && args.first < 0) {
|
||||
throw new CommonQueryRunnerException(
|
||||
'First argument must be non-negative',
|
||||
CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST,
|
||||
);
|
||||
}
|
||||
if (args.last !== undefined && args.last < 0) {
|
||||
throw new CommonQueryRunnerException(
|
||||
'Last argument must be non-negative',
|
||||
CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async processQueryArgs({
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
args,
|
||||
}: {
|
||||
authContext: WorkspaceAuthContext;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
args: FindManyQueryArgs;
|
||||
}): Promise<FindManyQueryArgs> {
|
||||
const hookedArgs =
|
||||
(await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
CommonQueryNames.findMany,
|
||||
args,
|
||||
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
|
||||
)) as FindManyQueryArgs;
|
||||
|
||||
return {
|
||||
...hookedArgs,
|
||||
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
|
||||
hookedArgs.filter,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
+5
-14
@@ -21,14 +21,13 @@ import {
|
||||
} from 'src/engine/api/common/types/common-query-args.type';
|
||||
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
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 CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService<ObjectRecord> {
|
||||
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService {
|
||||
async run({
|
||||
args,
|
||||
authContext: toValidateAuthContext,
|
||||
@@ -137,23 +136,15 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
const results = typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: objectRecords[0],
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}) as ObjectRecord;
|
||||
|
||||
return this.enrichResultsWithGettersAndHooks({
|
||||
results,
|
||||
const enrichedResults = await this.enrichResultsWithGettersAndHooks({
|
||||
results: objectRecords,
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
operationName: CommonQueryNames.findOne,
|
||||
});
|
||||
|
||||
return enrichedResults[0];
|
||||
}
|
||||
|
||||
async processQueryArgs({
|
||||
|
||||
+5
-1
@@ -1,3 +1,7 @@
|
||||
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
|
||||
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
||||
|
||||
export const CommonQueryRunners = [CommonFindOneQueryRunnerService];
|
||||
export const CommonQueryRunners = [
|
||||
CommonFindOneQueryRunnerService,
|
||||
CommonFindManyQueryRunnerService,
|
||||
];
|
||||
|
||||
+3
@@ -6,4 +6,7 @@ export enum CommonQueryRunnerExceptionCode {
|
||||
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
|
||||
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
|
||||
INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT',
|
||||
ARGS_CONFLICT = 'ARGS_CONFLICT',
|
||||
INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST',
|
||||
INVALID_ARGS_LAST = 'INVALID_ARGS_LAST',
|
||||
}
|
||||
|
||||
+3
@@ -16,6 +16,9 @@ export const commonQueryRunnerToGraphqlApiExceptionHandler = (
|
||||
switch (error.code) {
|
||||
case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case CommonQueryRunnerExceptionCode.ARGS_CONFLICT:
|
||||
case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
|
||||
case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST:
|
||||
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
|
||||
throw new UserInputError(error);
|
||||
case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT:
|
||||
|
||||
+3
@@ -15,6 +15,9 @@ export const commonQueryRunnerToRestApiExceptionHandler = (
|
||||
error: CommonQueryRunnerException,
|
||||
): never => {
|
||||
switch (error.code) {
|
||||
case CommonQueryRunnerExceptionCode.ARGS_CONFLICT:
|
||||
case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
|
||||
case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST:
|
||||
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
|
||||
throw new BadRequestException(error.message);
|
||||
case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND:
|
||||
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
|
||||
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
|
||||
|
||||
import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler';
|
||||
import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler';
|
||||
import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler';
|
||||
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
// TODO: find a way to prevent conflict between handlers executing logic on object relations
|
||||
// And this factory that is also executing logic on object relations
|
||||
// Right now the factory will override any change made on relations by the handlers
|
||||
@Injectable()
|
||||
export class CommonResultGettersService {
|
||||
private readonly logger = new Logger(CommonResultGettersService.name);
|
||||
private handlers: Map<string, QueryResultGetterHandlerInterface>;
|
||||
|
||||
constructor(private readonly fileService: FileService) {
|
||||
this.initializeHandlers();
|
||||
}
|
||||
|
||||
private initializeHandlers() {
|
||||
this.handlers = new Map<string, QueryResultGetterHandlerInterface>([
|
||||
['attachment', new AttachmentQueryResultGetterHandler(this.fileService)],
|
||||
['person', new PersonQueryResultGetterHandler(this.fileService)],
|
||||
[
|
||||
'workspaceMember',
|
||||
new WorkspaceMemberQueryResultGetterHandler(this.fileService),
|
||||
],
|
||||
['note', new ActivityQueryResultGetterHandler(this.fileService)],
|
||||
['task', new ActivityQueryResultGetterHandler(this.fileService)],
|
||||
]);
|
||||
}
|
||||
|
||||
private async processRecordArray(
|
||||
recordArray: ObjectRecord[],
|
||||
objectMetadataItemId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
workspaceId: string,
|
||||
) {
|
||||
return await Promise.all(
|
||||
recordArray.map(
|
||||
async (record: ObjectRecord) =>
|
||||
await this.processRecord(
|
||||
record,
|
||||
objectMetadataItemId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async processRecord(
|
||||
record: ObjectRecord,
|
||||
objectMetadataItemId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecord> {
|
||||
const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId];
|
||||
|
||||
if (!isDefined(objectMetadataMapItem)) {
|
||||
throw new Error('Object metadata map item is not defined');
|
||||
}
|
||||
|
||||
const handler = this.getHandler(objectMetadataMapItem.nameSingular);
|
||||
|
||||
const relationFields = Object.keys(record)
|
||||
.map(
|
||||
(recordFieldName) =>
|
||||
objectMetadataMapItem.fieldsById[
|
||||
objectMetadataMapItem.fieldIdByName[recordFieldName]
|
||||
],
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((fieldMetadata) =>
|
||||
isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION),
|
||||
);
|
||||
|
||||
const relationFieldsProcessedMap = {} as Record<
|
||||
string,
|
||||
QueryResultFieldValue
|
||||
>;
|
||||
|
||||
for (const relationField of relationFields) {
|
||||
if (!isDefined(relationField.relationTargetObjectMetadataId)) {
|
||||
throw new Error('Relation target object metadata id is not defined');
|
||||
}
|
||||
|
||||
const recordFieldValue = record[relationField.name];
|
||||
|
||||
if (!isDefined(recordFieldValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
relationFieldsProcessedMap[relationField.name] =
|
||||
relationField.settings?.relationType === RelationType.ONE_TO_MANY
|
||||
? await this.processRecordArray(
|
||||
record[relationField.name],
|
||||
relationField.relationTargetObjectMetadataId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
)
|
||||
: await this.processRecord(
|
||||
record[relationField.name],
|
||||
relationField.relationTargetObjectMetadataId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const objectRecordProcessedWithoutRelationFields = await handler.handle(
|
||||
record,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const processedRecord = {
|
||||
...objectRecordProcessedWithoutRelationFields,
|
||||
...relationFieldsProcessedMap,
|
||||
};
|
||||
|
||||
return processedRecord;
|
||||
}
|
||||
|
||||
async processQueryResult(
|
||||
queryResultField: ObjectRecord[],
|
||||
objectMetadataItemId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecord[]> {
|
||||
return await this.processRecordArray(
|
||||
queryResultField,
|
||||
objectMetadataItemId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private getHandler(objectType: string): QueryResultGetterHandlerInterface {
|
||||
return (
|
||||
this.handlers.get(objectType) || {
|
||||
handle: (result: ObjectRecord): Promise<ObjectRecord> =>
|
||||
Promise.resolve(result),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { CommonArgsHandlers } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers';
|
||||
import { CommonQueryRunners } from 'src/engine/api/common/common-query-runners/common-query-runners';
|
||||
import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service';
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper';
|
||||
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
|
||||
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
||||
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
@@ -23,6 +25,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
|
||||
UserRoleModule,
|
||||
ApiKeyModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
FileModule,
|
||||
],
|
||||
providers: [
|
||||
ProcessNestedRelationsHelper,
|
||||
@@ -30,6 +33,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
|
||||
...CommonArgsHandlers,
|
||||
ProcessAggregateHelper,
|
||||
...CommonQueryRunners,
|
||||
CommonResultGettersService,
|
||||
],
|
||||
exports: [...CommonQueryRunners],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { type PageInfo } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
export type CommonPageInfo = {
|
||||
hasNextPage: NonNullable<PageInfo['hasNextPage']>;
|
||||
hasPreviousPage: NonNullable<PageInfo['hasPreviousPage']>;
|
||||
startCursor: string | null;
|
||||
endCursor: string | null;
|
||||
};
|
||||
@@ -1,9 +1,13 @@
|
||||
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import {
|
||||
type ObjectRecordFilter,
|
||||
type ObjectRecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { type CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
||||
|
||||
export enum CommonQueryNames {
|
||||
findOne = 'findOne',
|
||||
findMany = 'findMany',
|
||||
}
|
||||
|
||||
export interface FindOneQueryArgs {
|
||||
@@ -11,4 +15,14 @@ export interface FindOneQueryArgs {
|
||||
filter?: ObjectRecordFilter;
|
||||
}
|
||||
|
||||
export type CommonQueryArgs = FindOneQueryArgs;
|
||||
export interface FindManyQueryArgs {
|
||||
selectedFieldsResult: CommonSelectedFieldsResult;
|
||||
filter?: ObjectRecordFilter;
|
||||
orderBy?: ObjectRecordOrderBy;
|
||||
first?: number;
|
||||
last?: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
}
|
||||
|
||||
export type CommonQueryArgs = FindOneQueryArgs | FindManyQueryArgs;
|
||||
|
||||
+4
-1
@@ -1,3 +1,5 @@
|
||||
import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
|
||||
|
||||
interface SelectedFields {
|
||||
[key: string]: boolean | SelectedFields;
|
||||
}
|
||||
@@ -5,5 +7,6 @@ interface SelectedFields {
|
||||
export type CommonSelectedFieldsResult = {
|
||||
select: SelectedFields;
|
||||
relations: SelectedFields;
|
||||
aggregate: SelectedFields;
|
||||
//TODO = Refacto-common - to update when rest api will handle aggregates
|
||||
aggregate: Record<string, AggregationField>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
type ObjectRecord,
|
||||
type ObjectRecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { type CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type';
|
||||
import {
|
||||
encodeCursor,
|
||||
getPaginationInfo,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||
|
||||
export const getPageInfo = (
|
||||
records: ObjectRecord[],
|
||||
orderBy: ObjectRecordOrderBy,
|
||||
limit: number,
|
||||
isForwardPagination: boolean,
|
||||
): CommonPageInfo => {
|
||||
const { hasNextPage, hasPreviousPage } = getPaginationInfo(
|
||||
records,
|
||||
limit,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
const startCursor =
|
||||
records.length > 0 ? encodeCursor(records[0], orderBy) : null;
|
||||
const endCursor =
|
||||
records.length > 0
|
||||
? encodeCursor(records[records.length - 1], orderBy)
|
||||
: null;
|
||||
|
||||
return { startCursor, endCursor, hasNextPage, hasPreviousPage };
|
||||
};
|
||||
+1
@@ -25,6 +25,7 @@ import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modu
|
||||
import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
||||
|
||||
// TODO: Refacto-common - Rename CommonRecordsToGraphqlConnectionHelper
|
||||
export class ObjectRecordsToGraphqlConnectionHelper {
|
||||
private objectMetadataMaps: ObjectMetadataMaps;
|
||||
|
||||
|
||||
+3
-1
@@ -175,7 +175,9 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return typeORMObjectRecordsParser.createConnection({
|
||||
objectRecords,
|
||||
objectRecords: isForwardPagination
|
||||
? objectRecords
|
||||
: objectRecords.reverse(),
|
||||
objectRecordsAggregatedValues: parentObjectRecordsAggregatedValues,
|
||||
selectedAggregatedFields:
|
||||
executionArgs.graphqlQuerySelectedFieldsResult.aggregate,
|
||||
|
||||
+1
-2
@@ -64,8 +64,7 @@ export const getCursor = (
|
||||
};
|
||||
|
||||
export const getPaginationInfo = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
objectRecords: any[],
|
||||
objectRecords: ObjectRecord[],
|
||||
limit: number,
|
||||
isForwardPagination: boolean,
|
||||
) => {
|
||||
|
||||
+66
-9
@@ -1,15 +1,23 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type FindManyResolverArgs,
|
||||
type Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
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 FindManyResolverFactory
|
||||
@@ -18,7 +26,9 @@ export class FindManyResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.FIND_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly commonFindManyQueryRunnerService: CommonFindManyQueryRunnerService,
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryFindManyResolverService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -27,17 +37,64 @@ export class FindManyResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
internalContext.authContext.workspace?.id as string,
|
||||
);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
const selectedFieldsResult = graphqlQueryParser.parseSelectedFields(
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
graphqlFields(info),
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
try {
|
||||
const { records, aggregatedValues, totalCount, pageInfo } =
|
||||
await this.commonFindManyQueryRunnerService.run({
|
||||
args: { ...args, selectedFieldsResult },
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.createConnection({
|
||||
objectRecords: records,
|
||||
objectRecordsAggregatedValues: aggregatedValues,
|
||||
selectedAggregatedFields: selectedFieldsResult.aggregate,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: args.first ?? args.last ?? QUERY_MAX_RECORDS,
|
||||
totalCount,
|
||||
order: args.orderBy,
|
||||
hasNextPage: pageInfo.hasNextPage,
|
||||
hasPreviousPage: pageInfo.hasPreviousPage,
|
||||
});
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
{
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
},
|
||||
FindManyResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
|
||||
+16
-2
@@ -11,6 +11,7 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
|
||||
|
||||
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
@@ -53,15 +54,28 @@ export class FindOneResolverFactory
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return await this.commonFindOneQueryRunnerService.run({
|
||||
const record = await this.commonFindOneQueryRunnerService.run({
|
||||
args: { ...args, selectedFieldsResult },
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
return workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@@ -85,7 +86,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
recordIds: createdRecordsIds,
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
depth: parseDepthRestRequest(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@@ -66,7 +67,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
recordIds: [createdRecord.id],
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
depth: parseDepthRestRequest(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
|
||||
+88
-1
@@ -1,11 +1,29 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import {
|
||||
PageInfo,
|
||||
RestApiBaseHandler,
|
||||
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util';
|
||||
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
|
||||
import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util';
|
||||
import { parseOrderByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util';
|
||||
import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util';
|
||||
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 RestApiFindManyHandler extends RestApiBaseHandler {
|
||||
constructor(
|
||||
private readonly commonFindManyQueryRunnerService: CommonFindManyQueryRunnerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const {
|
||||
repository,
|
||||
@@ -73,4 +91,73 @@ export class RestApiFindManyHandler extends RestApiBaseHandler {
|
||||
totalCount,
|
||||
});
|
||||
}
|
||||
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
const parsedArgs = this.parseRequestArgs(request);
|
||||
const {
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
} = await this.buildCommonOptions(request);
|
||||
|
||||
const selectedFieldsResult = await this.computeSelectedFields({
|
||||
depth: parsedArgs.depth,
|
||||
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
authContext,
|
||||
});
|
||||
|
||||
const { records, aggregatedValues, pageInfo } =
|
||||
await this.commonFindManyQueryRunnerService.run({
|
||||
args: { ...parsedArgs, selectedFieldsResult },
|
||||
authContext,
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
return this.formatRestResponse(
|
||||
records,
|
||||
aggregatedValues,
|
||||
objectMetadataItemWithFieldMaps.namePlural,
|
||||
pageInfo,
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
private formatRestResponse(
|
||||
records: ObjectRecord[],
|
||||
aggregatedValues: Record<string, number>,
|
||||
objectNamePlural: string,
|
||||
pageInfo: PageInfo,
|
||||
) {
|
||||
return {
|
||||
data: {
|
||||
[objectNamePlural]: records,
|
||||
},
|
||||
totalCount: aggregatedValues.totalCount,
|
||||
pageInfo,
|
||||
};
|
||||
}
|
||||
|
||||
private parseRequestArgs(request: AuthenticatedRequest) {
|
||||
const depth = parseDepthRestRequest(request);
|
||||
const limit = parseLimitRestRequest(request);
|
||||
const orderBy = parseOrderByRestRequest(request);
|
||||
const filter = parseFilterRestRequest(request);
|
||||
const endingBefore = parseEndingBeforeRestRequest(request);
|
||||
const startingAfter = parseStartingAfterRestRequest(request);
|
||||
|
||||
return {
|
||||
filter,
|
||||
orderBy,
|
||||
first: !endingBefore ? limit : undefined,
|
||||
last: endingBefore ? limit : undefined,
|
||||
before: endingBefore,
|
||||
after: startingAfter,
|
||||
depth,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-19
@@ -7,6 +7,7 @@ import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api
|
||||
|
||||
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 { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
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';
|
||||
|
||||
@@ -95,29 +96,11 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||
private async parseRequestArgs(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
const filter = { id: { eq: recordId } };
|
||||
const depth = this.depthInputFactory.create(request);
|
||||
const depth = parseDepthRestRequest(request);
|
||||
|
||||
return {
|
||||
filter,
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
private async buildCommonOptions(request: AuthenticatedRequest) {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
const { objectMetadataMaps, objectMetadataMapItem } =
|
||||
await this.coreQueryBuilderFactory.getObjectMetadata(
|
||||
request,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
const authContext = this.getAuthContextFromRequest(request);
|
||||
|
||||
return {
|
||||
authContext: authContext,
|
||||
objectMetadataItemWithFieldMaps: objectMetadataMapItem,
|
||||
objectMetadataMaps: objectMetadataMaps,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ 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 { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
|
||||
@@ -62,7 +63,7 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
recordIds: [updatedRecordId],
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
depth: parseDepthRestRequest(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
|
||||
+23
-10
@@ -1,6 +1,5 @@
|
||||
import { BadRequestException, Inject } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import chunk from 'lodash.chunk';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import {
|
||||
@@ -23,11 +22,9 @@ import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/fact
|
||||
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 {
|
||||
DepthInputFactory,
|
||||
MAX_DEPTH,
|
||||
type Depth,
|
||||
} from 'src/engine/api/rest/input-factories/depth-input.factory';
|
||||
import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type';
|
||||
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';
|
||||
@@ -93,8 +90,6 @@ export abstract class RestApiBaseHandler {
|
||||
@Inject()
|
||||
protected readonly getVariablesFactory: GetVariablesFactory;
|
||||
@Inject()
|
||||
protected readonly depthInputFactory: DepthInputFactory;
|
||||
@Inject()
|
||||
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
|
||||
@Inject()
|
||||
protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService;
|
||||
@@ -392,7 +387,7 @@ export abstract class RestApiBaseHandler {
|
||||
extraFilters,
|
||||
restrictedFields,
|
||||
}: {
|
||||
request: Request;
|
||||
request: AuthenticatedRequest;
|
||||
recordId?: string;
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
objectMetadata: {
|
||||
@@ -463,7 +458,7 @@ export abstract class RestApiBaseHandler {
|
||||
recordIds: recordIds.map((record) => record.id),
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
depth: parseDepthRestRequest(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
@@ -610,4 +605,22 @@ export abstract class RestApiBaseHandler {
|
||||
depth,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
objectMetadataItemWithFieldMaps: objectMetadataMapItem,
|
||||
objectMetadataMaps: objectMetadataMaps,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+8
-11
@@ -1,30 +1,27 @@
|
||||
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';
|
||||
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 { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util';
|
||||
import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util';
|
||||
import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class GetVariablesFactory {
|
||||
constructor(
|
||||
private readonly startingAfterInputFactory: StartingAfterInputFactory,
|
||||
private readonly endingBeforeInputFactory: EndingBeforeInputFactory,
|
||||
private readonly limitInputFactory: LimitInputFactory,
|
||||
private readonly orderByInputFactory: OrderByInputFactory,
|
||||
private readonly filterInputFactory: FilterInputFactory,
|
||||
) {}
|
||||
|
||||
create(
|
||||
id: string | undefined,
|
||||
request: Request,
|
||||
request: AuthenticatedRequest,
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
@@ -35,10 +32,10 @@ export class GetVariablesFactory {
|
||||
}
|
||||
|
||||
const filter = this.filterInputFactory.create(request, objectMetadata);
|
||||
const limit = this.limitInputFactory.create(request);
|
||||
const limit = parseLimitRestRequest(request);
|
||||
const orderBy = this.orderByInputFactory.create(request, objectMetadata);
|
||||
const endingBefore = this.endingBeforeInputFactory.create(request);
|
||||
const startingAfter = this.startingAfterInputFactory.create(request);
|
||||
const endingBefore = parseEndingBeforeRestRequest(request);
|
||||
const startingAfter = parseStartingAfterRestRequest(request);
|
||||
|
||||
return {
|
||||
filter,
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ describe('parseFilter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handler not', () => {
|
||||
it('should handle not', () => {
|
||||
expect(
|
||||
parseFilter(
|
||||
'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))',
|
||||
|
||||
+5
-7
@@ -1,12 +1,12 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils';
|
||||
import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values';
|
||||
import { formatFieldValue } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils';
|
||||
import { parseBaseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';
|
||||
import { parseFilterContent } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter-content.utils';
|
||||
import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils';
|
||||
import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
import { checkFilterEnumValues } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util';
|
||||
import { formatFieldValue } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util';
|
||||
import { parseBaseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util';
|
||||
import { parseFilterContent } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export enum Conjunctions {
|
||||
@@ -19,7 +19,7 @@ export const parseFilter = (
|
||||
filterQuery: string,
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
): Record<string, FieldValue> => {
|
||||
const result = {};
|
||||
const result: Record<string, FieldValue> = {};
|
||||
const match = filterQuery.match(
|
||||
`^(${Object.values(Conjunctions).join('|')})\\((.+)\\)$`,
|
||||
);
|
||||
@@ -42,10 +42,8 @@ export const parseFilter = (
|
||||
`'filter' invalid. 'not' conjunction should contain only 1 condition. eg: not(field[eq]:1)`,
|
||||
);
|
||||
}
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
result[conjunction] = subResult[0];
|
||||
} else {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
result[conjunction] = subResult;
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -4,10 +4,8 @@ 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 { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant';
|
||||
import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type';
|
||||
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';
|
||||
|
||||
@@ -27,6 +27,13 @@ export class RestApiCoreService {
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
private async isCommonApiEnabled(request: AuthenticatedRequest) {
|
||||
return await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
request.workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
async delete(request: AuthenticatedRequest) {
|
||||
return await this.restApiDeleteOneHandler.handle(request);
|
||||
}
|
||||
@@ -49,20 +56,20 @@ export class RestApiCoreService {
|
||||
|
||||
async get(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||
|
||||
if (isDefined(recordId)) {
|
||||
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
request.workspace.id,
|
||||
);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
if (isCommonApiEnabled) {
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiFindOneHandler.commonHandle(request);
|
||||
} else {
|
||||
return await this.restApiFindManyHandler.commonHandle(request);
|
||||
}
|
||||
|
||||
return await this.restApiFindOneHandler.handle(request);
|
||||
} else {
|
||||
return await this.restApiFindManyHandler.handle(request);
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiFindOneHandler.handle(request);
|
||||
} else {
|
||||
return await this.restApiFindManyHandler.handle(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
|
||||
|
||||
describe('EndingBeforeInputFactory', () => {
|
||||
let service: EndingBeforeInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [EndingBeforeInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EndingBeforeInputFactory>(EndingBeforeInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if ending_before missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return ending_before', () => {
|
||||
const request: any = { query: { ending_before: 'uuid' } };
|
||||
|
||||
expect(service.create(request)).toEqual('uuid');
|
||||
});
|
||||
});
|
||||
});
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
|
||||
|
||||
describe('LimitInputFactory', () => {
|
||||
let service: LimitInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [LimitInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LimitInputFactory>(LimitInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if limit missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request)).toEqual(60);
|
||||
});
|
||||
|
||||
it('should return limit', () => {
|
||||
const request: any = { query: { limit: '10' } };
|
||||
|
||||
expect(service.create(request)).toEqual(10);
|
||||
});
|
||||
|
||||
it('should throw if not integer', () => {
|
||||
const request: any = { query: { limit: 'aaa' } };
|
||||
|
||||
expect(() => service.create(request)).toThrow(
|
||||
"limit 'aaa' is invalid. Should be an integer",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if limit negative', () => {
|
||||
const request: any = { query: { limit: -1 } };
|
||||
|
||||
expect(() => service.create(request)).toThrow(
|
||||
"limit '-1' is invalid. Should be an integer",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
|
||||
|
||||
describe('StartingAfterInputFactory', () => {
|
||||
let service: StartingAfterInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [StartingAfterInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<StartingAfterInputFactory>(StartingAfterInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if starting_after missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return starting_after', () => {
|
||||
const request: any = { query: { starting_after: 'uuid' } };
|
||||
|
||||
expect(service.create(request)).toEqual('uuid');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
export const MAX_DEPTH = 2;
|
||||
|
||||
export type Depth = 0 | 1 | 2;
|
||||
|
||||
const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1];
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,4 @@
|
||||
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
|
||||
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 { 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';
|
||||
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
|
||||
export const inputFactories = [
|
||||
DepthInputFactory,
|
||||
EndingBeforeInputFactory,
|
||||
FilterInputFactory,
|
||||
LimitInputFactory,
|
||||
OrderByInputFactory,
|
||||
StartingAfterInputFactory,
|
||||
];
|
||||
export const inputFactories = [FilterInputFactory, OrderByInputFactory];
|
||||
|
||||
@@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils';
|
||||
import { checkFilterQuery } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-query.utils';
|
||||
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util';
|
||||
import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util';
|
||||
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';
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
QUERY_DEFAULT_LIMIT_RECORDS,
|
||||
QUERY_MAX_RECORDS,
|
||||
} from 'twenty-shared/constants';
|
||||
|
||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class LimitInputFactory {
|
||||
create(
|
||||
request: RequestContext,
|
||||
defaultLimit = QUERY_DEFAULT_LIMIT_RECORDS,
|
||||
): number {
|
||||
if (!request.query?.limit) {
|
||||
return defaultLimit;
|
||||
}
|
||||
const limit = +request.query.limit;
|
||||
|
||||
if (isNaN(limit) || limit < 0) {
|
||||
throw new BadRequestException(
|
||||
`limit '${request.query.limit}' is invalid. Should be an integer`,
|
||||
);
|
||||
}
|
||||
|
||||
return Math.min(limit, QUERY_MAX_RECORDS);
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class StartingAfterInputFactory {
|
||||
create(request: RequestContext): string | undefined {
|
||||
const cursorQuery = request.query?.starting_after;
|
||||
|
||||
if (typeof cursorQuery !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cursorQuery;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const MAX_DEPTH = 2;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { type Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
export const parseDepthRestRequest = (request: AuthenticatedRequest): Depth => {
|
||||
if (!request.query.depth) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const depth = +request.query.depth as Depth;
|
||||
|
||||
const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1];
|
||||
|
||||
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;
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util';
|
||||
|
||||
describe('parseEndingBeforeRestRequest', () => {
|
||||
it('should return default if ending_before missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(parseEndingBeforeRestRequest(request)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return ending_before', () => {
|
||||
const request: any = { query: { ending_before: 'uuid' } };
|
||||
|
||||
expect(parseEndingBeforeRestRequest(request)).toEqual('uuid');
|
||||
});
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
export const parseEndingBeforeRestRequest = (
|
||||
request: AuthenticatedRequest | RequestContext,
|
||||
): string | undefined => {
|
||||
const cursorQuery = request.query?.ending_before;
|
||||
|
||||
if (typeof cursorQuery !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cursorQuery;
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils';
|
||||
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util';
|
||||
|
||||
describe('addDefaultConjunctionIfMissing', () => {
|
||||
it('should add default conjunction if missing', () => {
|
||||
+1
-1
@@ -4,7 +4,7 @@ import {
|
||||
fieldSelectMock,
|
||||
objectMetadataItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values';
|
||||
import { checkFilterEnumValues } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util';
|
||||
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { checkFilterQuery } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-query.utils';
|
||||
import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util';
|
||||
|
||||
describe('checkFilterQuery', () => {
|
||||
it('should check filter query', () => {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { formatFieldValue } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils';
|
||||
import { formatFieldValue } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util';
|
||||
|
||||
describe('formatFieldValue', () => {
|
||||
it('should format fieldNumber value', () => {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { parseBaseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';
|
||||
import { parseBaseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util';
|
||||
|
||||
describe('parseBaseFilter', () => {
|
||||
it('should parse simple filter string test 1', () => {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { parseFilterContent } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter-content.utils';
|
||||
import { parseFilterContent } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util';
|
||||
|
||||
describe('parseFilterContent', () => {
|
||||
it('should parse query filter test 1', () => {
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { parseFilterWithoutMetadataValidation } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util';
|
||||
|
||||
describe('parseFilterWithoutMetadataValidation', () => {
|
||||
it('should parse string filter test 1', () => {
|
||||
expect(
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldNumber[eq]:1,fieldNumber[eq]:2)',
|
||||
),
|
||||
).toEqual({
|
||||
and: [{ fieldNumber: { eq: '1' } }, { fieldNumber: { eq: '2' } }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse string filter test 2', () => {
|
||||
expect(
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3))',
|
||||
),
|
||||
).toEqual({
|
||||
and: [
|
||||
{ fieldNumber: { eq: '1' } },
|
||||
{ or: [{ fieldNumber: { eq: '2' } }, { fieldNumber: { eq: '3' } }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse string filter test 3', () => {
|
||||
expect(
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3,and(fieldNumber[eq]:6,fieldNumber[eq]:7)),or(fieldNumber[eq]:4,fieldNumber[eq]:5))',
|
||||
),
|
||||
).toEqual({
|
||||
and: [
|
||||
{ fieldNumber: { eq: '1' } },
|
||||
{
|
||||
or: [
|
||||
{ fieldNumber: { eq: '2' } },
|
||||
{ fieldNumber: { eq: '3' } },
|
||||
{
|
||||
and: [{ fieldNumber: { eq: '6' } }, { fieldNumber: { eq: '7' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ or: [{ fieldNumber: { eq: '4' } }, { fieldNumber: { eq: '5' } }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse string filter test 4', () => {
|
||||
expect(
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldText[gt]:"val,ue",or(fieldNumber[is]:NOT_NULL,not(fieldText[startsWith]:"val"),and(fieldNumber[eq]:6,fieldText[ilike]:"%val%")),or(fieldNumber[eq]:4,fieldText[is]:NULL))',
|
||||
),
|
||||
).toEqual({
|
||||
and: [
|
||||
{ fieldText: { gt: 'val,ue' } },
|
||||
{
|
||||
or: [
|
||||
{ fieldNumber: { is: 'NOT_NULL' } },
|
||||
{ not: { fieldText: { startsWith: 'val' } } },
|
||||
{
|
||||
and: [
|
||||
{ fieldNumber: { eq: '6' } },
|
||||
{ fieldText: { ilike: '%val%' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ or: [{ fieldNumber: { eq: '4' } }, { fieldText: { is: 'NULL' } }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle not', () => {
|
||||
expect(
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))',
|
||||
),
|
||||
).toEqual({
|
||||
and: [
|
||||
{ fieldNumber: { eq: '1' } },
|
||||
{
|
||||
not: { fieldNumber: { eq: '2' } },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
//TODO : Refacto-common - remove this comment - This parser is a copy of the filter input factory without objectMetadata dependency. Validation will be done in common layer
|
||||
import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util';
|
||||
import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util';
|
||||
import { parseFilterWithoutMetadataValidation } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
export const parseFilterRestRequest = (
|
||||
request: AuthenticatedRequest,
|
||||
): Record<string, FieldValue> => {
|
||||
let filterQuery = request.query.filter;
|
||||
|
||||
if (typeof filterQuery !== 'string') {
|
||||
return {};
|
||||
}
|
||||
|
||||
checkFilterQuery(filterQuery);
|
||||
|
||||
filterQuery = addDefaultConjunctionIfMissing(filterQuery);
|
||||
|
||||
return parseFilterWithoutMetadataValidation(filterQuery);
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { Conjunctions } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
import { formatFieldValue } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util';
|
||||
import { parseBaseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util';
|
||||
import { parseFilterContent } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util';
|
||||
|
||||
//TODO : Refacto-common - Rename after deleting parseFilter
|
||||
export const parseFilterWithoutMetadataValidation = (
|
||||
filterQuery: string,
|
||||
): Record<string, FieldValue> => {
|
||||
const result = {};
|
||||
const match = filterQuery.match(
|
||||
`^(${Object.values(Conjunctions).join('|')})\\((.+)\\)$`,
|
||||
);
|
||||
|
||||
if (match) {
|
||||
const conjunction = match?.[1];
|
||||
|
||||
if (!conjunction) {
|
||||
throw new BadRequestException(
|
||||
'Error while matching filter query. Conjunction not found',
|
||||
);
|
||||
}
|
||||
const subResult = parseFilterContent(filterQuery).map((elem) =>
|
||||
parseFilterWithoutMetadataValidation(elem),
|
||||
);
|
||||
|
||||
if (conjunction === Conjunctions.not) {
|
||||
if (subResult.length > 1) {
|
||||
throw new BadRequestException(
|
||||
`'filter' invalid. 'not' conjunction should contain only 1 condition. eg: not(field[eq]:1)`,
|
||||
);
|
||||
}
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
result[conjunction] = subResult[0];
|
||||
} else {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
result[conjunction] = subResult;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
const { fields, comparator, value } = parseBaseFilter(filterQuery);
|
||||
|
||||
const formattedValue = formatFieldValue(value, undefined, comparator);
|
||||
|
||||
return fields.reverse().reduce(
|
||||
(acc, currentValue) => {
|
||||
return { [currentValue]: acc };
|
||||
},
|
||||
{ [comparator]: formattedValue },
|
||||
);
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util';
|
||||
|
||||
describe('parseLimitRestRequest', () => {
|
||||
it('should return default if limit missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(parseLimitRestRequest(request)).toEqual(60);
|
||||
});
|
||||
|
||||
it('should return limit', () => {
|
||||
const request: any = { query: { limit: '10' } };
|
||||
|
||||
expect(parseLimitRestRequest(request)).toEqual(10);
|
||||
});
|
||||
|
||||
it('should throw if not integer', () => {
|
||||
const request: any = { query: { limit: 'aaa' } };
|
||||
|
||||
expect(() => parseLimitRestRequest(request)).toThrow(
|
||||
"limit 'aaa' is invalid. Should be an integer",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if limit negative', () => {
|
||||
const request: any = { query: { limit: -1 } };
|
||||
|
||||
expect(() => parseLimitRestRequest(request)).toThrow(
|
||||
"limit '-1' is invalid. Should be an integer",
|
||||
);
|
||||
});
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
QUERY_DEFAULT_LIMIT_RECORDS,
|
||||
QUERY_MAX_RECORDS,
|
||||
} from 'twenty-shared/constants';
|
||||
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
export const parseLimitRestRequest = (
|
||||
request: AuthenticatedRequest | RequestContext,
|
||||
defaultLimit = QUERY_DEFAULT_LIMIT_RECORDS,
|
||||
): number => {
|
||||
if (!request.query?.limit) {
|
||||
return defaultLimit;
|
||||
}
|
||||
const limit = +request.query.limit;
|
||||
|
||||
if (isNaN(limit) || limit < 0) {
|
||||
throw new BadRequestException(
|
||||
`limit '${request.query.limit}' is invalid. Should be an integer`,
|
||||
);
|
||||
}
|
||||
|
||||
return Math.min(limit, QUERY_MAX_RECORDS);
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { parseOrderByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util';
|
||||
|
||||
describe('parseOrderByRestRequest', () => {
|
||||
it('should return default if order by missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(parseOrderByRestRequest(request)).toEqual([
|
||||
{},
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create order by parser properly', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldNumber[AscNullsFirst],fieldText[DescNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(parseOrderByRestRequest(request)).toEqual([
|
||||
{ fieldNumber: OrderByDirection.AscNullsFirst },
|
||||
{ fieldText: OrderByDirection.DescNullsLast },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should choose default direction if missing', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldNumber',
|
||||
},
|
||||
};
|
||||
|
||||
expect(parseOrderByRestRequest(request)).toEqual([
|
||||
{ fieldNumber: OrderByDirection.AscNullsFirst },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle complex fields', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldCurrency.amountMicros',
|
||||
},
|
||||
};
|
||||
|
||||
expect(parseOrderByRestRequest(request)).toEqual([
|
||||
{ fieldCurrency: { amountMicros: OrderByDirection.AscNullsFirst } },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle complex fields with direction', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldCurrency.amountMicros[DescNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(parseOrderByRestRequest(request)).toEqual([
|
||||
{ fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle multiple complex fields with direction', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by:
|
||||
'fieldCurrency.amountMicros[DescNullsLast],fieldText.label[AscNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(parseOrderByRestRequest(request)).toEqual([
|
||||
{ fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } },
|
||||
{ fieldText: { label: OrderByDirection.AscNullsLast } },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw if direction invalid', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldText[invalid]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => parseOrderByRestRequest(request)).toThrow(
|
||||
"'order_by' direction 'invalid' invalid. Allowed values are 'AscNullsFirst', 'AscNullsLast', 'DescNullsFirst', 'DescNullsLast'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3",
|
||||
);
|
||||
});
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import {
|
||||
type ObjectRecordOrderBy,
|
||||
OrderByDirection,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
export const addDefaultOrderById = (orderBy: ObjectRecordOrderBy) => {
|
||||
const hasIdOrder = orderBy.some((o) => Object.keys(o).includes('id'));
|
||||
|
||||
return hasIdOrder
|
||||
? orderBy
|
||||
: [...orderBy, { id: OrderByDirection.AscNullsFirst }];
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
//TODO : Refacto-common - remove this comment - This parser is a copy of the OrderByInputFactory without objectMetadata dependency. Validation will be done in common layer
|
||||
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type ObjectRecordOrderBy,
|
||||
OrderByDirection,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
import { addDefaultOrderById } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
export const parseOrderByRestRequest = (
|
||||
request: AuthenticatedRequest,
|
||||
): ObjectRecordOrderBy => {
|
||||
const orderByQuery = request.query.order_by;
|
||||
|
||||
if (typeof orderByQuery !== 'string') {
|
||||
return addDefaultOrderById([{}]);
|
||||
}
|
||||
|
||||
//orderByQuery = field_1[AscNullsFirst],field_2[DescNullsLast],field_3
|
||||
const orderByItems = orderByQuery.split(',');
|
||||
let result: Array<Record<string, OrderByDirection>> = [];
|
||||
let itemDirection = '';
|
||||
let itemFields = '';
|
||||
|
||||
for (const orderByItem of orderByItems) {
|
||||
// orderByItem -> field_1[AscNullsFirst]
|
||||
if (orderByItem.includes('[') && orderByItem.includes(']')) {
|
||||
const [fieldName, directionWithRightBracket] = orderByItem.split('[');
|
||||
const direction = directionWithRightBracket.replace(']', '');
|
||||
|
||||
// fields -> [field_1] ; direction -> AscNullsFirst
|
||||
if (!(direction in OrderByDirection)) {
|
||||
throw new BadRequestException(
|
||||
`'order_by' direction '${direction}' invalid. Allowed values are '${Object.values(
|
||||
OrderByDirection,
|
||||
).join(
|
||||
"', '",
|
||||
)}'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3`,
|
||||
);
|
||||
}
|
||||
|
||||
itemDirection = direction;
|
||||
itemFields = fieldName;
|
||||
} else {
|
||||
// orderByItem -> field_3
|
||||
itemDirection = DEFAULT_ORDER_DIRECTION;
|
||||
itemFields = orderByItem;
|
||||
}
|
||||
|
||||
let fieldResult = {};
|
||||
|
||||
itemFields
|
||||
.split('.')
|
||||
.reverse()
|
||||
.forEach((field) => {
|
||||
if (Object.keys(fieldResult).length) {
|
||||
fieldResult = { [field]: fieldResult };
|
||||
} else {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
fieldResult[field] = itemDirection;
|
||||
}
|
||||
}, itemDirection);
|
||||
|
||||
const resultFields = Object.keys(fieldResult).map((key) => ({
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
[key]: fieldResult[key],
|
||||
}));
|
||||
|
||||
result = [...result, ...resultFields];
|
||||
}
|
||||
|
||||
return addDefaultOrderById(result);
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util';
|
||||
|
||||
describe('parseStartingAfterRestRequest', () => {
|
||||
it('should return default if starting_after missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(parseStartingAfterRestRequest(request)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return starting_after', () => {
|
||||
const request: any = { query: { starting_after: 'uuid' } };
|
||||
|
||||
expect(parseStartingAfterRestRequest(request)).toEqual('uuid');
|
||||
});
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
export const parseStartingAfterRestRequest = (
|
||||
request: AuthenticatedRequest | RequestContext,
|
||||
): string | undefined => {
|
||||
const cursorQuery = request.query?.starting_after;
|
||||
|
||||
if (typeof cursorQuery !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cursorQuery;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type Depth = 0 | 1 | 2;
|
||||
+8
-14
@@ -1,19 +1,13 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
|
||||
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
|
||||
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
|
||||
import { type MetadataQueryVariables } from 'src/engine/api/rest/metadata/types/metadata-query-variables.type';
|
||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util';
|
||||
import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util';
|
||||
import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util';
|
||||
import { MetadataQueryVariables } from 'src/engine/api/rest/metadata/types/metadata-query-variables.type';
|
||||
import { RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class GetMetadataVariablesFactory {
|
||||
constructor(
|
||||
private readonly startingAfterInputFactory: StartingAfterInputFactory,
|
||||
private readonly endingBeforeInputFactory: EndingBeforeInputFactory,
|
||||
private readonly limitInputFactory: LimitInputFactory,
|
||||
) {}
|
||||
|
||||
create(
|
||||
id: string | undefined,
|
||||
requestContext: RequestContext,
|
||||
@@ -22,9 +16,9 @@ export class GetMetadataVariablesFactory {
|
||||
return { id };
|
||||
}
|
||||
|
||||
const limit = this.limitInputFactory.create(requestContext, 1000);
|
||||
const before = this.endingBeforeInputFactory.create(requestContext);
|
||||
const after = this.startingAfterInputFactory.create(requestContext);
|
||||
const limit = parseLimitRestRequest(requestContext, 1000);
|
||||
const before = parseEndingBeforeRestRequest(requestContext);
|
||||
const after = parseStartingAfterRestRequest(requestContext);
|
||||
|
||||
if (before && after) {
|
||||
throw new BadRequestException(
|
||||
|
||||
+2
-2
@@ -257,7 +257,7 @@ describe('GraphQL People Pagination with Composite Field Sorting', () => {
|
||||
|
||||
expect(backwardPagePeople).toHaveLength(2);
|
||||
|
||||
expect(backwardPagePeople[0].id).toBe(allPeople.at(-2)?.id);
|
||||
expect(backwardPagePeople[1].id).toBe(allPeople.at(-3)?.id);
|
||||
expect(backwardPagePeople[0].id).toBe(allPeople.at(-3)?.id);
|
||||
expect(backwardPagePeople[1].id).toBe(allPeople.at(-2)?.id);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user