Common api - Group by query (#15108)
closes https://github.com/twentyhq/core-team-issues/issues/1626
This commit is contained in:
+37
-19
@@ -20,7 +20,9 @@ import {
|
|||||||
CommonQueryNames,
|
CommonQueryNames,
|
||||||
CreateManyQueryArgs,
|
CreateManyQueryArgs,
|
||||||
} from 'src/engine/api/common/types/common-query-args.type';
|
} from 'src/engine/api/common/types/common-query-args.type';
|
||||||
|
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
||||||
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.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 { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||||
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
||||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||||
@@ -37,7 +39,7 @@ import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-perm
|
|||||||
export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService {
|
export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService {
|
||||||
async run({
|
async run({
|
||||||
args,
|
args,
|
||||||
authContext: toValidateAuthContext,
|
authContext,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
}: {
|
}: {
|
||||||
@@ -46,8 +48,6 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
objectMetadataMaps: ObjectMetadataMaps;
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
}): Promise<ObjectRecord[]> {
|
}): Promise<ObjectRecord[]> {
|
||||||
const authContext = toValidateAuthContext;
|
|
||||||
|
|
||||||
if (!isWorkspaceAuthContext(authContext)) {
|
if (!isWorkspaceAuthContext(authContext)) {
|
||||||
throw new CommonQueryRunnerException(
|
throw new CommonQueryRunnerException(
|
||||||
'Invalid auth context',
|
'Invalid auth context',
|
||||||
@@ -63,6 +63,18 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//TODO : Refacto-common - QueryParser should be common branded service
|
||||||
|
const commonQueryParser = new GraphqlQueryParser(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
objectMetadataMaps,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
args.selectedFields,
|
||||||
|
objectMetadataMaps,
|
||||||
|
);
|
||||||
|
|
||||||
const { workspaceDataSource, repository, rolePermissionConfig } =
|
const { workspaceDataSource, repository, rolePermissionConfig } =
|
||||||
await this.prepareQueryRunnerContext({
|
await this.prepareQueryRunnerContext({
|
||||||
authContext,
|
authContext,
|
||||||
@@ -80,24 +92,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
args: processedArgs,
|
args: processedArgs,
|
||||||
|
selectedFieldsResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
const upsertedRecords = await this.fetchUpsertedRecords({
|
const upsertedRecords = await this.fetchUpsertedRecords({
|
||||||
args: processedArgs,
|
|
||||||
objectRecords,
|
objectRecords,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
repository,
|
repository,
|
||||||
|
selectedFieldsResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.processNestedRelationsIfNeeded({
|
await this.processNestedRelationsIfNeeded({
|
||||||
args: processedArgs,
|
|
||||||
records: upsertedRecords,
|
records: upsertedRecords,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
rolePermissionConfig,
|
rolePermissionConfig,
|
||||||
authContext,
|
authContext,
|
||||||
workspaceDataSource,
|
workspaceDataSource,
|
||||||
|
selectedFieldsResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
return upsertedRecords;
|
return upsertedRecords;
|
||||||
@@ -136,16 +149,18 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
args,
|
args,
|
||||||
|
selectedFieldsResult,
|
||||||
}: {
|
}: {
|
||||||
repository: WorkspaceRepository<ObjectLiteral>;
|
repository: WorkspaceRepository<ObjectLiteral>;
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
objectMetadataMaps: ObjectMetadataMaps;
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
args: CreateManyQueryArgs;
|
args: CreateManyQueryArgs;
|
||||||
|
selectedFieldsResult: CommonSelectedFieldsResult;
|
||||||
}): Promise<InsertResult> {
|
}): Promise<InsertResult> {
|
||||||
if (!args.upsert) {
|
if (!args.upsert) {
|
||||||
const selectedColumns = buildColumnsToReturn({
|
const selectedColumns = buildColumnsToReturn({
|
||||||
select: args.selectedFieldsResult.select,
|
select: selectedFieldsResult.select,
|
||||||
relations: args.selectedFieldsResult.relations,
|
relations: selectedFieldsResult.relations,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
});
|
});
|
||||||
@@ -158,6 +173,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
args,
|
args,
|
||||||
|
selectedFieldsResult,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,11 +182,13 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
args,
|
args,
|
||||||
|
selectedFieldsResult,
|
||||||
}: {
|
}: {
|
||||||
repository: WorkspaceRepository<ObjectLiteral>;
|
repository: WorkspaceRepository<ObjectLiteral>;
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
objectMetadataMaps: ObjectMetadataMaps;
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
args: CreateManyQueryArgs;
|
args: CreateManyQueryArgs;
|
||||||
|
selectedFieldsResult: CommonSelectedFieldsResult;
|
||||||
}): Promise<InsertResult> {
|
}): Promise<InsertResult> {
|
||||||
const conflictingFields = getConflictingFields(
|
const conflictingFields = getConflictingFields(
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
@@ -195,8 +213,8 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
};
|
};
|
||||||
|
|
||||||
const columnsToReturn = buildColumnsToReturn({
|
const columnsToReturn = buildColumnsToReturn({
|
||||||
select: args.selectedFieldsResult.select,
|
select: selectedFieldsResult.select,
|
||||||
relations: args.selectedFieldsResult.relations,
|
relations: selectedFieldsResult.relations,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
});
|
});
|
||||||
@@ -325,25 +343,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async fetchUpsertedRecords({
|
private async fetchUpsertedRecords({
|
||||||
args,
|
|
||||||
objectRecords,
|
objectRecords,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
repository,
|
repository,
|
||||||
|
selectedFieldsResult,
|
||||||
}: {
|
}: {
|
||||||
args: CreateManyQueryArgs;
|
|
||||||
objectRecords: InsertResult;
|
objectRecords: InsertResult;
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
objectMetadataMaps: ObjectMetadataMaps;
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
repository: WorkspaceRepository<ObjectLiteral>;
|
repository: WorkspaceRepository<ObjectLiteral>;
|
||||||
|
selectedFieldsResult: CommonSelectedFieldsResult;
|
||||||
}): Promise<ObjectRecord[]> {
|
}): Promise<ObjectRecord[]> {
|
||||||
const queryBuilder = repository.createQueryBuilder(
|
const queryBuilder = repository.createQueryBuilder(
|
||||||
objectMetadataItemWithFieldMaps.nameSingular,
|
objectMetadataItemWithFieldMaps.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
const columnsToSelect = buildColumnsToSelect({
|
const columnsToSelect = buildColumnsToSelect({
|
||||||
select: args.selectedFieldsResult.select,
|
select: selectedFieldsResult.select,
|
||||||
relations: args.selectedFieldsResult.relations,
|
relations: selectedFieldsResult.relations,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
});
|
});
|
||||||
@@ -363,23 +381,23 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async processNestedRelationsIfNeeded({
|
private async processNestedRelationsIfNeeded({
|
||||||
args,
|
|
||||||
records,
|
records,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
rolePermissionConfig,
|
rolePermissionConfig,
|
||||||
authContext,
|
authContext,
|
||||||
workspaceDataSource,
|
workspaceDataSource,
|
||||||
|
selectedFieldsResult,
|
||||||
}: {
|
}: {
|
||||||
args: CreateManyQueryArgs;
|
|
||||||
records: ObjectRecord[];
|
records: ObjectRecord[];
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
objectMetadataMaps: ObjectMetadataMaps;
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
rolePermissionConfig?: RolePermissionConfig;
|
|
||||||
authContext: AuthContext;
|
authContext: AuthContext;
|
||||||
workspaceDataSource: WorkspaceDataSource;
|
workspaceDataSource: WorkspaceDataSource;
|
||||||
|
rolePermissionConfig?: RolePermissionConfig;
|
||||||
|
selectedFieldsResult: CommonSelectedFieldsResult;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (!args.selectedFieldsResult.relations) {
|
if (!selectedFieldsResult.relations) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,7 +406,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||||
parentObjectRecords: records,
|
parentObjectRecords: records,
|
||||||
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
|
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
|
||||||
relations: args.selectedFieldsResult.relations as Record<
|
relations: selectedFieldsResult.relations as Record<
|
||||||
string,
|
string,
|
||||||
FindOptionsRelations<ObjectLiteral>
|
FindOptionsRelations<ObjectLiteral>
|
||||||
>,
|
>,
|
||||||
@@ -396,7 +414,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
|||||||
authContext,
|
authContext,
|
||||||
workspaceDataSource,
|
workspaceDataSource,
|
||||||
rolePermissionConfig,
|
rolePermissionConfig,
|
||||||
selectedFields: args.selectedFieldsResult.select,
|
selectedFields: selectedFieldsResult.select,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-14
@@ -21,6 +21,7 @@ import {
|
|||||||
CommonQueryNames,
|
CommonQueryNames,
|
||||||
FindManyQueryArgs,
|
FindManyQueryArgs,
|
||||||
} from 'src/engine/api/common/types/common-query-args.type';
|
} from 'src/engine/api/common/types/common-query-args.type';
|
||||||
|
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
||||||
import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util';
|
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 { 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 { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||||
@@ -36,7 +37,7 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
|
|||||||
export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService {
|
export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService {
|
||||||
async run({
|
async run({
|
||||||
args,
|
args,
|
||||||
authContext: toValidateAuthContext,
|
authContext,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
}: {
|
}: {
|
||||||
@@ -49,9 +50,9 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
|
|||||||
aggregatedValues: Record<string, number>;
|
aggregatedValues: Record<string, number>;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
pageInfo: CommonPageInfo;
|
pageInfo: CommonPageInfo;
|
||||||
|
selectedFieldsResult: CommonSelectedFieldsResult;
|
||||||
}> {
|
}> {
|
||||||
this.validate(args);
|
this.validate(args);
|
||||||
const authContext = toValidateAuthContext;
|
|
||||||
|
|
||||||
if (!isWorkspaceAuthContext(authContext)) {
|
if (!isWorkspaceAuthContext(authContext)) {
|
||||||
throw new CommonQueryRunnerException(
|
throw new CommonQueryRunnerException(
|
||||||
@@ -66,6 +67,17 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
|
|||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const commonQueryParser = new GraphqlQueryParser(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
objectMetadataMaps,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
args.selectedFields,
|
||||||
|
objectMetadataMaps,
|
||||||
|
);
|
||||||
|
|
||||||
const processedArgs = await this.processQueryArgs({
|
const processedArgs = await this.processQueryArgs({
|
||||||
authContext,
|
authContext,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
@@ -80,11 +92,6 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
|
|||||||
|
|
||||||
let appliedFilters = processedArgs.filter ?? ({} as ObjectRecordFilter);
|
let appliedFilters = processedArgs.filter ?? ({} as ObjectRecordFilter);
|
||||||
|
|
||||||
const commonQueryParser = new GraphqlQueryParser(
|
|
||||||
objectMetadataItemWithFieldMaps,
|
|
||||||
objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
commonQueryParser.applyFilterToBuilder(
|
commonQueryParser.applyFilterToBuilder(
|
||||||
aggregateQueryBuilder,
|
aggregateQueryBuilder,
|
||||||
objectMetadataItemWithFieldMaps.nameSingular,
|
objectMetadataItemWithFieldMaps.nameSingular,
|
||||||
@@ -136,7 +143,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
|
|||||||
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
|
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
|
||||||
|
|
||||||
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
|
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
|
||||||
selectedAggregatedFields: processedArgs.selectedFieldsResult.aggregate,
|
selectedAggregatedFields: selectedFieldsResult.aggregate,
|
||||||
queryBuilder: aggregateQueryBuilder,
|
queryBuilder: aggregateQueryBuilder,
|
||||||
objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular,
|
objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular,
|
||||||
});
|
});
|
||||||
@@ -145,8 +152,8 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
|
|||||||
processedArgs.first ?? processedArgs.last ?? QUERY_MAX_RECORDS;
|
processedArgs.first ?? processedArgs.last ?? QUERY_MAX_RECORDS;
|
||||||
|
|
||||||
const columnsToSelect = buildColumnsToSelect({
|
const columnsToSelect = buildColumnsToSelect({
|
||||||
select: processedArgs.selectedFieldsResult.select,
|
select: selectedFieldsResult.select,
|
||||||
relations: processedArgs.selectedFieldsResult.relations,
|
relations: selectedFieldsResult.relations,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
});
|
});
|
||||||
@@ -176,23 +183,23 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
|
|||||||
const parentObjectRecordsAggregatedValues =
|
const parentObjectRecordsAggregatedValues =
|
||||||
await aggregateQueryBuilder.getRawOne();
|
await aggregateQueryBuilder.getRawOne();
|
||||||
|
|
||||||
if (processedArgs.selectedFieldsResult.relations) {
|
if (selectedFieldsResult.relations) {
|
||||||
await this.processNestedRelationsHelper.processNestedRelations({
|
await this.processNestedRelationsHelper.processNestedRelations({
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||||
parentObjectRecords: objectRecords,
|
parentObjectRecords: objectRecords,
|
||||||
parentObjectRecordsAggregatedValues,
|
parentObjectRecordsAggregatedValues,
|
||||||
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
|
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
|
||||||
relations: processedArgs.selectedFieldsResult.relations as Record<
|
relations: selectedFieldsResult.relations as Record<
|
||||||
string,
|
string,
|
||||||
FindOptionsRelations<ObjectLiteral>
|
FindOptionsRelations<ObjectLiteral>
|
||||||
>,
|
>,
|
||||||
aggregate: processedArgs.selectedFieldsResult.aggregate,
|
aggregate: selectedFieldsResult.aggregate,
|
||||||
limit: QUERY_MAX_RECORDS,
|
limit: QUERY_MAX_RECORDS,
|
||||||
authContext,
|
authContext,
|
||||||
workspaceDataSource,
|
workspaceDataSource,
|
||||||
rolePermissionConfig,
|
rolePermissionConfig,
|
||||||
selectedFields: processedArgs.selectedFieldsResult.select,
|
selectedFields: selectedFieldsResult.select,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +216,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
|
|||||||
aggregatedValues: parentObjectRecordsAggregatedValues,
|
aggregatedValues: parentObjectRecordsAggregatedValues,
|
||||||
totalCount: parentObjectRecordsAggregatedValues?.totalCount,
|
totalCount: parentObjectRecordsAggregatedValues?.totalCount,
|
||||||
pageInfo,
|
pageInfo,
|
||||||
|
selectedFieldsResult,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-14
@@ -28,7 +28,7 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
|
|||||||
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService {
|
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService {
|
||||||
async run({
|
async run({
|
||||||
args,
|
args,
|
||||||
authContext: toValidateAuthContext,
|
authContext,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
}: {
|
}: {
|
||||||
@@ -37,8 +37,6 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
|
|||||||
objectMetadataMaps: ObjectMetadataMaps;
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
}): Promise<ObjectRecord> {
|
}): Promise<ObjectRecord> {
|
||||||
const authContext = toValidateAuthContext;
|
|
||||||
|
|
||||||
if (!isWorkspaceAuthContext(authContext)) {
|
if (!isWorkspaceAuthContext(authContext)) {
|
||||||
throw new CommonQueryRunnerException(
|
throw new CommonQueryRunnerException(
|
||||||
'Invalid auth context',
|
'Invalid auth context',
|
||||||
@@ -52,6 +50,18 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
|
|||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//TODO : Refacto-common - QueryParser should be common branded service
|
||||||
|
const commonQueryParser = new GraphqlQueryParser(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
objectMetadataMaps,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
args.selectedFields,
|
||||||
|
objectMetadataMaps,
|
||||||
|
);
|
||||||
|
|
||||||
const processedArgs = await this.processQueryArgs({
|
const processedArgs = await this.processQueryArgs({
|
||||||
authContext,
|
authContext,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
@@ -72,12 +82,6 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
|
|||||||
objectMetadataItemWithFieldMaps.nameSingular,
|
objectMetadataItemWithFieldMaps.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
//TODO : Refacto-common - QueryParser should be common branded service
|
|
||||||
const commonQueryParser = new GraphqlQueryParser(
|
|
||||||
objectMetadataItemWithFieldMaps,
|
|
||||||
objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
commonQueryParser.applyFilterToBuilder(
|
commonQueryParser.applyFilterToBuilder(
|
||||||
queryBuilder,
|
queryBuilder,
|
||||||
objectMetadataItemWithFieldMaps.nameSingular,
|
objectMetadataItemWithFieldMaps.nameSingular,
|
||||||
@@ -90,8 +94,8 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
|
|||||||
);
|
);
|
||||||
|
|
||||||
const columnsToSelect = buildColumnsToSelect({
|
const columnsToSelect = buildColumnsToSelect({
|
||||||
select: args.selectedFieldsResult.select,
|
select: selectedFieldsResult.select,
|
||||||
relations: args.selectedFieldsResult.relations,
|
relations: selectedFieldsResult.relations,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
});
|
});
|
||||||
@@ -111,13 +115,13 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
|
|||||||
|
|
||||||
const objectRecords = [objectRecord] as ObjectRecord[];
|
const objectRecords = [objectRecord] as ObjectRecord[];
|
||||||
|
|
||||||
if (isDefined(args.selectedFieldsResult.relations)) {
|
if (isDefined(selectedFieldsResult.relations)) {
|
||||||
await this.processNestedRelationsHelper.processNestedRelations({
|
await this.processNestedRelationsHelper.processNestedRelations({
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||||
parentObjectRecords: objectRecords,
|
parentObjectRecords: objectRecords,
|
||||||
//TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common
|
//TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common
|
||||||
relations: args.selectedFieldsResult.relations as Record<
|
relations: selectedFieldsResult.relations as Record<
|
||||||
string,
|
string,
|
||||||
FindOptionsRelations<ObjectLiteral>
|
FindOptionsRelations<ObjectLiteral>
|
||||||
>,
|
>,
|
||||||
@@ -125,7 +129,7 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
|
|||||||
authContext,
|
authContext,
|
||||||
workspaceDataSource,
|
workspaceDataSource,
|
||||||
rolePermissionConfig,
|
rolePermissionConfig,
|
||||||
selectedFields: args.selectedFieldsResult.select,
|
selectedFields: selectedFieldsResult.select,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+327
@@ -0,0 +1,327 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CompositeFieldSubFieldName,
|
||||||
|
PartialFieldMetadataItemOption,
|
||||||
|
RecordFilterGroupLogicalOperator,
|
||||||
|
} from 'twenty-shared/types';
|
||||||
|
import {
|
||||||
|
assertIsDefinedOrThrow,
|
||||||
|
combineFilters,
|
||||||
|
computeRecordGqlOperationFilter,
|
||||||
|
convertViewFilterValueToString,
|
||||||
|
getFilterTypeFromFieldType,
|
||||||
|
turnAnyFieldFilterIntoRecordGqlFilter,
|
||||||
|
} from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||||
|
import { ObjectRecordFilter } 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 { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
|
||||||
|
import {
|
||||||
|
CommonQueryNames,
|
||||||
|
GroupByQueryArgs,
|
||||||
|
} 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 { computeIsNumericReturningAggregate } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/compute-is-numeric-returning-aggregate.util';
|
||||||
|
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
|
||||||
|
import { getGroupByExpression } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util';
|
||||||
|
import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util';
|
||||||
|
import { parseGroupByArgs } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util';
|
||||||
|
import { removeQuotes } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/remove-quote.util';
|
||||||
|
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||||
|
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';
|
||||||
|
import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service';
|
||||||
|
import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
|
||||||
|
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||||
|
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||||
|
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerService {
|
||||||
|
constructor(
|
||||||
|
private readonly viewFilterService: ViewFilterService,
|
||||||
|
private readonly viewFilterGroupService: ViewFilterGroupService,
|
||||||
|
private readonly viewService: ViewService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async run({
|
||||||
|
args,
|
||||||
|
authContext,
|
||||||
|
objectMetadataMaps,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
}: {
|
||||||
|
args: GroupByQueryArgs;
|
||||||
|
authContext: AuthContext;
|
||||||
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
|
}): Promise<CommonGroupByOutputItem[]> {
|
||||||
|
if (!isWorkspaceAuthContext(authContext)) {
|
||||||
|
throw new CommonQueryRunnerException(
|
||||||
|
'Invalid auth context',
|
||||||
|
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { repository } = await this.prepareQueryRunnerContext({
|
||||||
|
authContext,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
});
|
||||||
|
|
||||||
|
//TODO : Refacto-common - QueryParser should be common branded service
|
||||||
|
const commonQueryParser = new GraphqlQueryParser(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
objectMetadataMaps,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
args.selectedFields,
|
||||||
|
objectMetadataMaps,
|
||||||
|
);
|
||||||
|
|
||||||
|
const processedArgs = await this.processQueryArgs({
|
||||||
|
authContext,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectMetadataNameSingular =
|
||||||
|
objectMetadataItemWithFieldMaps.nameSingular;
|
||||||
|
|
||||||
|
let queryBuilder = repository.createQueryBuilder(
|
||||||
|
objectMetadataNameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
let appliedFilters = args.filter ?? ({} as ObjectRecordFilter);
|
||||||
|
|
||||||
|
if (args.viewId) {
|
||||||
|
appliedFilters = await this.addFiltersFromView({
|
||||||
|
args: processedArgs,
|
||||||
|
authContext,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
appliedFilters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
commonQueryParser.applyFilterToBuilder(
|
||||||
|
queryBuilder,
|
||||||
|
objectMetadataNameSingular,
|
||||||
|
appliedFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
|
||||||
|
|
||||||
|
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
|
||||||
|
selectedAggregatedFields: selectedFieldsResult.aggregate,
|
||||||
|
queryBuilder,
|
||||||
|
objectMetadataNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupByFields = parseGroupByArgs(
|
||||||
|
processedArgs,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupByDefinitions = groupByFields.map((groupByField) => {
|
||||||
|
const columnNameWithQuotes = `"${
|
||||||
|
formatColumnNamesFromCompositeFieldAndSubfields(
|
||||||
|
groupByField.fieldMetadata.name,
|
||||||
|
groupByField.subFieldName ? [groupByField.subFieldName] : undefined,
|
||||||
|
)[0]
|
||||||
|
}"`;
|
||||||
|
const alias =
|
||||||
|
removeQuotes(columnNameWithQuotes) +
|
||||||
|
(isGroupByDateField(groupByField)
|
||||||
|
? `_${groupByField.dateGranularity}`
|
||||||
|
: '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
columnNameWithQuotes,
|
||||||
|
expression: getGroupByExpression({
|
||||||
|
groupByField,
|
||||||
|
columnNameWithQuotes,
|
||||||
|
}),
|
||||||
|
alias,
|
||||||
|
dateGranularity: isGroupByDateField(groupByField)
|
||||||
|
? groupByField.dateGranularity
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
groupByDefinitions.forEach((groupByColumn, index) => {
|
||||||
|
queryBuilder.addSelect(groupByColumn.expression, groupByColumn.alias);
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
queryBuilder.groupBy(groupByColumn.expression);
|
||||||
|
} else {
|
||||||
|
queryBuilder.addGroupBy(groupByColumn.expression);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (processedArgs.omitNullValues) {
|
||||||
|
const aggregateFields = selectedFieldsResult.aggregate ?? {};
|
||||||
|
|
||||||
|
Object.values(aggregateFields).forEach((aggregationField) => {
|
||||||
|
const aggregateExpression =
|
||||||
|
ProcessAggregateHelper.getAggregateExpression(
|
||||||
|
aggregationField,
|
||||||
|
objectMetadataNameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (aggregateExpression) {
|
||||||
|
queryBuilder.andHaving(`${aggregateExpression} IS NOT NULL`);
|
||||||
|
|
||||||
|
const isNumericReturningAggregate =
|
||||||
|
computeIsNumericReturningAggregate(
|
||||||
|
aggregationField.aggregateOperation,
|
||||||
|
aggregationField.fromFieldType,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNumericReturningAggregate) {
|
||||||
|
queryBuilder.andHaving(`${aggregateExpression} != 0`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
commonQueryParser.applyGroupByOrderToBuilder(
|
||||||
|
queryBuilder,
|
||||||
|
processedArgs.orderBy ?? [],
|
||||||
|
groupByFields,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await queryBuilder.getRawMany();
|
||||||
|
|
||||||
|
return formatResultWithGroupByDimensionValues(
|
||||||
|
result,
|
||||||
|
groupByDefinitions,
|
||||||
|
Object.keys(selectedFieldsResult.aggregate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addFiltersFromView({
|
||||||
|
args,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
appliedFilters,
|
||||||
|
authContext,
|
||||||
|
}: {
|
||||||
|
args: GroupByQueryArgs;
|
||||||
|
authContext: WorkspaceAuthContext;
|
||||||
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
|
appliedFilters: ObjectRecordFilter;
|
||||||
|
}): Promise<ObjectRecordFilter> {
|
||||||
|
assertIsDefinedOrThrow(args.viewId);
|
||||||
|
|
||||||
|
const workspaceId = authContext.workspace.id;
|
||||||
|
|
||||||
|
const viewFilters = await this.viewFilterService.findByViewId(
|
||||||
|
workspaceId,
|
||||||
|
args.viewId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewFilterGroups = await this.viewFilterGroupService.findByViewId(
|
||||||
|
workspaceId,
|
||||||
|
args.viewId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const recordFilters = viewFilters.map((viewFilter) => {
|
||||||
|
const fieldMetadataItem =
|
||||||
|
objectMetadataItemWithFieldMaps.fieldsById[viewFilter.fieldMetadataId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: viewFilter.id,
|
||||||
|
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||||
|
value: convertViewFilterValueToString(viewFilter.value),
|
||||||
|
type: getFilterTypeFromFieldType(fieldMetadataItem.type),
|
||||||
|
operand: viewFilter.operand,
|
||||||
|
recordFilterGroupId: viewFilter.viewFilterGroupId,
|
||||||
|
positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||||
|
subFieldName: viewFilter.subFieldName as CompositeFieldSubFieldName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const recordFilterGroups = viewFilterGroups.map((viewFilterGroup) => {
|
||||||
|
return {
|
||||||
|
id: viewFilterGroup.id,
|
||||||
|
logicalOperator:
|
||||||
|
viewFilterGroup.logicalOperator as unknown as RecordFilterGroupLogicalOperator, // TODO - https://github.com/twentyhq/twenty/issues/14746
|
||||||
|
parentRecordFilterGroupId: viewFilterGroup.parentViewFilterGroupId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const fields = Object.values(
|
||||||
|
objectMetadataItemWithFieldMaps.fieldsById,
|
||||||
|
).map((field) => ({
|
||||||
|
id: field.id,
|
||||||
|
name: field.name,
|
||||||
|
type: field.type,
|
||||||
|
label: field.label,
|
||||||
|
options: field.options as PartialFieldMetadataItemOption[],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const filtersFromView = computeRecordGqlOperationFilter({
|
||||||
|
recordFilters,
|
||||||
|
recordFilterGroups: recordFilterGroups,
|
||||||
|
fields,
|
||||||
|
filterValueDependencies: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
let view: ViewEntity | null = viewFilters[0]?.view;
|
||||||
|
|
||||||
|
if (!view) {
|
||||||
|
view = await this.viewService.findById(args.viewId, workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { recordGqlOperationFilter: anyFieldFilter } =
|
||||||
|
turnAnyFieldFilterIntoRecordGqlFilter({
|
||||||
|
fields,
|
||||||
|
filterValue: view?.anyFieldFilterValue ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
appliedFilters = combineFilters([
|
||||||
|
appliedFilters,
|
||||||
|
filtersFromView,
|
||||||
|
anyFieldFilter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return appliedFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
async processQueryArgs({
|
||||||
|
authContext,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
args,
|
||||||
|
}: {
|
||||||
|
authContext: WorkspaceAuthContext;
|
||||||
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
|
args: GroupByQueryArgs;
|
||||||
|
}): Promise<GroupByQueryArgs> {
|
||||||
|
const hookedArgs =
|
||||||
|
(await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
|
authContext,
|
||||||
|
objectMetadataItemWithFieldMaps.nameSingular,
|
||||||
|
CommonQueryNames.groupBy,
|
||||||
|
args,
|
||||||
|
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
|
||||||
|
)) as GroupByQueryArgs;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...hookedArgs,
|
||||||
|
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
|
||||||
|
hookedArgs.filter,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -2,10 +2,12 @@ import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common
|
|||||||
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
|
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
|
||||||
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
|
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';
|
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
||||||
|
import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
|
||||||
|
|
||||||
export const CommonQueryRunners = [
|
export const CommonQueryRunners = [
|
||||||
CommonFindOneQueryRunnerService,
|
CommonFindOneQueryRunnerService,
|
||||||
CommonFindManyQueryRunnerService,
|
CommonFindManyQueryRunnerService,
|
||||||
CommonCreateOneQueryRunnerService,
|
CommonCreateOneQueryRunnerService,
|
||||||
CommonCreateManyQueryRunnerService,
|
CommonCreateManyQueryRunnerService,
|
||||||
|
CommonGroupByQueryRunnerService,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
|
|||||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||||
|
import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-group/view-filter-group.module';
|
||||||
|
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
|
||||||
|
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -26,6 +29,9 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
|
|||||||
ApiKeyModule,
|
ApiKeyModule,
|
||||||
WorkspacePermissionsCacheModule,
|
WorkspacePermissionsCacheModule,
|
||||||
FileModule,
|
FileModule,
|
||||||
|
ViewModule,
|
||||||
|
ViewFilterModule,
|
||||||
|
ViewFilterGroupModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ProcessNestedRelationsHelper,
|
ProcessNestedRelationsHelper,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
type AggregateValues = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GroupByDimensionValues = {
|
||||||
|
groupByDimensionValues: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CommonGroupByOutputItem = AggregateValues & GroupByDimensionValues;
|
||||||
@@ -1,25 +1,30 @@
|
|||||||
import { type ObjectRecord } from 'twenty-shared/types';
|
import {
|
||||||
|
type ObjectRecord,
|
||||||
|
type OrderByWithGroupBy,
|
||||||
|
} from 'twenty-shared/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type ObjectRecordFilter,
|
type ObjectRecordFilter,
|
||||||
|
type ObjectRecordGroupBy,
|
||||||
type ObjectRecordOrderBy,
|
type ObjectRecordOrderBy,
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
} 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';
|
import { type CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
||||||
|
|
||||||
export enum CommonQueryNames {
|
export enum CommonQueryNames {
|
||||||
findOne = 'findOne',
|
findOne = 'findOne',
|
||||||
findMany = 'findMany',
|
findMany = 'findMany',
|
||||||
createMany = 'createMany',
|
createMany = 'createMany',
|
||||||
|
groupBy = 'groupBy',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FindOneQueryArgs {
|
export interface FindOneQueryArgs {
|
||||||
selectedFieldsResult: CommonSelectedFieldsResult;
|
selectedFields: CommonSelectedFields;
|
||||||
filter?: ObjectRecordFilter;
|
filter?: ObjectRecordFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FindManyQueryArgs {
|
export interface FindManyQueryArgs {
|
||||||
selectedFieldsResult: CommonSelectedFieldsResult;
|
selectedFields: CommonSelectedFields;
|
||||||
filter?: ObjectRecordFilter;
|
filter?: ObjectRecordFilter;
|
||||||
orderBy?: ObjectRecordOrderBy;
|
orderBy?: ObjectRecordOrderBy;
|
||||||
first?: number;
|
first?: number;
|
||||||
@@ -29,13 +34,26 @@ export interface FindManyQueryArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateManyQueryArgs {
|
export interface CreateManyQueryArgs {
|
||||||
selectedFieldsResult: CommonSelectedFieldsResult;
|
selectedFields: CommonSelectedFields;
|
||||||
data: Partial<ObjectRecord>[];
|
data: Partial<ObjectRecord>[];
|
||||||
upsert?: boolean;
|
upsert?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateOneQueryArgs {
|
export interface CreateOneQueryArgs {
|
||||||
selectedFieldsResult: CommonSelectedFieldsResult;
|
selectedFields: CommonSelectedFields;
|
||||||
data: Partial<ObjectRecord>;
|
data: Partial<ObjectRecord>;
|
||||||
upsert?: boolean;
|
upsert?: boolean;
|
||||||
}
|
}
|
||||||
|
export interface GroupByQueryArgs {
|
||||||
|
selectedFields: CommonSelectedFields;
|
||||||
|
filter?: ObjectRecordFilter;
|
||||||
|
orderBy?: OrderByWithGroupBy;
|
||||||
|
groupBy: ObjectRecordGroupBy;
|
||||||
|
omitNullValues?: boolean;
|
||||||
|
viewId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommonQueryArgs =
|
||||||
|
| FindOneQueryArgs
|
||||||
|
| FindManyQueryArgs
|
||||||
|
| GroupByQueryArgs;
|
||||||
|
|||||||
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
|
import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
|
||||||
|
|
||||||
interface SelectedFields {
|
export interface CommonSelectedFields {
|
||||||
[key: string]: boolean | SelectedFields;
|
[key: string]: boolean | CommonSelectedFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommonSelectedFieldsResult = {
|
export type CommonSelectedFieldsResult = {
|
||||||
select: SelectedFields;
|
select: CommonSelectedFields;
|
||||||
relations: SelectedFields;
|
relations: CommonSelectedFields;
|
||||||
//TODO = Refacto-common - to update when rest api will handle aggregates
|
//TODO = Refacto-common - to update when rest api will handle aggregates
|
||||||
aggregate: Record<string, AggregationField>;
|
aggregate: Record<string, AggregationField>;
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-40
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CompositeFieldSubFieldName,
|
CompositeFieldSubFieldName,
|
||||||
FieldMetadataType,
|
|
||||||
ObjectRecord,
|
ObjectRecord,
|
||||||
PartialFieldMetadataItemOption,
|
PartialFieldMetadataItemOption,
|
||||||
RecordFilterGroupLogicalOperator,
|
RecordFilterGroupLogicalOperator,
|
||||||
@@ -26,7 +25,7 @@ import { IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runne
|
|||||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||||
import { GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
import { GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
import { computeIsNumericReturningAggregate } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/compute-is-numeric-returning-aggregate.util';
|
||||||
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
|
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
|
||||||
import { getGroupByExpression } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util';
|
import { getGroupByExpression } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util';
|
||||||
import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util';
|
import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util';
|
||||||
@@ -152,10 +151,11 @@ export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolver
|
|||||||
if (aggregateExpression) {
|
if (aggregateExpression) {
|
||||||
queryBuilder.andHaving(`${aggregateExpression} IS NOT NULL`);
|
queryBuilder.andHaving(`${aggregateExpression} IS NOT NULL`);
|
||||||
|
|
||||||
const isNumericReturningAggregate = this.isNumericReturningAggregate(
|
const isNumericReturningAggregate =
|
||||||
aggregationField.aggregateOperation,
|
computeIsNumericReturningAggregate(
|
||||||
aggregationField.fromFieldType,
|
aggregationField.aggregateOperation,
|
||||||
);
|
aggregationField.fromFieldType,
|
||||||
|
);
|
||||||
|
|
||||||
if (isNumericReturningAggregate) {
|
if (isNumericReturningAggregate) {
|
||||||
queryBuilder.andHaving(`${aggregateExpression} != 0`);
|
queryBuilder.andHaving(`${aggregateExpression} != 0`);
|
||||||
@@ -172,7 +172,11 @@ export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolver
|
|||||||
|
|
||||||
const result = await queryBuilder.getRawMany();
|
const result = await queryBuilder.getRawMany();
|
||||||
|
|
||||||
return formatResultWithGroupByDimensionValues(result, groupByDefinitions);
|
return formatResultWithGroupByDimensionValues(
|
||||||
|
result,
|
||||||
|
groupByDefinitions,
|
||||||
|
Object.keys(executionArgs.graphqlQuerySelectedFieldsResult.aggregate),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async addFiltersFromView({
|
private async addFiltersFromView({
|
||||||
@@ -266,39 +270,6 @@ export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolver
|
|||||||
return appliedFilters;
|
return appliedFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isNumericReturningAggregate(
|
|
||||||
operation: AggregateOperations,
|
|
||||||
fromFieldType: FieldMetadataType,
|
|
||||||
): boolean {
|
|
||||||
if (
|
|
||||||
operation === AggregateOperations.COUNT ||
|
|
||||||
operation === AggregateOperations.COUNT_UNIQUE_VALUES ||
|
|
||||||
operation === AggregateOperations.COUNT_EMPTY ||
|
|
||||||
operation === AggregateOperations.COUNT_NOT_EMPTY ||
|
|
||||||
operation === AggregateOperations.COUNT_TRUE ||
|
|
||||||
operation === AggregateOperations.COUNT_FALSE ||
|
|
||||||
operation === AggregateOperations.PERCENTAGE_EMPTY ||
|
|
||||||
operation === AggregateOperations.PERCENTAGE_NOT_EMPTY
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
operation === AggregateOperations.MIN ||
|
|
||||||
operation === AggregateOperations.MAX ||
|
|
||||||
operation === AggregateOperations.AVG ||
|
|
||||||
operation === AggregateOperations.SUM
|
|
||||||
) {
|
|
||||||
return [
|
|
||||||
FieldMetadataType.NUMBER,
|
|
||||||
FieldMetadataType.NUMERIC,
|
|
||||||
FieldMetadataType.CURRENCY,
|
|
||||||
].includes(fromFieldType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(
|
async validate(
|
||||||
_args: GroupByResolverArgs<ObjectRecordFilter>,
|
_args: GroupByResolverArgs<ObjectRecordFilter>,
|
||||||
_options: WorkspaceQueryRunnerOptions,
|
_options: WorkspaceQueryRunnerOptions,
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||||
|
|
||||||
|
export const computeIsNumericReturningAggregate = (
|
||||||
|
operation: AggregateOperations,
|
||||||
|
fromFieldType: FieldMetadataType,
|
||||||
|
): boolean => {
|
||||||
|
if (
|
||||||
|
operation === AggregateOperations.COUNT ||
|
||||||
|
operation === AggregateOperations.COUNT_UNIQUE_VALUES ||
|
||||||
|
operation === AggregateOperations.COUNT_EMPTY ||
|
||||||
|
operation === AggregateOperations.COUNT_NOT_EMPTY ||
|
||||||
|
operation === AggregateOperations.COUNT_TRUE ||
|
||||||
|
operation === AggregateOperations.COUNT_FALSE ||
|
||||||
|
operation === AggregateOperations.PERCENTAGE_EMPTY ||
|
||||||
|
operation === AggregateOperations.PERCENTAGE_NOT_EMPTY
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
operation === AggregateOperations.MIN ||
|
||||||
|
operation === AggregateOperations.MAX ||
|
||||||
|
operation === AggregateOperations.AVG ||
|
||||||
|
operation === AggregateOperations.SUM
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
FieldMetadataType.NUMBER,
|
||||||
|
FieldMetadataType.NUMERIC,
|
||||||
|
FieldMetadataType.CURRENCY,
|
||||||
|
].includes(fromFieldType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
+31
-28
@@ -7,57 +7,60 @@ import {
|
|||||||
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
||||||
import { type IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/group-by-connection.interface';
|
import { type IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/group-by-connection.interface';
|
||||||
|
|
||||||
import { removeQuotes } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/remove-quote.util';
|
import { type CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
|
||||||
|
|
||||||
export const formatResultWithGroupByDimensionValues = (
|
export const formatResultWithGroupByDimensionValues = <
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
T extends
|
||||||
result: any[],
|
| IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>
|
||||||
|
| CommonGroupByOutputItem,
|
||||||
|
>(
|
||||||
|
result: Record<string, string>[],
|
||||||
groupByColumnsWithQuotes: {
|
groupByColumnsWithQuotes: {
|
||||||
columnNameWithQuotes: string;
|
columnNameWithQuotes: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
dateGranularity?: ObjectRecordGroupByDateGranularity;
|
dateGranularity?: ObjectRecordGroupByDateGranularity;
|
||||||
}[],
|
}[],
|
||||||
): IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[] => {
|
aggregateFieldNames: string[],
|
||||||
let formattedResult: IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[] =
|
): T[] => {
|
||||||
[];
|
let formattedResult: T[] = [];
|
||||||
|
|
||||||
result.forEach((group) => {
|
result.forEach((group) => {
|
||||||
let dimensionValues = [];
|
let dimensionValues: string[] = [];
|
||||||
|
|
||||||
for (const groupByColumn of groupByColumnsWithQuotes) {
|
for (const groupByColumn of groupByColumnsWithQuotes) {
|
||||||
dimensionValues.push(group[groupByColumn.alias]);
|
dimensionValues.push(
|
||||||
|
getTranslatedValueIfApplicable(
|
||||||
|
group[groupByColumn.alias],
|
||||||
|
groupByColumn.dateGranularity,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const groupWithValueMappedToUnaliasedColumn = {
|
|
||||||
...group,
|
|
||||||
...groupByColumnsWithQuotes.reduce<Record<string, unknown>>(
|
|
||||||
(acc, groupByColumn) => {
|
|
||||||
const value = group[groupByColumn.alias];
|
|
||||||
|
|
||||||
acc[removeQuotes(groupByColumn.columnNameWithQuotes)] =
|
const aggregateValues = aggregateFieldNames.reduce(
|
||||||
getTranslatedValueIfApplicable(
|
(acc, fieldName) => {
|
||||||
value,
|
if (fieldName in group) {
|
||||||
groupByColumn.dateGranularity,
|
acc[fieldName] = group[fieldName];
|
||||||
);
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{},
|
{} as Record<string, string>,
|
||||||
),
|
);
|
||||||
};
|
|
||||||
|
|
||||||
formattedResult.push({
|
formattedResult.push({
|
||||||
groupByDimensionValues: dimensionValues,
|
groupByDimensionValues: dimensionValues,
|
||||||
...groupWithValueMappedToUnaliasedColumn,
|
...aggregateValues,
|
||||||
});
|
//TODO: Refacto-common - remove generic type
|
||||||
|
} as T);
|
||||||
});
|
});
|
||||||
|
|
||||||
return formattedResult;
|
return formattedResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTranslatedValueIfApplicable = (
|
const getTranslatedValueIfApplicable = (
|
||||||
value: unknown,
|
value: string,
|
||||||
dateGranularity?: ObjectRecordGroupByDateGranularity,
|
dateGranularity?: ObjectRecordGroupByDateGranularity,
|
||||||
) => {
|
): string => {
|
||||||
switch (dateGranularity) {
|
switch (dateGranularity) {
|
||||||
case ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK:
|
case ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK:
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
|||||||
+9
-1
@@ -11,6 +11,7 @@ import {
|
|||||||
type FindDuplicatesResolverArgs,
|
type FindDuplicatesResolverArgs,
|
||||||
type FindManyResolverArgs,
|
type FindManyResolverArgs,
|
||||||
type FindOneResolverArgs,
|
type FindOneResolverArgs,
|
||||||
|
GroupByResolverArgs,
|
||||||
type MergeManyResolverArgs,
|
type MergeManyResolverArgs,
|
||||||
type ResolverArgs,
|
type ResolverArgs,
|
||||||
ResolverArgsType,
|
ResolverArgsType,
|
||||||
@@ -108,7 +109,6 @@ export class QueryRunnerArgsFactory {
|
|||||||
options.objectMetadataItemWithFieldMaps,
|
options.objectMetadataItemWithFieldMaps,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
case ResolverArgsType.FindDuplicates:
|
case ResolverArgsType.FindDuplicates:
|
||||||
return {
|
return {
|
||||||
...args,
|
...args,
|
||||||
@@ -146,6 +146,14 @@ export class QueryRunnerArgsFactory {
|
|||||||
.conflictPriorityIndex,
|
.conflictPriorityIndex,
|
||||||
dryRun: (args as MergeManyResolverArgs).dryRun,
|
dryRun: (args as MergeManyResolverArgs).dryRun,
|
||||||
} satisfies MergeManyResolverArgs;
|
} satisfies MergeManyResolverArgs;
|
||||||
|
case ResolverArgsType.GroupBy:
|
||||||
|
return {
|
||||||
|
...args,
|
||||||
|
filter: this.overrideFilterByFieldMetadata(
|
||||||
|
(args as GroupByResolverArgs).filter,
|
||||||
|
options.objectMetadataItemWithFieldMaps,
|
||||||
|
),
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -8,6 +8,7 @@ import {
|
|||||||
type FindDuplicatesResolverArgs,
|
type FindDuplicatesResolverArgs,
|
||||||
type FindManyResolverArgs,
|
type FindManyResolverArgs,
|
||||||
type FindOneResolverArgs,
|
type FindOneResolverArgs,
|
||||||
|
type GroupByResolverArgs,
|
||||||
type MergeManyResolverArgs,
|
type MergeManyResolverArgs,
|
||||||
type RestoreManyResolverArgs,
|
type RestoreManyResolverArgs,
|
||||||
type UpdateManyResolverArgs,
|
type UpdateManyResolverArgs,
|
||||||
@@ -45,4 +46,6 @@ export type WorkspacePreQueryHookPayload<T> = T extends 'createMany'
|
|||||||
? DestroyOneResolverArgs
|
? DestroyOneResolverArgs
|
||||||
: T extends 'mergeMany'
|
: T extends 'mergeMany'
|
||||||
? MergeManyResolverArgs
|
? MergeManyResolverArgs
|
||||||
: never;
|
: T extends 'groupBy'
|
||||||
|
? GroupByResolverArgs
|
||||||
|
: never;
|
||||||
|
|||||||
+2
-12
@@ -12,7 +12,6 @@ import {
|
|||||||
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||||
|
|
||||||
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
|
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-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 { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||||
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
|
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
|
||||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||||
@@ -46,20 +45,11 @@ export class CreateManyResolverFactory
|
|||||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||||
|
|
||||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||||
const graphqlQueryParser = new GraphqlQueryParser(
|
const selectedFields = graphqlFields(info);
|
||||||
internalContext.objectMetadataItemWithFieldMaps,
|
|
||||||
internalContext.objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedFieldsResult = graphqlQueryParser.parseSelectedFields(
|
|
||||||
internalContext.objectMetadataItemWithFieldMaps,
|
|
||||||
graphqlFields(info),
|
|
||||||
internalContext.objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const records = await this.commonCreateManyQueryRunnerService.run({
|
const records = await this.commonCreateManyQueryRunnerService.run({
|
||||||
args: { ...args, selectedFieldsResult },
|
args: { ...args, selectedFields },
|
||||||
authContext: internalContext.authContext,
|
authContext: internalContext.authContext,
|
||||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps:
|
objectMetadataItemWithFieldMaps:
|
||||||
|
|||||||
+2
-12
@@ -11,7 +11,6 @@ import {
|
|||||||
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||||
|
|
||||||
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
|
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-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 { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||||
import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service';
|
import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service';
|
||||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||||
@@ -43,20 +42,11 @@ export class CreateOneResolverFactory
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isCommonApiEnabled) {
|
if (isCommonApiEnabled) {
|
||||||
const graphqlQueryParser = new GraphqlQueryParser(
|
const selectedFields = graphqlFields(info);
|
||||||
internalContext.objectMetadataItemWithFieldMaps,
|
|
||||||
internalContext.objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedFieldsResult = graphqlQueryParser.parseSelectedFields(
|
|
||||||
internalContext.objectMetadataItemWithFieldMaps,
|
|
||||||
graphqlFields(info),
|
|
||||||
internalContext.objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const record = await this.commonCreateOneQueryRunnerService.run({
|
const record = await this.commonCreateOneQueryRunnerService.run({
|
||||||
args: { ...args, selectedFieldsResult },
|
args: { ...args, selectedFields },
|
||||||
authContext: internalContext.authContext,
|
authContext: internalContext.authContext,
|
||||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps:
|
objectMetadataItemWithFieldMaps:
|
||||||
|
|||||||
+14
-19
@@ -11,7 +11,6 @@ import {
|
|||||||
import { 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 { 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 { 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 { 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 { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||||
@@ -43,26 +42,22 @@ export class FindManyResolverFactory
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isCommonApiEnabled) {
|
if (isCommonApiEnabled) {
|
||||||
const graphqlQueryParser = new GraphqlQueryParser(
|
const selectedFields = graphqlFields(info);
|
||||||
internalContext.objectMetadataItemWithFieldMaps,
|
|
||||||
internalContext.objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedFieldsResult = graphqlQueryParser.parseSelectedFields(
|
|
||||||
internalContext.objectMetadataItemWithFieldMaps,
|
|
||||||
graphqlFields(info),
|
|
||||||
internalContext.objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { records, aggregatedValues, totalCount, pageInfo } =
|
const {
|
||||||
await this.commonFindManyQueryRunnerService.run({
|
records,
|
||||||
args: { ...args, selectedFieldsResult },
|
aggregatedValues,
|
||||||
authContext: internalContext.authContext,
|
totalCount,
|
||||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
pageInfo,
|
||||||
objectMetadataItemWithFieldMaps:
|
selectedFieldsResult,
|
||||||
internalContext.objectMetadataItemWithFieldMaps,
|
} = await this.commonFindManyQueryRunnerService.run({
|
||||||
});
|
args: { ...args, selectedFields },
|
||||||
|
authContext: internalContext.authContext,
|
||||||
|
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||||
|
objectMetadataItemWithFieldMaps:
|
||||||
|
internalContext.objectMetadataItemWithFieldMaps,
|
||||||
|
});
|
||||||
|
|
||||||
const typeORMObjectRecordsParser =
|
const typeORMObjectRecordsParser =
|
||||||
new ObjectRecordsToGraphqlConnectionHelper(
|
new ObjectRecordsToGraphqlConnectionHelper(
|
||||||
|
|||||||
+2
-12
@@ -10,7 +10,6 @@ import {
|
|||||||
import { 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 { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
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 { 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 { 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 { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||||
@@ -43,19 +42,10 @@ export class FindOneResolverFactory
|
|||||||
|
|
||||||
if (isCommonApiEnabled) {
|
if (isCommonApiEnabled) {
|
||||||
try {
|
try {
|
||||||
const graphqlQueryParser = new GraphqlQueryParser(
|
const selectedFields = graphqlFields(info);
|
||||||
internalContext.objectMetadataItemWithFieldMaps,
|
|
||||||
internalContext.objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedFieldsResult = graphqlQueryParser.parseSelectedFields(
|
|
||||||
internalContext.objectMetadataItemWithFieldMaps,
|
|
||||||
graphqlFields(info),
|
|
||||||
internalContext.objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const record = await this.commonFindOneQueryRunnerService.run({
|
const record = await this.commonFindOneQueryRunnerService.run({
|
||||||
args: { ...args, selectedFieldsResult },
|
args: { ...args, selectedFields },
|
||||||
authContext: internalContext.authContext,
|
authContext: internalContext.authContext,
|
||||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps:
|
objectMetadataItemWithFieldMaps:
|
||||||
|
|||||||
+29
@@ -1,5 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import graphqlFields from 'graphql-fields';
|
||||||
|
|
||||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||||
import {
|
import {
|
||||||
@@ -8,8 +10,12 @@ import {
|
|||||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
import { 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 { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
|
||||||
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/graphql-query-group-by-resolver.service';
|
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/graphql-query-group-by-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 { 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()
|
@Injectable()
|
||||||
export class GroupByResolverFactory
|
export class GroupByResolverFactory
|
||||||
@@ -19,6 +25,8 @@ export class GroupByResolverFactory
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly graphqlQueryRunnerService: GraphqlQueryGroupByResolverService,
|
private readonly graphqlQueryRunnerService: GraphqlQueryGroupByResolverService,
|
||||||
|
private readonly commonGroupByQueryRunnerService: CommonGroupByQueryRunnerService,
|
||||||
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
create(
|
create(
|
||||||
@@ -27,6 +35,27 @@ export class GroupByResolverFactory
|
|||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, _context, info) => {
|
return async (_source, args, _context, info) => {
|
||||||
|
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||||
|
internalContext.authContext.workspace?.id as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCommonApiEnabled) {
|
||||||
|
const selectedFields = graphqlFields(info);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.commonGroupByQueryRunnerService.run({
|
||||||
|
args: { ...args, selectedFields },
|
||||||
|
authContext: internalContext.authContext,
|
||||||
|
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||||
|
objectMetadataItemWithFieldMaps:
|
||||||
|
internalContext.objectMetadataItemWithFieldMaps,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const options: WorkspaceQueryRunnerOptions = {
|
const options: WorkspaceQueryRunnerOptions = {
|
||||||
authContext: internalContext.authContext,
|
authContext: internalContext.authContext,
|
||||||
info,
|
info,
|
||||||
|
|||||||
+1
@@ -29,6 +29,7 @@ export enum ResolverArgsType {
|
|||||||
DestroyMany = 'DestroyMany',
|
DestroyMany = 'DestroyMany',
|
||||||
DestroyOne = 'DestroyOne',
|
DestroyOne = 'DestroyOne',
|
||||||
MergeMany = 'MergeMany',
|
MergeMany = 'MergeMany',
|
||||||
|
GroupBy = 'GroupBy',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FindManyResolverArgs<
|
export interface FindManyResolverArgs<
|
||||||
|
|||||||
+14
@@ -66,6 +66,20 @@ export class RestApiCoreController {
|
|||||||
res.status(201).send(result);
|
res.status(201).send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Refacto-common - Document this endpoint
|
||||||
|
@Get('*/groupBy')
|
||||||
|
async handleApiGroupBy(
|
||||||
|
@Req() request: AuthenticatedRequest,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
this.logger.log(
|
||||||
|
`[REST API] Processing GROUP BY request to ${request.path} on workspace ${request.workspaceId}`,
|
||||||
|
);
|
||||||
|
const result = await this.restApiCoreService.groupBy(request);
|
||||||
|
|
||||||
|
res.status(200).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('*')
|
@Get('*')
|
||||||
async handleApiGet(
|
async handleApiGet(
|
||||||
@Req() request: AuthenticatedRequest,
|
@Req() request: AuthenticatedRequest,
|
||||||
|
|||||||
+2
-3
@@ -16,7 +16,6 @@ import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parser
|
|||||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
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';
|
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -35,7 +34,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
|||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
} = await this.buildCommonOptions(request);
|
} = await this.buildCommonOptions(request);
|
||||||
|
|
||||||
const selectedFieldsResult = await this.computeSelectedFields({
|
const selectedFields = await this.computeSelectedFields({
|
||||||
depth,
|
depth,
|
||||||
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
@@ -43,7 +42,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const records = await this.commonCreateManyQueryRunnerService.run({
|
const records = await this.commonCreateManyQueryRunnerService.run({
|
||||||
args: { data, selectedFieldsResult, upsert },
|
args: { data, selectedFields, upsert },
|
||||||
authContext,
|
authContext,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
|
|||||||
+2
-2
@@ -35,7 +35,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
|||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
} = await this.buildCommonOptions(request);
|
} = await this.buildCommonOptions(request);
|
||||||
|
|
||||||
const selectedFieldsResult = await this.computeSelectedFields({
|
const selectedFields = await this.computeSelectedFields({
|
||||||
depth,
|
depth,
|
||||||
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
@@ -43,7 +43,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const record = await this.commonCreateOneQueryRunnerService.run({
|
const record = await this.commonCreateOneQueryRunnerService.run({
|
||||||
args: { data, selectedFieldsResult, upsert },
|
args: { data, selectedFields, upsert },
|
||||||
authContext,
|
authContext,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
|
|||||||
+2
-2
@@ -102,7 +102,7 @@ export class RestApiFindManyHandler extends RestApiBaseHandler {
|
|||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
} = await this.buildCommonOptions(request);
|
} = await this.buildCommonOptions(request);
|
||||||
|
|
||||||
const selectedFieldsResult = await this.computeSelectedFields({
|
const selectedFields = await this.computeSelectedFields({
|
||||||
depth: parsedArgs.depth,
|
depth: parsedArgs.depth,
|
||||||
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
@@ -111,7 +111,7 @@ export class RestApiFindManyHandler extends RestApiBaseHandler {
|
|||||||
|
|
||||||
const { records, aggregatedValues, pageInfo } =
|
const { records, aggregatedValues, pageInfo } =
|
||||||
await this.commonFindManyQueryRunnerService.run({
|
await this.commonFindManyQueryRunnerService.run({
|
||||||
args: { ...parsedArgs, selectedFieldsResult },
|
args: { ...parsedArgs, selectedFields },
|
||||||
authContext,
|
authContext,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
|
|||||||
+2
-2
@@ -66,7 +66,7 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
|
|||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
} = await this.buildCommonOptions(request);
|
} = await this.buildCommonOptions(request);
|
||||||
|
|
||||||
const selectedFieldsResult = await this.computeSelectedFields({
|
const selectedFields = await this.computeSelectedFields({
|
||||||
depth,
|
depth,
|
||||||
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
@@ -74,7 +74,7 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const record = await this.commonFindOneQueryRunnerService.run({
|
const record = await this.commonFindOneQueryRunnerService.run({
|
||||||
args: { filter, selectedFieldsResult },
|
args: { filter, selectedFields },
|
||||||
authContext,
|
authContext,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||||
|
|
||||||
|
import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
|
||||||
|
import { parseAggregateFieldsRestRequest } from 'src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util';
|
||||||
|
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
|
||||||
|
import { parseGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/group-by-parser-utils/parse-group-by-rest-request.util';
|
||||||
|
import { parseOmitNullValuesRestRequest } from 'src/engine/api/rest/input-request-parsers/omit-null-values-parser-utils/parse-omit-null-values-rest-request.util';
|
||||||
|
import { parseOrderByWithGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-with-group-by-rest-request.util';
|
||||||
|
import { parseViewIdRestRequest } from 'src/engine/api/rest/input-request-parsers/view-id-parser-utils/parse-view-id-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 RestApiGroupByHandler extends RestApiBaseHandler {
|
||||||
|
constructor(
|
||||||
|
private readonly commonGroupByQueryRunnerService: CommonGroupByQueryRunnerService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(request: AuthenticatedRequest) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
authContext,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
objectMetadataMaps,
|
||||||
|
} = await this.buildCommonOptions(request);
|
||||||
|
|
||||||
|
const {
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
viewId,
|
||||||
|
groupBy,
|
||||||
|
selectedFields,
|
||||||
|
omitNullValues,
|
||||||
|
} = this.parseRequestArgs(request);
|
||||||
|
|
||||||
|
return await this.commonGroupByQueryRunnerService.run({
|
||||||
|
args: {
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
viewId,
|
||||||
|
groupBy,
|
||||||
|
selectedFields,
|
||||||
|
omitNullValues,
|
||||||
|
},
|
||||||
|
authContext,
|
||||||
|
objectMetadataMaps,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseRequestArgs(request: AuthenticatedRequest) {
|
||||||
|
const orderByWithGroupBy = parseOrderByWithGroupByRestRequest(request);
|
||||||
|
const filter = parseFilterRestRequest(request);
|
||||||
|
const viewId = parseViewIdRestRequest(request);
|
||||||
|
const groupBy = parseGroupByRestRequest(request);
|
||||||
|
const aggregateFields = parseAggregateFieldsRestRequest(request);
|
||||||
|
const omitNullValues = parseOmitNullValuesRestRequest(request);
|
||||||
|
const selectedFields = { ...aggregateFields, groupByDimensionValues: true };
|
||||||
|
|
||||||
|
return {
|
||||||
|
filter,
|
||||||
|
orderBy: orderByWithGroupBy,
|
||||||
|
viewId,
|
||||||
|
groupBy,
|
||||||
|
selectedFields,
|
||||||
|
omitNullValues,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { In, type ObjectLiteral } from 'typeorm';
|
|||||||
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||||
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||||
|
|
||||||
|
import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
|
||||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||||
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||||
@@ -104,7 +105,9 @@ export abstract class RestApiBaseHandler {
|
|||||||
|
|
||||||
protected abstract handle(
|
protected abstract handle(
|
||||||
request: AuthenticatedRequest,
|
request: AuthenticatedRequest,
|
||||||
): Promise<FormatResult | { data: FormatResult[] }>;
|
): Promise<
|
||||||
|
FormatResult | { data: FormatResult[] } | CommonGroupByOutputItem[]
|
||||||
|
>;
|
||||||
|
|
||||||
public async getRepositoryAndMetadataOrFail(request: AuthenticatedRequest) {
|
public async getRepositoryAndMetadataOrFail(request: AuthenticatedRequest) {
|
||||||
const { workspace, apiKey, userWorkspaceId } = request;
|
const { workspace, apiKey, userWorkspaceId } = request;
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ export const parseCorePath = (
|
|||||||
return { object: queryAction[1] };
|
return { object: queryAction[1] };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryAction[1] === 'duplicates') {
|
if (queryAction[1] === 'duplicates' || queryAction[1] === 'group') {
|
||||||
return { object: queryAction[0] };
|
return { object: queryAction[0] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-
|
|||||||
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
|
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
|
||||||
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
||||||
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
||||||
|
import { RestApiGroupByHandler } from 'src/engine/api/rest/core/handlers/rest-api-group-by.handler';
|
||||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||||
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
||||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||||
@@ -35,6 +36,7 @@ const restApiCoreResolvers = [
|
|||||||
RestApiFindOneHandler,
|
RestApiFindOneHandler,
|
||||||
RestApiFindManyHandler,
|
RestApiFindManyHandler,
|
||||||
RestApiFindDuplicatesHandler,
|
RestApiFindDuplicatesHandler,
|
||||||
|
RestApiGroupByHandler,
|
||||||
];
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
+24
-48
@@ -3,7 +3,7 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
|||||||
import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types';
|
import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
import { CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
||||||
import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant';
|
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 { 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 { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||||
@@ -11,6 +11,10 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
|
|||||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||||
|
|
||||||
|
type SelectFields = {
|
||||||
|
[key: string]: boolean | SelectFields;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RestToCommonSelectedFieldsHandler {
|
export class RestToCommonSelectedFieldsHandler {
|
||||||
computeFromDepth = ({
|
computeFromDepth = ({
|
||||||
@@ -23,17 +27,16 @@ export class RestToCommonSelectedFieldsHandler {
|
|||||||
objectMetadataMaps: ObjectMetadataMaps;
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||||
depth: Depth | undefined;
|
depth: Depth | undefined;
|
||||||
}): CommonSelectedFieldsResult => {
|
}): CommonSelectedFields => {
|
||||||
const restrictedFields =
|
const restrictedFields =
|
||||||
objectsPermissions[objectMetadataMapItem.id].restrictedFields;
|
objectsPermissions[objectMetadataMapItem.id].restrictedFields;
|
||||||
|
|
||||||
const { relations, relationsSelectFields } =
|
const relationsSelectFields = this.getRelationsAndRelationsSelectFields({
|
||||||
this.getRelationsAndRelationsSelectFields({
|
objectMetadataMaps,
|
||||||
objectMetadataMaps,
|
objectMetadataMapItem,
|
||||||
objectMetadataMapItem,
|
objectsPermissions,
|
||||||
objectsPermissions,
|
depth,
|
||||||
depth,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const selectableFields = getAllSelectableFields({
|
const selectableFields = getAllSelectableFields({
|
||||||
restrictedFields,
|
restrictedFields,
|
||||||
@@ -43,12 +46,8 @@ export class RestToCommonSelectedFieldsHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
select: {
|
...selectableFields,
|
||||||
...selectableFields,
|
...relationsSelectFields,
|
||||||
...relationsSelectFields,
|
|
||||||
},
|
|
||||||
relations,
|
|
||||||
aggregate: {},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,20 +62,9 @@ export class RestToCommonSelectedFieldsHandler {
|
|||||||
objectsPermissions: ObjectsPermissions;
|
objectsPermissions: ObjectsPermissions;
|
||||||
depth: Depth | undefined;
|
depth: Depth | undefined;
|
||||||
}) {
|
}) {
|
||||||
if (!isDefined(depth) || depth === 0) {
|
if (!isDefined(depth) || depth === 0) return {};
|
||||||
return {
|
|
||||||
relations: {},
|
|
||||||
relationsSelectFields: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let relations: { [key: string]: boolean | { [key: string]: boolean } } = {};
|
let relationsSelectFields: SelectFields = {};
|
||||||
|
|
||||||
let relationsSelectFields: {
|
|
||||||
[key: string]:
|
|
||||||
| boolean
|
|
||||||
| { [key: string]: boolean | { [key: string]: boolean } };
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
for (const field of Object.values(objectMetadataMapItem.fieldsById)) {
|
for (const field of Object.values(objectMetadataMapItem.fieldsById)) {
|
||||||
if (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION))
|
if (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION))
|
||||||
@@ -104,35 +92,23 @@ export class RestToCommonSelectedFieldsHandler {
|
|||||||
depth === MAX_DEPTH &&
|
depth === MAX_DEPTH &&
|
||||||
isDefined(field.relationTargetObjectMetadataId)
|
isDefined(field.relationTargetObjectMetadataId)
|
||||||
) {
|
) {
|
||||||
const {
|
const depth2RelationsSelectFields =
|
||||||
relations: depth2Relations,
|
this.getRelationsAndRelationsSelectFields({
|
||||||
relationsSelectFields: depth2RelationsSelectFields,
|
objectMetadataMaps,
|
||||||
} = this.getRelationsAndRelationsSelectFields({
|
objectMetadataMapItem: relationTargetObjectMetadata,
|
||||||
objectMetadataMaps,
|
objectsPermissions,
|
||||||
objectMetadataMapItem: relationTargetObjectMetadata,
|
depth: 1,
|
||||||
objectsPermissions,
|
});
|
||||||
depth: 1,
|
|
||||||
}) as {
|
|
||||||
relations: { [key: string]: boolean };
|
|
||||||
relationsSelectFields: {
|
|
||||||
[key: string]: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
relations[field.name] = depth2Relations as {
|
|
||||||
[key: string]: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
relationsSelectFields[field.name] = {
|
relationsSelectFields[field.name] = {
|
||||||
...relationFieldSelectFields,
|
...relationFieldSelectFields,
|
||||||
...depth2RelationsSelectFields,
|
...depth2RelationsSelectFields,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
relations[field.name] = true;
|
|
||||||
relationsSelectFields[field.name] = relationFieldSelectFields;
|
relationsSelectFields[field.name] = relationFieldSelectFields;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { relations, relationsSelectFields };
|
return relationsSelectFields;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-
|
|||||||
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
|
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
|
||||||
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
||||||
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
||||||
|
import { RestApiGroupByHandler } from 'src/engine/api/rest/core/handlers/rest-api-group-by.handler';
|
||||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||||
@@ -24,6 +25,7 @@ export class RestApiCoreService {
|
|||||||
private readonly restApiFindOneHandler: RestApiFindOneHandler,
|
private readonly restApiFindOneHandler: RestApiFindOneHandler,
|
||||||
private readonly restApiFindManyHandler: RestApiFindManyHandler,
|
private readonly restApiFindManyHandler: RestApiFindManyHandler,
|
||||||
private readonly restApiFindDuplicatesHandler: RestApiFindDuplicatesHandler,
|
private readonly restApiFindDuplicatesHandler: RestApiFindDuplicatesHandler,
|
||||||
|
private readonly restApiGroupByHandler: RestApiGroupByHandler,
|
||||||
private readonly featureFlagService: FeatureFlagService,
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -84,4 +86,16 @@ export class RestApiCoreService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async groupBy(request: AuthenticatedRequest) {
|
||||||
|
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||||
|
|
||||||
|
if (isCommonApiEnabled) {
|
||||||
|
return await this.restApiGroupByHandler.handle(request);
|
||||||
|
} else {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Activate feature flag to use GroupBy in the REST API',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
import { parseAggregateFieldsRestRequest } from 'src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util';
|
||||||
|
import {
|
||||||
|
RestInputRequestParserException,
|
||||||
|
RestInputRequestParserExceptionCode,
|
||||||
|
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
|
|
||||||
|
describe('parseAggregateFieldsRestRequest', () => {
|
||||||
|
it('should parse single aggregate field', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { aggregate: '["countNotEmptyId"]' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseAggregateFieldsRestRequest(request)).toEqual({
|
||||||
|
countNotEmptyId: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse multiple aggregate fields', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: {
|
||||||
|
aggregate: '["countNotEmptyId", "countEmptyId"]',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseAggregateFieldsRestRequest(request)).toEqual({
|
||||||
|
countNotEmptyId: true,
|
||||||
|
countEmptyId: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse empty array', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { aggregate: '[]' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseAggregateFieldsRestRequest(request)).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if aggregate parameter is not a string', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { aggregate: ['countNotEmptyId'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseAggregateFieldsRestRequest(request)).toThrow(
|
||||||
|
new RestInputRequestParserException(
|
||||||
|
'Invalid aggregate query parameter - should be a valid array of string - ex: ["countNotEmptyId", "countEmptyField"]',
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if aggregate parameter is not valid JSON', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { aggregate: 'not-valid-json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseAggregateFieldsRestRequest(request)).toThrow(
|
||||||
|
new RestInputRequestParserException(
|
||||||
|
'Invalid aggregate query parameter - should be a valid array of string - ex: ["countNotEmptyId", "countEmptyField"]',
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if aggregate parameter is undefined', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseAggregateFieldsRestRequest(request)).toThrow(
|
||||||
|
new RestInputRequestParserException(
|
||||||
|
'Invalid aggregate query parameter - should be a valid array of string - ex: ["countNotEmptyId", "countEmptyField"]',
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
import { type CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
||||||
|
import {
|
||||||
|
RestInputRequestParserException,
|
||||||
|
RestInputRequestParserExceptionCode,
|
||||||
|
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
|
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||||
|
|
||||||
|
export const parseAggregateFieldsRestRequest = (
|
||||||
|
request: AuthenticatedRequest,
|
||||||
|
): CommonSelectedFields => {
|
||||||
|
const aggregateFieldsQuery = request.query.aggregate;
|
||||||
|
|
||||||
|
if (typeof aggregateFieldsQuery !== 'string') {
|
||||||
|
throw new RestInputRequestParserException(
|
||||||
|
`Invalid aggregate query parameter - should be a valid array of string - ex: ["countNotEmptyId", "countEmptyField"]`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const aggregateFields = JSON.parse(aggregateFieldsQuery);
|
||||||
|
|
||||||
|
return aggregateFields.reduce(
|
||||||
|
(acc: CommonSelectedFields, field: string) => {
|
||||||
|
acc[field] = true;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
throw new RestInputRequestParserException(
|
||||||
|
`Invalid aggregate query parameter - should be a valid array of string - ex: ["countNotEmptyId", "countEmptyField"]`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||||
|
import { RestInputRequestParserException } from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
|
|
||||||
|
describe('parseDepthRestRequest', () => {
|
||||||
|
it('should return 0 when depth parameter is not provided', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseDepthRestRequest(request)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse depth=0', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { depth: '0' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseDepthRestRequest(request)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if depth is not a number', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { depth: 'invalid' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseDepthRestRequest(request)).toThrow(
|
||||||
|
RestInputRequestParserException,
|
||||||
|
);
|
||||||
|
expect(() => parseDepthRestRequest(request)).toThrow(
|
||||||
|
"'depth=invalid' parameter invalid. Allowed values are 0, 1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if depth is not in allowed values (2)', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { depth: '2' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseDepthRestRequest(request)).toThrow(
|
||||||
|
RestInputRequestParserException,
|
||||||
|
);
|
||||||
|
expect(() => parseDepthRestRequest(request)).toThrow(
|
||||||
|
"'depth=2' parameter invalid. Allowed values are 0, 1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+6
-3
@@ -1,5 +1,7 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import {
|
||||||
|
RestInputRequestParserException,
|
||||||
|
RestInputRequestParserExceptionCode,
|
||||||
|
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
import { type Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type';
|
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';
|
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||||
|
|
||||||
@@ -13,12 +15,13 @@ export const parseDepthRestRequest = (request: AuthenticatedRequest): Depth => {
|
|||||||
const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1];
|
const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1];
|
||||||
|
|
||||||
if (isNaN(depth) || !ALLOWED_DEPTH_VALUES.includes(depth)) {
|
if (isNaN(depth) || !ALLOWED_DEPTH_VALUES.includes(depth)) {
|
||||||
throw new BadRequestException(
|
throw new RestInputRequestParserException(
|
||||||
`'depth=${
|
`'depth=${
|
||||||
request.query.depth
|
request.query.depth
|
||||||
}' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join(
|
}' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join(
|
||||||
', ',
|
', ',
|
||||||
)}`,
|
)}`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_DEPTH_QUERY_PARAM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -1,4 +1,7 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import {
|
||||||
|
RestInputRequestParserException,
|
||||||
|
RestInputRequestParserExceptionCode,
|
||||||
|
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
|
|
||||||
export const checkFilterQuery = (filterQuery: string): void => {
|
export const checkFilterQuery = (filterQuery: string): void => {
|
||||||
const countOpenedBrackets = (filterQuery.match(/\(/g) || []).length;
|
const countOpenedBrackets = (filterQuery.match(/\(/g) || []).length;
|
||||||
@@ -13,8 +16,9 @@ export const checkFilterQuery = (filterQuery: string): void => {
|
|||||||
Math.abs(diff) > 1 ? 's are' : ' is'
|
Math.abs(diff) > 1 ? 's are' : ' is'
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
throw new BadRequestException(
|
throw new RestInputRequestParserException(
|
||||||
`'filter' invalid. ${hint} missing in the query`,
|
`'filter' invalid. ${hint} missing in the query`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_FILTER_QUERY_PARAM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -5,6 +5,10 @@ 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 { 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 { 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 { parseFilterContent } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util';
|
||||||
|
import {
|
||||||
|
RestInputRequestParserException,
|
||||||
|
RestInputRequestParserExceptionCode,
|
||||||
|
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
|
|
||||||
//TODO : Refacto-common - Rename after deleting parseFilter
|
//TODO : Refacto-common - Rename after deleting parseFilter
|
||||||
export const parseFilterWithoutMetadataValidation = (
|
export const parseFilterWithoutMetadataValidation = (
|
||||||
@@ -29,8 +33,9 @@ export const parseFilterWithoutMetadataValidation = (
|
|||||||
|
|
||||||
if (conjunction === Conjunctions.not) {
|
if (conjunction === Conjunctions.not) {
|
||||||
if (subResult.length > 1) {
|
if (subResult.length > 1) {
|
||||||
throw new BadRequestException(
|
throw new RestInputRequestParserException(
|
||||||
`'filter' invalid. 'not' conjunction should contain only 1 condition. eg: not(field[eq]:1)`,
|
`'filter' invalid. 'not' conjunction should contain only 1 condition. eg: not(field[eq]:1)`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_FILTER_QUERY_PARAM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// @ts-expect-error legacy noImplicitAny
|
// @ts-expect-error legacy noImplicitAny
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
import { parseGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/group-by-parser-utils/parse-group-by-rest-request.util';
|
||||||
|
import { RestInputRequestParserException } from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
|
|
||||||
|
describe('parseGroupByRestRequest', () => {
|
||||||
|
it('should parse mixed field types', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: {
|
||||||
|
group_by:
|
||||||
|
'[{"firstField": true}, {"fieldCurrency": {"amountMicros": true}}, {"createdAt": {"granularity": "WEEK"}}]',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseGroupByRestRequest(request)).toEqual([
|
||||||
|
{ firstField: true },
|
||||||
|
{ fieldCurrency: { amountMicros: true } },
|
||||||
|
{ createdAt: { granularity: 'WEEK' } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse empty array', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { group_by: '[]' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseGroupByRestRequest(request)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if group_by parameter is not a string', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { group_by: [{ firstField: true }] },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseGroupByRestRequest(request)).toThrow(
|
||||||
|
RestInputRequestParserException,
|
||||||
|
);
|
||||||
|
expect(() => parseGroupByRestRequest(request)).toThrow(
|
||||||
|
`Invalid group_by query parameter - should be a valid array of objects - ex: [{"firstField": true}, {"secondField": {"subField": true}}, {"dateField": {"granularity": 'DAY'}}]`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if group_by parameter is not valid JSON', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { group_by: 'not-valid-json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseGroupByRestRequest(request)).toThrow(
|
||||||
|
RestInputRequestParserException,
|
||||||
|
);
|
||||||
|
expect(() => parseGroupByRestRequest(request)).toThrow(
|
||||||
|
`Invalid group_by query parameter - should be a valid array of objects - ex: [{"firstField": true}, {"secondField": {"subField": true}}, {"dateField": {"granularity": 'DAY'}}]`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if group_by parameter is undefined', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseGroupByRestRequest(request)).toThrow(
|
||||||
|
RestInputRequestParserException,
|
||||||
|
);
|
||||||
|
expect(() => parseGroupByRestRequest(request)).toThrow(
|
||||||
|
`Invalid group_by query parameter - should be a valid array of objects - ex: [{"firstField": true}, {"secondField": {"subField": true}}, {"dateField": {"granularity": 'DAY'}}]`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { type ObjectRecordGroupBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
RestInputRequestParserException,
|
||||||
|
RestInputRequestParserExceptionCode,
|
||||||
|
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
|
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||||
|
|
||||||
|
export const parseGroupByRestRequest = (
|
||||||
|
request: AuthenticatedRequest,
|
||||||
|
): ObjectRecordGroupBy => {
|
||||||
|
const groupByQuery = request.query.group_by;
|
||||||
|
|
||||||
|
if (typeof groupByQuery !== 'string') {
|
||||||
|
throw new RestInputRequestParserException(
|
||||||
|
`Invalid group_by query parameter - should be a valid array of objects - ex: [{"firstField": true}, {"secondField": {"subField": true}}, {"dateField": {"granularity": 'DAY'}}]`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_GROUP_BY_QUERY_PARAM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(groupByQuery);
|
||||||
|
} catch {
|
||||||
|
throw new RestInputRequestParserException(
|
||||||
|
`Invalid group_by query parameter - should be a valid array of objects - ex: [{"firstField": true}, {"secondField": {"subField": true}}, {"dateField": {"granularity": 'DAY'}}]`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_GROUP_BY_QUERY_PARAM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
+6
-3
@@ -1,10 +1,12 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
QUERY_DEFAULT_LIMIT_RECORDS,
|
QUERY_DEFAULT_LIMIT_RECORDS,
|
||||||
QUERY_MAX_RECORDS,
|
QUERY_MAX_RECORDS,
|
||||||
} from 'twenty-shared/constants';
|
} from 'twenty-shared/constants';
|
||||||
|
|
||||||
|
import {
|
||||||
|
RestInputRequestParserException,
|
||||||
|
RestInputRequestParserExceptionCode,
|
||||||
|
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||||
|
|
||||||
@@ -18,8 +20,9 @@ export const parseLimitRestRequest = (
|
|||||||
const limit = +request.query.limit;
|
const limit = +request.query.limit;
|
||||||
|
|
||||||
if (isNaN(limit) || limit < 0) {
|
if (isNaN(limit) || limit < 0) {
|
||||||
throw new BadRequestException(
|
throw new RestInputRequestParserException(
|
||||||
`limit '${request.query.limit}' is invalid. Should be an integer`,
|
`limit '${request.query.limit}' is invalid. Should be an integer`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_LIMIT_QUERY_PARAM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||||
|
|
||||||
|
export const parseOmitNullValuesRestRequest = (
|
||||||
|
request: AuthenticatedRequest,
|
||||||
|
): boolean => {
|
||||||
|
if (!isDefined(request.query.omit_null_values)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.query.omit_null_values === 'true';
|
||||||
|
};
|
||||||
+6
-3
@@ -1,13 +1,15 @@
|
|||||||
//TODO : Refacto-common - remove this comment - This parser is a copy of the OrderByInputFactory without objectMetadata dependency. Validation will be done in common layer
|
//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 { OrderByDirection } from 'twenty-shared/types';
|
import { OrderByDirection } from 'twenty-shared/types';
|
||||||
|
|
||||||
import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
import { type ObjectRecordOrderBy } 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 { 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 { addDefaultOrderById } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util';
|
||||||
|
import {
|
||||||
|
RestInputRequestParserException,
|
||||||
|
RestInputRequestParserExceptionCode,
|
||||||
|
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||||
|
|
||||||
export const parseOrderByRestRequest = (
|
export const parseOrderByRestRequest = (
|
||||||
@@ -33,12 +35,13 @@ export const parseOrderByRestRequest = (
|
|||||||
|
|
||||||
// fields -> [field_1] ; direction -> AscNullsFirst
|
// fields -> [field_1] ; direction -> AscNullsFirst
|
||||||
if (!(direction in OrderByDirection)) {
|
if (!(direction in OrderByDirection)) {
|
||||||
throw new BadRequestException(
|
throw new RestInputRequestParserException(
|
||||||
`'order_by' direction '${direction}' invalid. Allowed values are '${Object.values(
|
`'order_by' direction '${direction}' invalid. Allowed values are '${Object.values(
|
||||||
OrderByDirection,
|
OrderByDirection,
|
||||||
).join(
|
).join(
|
||||||
"', '",
|
"', '",
|
||||||
)}'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3`,
|
)}'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_ORDER_BY_QUERY_PARAM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { parseOrderByWithGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-with-group-by-rest-request.util';
|
||||||
|
|
||||||
|
describe('parseOrderByWithGroupByRestRequest', () => {
|
||||||
|
it('should parse mixed order by types', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: {
|
||||||
|
order_by:
|
||||||
|
'[{"field_1": "AscNullsFirst"}, {"fieldCurrency": {"amountMicros": "DescNullsLast"}}, {"aggregate": {"countNotEmptyId": "AscNullsFirst"}}, {"createdAt": {"orderBy": "DescNullsLast", "granularity": "WEEK"}}]',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseOrderByWithGroupByRestRequest(request)).toEqual([
|
||||||
|
{ field_1: 'AscNullsFirst' },
|
||||||
|
{ fieldCurrency: { amountMicros: 'DescNullsLast' } },
|
||||||
|
{ aggregate: { countNotEmptyId: 'AscNullsFirst' } },
|
||||||
|
{ createdAt: { orderBy: 'DescNullsLast', granularity: 'WEEK' } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse empty array', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { order_by: '[]' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseOrderByWithGroupByRestRequest(request)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if order_by parameter is undefined', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseOrderByWithGroupByRestRequest(request)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if order_by parameter is not valid JSON', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { order_by: 'not-valid-json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseOrderByWithGroupByRestRequest(request)).toThrow(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
expect(() => parseOrderByWithGroupByRestRequest(request)).toThrow(
|
||||||
|
`Invalid order_by query parameter - should be a valid array of objects - ex: [{"firstField": "AscNullsFirst"}, {"secondField": {"subField": "DescNullsLast"}}, {"aggregate": {"aggregateField": "DescNullsLast"}}, {dateField: {"orderBy": "AscNullsFirst", "granularity": "DAY"}}]`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { type OrderByWithGroupBy } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { RestInputRequestParserExceptionCode } from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
|
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||||
|
|
||||||
|
export const parseOrderByWithGroupByRestRequest = (
|
||||||
|
request: AuthenticatedRequest,
|
||||||
|
): OrderByWithGroupBy | undefined => {
|
||||||
|
const orderByWithGroupByQuery = request.query.order_by;
|
||||||
|
|
||||||
|
if (typeof orderByWithGroupByQuery !== 'string') return undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(orderByWithGroupByQuery);
|
||||||
|
} catch {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Invalid order_by query parameter - should be a valid array of objects - ex: [{"firstField": "AscNullsFirst"}, {"secondField": {"subField": "DescNullsLast"}}, {"aggregate": {"aggregateField": "DescNullsLast"}}, {dateField: {"orderBy": "AscNullsFirst", "granularity": "DAY"}}]`,
|
||||||
|
RestInputRequestParserExceptionCode.INVALID_ORDER_BY_WITH_GROUP_BY_QUERY_PARAM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import { CustomException } from 'src/utils/custom-exception';
|
||||||
|
|
||||||
|
export class RestInputRequestParserException extends CustomException<RestInputRequestParserExceptionCode> {}
|
||||||
|
|
||||||
|
export enum RestInputRequestParserExceptionCode {
|
||||||
|
INVALID_AGGREGATE_FIELDS_QUERY_PARAM = 'INVALID_AGGREGATE_FIELDS_QUERY_PARAM',
|
||||||
|
INVALID_GROUP_BY_QUERY_PARAM = 'INVALID_GROUP_BY_QUERY_PARAM',
|
||||||
|
INVALID_ORDER_BY_WITH_GROUP_BY_QUERY_PARAM = 'INVALID_ORDER_BY_WITH_GROUP_BY_QUERY_PARAM',
|
||||||
|
INVALID_ORDER_BY_QUERY_PARAM = 'INVALID_ORDER_BY_QUERY_PARAM',
|
||||||
|
INVALID_DEPTH_QUERY_PARAM = 'INVALID_DEPTH_QUERY_PARAM',
|
||||||
|
INVALID_LIMIT_QUERY_PARAM = 'INVALID_LIMIT_QUERY_PARAM',
|
||||||
|
INVALID_FILTER_QUERY_PARAM = 'INVALID_FILTER_QUERY_PARAM',
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
import { parseViewIdRestRequest } from 'src/engine/api/rest/input-request-parsers/view-id-parser-utils/parse-view-id-rest-request.util';
|
||||||
|
|
||||||
|
describe('parseViewIdRestRequest', () => {
|
||||||
|
it('should return undefined if viewId missing', () => {
|
||||||
|
const request: any = { query: {} };
|
||||||
|
|
||||||
|
expect(parseViewIdRestRequest(request)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return viewId when provided as string', () => {
|
||||||
|
const request: any = {
|
||||||
|
query: { viewId: '20202020-e29b-41d4-a716-446655440000' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parseViewIdRestRequest(request)).toEqual(
|
||||||
|
'20202020-e29b-41d4-a716-446655440000',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||||
|
|
||||||
|
export const parseViewIdRestRequest = (
|
||||||
|
request: AuthenticatedRequest,
|
||||||
|
): string | undefined => {
|
||||||
|
if (
|
||||||
|
!isDefined(request.query.viewId) ||
|
||||||
|
typeof request.query.viewId !== 'string'
|
||||||
|
)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
return request.query.viewId;
|
||||||
|
};
|
||||||
+5
@@ -1,7 +1,10 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
import { type QueryFailedError } from 'typeorm';
|
import { type QueryFailedError } from 'typeorm';
|
||||||
|
|
||||||
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
|
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
|
||||||
import { commonQueryRunnerToRestApiExceptionHandler } from 'src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util';
|
import { commonQueryRunnerToRestApiExceptionHandler } from 'src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util';
|
||||||
|
import { RestInputRequestParserException } from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||||
|
|
||||||
interface QueryFailedErrorWithCode extends QueryFailedError {
|
interface QueryFailedErrorWithCode extends QueryFailedError {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -13,6 +16,8 @@ export const workspaceQueryRunnerRestApiExceptionHandler = (
|
|||||||
switch (true) {
|
switch (true) {
|
||||||
case error instanceof CommonQueryRunnerException:
|
case error instanceof CommonQueryRunnerException:
|
||||||
return commonQueryRunnerToRestApiExceptionHandler(error);
|
return commonQueryRunnerToRestApiExceptionHandler(error);
|
||||||
|
case error instanceof RestInputRequestParserException:
|
||||||
|
throw new BadRequestException(error.message);
|
||||||
default:
|
default:
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
+66
@@ -105,6 +105,72 @@ describe('group-by resolvers (integration)', () => {
|
|||||||
expect(groupWithCityB.groupByDimensionValues).toEqual([cityB]);
|
expect(groupWithCityB.groupByDimensionValues).toEqual([cityB]);
|
||||||
expect(groupWithCityB.totalCount).toEqual(2);
|
expect(groupWithCityB.totalCount).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('computes aggregated metrics on date time field', async () => {
|
||||||
|
const cityA = 'City A';
|
||||||
|
const cityB = 'City B';
|
||||||
|
|
||||||
|
const person1 = (
|
||||||
|
await makeGraphqlAPIRequest(
|
||||||
|
createOneOperationFactory({
|
||||||
|
objectMetadataSingularName: 'person',
|
||||||
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
|
data: { id: testPersonId, city: cityA },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).body.data.createPerson;
|
||||||
|
|
||||||
|
const person2 = (
|
||||||
|
await makeGraphqlAPIRequest(
|
||||||
|
createOneOperationFactory({
|
||||||
|
objectMetadataSingularName: 'person',
|
||||||
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
|
data: { id: testPerson2Id, city: cityB },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).body.data.createPerson;
|
||||||
|
|
||||||
|
await makeGraphqlAPIRequest(
|
||||||
|
createOneOperationFactory({
|
||||||
|
objectMetadataSingularName: 'person',
|
||||||
|
gqlFields: PERSON_GQL_FIELDS,
|
||||||
|
data: { id: testPerson3Id, city: cityB },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await makeGraphqlAPIRequest(
|
||||||
|
groupByOperationFactory({
|
||||||
|
objectMetadataSingularName: 'person',
|
||||||
|
objectMetadataPluralName: 'people',
|
||||||
|
groupBy: [{ city: true }],
|
||||||
|
gqlFields: 'minCreatedAt',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const groups = response.body.data.peopleGroupBy;
|
||||||
|
|
||||||
|
expect(groups).toBeDefined();
|
||||||
|
expect(groups).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ groupByDimensionValues: [cityA] }),
|
||||||
|
expect.objectContaining({ groupByDimensionValues: [cityB] }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupWithCityA = groups.find(
|
||||||
|
(group: any) => group.groupByDimensionValues[0] === cityA,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(groupWithCityA.groupByDimensionValues).toEqual([cityA]);
|
||||||
|
expect(groupWithCityA.minCreatedAt).toEqual(person1.createdAt);
|
||||||
|
|
||||||
|
const groupWithCityB = groups.find(
|
||||||
|
(group: any) => group.groupByDimensionValues[0] === cityB,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(groupWithCityB.groupByDimensionValues).toEqual([cityB]);
|
||||||
|
expect(groupWithCityB.minCreatedAt).toEqual(person2.createdAt);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('date range', () => {
|
describe('date range', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user