Show relation record labels instead of UUIDs in dashboard charts (#23163)
https://github.com/user-attachments/assets/d012a013-2c90-49a1-a27e-b8e4b684a84f Charts grouped by a relation without a sub-field rendered raw FK UUIDs on axis ticks, legends and tooltips. The server now batch-resolves the grouped record ids to their label identifier through a permission-scoped query and formats every bucket with the record's display name. Unresolvable records (deleted or not readable) render as Unknown and their ids are stripped from the response payload. Same-named records get an ordinal suffix so their buckets don't merge. Covers bar, line and pie, plain and morph relations. ```mermaid flowchart TD A["Dashboard widget load"] --> B["Chart data service<br/>(bar / line / pie)"] B --> C["executeGroupByQuery:<br/>group by relation FK id,<br/>ORDER BY target label identifier,<br/>scoped to source object permissions"] C --> D["filterOutEmptyChartBuckets"] D --> E{"Bare relation axis?<br/>(no sub-field)"} subgraph RL["ChartRelationLabelService.resolveRelationLabels"] direction TB G1["Collect distinct record ids<br/>per target object"] --> G2["Batch SELECT label identifier columns,<br/>scoped to TARGET object permissions"] G2 --> G3["buildRawLabelByRecordId:<br/>display name per record"] G3 --> G4["buildUniqueRelationLabels:<br/>suffix duplicates, Unknown for unresolved"] end E -- No --> H["formatDimensionValue per bucket"] E -- Yes --> G1 G4 --> H H --> I["Strip unresolved ids from<br/>formattedToRawLookup"] I --> J["Chart DTO to frontend"] ``` The chart settings sub-field dropdown gains a Record option to group by the related record itself, and now only offers sub-fields the backend accepts (system fields like a workspace member's updatedBy were selectable but rejected at query time). Chart-data errors are now logged server-side. Also fixes two latent bugs on this path: sorting a bare-relation chart by field threw `Cannot orderBy unknown field: agentId`, and the pie chart truncated slices before sorting. The AI dashboard tool guidance and the seeded dashboards no longer force the sub-field workaround. The group-by query orders buckets by the related record's label identifier at the database level (the engine now accepts ordering by a target field when grouping by its id), so with more than 100 distinct related records the surviving buckets match the label order.
This commit is contained in:
+273
@@ -2,11 +2,13 @@ import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
OrderByDirection,
|
||||
RelationType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
type GroupByDateField,
|
||||
type GroupByRegularField,
|
||||
type GroupByRelationField,
|
||||
} from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
|
||||
import { getObjectAlias } from 'src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util';
|
||||
import { GraphqlQueryOrderGroupByParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order-group-by.parser';
|
||||
@@ -141,3 +143,274 @@ describe('GraphqlQueryOrderGroupByParser - object alias in order-by clauses', ()
|
||||
expect(orderByKey).not.toContain(physicalTableName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GraphqlQueryOrderGroupByParser - relation order-by under target primary key group-by', () => {
|
||||
const SOURCE_OBJECT_ID = 'person-object-id';
|
||||
const SOURCE_OBJECT_UNIVERSAL_ID = 'person-object-universal-id';
|
||||
const COMPANY_RELATION_FIELD_ID = 'person-company-field-id';
|
||||
const COMPANY_RELATION_FIELD_UNIVERSAL_ID =
|
||||
'person-company-field-universal-id';
|
||||
const MEMBER_RELATION_FIELD_ID = 'person-member-field-id';
|
||||
const MEMBER_RELATION_FIELD_UNIVERSAL_ID = 'person-member-field-universal-id';
|
||||
const COMPANY_OBJECT_ID = 'company-object-id';
|
||||
const COMPANY_OBJECT_UNIVERSAL_ID = 'company-object-universal-id';
|
||||
const COMPANY_ID_FIELD_ID = 'company-id-field-id';
|
||||
const COMPANY_ID_FIELD_UNIVERSAL_ID = 'company-id-field-universal-id';
|
||||
const COMPANY_NAME_FIELD_ID = 'company-name-field-id';
|
||||
const COMPANY_NAME_FIELD_UNIVERSAL_ID = 'company-name-field-universal-id';
|
||||
const COMPANY_OWNER_FIELD_ID = 'company-owner-field-id';
|
||||
const COMPANY_OWNER_FIELD_UNIVERSAL_ID = 'company-owner-field-universal-id';
|
||||
const MEMBER_OBJECT_ID = 'member-object-id';
|
||||
const MEMBER_OBJECT_UNIVERSAL_ID = 'member-object-universal-id';
|
||||
const MEMBER_ID_FIELD_ID = 'member-id-field-id';
|
||||
const MEMBER_ID_FIELD_UNIVERSAL_ID = 'member-id-field-universal-id';
|
||||
const MEMBER_NAME_FIELD_ID = 'member-name-field-id';
|
||||
const MEMBER_NAME_FIELD_UNIVERSAL_ID = 'member-name-field-universal-id';
|
||||
|
||||
const companyRelationField = getFlatFieldMetadataMock({
|
||||
universalIdentifier: COMPANY_RELATION_FIELD_UNIVERSAL_ID,
|
||||
objectMetadataId: SOURCE_OBJECT_ID,
|
||||
type: FieldMetadataType.RELATION,
|
||||
id: COMPANY_RELATION_FIELD_ID,
|
||||
name: 'company',
|
||||
relationTargetObjectMetadataId: COMPANY_OBJECT_ID,
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'companyId',
|
||||
},
|
||||
});
|
||||
|
||||
const memberRelationField = getFlatFieldMetadataMock({
|
||||
universalIdentifier: MEMBER_RELATION_FIELD_UNIVERSAL_ID,
|
||||
objectMetadataId: SOURCE_OBJECT_ID,
|
||||
type: FieldMetadataType.RELATION,
|
||||
id: MEMBER_RELATION_FIELD_ID,
|
||||
name: 'member',
|
||||
relationTargetObjectMetadataId: MEMBER_OBJECT_ID,
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'memberId',
|
||||
},
|
||||
});
|
||||
|
||||
const companyIdField = getFlatFieldMetadataMock({
|
||||
universalIdentifier: COMPANY_ID_FIELD_UNIVERSAL_ID,
|
||||
objectMetadataId: COMPANY_OBJECT_ID,
|
||||
type: FieldMetadataType.UUID,
|
||||
id: COMPANY_ID_FIELD_ID,
|
||||
name: 'id',
|
||||
});
|
||||
|
||||
const companyNameField = getFlatFieldMetadataMock({
|
||||
universalIdentifier: COMPANY_NAME_FIELD_UNIVERSAL_ID,
|
||||
objectMetadataId: COMPANY_OBJECT_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
id: COMPANY_NAME_FIELD_ID,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
const companyOwnerField = getFlatFieldMetadataMock({
|
||||
universalIdentifier: COMPANY_OWNER_FIELD_UNIVERSAL_ID,
|
||||
objectMetadataId: COMPANY_OBJECT_ID,
|
||||
type: FieldMetadataType.RELATION,
|
||||
id: COMPANY_OWNER_FIELD_ID,
|
||||
name: 'owner',
|
||||
relationTargetObjectMetadataId: MEMBER_OBJECT_ID,
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'ownerId',
|
||||
},
|
||||
});
|
||||
|
||||
const memberIdField = getFlatFieldMetadataMock({
|
||||
universalIdentifier: MEMBER_ID_FIELD_UNIVERSAL_ID,
|
||||
objectMetadataId: MEMBER_OBJECT_ID,
|
||||
type: FieldMetadataType.UUID,
|
||||
id: MEMBER_ID_FIELD_ID,
|
||||
name: 'id',
|
||||
});
|
||||
|
||||
const memberNameField = getFlatFieldMetadataMock({
|
||||
universalIdentifier: MEMBER_NAME_FIELD_UNIVERSAL_ID,
|
||||
objectMetadataId: MEMBER_OBJECT_ID,
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
id: MEMBER_NAME_FIELD_ID,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
const sourceObjectMetadata = getFlatObjectMetadataMock({
|
||||
universalIdentifier: SOURCE_OBJECT_UNIVERSAL_ID,
|
||||
id: SOURCE_OBJECT_ID,
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
fieldIds: [COMPANY_RELATION_FIELD_ID, MEMBER_RELATION_FIELD_ID],
|
||||
});
|
||||
|
||||
const companyObjectMetadata = getFlatObjectMetadataMock({
|
||||
universalIdentifier: COMPANY_OBJECT_UNIVERSAL_ID,
|
||||
id: COMPANY_OBJECT_ID,
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
fieldIds: [
|
||||
COMPANY_ID_FIELD_ID,
|
||||
COMPANY_NAME_FIELD_ID,
|
||||
COMPANY_OWNER_FIELD_ID,
|
||||
],
|
||||
});
|
||||
|
||||
const memberObjectMetadata = getFlatObjectMetadataMock({
|
||||
universalIdentifier: MEMBER_OBJECT_UNIVERSAL_ID,
|
||||
id: MEMBER_OBJECT_ID,
|
||||
nameSingular: 'member',
|
||||
namePlural: 'members',
|
||||
fieldIds: [MEMBER_ID_FIELD_ID, MEMBER_NAME_FIELD_ID],
|
||||
});
|
||||
|
||||
const flatFieldMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
[COMPANY_RELATION_FIELD_UNIVERSAL_ID]: companyRelationField,
|
||||
[MEMBER_RELATION_FIELD_UNIVERSAL_ID]: memberRelationField,
|
||||
[COMPANY_ID_FIELD_UNIVERSAL_ID]: companyIdField,
|
||||
[COMPANY_NAME_FIELD_UNIVERSAL_ID]: companyNameField,
|
||||
[COMPANY_OWNER_FIELD_UNIVERSAL_ID]: companyOwnerField,
|
||||
[MEMBER_ID_FIELD_UNIVERSAL_ID]: memberIdField,
|
||||
[MEMBER_NAME_FIELD_UNIVERSAL_ID]: memberNameField,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[COMPANY_RELATION_FIELD_ID]: COMPANY_RELATION_FIELD_UNIVERSAL_ID,
|
||||
[MEMBER_RELATION_FIELD_ID]: MEMBER_RELATION_FIELD_UNIVERSAL_ID,
|
||||
[COMPANY_ID_FIELD_ID]: COMPANY_ID_FIELD_UNIVERSAL_ID,
|
||||
[COMPANY_NAME_FIELD_ID]: COMPANY_NAME_FIELD_UNIVERSAL_ID,
|
||||
[COMPANY_OWNER_FIELD_ID]: COMPANY_OWNER_FIELD_UNIVERSAL_ID,
|
||||
[MEMBER_ID_FIELD_ID]: MEMBER_ID_FIELD_UNIVERSAL_ID,
|
||||
[MEMBER_NAME_FIELD_ID]: MEMBER_NAME_FIELD_UNIVERSAL_ID,
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as unknown as FlatEntityMaps<FlatFieldMetadata>;
|
||||
|
||||
const flatObjectMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
[SOURCE_OBJECT_UNIVERSAL_ID]: sourceObjectMetadata,
|
||||
[COMPANY_OBJECT_UNIVERSAL_ID]: companyObjectMetadata,
|
||||
[MEMBER_OBJECT_UNIVERSAL_ID]: memberObjectMetadata,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[SOURCE_OBJECT_ID]: SOURCE_OBJECT_UNIVERSAL_ID,
|
||||
[COMPANY_OBJECT_ID]: COMPANY_OBJECT_UNIVERSAL_ID,
|
||||
[MEMBER_OBJECT_ID]: MEMBER_OBJECT_UNIVERSAL_ID,
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as unknown as FlatEntityMaps<FlatObjectMetadata>;
|
||||
|
||||
const groupByCompanyId: GroupByRelationField = {
|
||||
fieldMetadata: companyRelationField,
|
||||
nestedFieldMetadata: companyIdField,
|
||||
};
|
||||
|
||||
const groupByMemberId: GroupByRelationField = {
|
||||
fieldMetadata: memberRelationField,
|
||||
nestedFieldMetadata: memberIdField,
|
||||
};
|
||||
|
||||
const buildParser = () =>
|
||||
new GraphqlQueryOrderGroupByParser(
|
||||
sourceObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
it('orders by a target scalar field when grouping by the target id', () => {
|
||||
const parser = buildParser();
|
||||
|
||||
const result = parser.parse({
|
||||
orderBy: [{ company: { name: OrderByDirection.AscNullsLast } }],
|
||||
groupByFields: [groupByCompanyId],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ '"company"."name"': { order: 'ASC', nulls: 'NULLS LAST' } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('orders by FULL_NAME subfields when grouping by the target id', () => {
|
||||
const parser = buildParser();
|
||||
|
||||
const result = parser.parse({
|
||||
orderBy: [
|
||||
{ member: { name: { firstName: OrderByDirection.AscNullsLast } } },
|
||||
{ member: { name: { lastName: OrderByDirection.AscNullsLast } } },
|
||||
],
|
||||
groupByFields: [groupByMemberId],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ '"member"."nameFirstName"': { order: 'ASC', nulls: 'NULLS LAST' } },
|
||||
{ '"member"."nameLastName"': { order: 'ASC', nulls: 'NULLS LAST' } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('still orders by the exact nested field present in groupBy', () => {
|
||||
const parser = buildParser();
|
||||
|
||||
const groupByCompanyName: GroupByRelationField = {
|
||||
fieldMetadata: companyRelationField,
|
||||
nestedFieldMetadata: companyNameField,
|
||||
};
|
||||
|
||||
const result = parser.parse({
|
||||
orderBy: [{ company: { name: OrderByDirection.DescNullsLast } }],
|
||||
groupByFields: [groupByCompanyName],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ '"company"."name"': { order: 'DESC', nulls: 'NULLS LAST' } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws when ordering by a relation absent from groupBy', () => {
|
||||
const parser = buildParser();
|
||||
|
||||
expect(() =>
|
||||
parser.parse({
|
||||
orderBy: [
|
||||
{ member: { name: { firstName: OrderByDirection.AscNullsLast } } },
|
||||
],
|
||||
groupByFields: [groupByCompanyId],
|
||||
}),
|
||||
).toThrow(
|
||||
'Cannot order by a relation field that is not in groupBy criteria: member.name',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when ordering by an unknown composite subfield under target id group-by', () => {
|
||||
const parser = buildParser();
|
||||
|
||||
expect(() =>
|
||||
parser.parse({
|
||||
orderBy: [
|
||||
{
|
||||
member: {
|
||||
name: { unknownSubField: OrderByDirection.AscNullsLast },
|
||||
},
|
||||
},
|
||||
],
|
||||
groupByFields: [groupByMemberId],
|
||||
}),
|
||||
).toThrow(
|
||||
'Composite subfield "unknownSubField" is not orderable for "name"',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when ordering by a nested relation field under target id group-by', () => {
|
||||
const parser = buildParser();
|
||||
|
||||
expect(() =>
|
||||
parser.parse({
|
||||
orderBy: [{ company: { owner: OrderByDirection.AscNullsLast } }],
|
||||
groupByFields: [groupByCompanyId],
|
||||
}),
|
||||
).toThrow(
|
||||
'Cannot order by a relation field that is not in groupBy criteria: company.owner',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+13
-1
@@ -29,6 +29,7 @@ import {
|
||||
getAvailableAggregationsFromObjectFields,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { getGroupableSubFieldsForCompositeType } from 'src/engine/metadata-modules/field-metadata/utils/get-groupable-sub-fields-for-composite-type.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
@@ -524,6 +525,7 @@ export class GraphqlQueryOrderGroupByParser {
|
||||
associatedGroupByField,
|
||||
nestedFieldMetadata,
|
||||
nestedFieldOrderByValue,
|
||||
isMatchedOnTargetPrimaryKeyGroupBy,
|
||||
} = prepareForOrderByRelationFieldParsing({
|
||||
orderByArg,
|
||||
fieldMetadata,
|
||||
@@ -565,7 +567,17 @@ export class GraphqlQueryOrderGroupByParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
if (isMatchedOnTargetPrimaryKeyGroupBy === true) {
|
||||
const groupableSubFields = getGroupableSubFieldsForCompositeType(
|
||||
nestedFieldMetadata.type,
|
||||
);
|
||||
|
||||
if (!groupableSubFields?.includes(nestedSubFieldName)) {
|
||||
throw new UserInputError(
|
||||
`Composite subfield "${nestedSubFieldName}" is not orderable for "${nestedFieldMetadata.name}"`,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
!isDefined(associatedGroupByField.nestedSubFieldName) ||
|
||||
associatedGroupByField.nestedSubFieldName !== nestedSubFieldName
|
||||
) {
|
||||
|
||||
+20
-1
@@ -9,6 +9,7 @@ import {
|
||||
import { isGroupByRelationField } from 'src/engine/api/common/common-query-runners/utils/is-group-by-relation-field.util';
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
@@ -106,7 +107,7 @@ export const prepareForOrderByRelationFieldParsing = ({
|
||||
}
|
||||
}
|
||||
|
||||
const associatedGroupByField = groupByFields.find((groupByField) => {
|
||||
const exactGroupByFieldMatch = groupByFields.find((groupByField) => {
|
||||
if (!isGroupByRelationField(groupByField)) {
|
||||
return false;
|
||||
}
|
||||
@@ -126,6 +127,23 @@ export const prepareForOrderByRelationFieldParsing = ({
|
||||
return true;
|
||||
}) as GroupByRelationField | undefined;
|
||||
|
||||
const canOrderByFunctionallyDependentNestedField =
|
||||
!isMorphOrRelationFlatFieldMetadata(nestedFieldMetadata);
|
||||
|
||||
const groupByFieldOnTargetPrimaryKey =
|
||||
!isDefined(exactGroupByFieldMatch) &&
|
||||
canOrderByFunctionallyDependentNestedField
|
||||
? (groupByFields.find(
|
||||
(groupByField) =>
|
||||
isGroupByRelationField(groupByField) &&
|
||||
groupByField.fieldMetadata.id === fieldMetadata.id &&
|
||||
groupByField.nestedFieldMetadata.name === 'id',
|
||||
) as GroupByRelationField | undefined)
|
||||
: undefined;
|
||||
|
||||
const associatedGroupByField =
|
||||
exactGroupByFieldMatch ?? groupByFieldOnTargetPrimaryKey;
|
||||
|
||||
if (!isDefined(associatedGroupByField)) {
|
||||
throw new UserInputError(
|
||||
`Cannot order by a relation field that is not in groupBy criteria: ${relationFieldName}.${nestedFieldName}`,
|
||||
@@ -136,5 +154,6 @@ export const prepareForOrderByRelationFieldParsing = ({
|
||||
associatedGroupByField,
|
||||
nestedFieldMetadata,
|
||||
nestedFieldOrderByValue,
|
||||
isMatchedOnTargetPrimaryKeyGroupBy: !isDefined(exactGroupByFieldMatch),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -299,6 +299,14 @@ export const validateQueryIsPermittedOrThrow = ({
|
||||
});
|
||||
|
||||
expressionMapSelectsOnMainEntity = selectsWithoutJoinedAliases;
|
||||
|
||||
validateJoinedOrderByColumnsArePermittedOrThrow({
|
||||
expressionMap,
|
||||
objectsPermissions,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
});
|
||||
}
|
||||
|
||||
const allFieldsSelected = expressionMapSelectsOnMainEntity.some(
|
||||
@@ -411,6 +419,63 @@ const validatePermissionsForJoinsAndReturnSelectsWithoutJoins = ({
|
||||
return { selectsWithoutJoinedAliases };
|
||||
};
|
||||
|
||||
const validateJoinedOrderByColumnsArePermittedOrThrow = ({
|
||||
expressionMap,
|
||||
objectsPermissions,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
}: {
|
||||
expressionMap: QueryExpressionMap;
|
||||
objectsPermissions: ObjectsPermissions;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
objectIdByNameSingular: Record<string, string>;
|
||||
}) => {
|
||||
const columnsByJoinedAlias = new Map<string, Set<string>>();
|
||||
const columnReferenceRegex = /"(\w+)"\."(\w+)"/g;
|
||||
|
||||
for (const orderByExpression of Object.keys(expressionMap.orderBys)) {
|
||||
for (const [, alias, column] of orderByExpression.matchAll(
|
||||
columnReferenceRegex,
|
||||
)) {
|
||||
const columnsForAlias = columnsByJoinedAlias.get(alias) ?? new Set();
|
||||
|
||||
columnsForAlias.add(column);
|
||||
columnsByJoinedAlias.set(alias, columnsForAlias);
|
||||
}
|
||||
}
|
||||
|
||||
for (const joinAttribute of expressionMap.joinAttributes) {
|
||||
const joinedAlias = joinAttribute.alias.name;
|
||||
const referencedColumns = columnsByJoinedAlias.get(joinedAlias);
|
||||
|
||||
if (!isDefined(referencedColumns)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entity = expressionMap.aliases.find(
|
||||
(alias) => alias.type === 'join' && alias.name === joinedAlias,
|
||||
)?.metadata;
|
||||
|
||||
if (!isDefined(entity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
validateOperationIsPermittedOrThrow({
|
||||
entityName: entity.name,
|
||||
operationType: 'select',
|
||||
objectsPermissions,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
selectedColumns: [...referencedColumns],
|
||||
allFieldsSelected: false,
|
||||
updatedColumns: [],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const buildFieldPermissionDeniedMessage = ({
|
||||
action,
|
||||
column,
|
||||
|
||||
-4
@@ -490,11 +490,9 @@ const createOpportunitiesByOwner = ({
|
||||
aggregateFieldMetadataId: opportunityFields.id.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
primaryAxisGroupByFieldMetadataId: opportunityFields.owner.id,
|
||||
primaryAxisGroupBySubFieldName: 'name.firstName',
|
||||
primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.DAY,
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
secondaryAxisGroupByFieldMetadataId: opportunityFields.owner.id,
|
||||
secondaryAxisGroupBySubFieldName: 'name.firstName',
|
||||
secondaryAxisGroupByDateGranularity:
|
||||
ObjectRecordGroupByDateGranularity.DAY,
|
||||
secondaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
@@ -514,12 +512,10 @@ const createOpportunitiesByOwner = ({
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
primaryAxisGroupByFieldMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.owner.universalIdentifier,
|
||||
primaryAxisGroupBySubFieldName: 'name.firstName',
|
||||
primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.DAY,
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
secondaryAxisGroupByFieldMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.owner.universalIdentifier,
|
||||
secondaryAxisGroupBySubFieldName: 'name.firstName',
|
||||
secondaryAxisGroupByDateGranularity:
|
||||
ObjectRecordGroupByDateGranularity.DAY,
|
||||
secondaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
|
||||
Reference in New Issue
Block a user