Aggregated queries #1 (#8345)

First step of https://github.com/twentyhq/twenty/issues/6868

Adds min.., max.. queries for DATETIME fields
adds min.., max.., avg.., sum.. queries for NUMBER fields 

(count distinct operation and composite fields such as CURRENCY handling
will be dealt with in a future PR)

<img width="1422" alt="Capture d’écran 2024-11-06 à 15 48 46"
src="https://github.com/user-attachments/assets/4bcdece0-ad3e-4536-9720-fe4044a36719">

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Marie
2024-11-14 18:05:05 +01:00
committed by GitHub
parent c966533f26
commit a799370483
93 changed files with 1590 additions and 1178 deletions

View File

@@ -13,7 +13,6 @@ import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNaviga
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
const StyledMainSection = styled(NavigationDrawerSection)`
@@ -27,9 +26,7 @@ export const MainNavigationDrawerItems = () => {
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
);
const isWorkspaceFavoriteEnabled = useIsFeatureEnabled(
'IS_WORKSPACE_FAVORITE_ENABLED',
);
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useRecoilState(isNavigationDrawerExpandedState);
const setNavigationDrawerExpandedMemorized = useSetRecoilState(
@@ -58,18 +55,9 @@ export const MainNavigationDrawerItems = () => {
/>
</StyledMainSection>
)}
{isWorkspaceFavoriteEnabled && <NavigationDrawerOpenedSection />}
<NavigationDrawerOpenedSection />
<CurrentWorkspaceMemberFavorites />
{isWorkspaceFavoriteEnabled ? (
<WorkspaceFavorites />
) : (
<NavigationDrawerSectionForObjectMetadataItemsWrapper
isRemote={false}
/>
)}
<WorkspaceFavorites />
<NavigationDrawerSectionForObjectMetadataItemsWrapper isRemote={true} />
</>
);

View File

@@ -9,12 +9,11 @@ export type FeatureFlagKey =
| 'IS_FREE_ACCESS_ENABLED'
| 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED'
| 'IS_WORKFLOW_ENABLED'
| 'IS_WORKSPACE_FAVORITE_ENABLED'
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
| 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED'
| 'IS_ANALYTICS_V2_ENABLED'
| 'IS_SSO_ENABLED'
| 'IS_UNIQUE_INDEXES_ENABLED'
| 'IS_ARRAY_AND_JSON_FILTER_ENABLED'
| 'IS_MICROSOFT_SYNC_ENABLED'
| 'IS_ADVANCED_FILTERS_ENABLED';
| 'IS_ADVANCED_FILTERS_ENABLED'
| 'IS_AGGREGATE_QUERY_ENABLED';

View File

@@ -50,11 +50,6 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: false,
},
{
key: FeatureFlagKey.IsWorkspaceFavoriteEnabled,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IsAnalyticsV2Enabled,
workspaceId: workspaceId,
@@ -85,6 +80,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: false,
},
{
key: FeatureFlagKey.IsAggregateQueryEnabled,
workspaceId: workspaceId,
value: false,
},
])
.execute();
};

View File

@@ -5,27 +5,27 @@ import {
WhereExpressionBuilder,
} from 'typeorm';
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser';
export class GraphqlQueryFilterConditionParser {
private fieldMetadataMap: FieldMetadataMap;
private fieldMetadataMapByName: FieldMetadataMap;
private queryFilterFieldParser: GraphqlQueryFilterFieldParser;
constructor(fieldMetadataMap: FieldMetadataMap) {
this.fieldMetadataMap = fieldMetadataMap;
constructor(fieldMetadataMapByName: FieldMetadataMap) {
this.fieldMetadataMapByName = fieldMetadataMapByName;
this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser(
this.fieldMetadataMap,
this.fieldMetadataMapByName,
);
}
public parse(
queryBuilder: SelectQueryBuilder<any>,
objectNameSingular: string,
filter: Partial<RecordFilter>,
filter: Partial<ObjectRecordFilter>,
): SelectQueryBuilder<any> {
if (!filter || Object.keys(filter).length === 0) {
return queryBuilder;
@@ -50,7 +50,7 @@ export class GraphqlQueryFilterConditionParser {
switch (key) {
case 'and': {
const andWhereCondition = new Brackets((qb) => {
value.forEach((filter: RecordFilter, index: number) => {
value.forEach((filter: ObjectRecordFilter, index: number) => {
const whereCondition = new Brackets((qb2) => {
Object.entries(filter).forEach(
([subFilterkey, subFilterValue], index) => {
@@ -82,7 +82,7 @@ export class GraphqlQueryFilterConditionParser {
}
case 'or': {
const orWhereCondition = new Brackets((qb) => {
value.forEach((filter: RecordFilter, index: number) => {
value.forEach((filter: ObjectRecordFilter, index: number) => {
const whereCondition = new Brackets((qb2) => {
Object.entries(filter).forEach(
([subFilterkey, subFilterValue], index) => {

View File

@@ -9,17 +9,17 @@ import {
import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { capitalize } from 'src/utils/capitalize';
const ARRAY_OPERATORS = ['in', 'contains', 'not_contains'];
export class GraphqlQueryFilterFieldParser {
private fieldMetadataMap: FieldMetadataMap;
private fieldMetadataMapByName: FieldMetadataMap;
constructor(fieldMetadataMap: FieldMetadataMap) {
this.fieldMetadataMap = fieldMetadataMap;
constructor(fieldMetadataMapByName: FieldMetadataMap) {
this.fieldMetadataMapByName = fieldMetadataMapByName;
}
public parse(
@@ -29,7 +29,7 @@ export class GraphqlQueryFilterFieldParser {
filterValue: any,
isFirst = false,
): void {
const fieldMetadata = this.fieldMetadataMap[`${key}`];
const fieldMetadata = this.fieldMetadataMapByName[`${key}`];
if (!fieldMetadata) {
throw new Error(`Field metadata not found for field: ${key}`);

View File

@@ -1,7 +1,7 @@
import {
ObjectRecordOrderBy,
OrderByDirection,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import {
@@ -10,25 +10,25 @@ import {
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { capitalize } from 'src/utils/capitalize';
export class GraphqlQueryOrderFieldParser {
private fieldMetadataMap: FieldMetadataMap;
private fieldMetadataMapByName: FieldMetadataMap;
constructor(fieldMetadataMap: FieldMetadataMap) {
this.fieldMetadataMap = fieldMetadataMap;
constructor(fieldMetadataMapByName: FieldMetadataMap) {
this.fieldMetadataMapByName = fieldMetadataMapByName;
}
parse(
orderBy: RecordOrderBy,
orderBy: ObjectRecordOrderBy,
objectNameSingular: string,
isForwardPagination = true,
): Record<string, string> {
return orderBy.reduce(
(acc, item) => {
Object.entries(item).forEach(([key, value]) => {
const fieldMetadata = this.fieldMetadataMap[key];
const fieldMetadata = this.fieldMetadataMapByName[key];
if (!fieldMetadata || value === undefined) {
throw new GraphqlQueryRunnerException(

View File

@@ -0,0 +1,30 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import {
AggregationField,
getAvailableAggregationsFromObjectFields,
} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
export class GraphqlQuerySelectedFieldsAggregateParser {
parse(
graphqlSelectedFields: Partial<Record<string, any>>,
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
accumulator: GraphqlQuerySelectedFieldsResult,
): void {
const availableAggregations: Record<string, AggregationField> =
getAvailableAggregationsFromObjectFields(
Object.values(fieldMetadataMapByName),
);
for (const selectedField of Object.keys(graphqlSelectedFields)) {
const selectedAggregation = availableAggregations[selectedField];
if (!selectedAggregation) {
continue;
}
accumulator.aggregate[selectedField] = selectedAggregation;
}
}
}

View File

@@ -1,43 +1,47 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import {
GraphqlQuerySelectedFieldsParser,
GraphqlQuerySelectedFieldsResult,
} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export class GraphqlQuerySelectedFieldsRelationParser {
private objectMetadataMap: ObjectMetadataMap;
private objectMetadataMaps: ObjectMetadataMaps;
constructor(objectMetadataMap: ObjectMetadataMap) {
this.objectMetadataMap = objectMetadataMap;
constructor(objectMetadataMaps: ObjectMetadataMaps) {
this.objectMetadataMaps = objectMetadataMaps;
}
parseRelationField(
fieldMetadata: FieldMetadataInterface,
fieldKey: string,
fieldValue: any,
result: { select: Record<string, any>; relations: Record<string, any> },
accumulator: GraphqlQuerySelectedFieldsResult,
): void {
if (!fieldValue || typeof fieldValue !== 'object') {
return;
}
result.relations[fieldKey] = true;
accumulator.relations[fieldKey] = true;
const referencedObjectMetadata = getRelationObjectMetadata(
fieldMetadata,
this.objectMetadataMap,
this.objectMetadataMaps,
);
const relationFields = referencedObjectMetadata.fields;
const relationFields = referencedObjectMetadata.fieldsByName;
const fieldParser = new GraphqlQuerySelectedFieldsParser(
this.objectMetadataMap,
this.objectMetadataMaps,
);
const subResult = fieldParser.parse(fieldValue, relationFields);
const relationAccumulator = fieldParser.parse(fieldValue, relationFields);
result.select[fieldKey] = {
accumulator.select[fieldKey] = {
id: true,
...subResult.select,
...relationAccumulator.select,
};
result.relations[fieldKey] = subResult.relations;
accumulator.relations[fieldKey] = relationAccumulator.relations;
accumulator.aggregate[fieldKey] = relationAccumulator.aggregate;
}
}

View File

@@ -1,59 +1,71 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser';
import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { capitalize } from 'src/utils/capitalize';
import { isPlainObject } from 'src/utils/is-plain-object';
export type GraphqlQuerySelectedFieldsResult = {
select: Record<string, any>;
relations: Record<string, any>;
aggregate: Record<string, any>;
};
export class GraphqlQuerySelectedFieldsParser {
private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser;
private aggregateParser: GraphqlQuerySelectedFieldsAggregateParser;
constructor(objectMetadataMap: ObjectMetadataMap) {
constructor(objectMetadataMaps: ObjectMetadataMaps) {
this.graphqlQuerySelectedFieldsRelationParser =
new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMap);
new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMaps);
this.aggregateParser = new GraphqlQuerySelectedFieldsAggregateParser();
}
parse(
graphqlSelectedFields: Partial<Record<string, any>>,
fieldMetadataMap: Record<string, FieldMetadataInterface>,
): { select: Record<string, any>; relations: Record<string, any> } {
const result: {
select: Record<string, any>;
relations: Record<string, any>;
} = {
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
): GraphqlQuerySelectedFieldsResult {
const accumulator: GraphqlQuerySelectedFieldsResult = {
select: {},
relations: {},
aggregate: {},
};
if (this.isRootConnection(graphqlSelectedFields)) {
this.parseConnectionField(
graphqlSelectedFields,
fieldMetadataMapByName,
accumulator,
);
return accumulator;
}
this.parseRecordField(
graphqlSelectedFields,
fieldMetadataMapByName,
accumulator,
);
return accumulator;
}
private parseRecordField(
graphqlSelectedFields: Partial<Record<string, any>>,
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
accumulator: GraphqlQuerySelectedFieldsResult,
): void {
for (const [fieldKey, fieldValue] of Object.entries(
graphqlSelectedFields,
)) {
if (this.shouldNotParseField(fieldKey)) {
continue;
}
if (this.isConnectionField(fieldKey, fieldValue)) {
const subResult = this.parse(fieldValue, fieldMetadataMap);
Object.assign(result.select, subResult.select);
Object.assign(result.relations, subResult.relations);
continue;
}
const fieldMetadata = fieldMetadataMap[fieldKey];
const fieldMetadata = fieldMetadataMapByName[fieldKey];
if (!fieldMetadata) {
throw new GraphqlQueryRunnerException(
`Field "${fieldKey}" does not exist or is not selectable`,
GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND,
);
continue;
}
if (isRelationFieldMetadataType(fieldMetadata.type)) {
@@ -61,7 +73,7 @@ export class GraphqlQuerySelectedFieldsParser {
fieldMetadata,
fieldKey,
fieldValue,
result,
accumulator,
);
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const compositeResult = this.parseCompositeField(
@@ -69,23 +81,33 @@ export class GraphqlQuerySelectedFieldsParser {
fieldValue,
);
Object.assign(result.select, compositeResult);
Object.assign(accumulator.select, compositeResult);
} else {
result.select[fieldKey] = true;
accumulator.select[fieldKey] = true;
}
}
return result;
}
private isConnectionField(fieldKey: string, fieldValue: any): boolean {
return ['edges', 'node'].includes(fieldKey) && isPlainObject(fieldValue);
}
private shouldNotParseField(fieldKey: string): boolean {
return ['__typename', 'totalCount', 'pageInfo', 'cursor'].includes(
fieldKey,
private parseConnectionField(
graphqlSelectedFields: Partial<Record<string, any>>,
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
accumulator: GraphqlQuerySelectedFieldsResult,
): void {
this.aggregateParser.parse(
graphqlSelectedFields,
fieldMetadataMapByName,
accumulator,
);
const node = graphqlSelectedFields.edges.node;
this.parseRecordField(node, fieldMetadataMapByName, accumulator);
}
private isRootConnection(
graphqlSelectedFields: Partial<Record<string, any>>,
): boolean {
return Object.keys(graphqlSelectedFields).includes('edges');
}
private parseCompositeField(

View File

@@ -6,43 +6,44 @@ import {
} from 'typeorm';
import {
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
ObjectRecordFilter,
ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser';
import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser';
import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import {
FieldMetadataMap,
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
GraphqlQuerySelectedFieldsParser,
GraphqlQuerySelectedFieldsResult,
} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export class GraphqlQueryParser {
private fieldMetadataMap: FieldMetadataMap;
private objectMetadataMap: ObjectMetadataMap;
private fieldMetadataMapByName: FieldMetadataMap;
private objectMetadataMaps: ObjectMetadataMaps;
private filterConditionParser: GraphqlQueryFilterConditionParser;
private orderFieldParser: GraphqlQueryOrderFieldParser;
constructor(
fieldMetadataMap: FieldMetadataMap,
objectMetadataMap: ObjectMetadataMap,
fieldMetadataMapByName: FieldMetadataMap,
objectMetadataMaps: ObjectMetadataMaps,
) {
this.objectMetadataMap = objectMetadataMap;
this.fieldMetadataMap = fieldMetadataMap;
this.objectMetadataMaps = objectMetadataMaps;
this.fieldMetadataMapByName = fieldMetadataMapByName;
this.filterConditionParser = new GraphqlQueryFilterConditionParser(
this.fieldMetadataMap,
this.fieldMetadataMapByName,
);
this.orderFieldParser = new GraphqlQueryOrderFieldParser(
this.fieldMetadataMap,
this.fieldMetadataMapByName,
);
}
public applyFilterToBuilder(
queryBuilder: SelectQueryBuilder<any>,
objectNameSingular: string,
recordFilter: Partial<RecordFilter>,
recordFilter: Partial<ObjectRecordFilter>,
): SelectQueryBuilder<any> {
return this.filterConditionParser.parse(
queryBuilder,
@@ -53,7 +54,7 @@ export class GraphqlQueryParser {
public applyDeletedAtToBuilder(
queryBuilder: SelectQueryBuilder<any>,
recordFilter: RecordFilter,
recordFilter: ObjectRecordFilter,
): SelectQueryBuilder<any> {
if (this.checkForDeletedAtFilter(recordFilter)) {
queryBuilder.withDeleted();
@@ -90,7 +91,7 @@ export class GraphqlQueryParser {
public applyOrderToBuilder(
queryBuilder: SelectQueryBuilder<any>,
orderBy: RecordOrderBy,
orderBy: ObjectRecordOrderBy,
objectNameSingular: string,
isForwardPagination = true,
): SelectQueryBuilder<any> {
@@ -104,11 +105,12 @@ export class GraphqlQueryParser {
}
public parseSelectedFields(
parentObjectMetadata: ObjectMetadataMapItem,
parentObjectMetadata: ObjectMetadataItemWithFieldMaps,
graphqlSelectedFields: Partial<Record<string, any>>,
): { select: Record<string, any>; relations: Record<string, any> } {
): GraphqlQuerySelectedFieldsResult {
const parentFields =
this.objectMetadataMap[parentObjectMetadata.nameSingular]?.fields;
this.objectMetadataMaps.byNameSingular[parentObjectMetadata.nameSingular]
?.fieldsByName;
if (!parentFields) {
throw new Error(
@@ -117,7 +119,7 @@ export class GraphqlQueryParser {
}
const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser(
this.objectMetadataMap,
this.objectMetadataMaps,
);
return selectedFieldsParser.parse(graphqlSelectedFields, parentFields);

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common';
import {
Record as IRecord,
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
ObjectRecord,
ObjectRecordFilter,
ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
@@ -48,11 +48,11 @@ export class GraphqlQueryRunnerService {
/** QUERIES */
@LogExecutionTime()
async findOne<ObjectRecord extends IRecord, Filter extends RecordFilter>(
async findOne<T extends ObjectRecord, Filter extends ObjectRecordFilter>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
return this.executeQuery<FindOneResolverArgs<Filter>, ObjectRecord>(
): Promise<T> {
return this.executeQuery<FindOneResolverArgs<Filter>, T>(
'findOne',
args,
options,
@@ -61,36 +61,36 @@ export class GraphqlQueryRunnerService {
@LogExecutionTime()
async findMany<
ObjectRecord extends IRecord,
Filter extends RecordFilter,
OrderBy extends RecordOrderBy,
T extends ObjectRecord,
Filter extends ObjectRecordFilter,
OrderBy extends ObjectRecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord, IEdge<ObjectRecord>>> {
): Promise<IConnection<T, IEdge<T>>> {
return this.executeQuery<
FindManyResolverArgs<Filter, OrderBy>,
IConnection<ObjectRecord, IEdge<ObjectRecord>>
IConnection<T, IEdge<T>>
>('findMany', args, options);
}
@LogExecutionTime()
async findDuplicates<ObjectRecord extends IRecord>(
args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>,
async findDuplicates<T extends ObjectRecord>(
args: FindDuplicatesResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>[]> {
): Promise<IConnection<T>[]> {
return this.executeQuery<
FindDuplicatesResolverArgs<Partial<ObjectRecord>>,
IConnection<ObjectRecord>[]
FindDuplicatesResolverArgs<Partial<T>>,
IConnection<T>[]
>('findDuplicates', args, options);
}
@LogExecutionTime()
async search<ObjectRecord extends IRecord = IRecord>(
async search<T extends ObjectRecord = ObjectRecord>(
args: SearchResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
return this.executeQuery<SearchResolverArgs, IConnection<ObjectRecord>>(
): Promise<IConnection<T>> {
return this.executeQuery<SearchResolverArgs, IConnection<T>>(
'search',
args,
options,
@@ -100,13 +100,13 @@ export class GraphqlQueryRunnerService {
/** MUTATIONS */
@LogExecutionTime()
async createOne<ObjectRecord extends IRecord>(
args: CreateOneResolverArgs<Partial<ObjectRecord>>,
async createOne<T extends ObjectRecord>(
args: CreateOneResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
): Promise<T> {
const results = await this.executeQuery<
CreateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord[]
CreateManyResolverArgs<Partial<T>>,
T[]
>('createMany', { data: [args.data], upsert: args.upsert }, options);
// TODO: emitCreateEvents should be moved to the ORM layer
@@ -114,7 +114,7 @@ export class GraphqlQueryRunnerService {
this.apiEventEmitterService.emitCreateEvents(
results,
options.authContext,
options.objectMetadataItem,
options.objectMetadataItemWithFieldMaps,
);
}
@@ -122,20 +122,20 @@ export class GraphqlQueryRunnerService {
}
@LogExecutionTime()
async createMany<ObjectRecord extends IRecord>(
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
async createMany<T extends ObjectRecord>(
args: CreateManyResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
): Promise<T[]> {
const results = await this.executeQuery<
CreateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord[]
CreateManyResolverArgs<Partial<T>>,
T[]
>('createMany', args, options);
if (results) {
this.apiEventEmitterService.emitCreateEvents(
results,
options.authContext,
options.objectMetadataItem,
options.objectMetadataItemWithFieldMaps,
);
}
@@ -143,14 +143,11 @@ export class GraphqlQueryRunnerService {
}
@LogExecutionTime()
public async updateOne<ObjectRecord extends IRecord>(
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
public async updateOne<T extends ObjectRecord>(
args: UpdateOneResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const existingRecord = await this.executeQuery<
FindOneResolverArgs,
ObjectRecord
>(
): Promise<T> {
const existingRecord = await this.executeQuery<FindOneResolverArgs, T>(
'findOne',
{
filter: { id: { eq: args.id } },
@@ -159,8 +156,8 @@ export class GraphqlQueryRunnerService {
);
const result = await this.executeQuery<
UpdateOneResolverArgs<Partial<ObjectRecord>>,
ObjectRecord
UpdateOneResolverArgs<Partial<T>>,
T
>('updateOne', args, options);
this.apiEventEmitterService.emitUpdateEvents(
@@ -168,20 +165,20 @@ export class GraphqlQueryRunnerService {
[result],
Object.keys(args.data),
options.authContext,
options.objectMetadataItem,
options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
public async updateMany<ObjectRecord extends IRecord>(
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
public async updateMany<T extends ObjectRecord>(
args: UpdateManyResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
): Promise<T[]> {
const existingRecords = await this.executeQuery<
FindManyResolverArgs,
IConnection<ObjectRecord, IEdge<ObjectRecord>>
IConnection<T, IEdge<T>>
>(
'findMany',
{
@@ -191,8 +188,8 @@ export class GraphqlQueryRunnerService {
);
const result = await this.executeQuery<
UpdateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord[]
UpdateManyResolverArgs<Partial<T>>,
T[]
>('updateMany', args, options);
this.apiEventEmitterService.emitUpdateEvents(
@@ -200,25 +197,25 @@ export class GraphqlQueryRunnerService {
result,
Object.keys(args.data),
options.authContext,
options.objectMetadataItem,
options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
public async deleteOne<ObjectRecord extends IRecord & { deletedAt?: Date }>(
public async deleteOne<T extends ObjectRecord & { deletedAt?: Date }>(
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
): Promise<T> {
const result = await this.executeQuery<
UpdateOneResolverArgs<Partial<ObjectRecord>>,
ObjectRecord
UpdateOneResolverArgs<Partial<T>>,
T
>(
'deleteOne',
{
id: args.id,
data: { deletedAt: new Date() } as Partial<ObjectRecord>,
data: { deletedAt: new Date() } as Partial<T>,
},
options,
);
@@ -226,26 +223,26 @@ export class GraphqlQueryRunnerService {
this.apiEventEmitterService.emitDeletedEvents(
[result],
options.authContext,
options.objectMetadataItem,
options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
public async deleteMany<ObjectRecord extends IRecord & { deletedAt?: Date }>(
public async deleteMany<T extends ObjectRecord & { deletedAt?: Date }>(
args: DeleteManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
): Promise<T[]> {
const result = await this.executeQuery<
UpdateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord[]
UpdateManyResolverArgs<Partial<T>>,
T[]
>(
'deleteMany',
{
filter: args.filter,
data: { deletedAt: new Date() } as Partial<ObjectRecord>,
data: { deletedAt: new Date() } as Partial<T>,
},
options,
);
@@ -253,63 +250,62 @@ export class GraphqlQueryRunnerService {
this.apiEventEmitterService.emitDeletedEvents(
result,
options.authContext,
options.objectMetadataItem,
options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
async destroyOne<ObjectRecord extends IRecord>(
async destroyOne<T extends ObjectRecord>(
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const result = await this.executeQuery<
DestroyOneResolverArgs,
ObjectRecord
>('destroyOne', args, options);
): Promise<T> {
const result = await this.executeQuery<DestroyOneResolverArgs, T>(
'destroyOne',
args,
options,
);
this.apiEventEmitterService.emitDestroyEvents(
[result],
options.authContext,
options.objectMetadataItem,
options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
async destroyMany<ObjectRecord extends IRecord>(
async destroyMany<T extends ObjectRecord>(
args: DestroyManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
const result = await this.executeQuery<
DestroyManyResolverArgs,
ObjectRecord[]
>('destroyMany', args, options);
): Promise<T[]> {
const result = await this.executeQuery<DestroyManyResolverArgs, T[]>(
'destroyMany',
args,
options,
);
this.apiEventEmitterService.emitDestroyEvents(
result,
options.authContext,
options.objectMetadataItem,
options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
public async restoreMany<ObjectRecord extends IRecord>(
public async restoreMany<T extends ObjectRecord>(
args: RestoreManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
return await this.executeQuery<
UpdateManyResolverArgs<Partial<ObjectRecord>>,
ObjectRecord
>(
): Promise<T> {
return await this.executeQuery<UpdateManyResolverArgs<Partial<T>>, T>(
'restoreMany',
{
filter: args.filter,
data: { deletedAt: null } as Partial<ObjectRecord>,
data: { deletedAt: null } as Partial<T>,
},
options,
);
@@ -320,7 +316,7 @@ export class GraphqlQueryRunnerService {
args: Input,
options: WorkspaceQueryRunnerOptions,
): Promise<Response> {
const { authContext, objectMetadataItem } = options;
const { authContext, objectMetadataItemWithFieldMaps } = options;
const resolver =
this.graphqlQueryResolverFactory.getResolver(operationName);
@@ -330,7 +326,7 @@ export class GraphqlQueryRunnerService {
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
args,
);
@@ -345,7 +341,7 @@ export class GraphqlQueryRunnerService {
const resultWithGetters = await this.queryResultGettersFactory.create(
results,
objectMetadataItem,
objectMetadataItemWithFieldMaps,
authContext.workspace.id,
);
@@ -355,7 +351,7 @@ export class GraphqlQueryRunnerService {
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
objectMetadataItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
resultWithGettersArray,
);

View File

@@ -1,7 +1,7 @@
import {
Record as IRecord,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
ObjectRecord,
ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
@@ -12,23 +12,27 @@ import {
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { isPlainObject } from 'src/utils/is-plain-object';
export class ObjectRecordsToGraphqlConnectionHelper {
private objectMetadataMap: ObjectMetadataMap;
private objectMetadataMaps: ObjectMetadataMaps;
constructor(objectMetadataMap: ObjectMetadataMap) {
this.objectMetadataMap = objectMetadataMap;
constructor(objectMetadataMaps: ObjectMetadataMaps) {
this.objectMetadataMaps = objectMetadataMaps;
}
public createConnection<ObjectRecord extends IRecord = IRecord>({
public createConnection<T extends ObjectRecord = ObjectRecord>({
objectRecords,
parentObjectRecord,
objectRecordsAggregatedValues = {},
selectedAggregatedFields = [],
objectName,
take,
totalCount,
@@ -37,19 +41,24 @@ export class ObjectRecordsToGraphqlConnectionHelper {
hasPreviousPage,
depth = 0,
}: {
objectRecords: ObjectRecord[];
objectRecords: T[];
parentObjectRecord?: T;
objectRecordsAggregatedValues?: Record<string, any>;
selectedAggregatedFields?: Record<string, any>;
objectName: string;
take: number;
totalCount: number;
order?: RecordOrderBy;
order?: ObjectRecordOrderBy;
hasNextPage: boolean;
hasPreviousPage: boolean;
depth?: number;
}): IConnection<ObjectRecord> {
}): IConnection<T> {
const edges = (objectRecords ?? []).map((objectRecord) => ({
node: this.processRecord({
objectRecord,
objectName,
objectRecordsAggregatedValues,
selectedAggregatedFields,
take,
totalCount,
order,
@@ -58,7 +67,15 @@ export class ObjectRecordsToGraphqlConnectionHelper {
cursor: encodeCursor(objectRecord, order),
}));
const aggregatedFieldsValues = this.extractAggregatedFieldsValues({
selectedAggregatedFields,
objectRecordsAggregatedValues: parentObjectRecord
? objectRecordsAggregatedValues[parentObjectRecord.id]
: objectRecordsAggregatedValues,
});
return {
...aggregatedFieldsValues,
edges,
pageInfo: {
hasNextPage,
@@ -70,9 +87,41 @@ export class ObjectRecordsToGraphqlConnectionHelper {
};
}
private extractAggregatedFieldsValues = ({
selectedAggregatedFields,
objectRecordsAggregatedValues,
}: {
selectedAggregatedFields: Record<string, AggregationField[]>;
objectRecordsAggregatedValues: Record<string, any>;
}) => {
if (!objectRecordsAggregatedValues) {
return {};
}
return Object.entries(selectedAggregatedFields).reduce(
(acc, [aggregatedFieldName]) => {
const aggregatedFieldValue =
objectRecordsAggregatedValues[aggregatedFieldName];
if (!aggregatedFieldValue) {
return acc;
}
return {
...acc,
[aggregatedFieldName]:
objectRecordsAggregatedValues[aggregatedFieldName],
};
},
{},
);
};
public processRecord<T extends Record<string, any>>({
objectRecord,
objectName,
objectRecordsAggregatedValues = {},
selectedAggregatedFields = [],
take,
totalCount,
order,
@@ -80,9 +129,11 @@ export class ObjectRecordsToGraphqlConnectionHelper {
}: {
objectRecord: T;
objectName: string;
objectRecordsAggregatedValues?: Record<string, any>;
selectedAggregatedFields?: Record<string, any>;
take: number;
totalCount: number;
order?: RecordOrderBy;
order?: ObjectRecordOrderBy;
depth?: number;
}): T {
if (depth >= CONNECTION_MAX_DEPTH) {
@@ -92,7 +143,7 @@ export class ObjectRecordsToGraphqlConnectionHelper {
);
}
const objectMetadata = this.objectMetadataMap[objectName];
const objectMetadata = this.objectMetadataMaps.byNameSingular[objectName];
if (!objectMetadata) {
throw new GraphqlQueryRunnerException(
@@ -104,7 +155,7 @@ export class ObjectRecordsToGraphqlConnectionHelper {
const processedObjectRecord: Record<string, any> = {};
for (const [key, value] of Object.entries(objectRecord)) {
const fieldMetadata = objectMetadata.fields[key];
const fieldMetadata = objectMetadata.fieldsByName[key];
if (!fieldMetadata) {
processedObjectRecord[key] = value;
@@ -115,12 +166,19 @@ export class ObjectRecordsToGraphqlConnectionHelper {
if (Array.isArray(value)) {
processedObjectRecord[key] = this.createConnection({
objectRecords: value,
parentObjectRecord: objectRecord,
objectRecordsAggregatedValues:
objectRecordsAggregatedValues[fieldMetadata.name],
selectedAggregatedFields:
selectedAggregatedFields[fieldMetadata.name],
objectName: getRelationObjectMetadata(
fieldMetadata,
this.objectMetadataMap,
this.objectMetadataMaps,
).nameSingular,
take,
totalCount: value.length,
totalCount:
objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ??
value.length,
order,
hasNextPage: false,
hasPreviousPage: false,
@@ -129,9 +187,13 @@ export class ObjectRecordsToGraphqlConnectionHelper {
} else if (isPlainObject(value)) {
processedObjectRecord[key] = this.processRecord({
objectRecord: value,
objectRecordsAggregatedValues:
objectRecordsAggregatedValues[fieldMetadata.name],
selectedAggregatedFields:
selectedAggregatedFields[fieldMetadata.name],
objectName: getRelationObjectMetadata(
fieldMetadata,
this.objectMetadataMap,
this.objectMetadataMaps,
).nameSingular,
take,
totalCount,

View File

@@ -0,0 +1,37 @@
import { SelectQueryBuilder } from 'typeorm';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
export class ProcessAggregateHelper {
public addSelectedAggregatedFieldsQueriesToQueryBuilder = ({
fieldMetadataMapByName,
selectedAggregatedFields,
queryBuilder,
}: {
fieldMetadataMapByName: Record<string, FieldMetadataInterface>;
selectedAggregatedFields: Record<string, AggregationField>;
queryBuilder: SelectQueryBuilder<any>;
}) => {
queryBuilder.select([]);
for (const [aggregatedFieldName, aggregatedField] of Object.entries(
selectedAggregatedFields,
)) {
const fieldMetadata = fieldMetadataMapByName[aggregatedField.fromField];
if (!fieldMetadata) {
continue;
}
const fieldName = fieldMetadata.name;
const operation = aggregatedField.aggregationOperation;
queryBuilder.addSelect(
`${operation}("${fieldName}")`,
`${aggregatedFieldName}`,
);
}
};
}

View File

@@ -1,64 +1,95 @@
import {
DataSource,
FindManyOptions,
FindOptionsRelations,
In,
ObjectLiteral,
Repository,
SelectQueryBuilder,
} from 'typeorm';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import {
getRelationMetadata,
getRelationObjectMetadata,
} from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
import {
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
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 { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { deduceRelationDirection } from 'src/engine/utils/deduce-relation-direction.util';
export class ProcessNestedRelationsHelper {
constructor() {}
private processAggregateHelper: ProcessAggregateHelper;
public async processNestedRelations<ObjectRecord extends IRecord = IRecord>(
objectMetadataMap: ObjectMetadataMap,
parentObjectMetadataItem: ObjectMetadataMapItem,
parentObjectRecords: ObjectRecord[],
relations: Record<string, FindOptionsRelations<ObjectLiteral>>,
limit: number,
authContext: any,
dataSource: DataSource,
): Promise<void> {
constructor() {
this.processAggregateHelper = new ProcessAggregateHelper();
}
public async processNestedRelations<T extends ObjectRecord = ObjectRecord>({
objectMetadataMaps,
parentObjectMetadataItem,
parentObjectRecords,
parentObjectRecordsAggregatedValues = {},
relations,
aggregate = {},
limit,
authContext,
dataSource,
}: {
objectMetadataMaps: ObjectMetadataMaps;
parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
parentObjectRecords: T[];
parentObjectRecordsAggregatedValues?: Record<string, any>;
relations: Record<string, FindOptionsRelations<ObjectLiteral>>;
aggregate?: Record<string, AggregationField>;
limit: number;
authContext: any;
dataSource: DataSource;
}): Promise<void> {
const processRelationTasks = Object.entries(relations).map(
([relationName, nestedRelations]) =>
this.processRelation(
objectMetadataMap,
this.processRelation({
objectMetadataMaps,
parentObjectMetadataItem,
parentObjectRecords,
parentObjectRecordsAggregatedValues,
relationName,
nestedRelations,
aggregate,
limit,
authContext,
dataSource,
),
}),
);
await Promise.all(processRelationTasks);
}
private async processRelation<ObjectRecord extends IRecord = IRecord>(
objectMetadataMap: ObjectMetadataMap,
parentObjectMetadataItem: ObjectMetadataMapItem,
parentObjectRecords: ObjectRecord[],
relationName: string,
nestedRelations: any,
limit: number,
authContext: any,
dataSource: DataSource,
): Promise<void> {
const relationFieldMetadata = parentObjectMetadataItem.fields[relationName];
private async processRelation<T extends ObjectRecord = ObjectRecord>({
objectMetadataMaps,
parentObjectMetadataItem,
parentObjectRecords,
parentObjectRecordsAggregatedValues,
relationName,
nestedRelations,
aggregate,
limit,
authContext,
dataSource,
}: {
objectMetadataMaps: ObjectMetadataMaps;
parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
parentObjectRecords: T[];
parentObjectRecordsAggregatedValues: Record<string, any>;
relationName: string;
nestedRelations: any;
aggregate: Record<string, AggregationField>;
limit: number;
authContext: any;
dataSource: DataSource;
}): Promise<void> {
const relationFieldMetadata =
parentObjectMetadataItem.fieldsByName[relationName];
const relationMetadata = getRelationMetadata(relationFieldMetadata);
const relationDirection = deduceRelationDirection(
relationFieldMetadata,
@@ -70,181 +101,341 @@ export class ProcessNestedRelationsHelper {
? this.processToRelation
: this.processFromRelation;
await processor.call(
this,
objectMetadataMap,
await processor.call(this, {
objectMetadataMaps,
parentObjectMetadataItem,
parentObjectRecords,
parentObjectRecordsAggregatedValues,
relationName,
nestedRelations,
aggregate,
limit,
authContext,
dataSource,
);
});
}
private async processFromRelation<ObjectRecord extends IRecord = IRecord>(
objectMetadataMap: ObjectMetadataMap,
parentObjectMetadataItem: ObjectMetadataMapItem,
parentObjectRecords: ObjectRecord[],
relationName: string,
nestedRelations: any,
limit: number,
authContext: any,
dataSource: DataSource,
): Promise<void> {
private async processFromRelation<T extends ObjectRecord = ObjectRecord>({
objectMetadataMaps,
parentObjectMetadataItem,
parentObjectRecords,
parentObjectRecordsAggregatedValues,
relationName,
nestedRelations,
aggregate,
limit,
authContext,
dataSource,
}: {
objectMetadataMaps: ObjectMetadataMaps;
parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
parentObjectRecords: T[];
parentObjectRecordsAggregatedValues: Record<string, any>;
relationName: string;
nestedRelations: any;
aggregate: Record<string, AggregationField>;
limit: number;
authContext: any;
dataSource: DataSource;
}): Promise<void> {
const { inverseRelationName, referenceObjectMetadata } =
this.getRelationMetadata(
objectMetadataMap,
this.getRelationMetadata({
objectMetadataMaps,
parentObjectMetadataItem,
relationName,
);
});
const relationRepository = dataSource.getRepository(
referenceObjectMetadata.nameSingular,
);
const relationIds = this.getUniqueIds(parentObjectRecords, 'id');
const relationResults = await this.findRelations(
relationRepository,
inverseRelationName,
relationIds,
limit * parentObjectRecords.length,
const referenceQueryBuilder = relationRepository.createQueryBuilder(
referenceObjectMetadata.nameSingular,
);
this.assignRelationResults(
parentObjectRecords,
const relationIds = this.getUniqueIds({
records: parentObjectRecords,
idField: 'id',
});
const { relationResults, relationAggregatedFieldsResult } =
await this.findRelations({
referenceQueryBuilder,
column: `"${inverseRelationName}Id"`,
ids: relationIds,
limit: limit * parentObjectRecords.length,
objectMetadataMaps,
referenceObjectMetadata,
aggregate,
relationName,
});
this.assignFromRelationResults({
parentRecords: parentObjectRecords,
parentObjectRecordsAggregatedValues,
relationResults,
relationAggregatedFieldsResult,
relationName,
`${inverseRelationName}Id`,
);
joinField: `${inverseRelationName}Id`,
});
if (Object.keys(nestedRelations).length > 0) {
await this.processNestedRelations(
objectMetadataMap,
objectMetadataMap[referenceObjectMetadata.nameSingular],
relationResults as ObjectRecord[],
nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>,
await this.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem:
objectMetadataMaps.byNameSingular[
referenceObjectMetadata.nameSingular
],
parentObjectRecords: relationResults as ObjectRecord[],
parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult,
relations: nestedRelations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
aggregate,
limit,
authContext,
dataSource,
);
});
}
}
private async processToRelation<ObjectRecord extends IRecord = IRecord>(
objectMetadataMap: ObjectMetadataMap,
parentObjectMetadataItem: ObjectMetadataMapItem,
parentObjectRecords: ObjectRecord[],
relationName: string,
nestedRelations: any,
limit: number,
authContext: any,
dataSource: DataSource,
): Promise<void> {
const { referenceObjectMetadata } = this.getRelationMetadata(
objectMetadataMap,
private async processToRelation<T extends ObjectRecord = ObjectRecord>({
objectMetadataMaps,
parentObjectMetadataItem,
parentObjectRecords,
parentObjectRecordsAggregatedValues,
relationName,
nestedRelations,
aggregate,
limit,
authContext,
dataSource,
}: {
objectMetadataMaps: ObjectMetadataMaps;
parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
parentObjectRecords: T[];
parentObjectRecordsAggregatedValues: Record<string, any>;
relationName: string;
nestedRelations: any;
aggregate: Record<string, AggregationField>;
limit: number;
authContext: any;
dataSource: DataSource;
}): Promise<void> {
const { referenceObjectMetadata } = this.getRelationMetadata({
objectMetadataMaps,
parentObjectMetadataItem,
relationName,
);
});
const relationRepository = dataSource.getRepository(
referenceObjectMetadata.nameSingular,
);
const relationIds = this.getUniqueIds(
parentObjectRecords,
`${relationName}Id`,
);
const relationResults = await this.findRelations(
relationRepository,
'id',
relationIds,
limit,
const referenceQueryBuilder = relationRepository.createQueryBuilder(
referenceObjectMetadata.nameSingular,
);
this.assignToRelationResults(
parentObjectRecords,
const relationIds = this.getUniqueIds({
records: parentObjectRecords,
idField: `${relationName}Id`,
});
const { relationResults, relationAggregatedFieldsResult } =
await this.findRelations({
referenceQueryBuilder,
column: 'id',
ids: relationIds,
limit,
objectMetadataMaps,
referenceObjectMetadata,
aggregate,
relationName,
});
this.assignToRelationResults({
parentRecords: parentObjectRecords,
parentObjectRecordsAggregatedValues: parentObjectRecordsAggregatedValues,
relationResults,
relationAggregatedFieldsResult,
relationName,
);
});
if (Object.keys(nestedRelations).length > 0) {
await this.processNestedRelations(
objectMetadataMap,
objectMetadataMap[referenceObjectMetadata.nameSingular],
relationResults as ObjectRecord[],
nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>,
await this.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem:
objectMetadataMaps.byNameSingular[
referenceObjectMetadata.nameSingular
],
parentObjectRecords: relationResults as ObjectRecord[],
parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult,
relations: nestedRelations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
aggregate,
limit,
authContext,
dataSource,
);
});
}
}
private getRelationMetadata(
objectMetadataMap: ObjectMetadataMap,
parentObjectMetadataItem: ObjectMetadataMapItem,
relationName: string,
) {
const relationFieldMetadata = parentObjectMetadataItem.fields[relationName];
private getRelationMetadata({
objectMetadataMaps,
parentObjectMetadataItem,
relationName,
}: {
objectMetadataMaps: ObjectMetadataMaps;
parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
relationName: string;
}) {
const relationFieldMetadata =
parentObjectMetadataItem.fieldsByName[relationName];
const relationMetadata = getRelationMetadata(relationFieldMetadata);
const referenceObjectMetadata = getRelationObjectMetadata(
relationFieldMetadata,
objectMetadataMap,
objectMetadataMaps,
);
const inverseRelationName =
objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[
objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]?.fieldsById[
relationMetadata.toFieldMetadataId
]?.name;
return { inverseRelationName, referenceObjectMetadata };
}
private getUniqueIds(records: IRecord[], idField: string): any[] {
private getUniqueIds({
records,
idField,
}: {
records: ObjectRecord[];
idField: string;
}): any[] {
return [...new Set(records.map((item) => item[idField]))];
}
private async findRelations(
repository: Repository<any>,
field: string,
ids: any[],
limit: number,
): Promise<any[]> {
private async findRelations({
referenceQueryBuilder,
column,
ids,
limit,
objectMetadataMaps,
referenceObjectMetadata,
aggregate,
relationName,
}: {
referenceQueryBuilder: SelectQueryBuilder<any>;
column: string;
ids: any[];
limit: number;
objectMetadataMaps: ObjectMetadataMaps;
referenceObjectMetadata: ObjectMetadataItemWithFieldMaps;
aggregate: Record<string, any>;
relationName: string;
}): Promise<{ relationResults: any[]; relationAggregatedFieldsResult: any }> {
if (ids.length === 0) {
return [];
return { relationResults: [], relationAggregatedFieldsResult: {} };
}
const findOptions: FindManyOptions = {
where: { [field]: In(ids) },
take: limit,
};
return repository.find(findOptions);
const aggregateForRelation = aggregate[relationName];
let relationAggregatedFieldsResult: Record<string, any> = {};
if (aggregateForRelation) {
const aggregateQueryBuilder = referenceQueryBuilder.clone();
this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder(
{
fieldMetadataMapByName: referenceObjectMetadata.fieldsByName,
selectedAggregatedFields: aggregateForRelation,
queryBuilder: aggregateQueryBuilder,
},
);
const aggregatedFieldsValues = await aggregateQueryBuilder
.addSelect(column)
.where(`${column} IN (:...ids)`, {
ids,
})
.groupBy(column)
.getRawMany();
relationAggregatedFieldsResult = aggregatedFieldsValues.reduce(
(acc, item) => {
const columnWithoutQuotes = column.replace(/["']/g, '');
const key = item[columnWithoutQuotes];
const { [column]: _, ...itemWithoutColumn } = item;
acc[key] = itemWithoutColumn;
return acc;
},
{},
);
}
const result = await referenceQueryBuilder
.where(`${column} IN (:...ids)`, {
ids,
})
.take(limit)
.getMany();
const relationResults = formatResult(
result,
referenceObjectMetadata,
objectMetadataMaps,
);
return { relationResults, relationAggregatedFieldsResult };
}
private assignRelationResults(
parentRecords: IRecord[],
relationResults: any[],
relationName: string,
joinField: string,
): void {
private assignFromRelationResults({
parentRecords,
parentObjectRecordsAggregatedValues,
relationResults,
relationAggregatedFieldsResult,
relationName,
joinField,
}: {
parentRecords: ObjectRecord[];
parentObjectRecordsAggregatedValues: Record<string, any>;
relationResults: any[];
relationAggregatedFieldsResult: Record<string, any>;
relationName: string;
joinField: string;
}): void {
parentRecords.forEach((item) => {
(item as any)[relationName] = relationResults.filter(
item[relationName] = relationResults.filter(
(rel) => rel[joinField] === item.id,
);
});
parentObjectRecordsAggregatedValues[relationName] =
relationAggregatedFieldsResult;
}
private assignToRelationResults(
parentRecords: IRecord[],
relationResults: any[],
relationName: string,
): void {
private assignToRelationResults({
parentRecords,
parentObjectRecordsAggregatedValues,
relationResults,
relationAggregatedFieldsResult,
relationName,
}: {
parentRecords: ObjectRecord[];
parentObjectRecordsAggregatedValues: Record<string, any>;
relationResults: any[];
relationAggregatedFieldsResult: Record<string, any>;
relationName: string;
}): void {
parentRecords.forEach((item) => {
if (relationResults.length === 0) {
(item as any)[`${relationName}Id`] = null;
item[`${relationName}Id`] = null;
}
(item as any)[relationName] =
item[relationName] =
relationResults.find((rel) => rel.id === item[`${relationName}Id`]) ??
null;
});
parentObjectRecordsAggregatedValues[relationName] =
relationAggregatedFieldsResult;
}
}

View File

@@ -4,7 +4,7 @@ import graphqlFields from 'graphql-fields';
import { In, InsertResult } from 'typeorm';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -19,35 +19,40 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryCreateManyResolverService
implements ResolverService<CreateManyResolverArgs, IRecord[]>
implements ResolverService<CreateManyResolverArgs, ObjectRecord[]>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
async resolve<T extends ObjectRecord = ObjectRecord>(
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
const { authContext, info, objectMetadataMap, objectMetadataMapItem } =
options;
): Promise<T[]> {
const {
authContext,
info,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
} = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
objectMetadataItemWithFieldMaps,
selectedFields,
);
@@ -59,7 +64,7 @@ export class GraphqlQueryCreateManyResolverService
});
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const nonFormattedUpsertedRecords = (await queryBuilder
@@ -71,42 +76,42 @@ export class GraphqlQueryCreateManyResolverService
const upsertedRecords = formatResult(
nonFormattedUpsertedRecords,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
upsertedRecords,
await processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: upsertedRecords,
relations,
QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
);
});
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return upsertedRecords.map((record: ObjectRecord) =>
return upsertedRecords.map((record: T) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataMapItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
}),
);
}
async validate<ObjectRecord extends IRecord>(
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
async validate<T extends ObjectRecord>(
args: CreateManyResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<void> {
assertMutationNotOnRemoteObject(options.objectMetadataItem);
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
args.data.forEach((record) => {
if (record?.id) {

View File

@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -16,46 +16,51 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryDestroyManyResolverService
implements ResolverService<DestroyManyResolverArgs, IRecord[]>
implements ResolverService<DestroyManyResolverArgs, ObjectRecord[]>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
async resolve<T extends ObjectRecord = ObjectRecord>(
args: DestroyManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
options;
): Promise<T[]> {
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
info,
} = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
objectMetadataItemWithFieldMaps,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
args.filter,
);
@@ -66,31 +71,31 @@ export class GraphqlQueryDestroyManyResolverService
const deletedRecords = formatResult(
nonFormattedDeletedObjectRecords.raw,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
deletedRecords,
await processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: deletedRecords,
relations,
QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
);
});
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return deletedRecords.map((record: ObjectRecord) =>
return deletedRecords.map((record: T) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataMapItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
}),

View File

@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -20,45 +20,50 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryDestroyOneResolverService
implements ResolverService<DestroyOneResolverArgs, IRecord>
implements ResolverService<DestroyOneResolverArgs, ObjectRecord>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
async resolve<T extends ObjectRecord = ObjectRecord>(
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
options;
): Promise<T> {
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
info,
} = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
objectMetadataItemWithFieldMaps,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const nonFormattedDeletedObjectRecords = await queryBuilder
.where(`"${objectMetadataMapItem.nameSingular}".id = :id`, {
.where(`"${objectMetadataItemWithFieldMaps.nameSingular}".id = :id`, {
id: args.id,
})
.take(1)
@@ -75,30 +80,30 @@ export class GraphqlQueryDestroyOneResolverService
const recordBeforeDeletion = formatResult(
nonFormattedDeletedObjectRecords.raw,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
)[0];
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
[recordBeforeDeletion],
await processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: [recordBeforeDeletion],
relations,
QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
);
});
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return typeORMObjectRecordsParser.processRecord({
objectRecord: recordBeforeDeletion,
objectName: objectMetadataMapItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
});

View File

@@ -5,10 +5,10 @@ import { In } from 'typeorm';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
Record as IRecord,
ObjectRecord,
ObjectRecordFilter,
OrderByDirection,
RecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -21,7 +21,7 @@ import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { settings } from 'src/engine/constants/settings';
import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants';
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@@ -29,60 +29,63 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryFindDuplicatesResolverService
implements
ResolverService<FindDuplicatesResolverArgs, IConnection<IRecord>[]>
ResolverService<FindDuplicatesResolverArgs, IConnection<ObjectRecord>[]>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>,
async resolve<T extends ObjectRecord = ObjectRecord>(
args: FindDuplicatesResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>[]> {
const { authContext, objectMetadataMapItem, objectMetadataMap } = options;
): Promise<IConnection<T>[]> {
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const existingRecordsQueryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const duplicateRecordsQueryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMap[objectMetadataMapItem.nameSingular].fields,
objectMetadataMap,
objectMetadataMaps.byNameSingular[
objectMetadataItemWithFieldMaps.nameSingular
].fieldsByName,
objectMetadataMaps,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
let objectRecords: Partial<ObjectRecord>[] = [];
let objectRecords: Partial<T>[] = [];
if (args.ids) {
const nonFormattedObjectRecords = (await existingRecordsQueryBuilder
.where({ id: In(args.ids) })
.getMany()) as ObjectRecord[];
.getMany()) as T[];
objectRecords = formatResult(
nonFormattedObjectRecords,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
} else if (args.data && !isEmpty(args.data)) {
objectRecords = formatData(args.data, objectMetadataMapItem);
objectRecords = formatData(args.data, objectMetadataItemWithFieldMaps);
}
const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all(
const duplicateConnections: IConnection<T>[] = await Promise.all(
objectRecords.map(async (record) => {
const duplicateConditions = this.buildDuplicateConditions(
objectMetadataMapItem,
objectMetadataItemWithFieldMaps,
[record],
record.id,
);
@@ -90,7 +93,7 @@ export class GraphqlQueryFindDuplicatesResolverService
if (isEmpty(duplicateConditions)) {
return typeORMObjectRecordsParser.createConnection({
objectRecords: [],
objectName: objectMetadataMapItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 0,
totalCount: 0,
order: [{ id: OrderByDirection.AscNullsFirst }],
@@ -101,22 +104,22 @@ export class GraphqlQueryFindDuplicatesResolverService
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
duplicateRecordsQueryBuilder,
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
duplicateConditions,
);
const nonFormattedDuplicates =
(await withFilterQueryBuilder.getMany()) as ObjectRecord[];
(await withFilterQueryBuilder.getMany()) as T[];
const duplicates = formatResult(
nonFormattedDuplicates,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
return typeORMObjectRecordsParser.createConnection({
objectRecords: duplicates,
objectName: objectMetadataMapItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: duplicates.length,
totalCount: duplicates.length,
order: [{ id: OrderByDirection.AscNullsFirst }],
@@ -130,16 +133,16 @@ export class GraphqlQueryFindDuplicatesResolverService
}
private buildDuplicateConditions(
objectMetadataMapItem: ObjectMetadataMapItem,
records?: Partial<IRecord>[] | undefined,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
records?: Partial<ObjectRecord>[] | undefined,
filteringByExistingRecordId?: string,
): Partial<RecordFilter> {
): Partial<ObjectRecordFilter> {
if (!records || records.length === 0) {
return {};
}
const criteriaCollection = this.getApplicableDuplicateCriteriaCollection(
objectMetadataMapItem,
objectMetadataItemWithFieldMaps,
);
const conditions = records.flatMap((record) => {
@@ -164,7 +167,7 @@ export class GraphqlQueryFindDuplicatesResolverService
});
});
const filter: Partial<RecordFilter> = {};
const filter: Partial<ObjectRecordFilter> = {};
if (conditions && !isEmpty(conditions)) {
filter.or = conditions;
@@ -178,11 +181,12 @@ export class GraphqlQueryFindDuplicatesResolverService
}
private getApplicableDuplicateCriteriaCollection(
objectMetadataMapItem: ObjectMetadataMapItem,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
return DUPLICATE_CRITERIA_COLLECTION.filter(
(duplicateCriteria) =>
duplicateCriteria.objectName === objectMetadataMapItem.nameSingular,
duplicateCriteria.objectName ===
objectMetadataItemWithFieldMaps.nameSingular,
);
}

View File

@@ -1,15 +1,14 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
Record as IRecord,
ObjectRecord,
ObjectRecordFilter,
ObjectRecordOrderBy,
OrderByDirection,
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -19,35 +18,45 @@ import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
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 { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter';
import {
getCursor,
getPaginationInfo,
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
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';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { isDefined } from 'src/utils/is-defined';
@Injectable()
export class GraphqlQueryFindManyResolverService
implements ResolverService<FindManyResolverArgs, IConnection<IRecord>>
implements ResolverService<FindManyResolverArgs, IConnection<ObjectRecord>>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly featureFlagService: FeatureFlagService,
) {}
async resolve<
ObjectRecord extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
T extends ObjectRecord = ObjectRecord,
Filter extends ObjectRecordFilter = ObjectRecordFilter,
OrderBy extends ObjectRecordOrderBy = ObjectRecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
const { authContext, objectMetadataMapItem, info, objectMetadataMap } =
options;
): Promise<IConnection<T>> {
const {
authContext,
objectMetadataItemWithFieldMaps,
info,
objectMetadataMaps,
} = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
@@ -55,47 +64,43 @@ export class GraphqlQueryFindManyResolverService
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const countQueryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
const aggregateQueryBuilder = repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataMaps,
);
const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
countQueryBuilder,
objectMetadataMapItem.nameSingular,
args.filter ?? ({} as Filter),
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
selectedFields,
);
const isForwardPagination = !isDefined(args.before);
const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS;
const withDeletedCountQueryBuilder =
graphqlQueryParser.applyDeletedAtToBuilder(
withFilterCountQueryBuilder,
const withFilterAggregateQueryBuilder =
graphqlQueryParser.applyFilterToBuilder(
aggregateQueryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
args.filter ?? ({} as Filter),
);
const totalCount = isDefined(selectedFields.totalCount)
? await withDeletedCountQueryBuilder.getCount()
: 0;
const selectedFields = graphqlFields(info);
const graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult =
graphqlQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
selectedFields,
);
const isForwardPagination = !isDefined(args.before);
const withDeletedAggregateQueryBuilder =
graphqlQueryParser.applyDeletedAtToBuilder(
withFilterAggregateQueryBuilder,
args.filter ?? ({} as Filter),
);
const cursor = getCursor(args);
@@ -110,7 +115,7 @@ export class GraphqlQueryFindManyResolverService
const cursorArgFilter = computeCursorArgFilter(
cursor,
orderByWithIdCondition,
objectMetadataMapItem.fields,
objectMetadataItemWithFieldMaps.fieldsByName,
isForwardPagination,
);
@@ -123,14 +128,14 @@ export class GraphqlQueryFindManyResolverService
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
appliedFilters,
);
const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
withFilterQueryBuilder,
orderByWithIdCondition,
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
isForwardPagination,
);
@@ -139,14 +144,36 @@ export class GraphqlQueryFindManyResolverService
args.filter ?? ({} as Filter),
);
const isAggregationsEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsAggregateQueryEnabled,
authContext.workspace.id,
);
if (!isAggregationsEnabled) {
graphqlQuerySelectedFieldsResult.aggregate = {
totalCount: graphqlQuerySelectedFieldsResult.aggregate.totalCount,
};
}
const processAggregateHelper = new ProcessAggregateHelper();
processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
fieldMetadataMapByName: objectMetadataItemWithFieldMaps.fieldsByName,
selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate,
queryBuilder: withDeletedAggregateQueryBuilder,
});
const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS;
const nonFormattedObjectRecords = await withDeletedQueryBuilder
.take(limit + 1)
.getMany();
const objectRecords = formatResult(
nonFormattedObjectRecords,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const { hasNextPage, hasPreviousPage } = getPaginationInfo(
@@ -159,37 +186,42 @@ export class GraphqlQueryFindManyResolverService
objectRecords.pop();
}
const parentObjectRecordsAggregatedValues =
await withDeletedAggregateQueryBuilder.getRawOne();
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
objectRecords,
relations,
if (graphqlQuerySelectedFieldsResult.relations) {
await processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: objectRecords,
parentObjectRecordsAggregatedValues,
relations: graphqlQuerySelectedFieldsResult.relations,
aggregate: graphqlQuerySelectedFieldsResult.aggregate,
limit,
authContext,
dataSource,
);
});
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
const result = typeORMObjectRecordsParser.createConnection({
return typeORMObjectRecordsParser.createConnection({
objectRecords,
objectName: objectMetadataMapItem.nameSingular,
objectRecordsAggregatedValues: parentObjectRecordsAggregatedValues,
selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: limit,
totalCount,
totalCount: parentObjectRecordsAggregatedValues.totalCount,
order: orderByWithIdCondition,
hasNextPage,
hasPreviousPage,
});
return result;
}
async validate<Filter extends RecordFilter>(
async validate<Filter extends ObjectRecordFilter>(
args: FindManyResolverArgs<Filter>,
_options: WorkspaceQueryRunnerOptions,
): Promise<void> {

View File

@@ -4,9 +4,9 @@ import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
Record as IRecord,
RecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
ObjectRecord,
ObjectRecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -27,21 +27,25 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryFindOneResolverService
implements ResolverService<FindOneResolverArgs, IRecord>
implements ResolverService<FindOneResolverArgs, ObjectRecord>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<
ObjectRecord extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
T extends ObjectRecord = ObjectRecord,
Filter extends ObjectRecordFilter = ObjectRecordFilter,
>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const { authContext, objectMetadataMapItem, info, objectMetadataMap } =
options;
): Promise<T> {
const {
authContext,
objectMetadataItemWithFieldMaps,
info,
objectMetadataMaps,
} = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
@@ -49,28 +53,28 @@ export class GraphqlQueryFindOneResolverService
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
objectMetadataItemWithFieldMaps,
selectedFields,
);
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
args.filter ?? ({} as Filter),
);
@@ -83,8 +87,8 @@ export class GraphqlQueryFindOneResolverService
const objectRecord = formatResult(
nonFormattedObjectRecord,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
if (!objectRecord) {
@@ -99,29 +103,29 @@ export class GraphqlQueryFindOneResolverService
const objectRecords = [objectRecord];
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
objectRecords,
await processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: objectRecords,
relations,
QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
);
});
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return typeORMObjectRecordsParser.processRecord({
objectRecord: objectRecords[0],
objectName: objectMetadataMapItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
}) as ObjectRecord;
}) as T;
}
async validate<Filter extends RecordFilter>(
async validate<Filter extends ObjectRecordFilter>(
args: FindOneResolverArgs<Filter>,
_options: WorkspaceQueryRunnerOptions,
): Promise<void> {

View File

@@ -5,10 +5,10 @@ import { Brackets } from 'typeorm';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
Record as IRecord,
ObjectRecord,
ObjectRecordFilter,
OrderByDirection,
RecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -22,40 +22,39 @@ import { isDefined } from 'src/utils/is-defined';
@Injectable()
export class GraphqlQuerySearchResolverService
implements ResolverService<SearchResolverArgs, IConnection<IRecord>>
implements ResolverService<SearchResolverArgs, IConnection<ObjectRecord>>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<
ObjectRecord extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
T extends ObjectRecord = ObjectRecord,
Filter extends ObjectRecordFilter = ObjectRecordFilter,
>(
args: SearchResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
): Promise<IConnection<T>> {
const {
authContext,
objectMetadataItem,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
info,
} = options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
authContext.workspace.id,
objectMetadataItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
if (!isDefined(args.searchInput)) {
return typeORMObjectRecordsParser.createConnection({
objectRecords: [],
objectName: objectMetadataItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 0,
totalCount: 0,
order: [{ id: OrderByDirection.AscNullsFirst }],
@@ -69,16 +68,16 @@ export class GraphqlQuerySearchResolverService
const limit = args?.limit ?? QUERY_MAX_RECORDS;
const queryBuilder = repository.createQueryBuilder(
objectMetadataItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataMaps,
);
const queryBuilderWithFilter = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
args.filter ?? ({} as Filter),
);
@@ -109,7 +108,7 @@ export class GraphqlQuerySearchResolverService
.setParameter('searchTerms', searchTerms)
.setParameter('searchTermsOr', searchTermsOr)
.take(limit)
.getMany()) as ObjectRecord[];
.getMany()) as T[];
const objectRecords = await repository.formatResult(resultsWithTsVector);
@@ -122,7 +121,7 @@ export class GraphqlQuerySearchResolverService
return typeORMObjectRecordsParser.createConnection({
objectRecords: objectRecords ?? [],
objectName: objectMetadataItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: limit,
totalCount,
order,

View File

@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -20,18 +20,22 @@ import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryUpdateManyResolverService
implements ResolverService<UpdateManyResolverArgs, IRecord[]>
implements ResolverService<UpdateManyResolverArgs, ObjectRecord[]>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
async resolve<T extends ObjectRecord = ObjectRecord>(
args: UpdateManyResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
options;
): Promise<T[]> {
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
info,
} = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
@@ -39,28 +43,28 @@ export class GraphqlQueryUpdateManyResolverService
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
objectMetadataItemWithFieldMaps,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataMapItem.nameSingular,
objectMetadataMapItem.isCustom,
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
@@ -69,7 +73,7 @@ export class GraphqlQueryUpdateManyResolverService
args.filter,
);
const data = formatData(args.data, objectMetadataMapItem);
const data = formatData(args.data, objectMetadataItemWithFieldMaps);
const nonFormattedUpdatedObjectRecords = await withFilterQueryBuilder
.update(data)
@@ -78,42 +82,42 @@ export class GraphqlQueryUpdateManyResolverService
const updatedRecords = formatResult(
nonFormattedUpdatedObjectRecords.raw,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
updatedRecords,
await processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: updatedRecords,
relations,
QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
);
});
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return updatedRecords.map((record: ObjectRecord) =>
return updatedRecords.map((record: T) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataMapItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
}),
);
}
async validate<ObjectRecord extends IRecord = IRecord>(
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
async validate<T extends ObjectRecord = ObjectRecord>(
args: UpdateManyResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<void> {
assertMutationNotOnRemoteObject(options.objectMetadataMapItem);
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
if (!args.filter) {
throw new Error('Filter is required');
}

View File

@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -23,18 +23,22 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryUpdateOneResolverService
implements ResolverService<UpdateOneResolverArgs, IRecord>
implements ResolverService<UpdateOneResolverArgs, ObjectRecord>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
async resolve<T extends ObjectRecord = ObjectRecord>(
args: UpdateOneResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
options;
): Promise<T> {
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
info,
} = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
@@ -42,26 +46,26 @@ export class GraphqlQueryUpdateOneResolverService
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
objectMetadataItemWithFieldMaps,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
objectMetadataItemWithFieldMaps.nameSingular,
);
const data = formatData(args.data, objectMetadataMapItem);
const data = formatData(args.data, objectMetadataItemWithFieldMaps);
const result = await queryBuilder
.update(data)
@@ -73,8 +77,8 @@ export class GraphqlQueryUpdateOneResolverService
const updatedRecords = formatResult(
nonFormattedUpdatedObjectRecords,
objectMetadataMapItem,
objectMetadataMap,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
if (updatedRecords.length === 0) {
@@ -84,38 +88,38 @@ export class GraphqlQueryUpdateOneResolverService
);
}
const updatedRecord = updatedRecords[0] as ObjectRecord;
const updatedRecord = updatedRecords[0] as T;
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
[updatedRecord],
await processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: [updatedRecord],
relations,
QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
);
});
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return typeORMObjectRecordsParser.processRecord<ObjectRecord>({
return typeORMObjectRecordsParser.processRecord<T>({
objectRecord: updatedRecord,
objectName: objectMetadataMapItem.nameSingular,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
});
}
async validate<ObjectRecord extends IRecord = IRecord>(
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
async validate<T extends ObjectRecord = ObjectRecord>(
args: UpdateOneResolverArgs<Partial<T>>,
options: WorkspaceQueryRunnerOptions,
): Promise<void> {
assertMutationNotOnRemoteObject(options.objectMetadataMapItem);
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
assertIsValidUuid(args.id);
}
}

View File

@@ -1,18 +1,18 @@
import { Injectable } from '@nestjs/common';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
@Injectable()
export class ApiEventEmitterService {
constructor(private readonly workspaceEventEmitter: WorkspaceEventEmitter) {}
public emitCreateEvents<T extends IRecord>(
public emitCreateEvents<T extends ObjectRecord>(
records: T[],
authContext: AuthContext,
objectMetadataItem: ObjectMetadataInterface,
@@ -32,7 +32,7 @@ export class ApiEventEmitterService {
);
}
public emitUpdateEvents<T extends IRecord>(
public emitUpdateEvents<T extends ObjectRecord>(
existingRecords: T[],
records: T[],
updatedFields: string[],
@@ -77,7 +77,7 @@ export class ApiEventEmitterService {
);
}
public emitDeletedEvents<T extends IRecord>(
public emitDeletedEvents<T extends ObjectRecord>(
records: T[],
authContext: AuthContext,
objectMetadataItem: ObjectMetadataInterface,
@@ -99,7 +99,7 @@ export class ApiEventEmitterService {
);
}
public emitDestroyEvents<T extends IRecord>(
public emitDestroyEvents<T extends ObjectRecord>(
records: T[],
authContext: AuthContext,
objectMetadataItem: ObjectMetadataInterface,
@@ -121,9 +121,7 @@ export class ApiEventEmitterService {
);
}
private removeGraphQLAndNestedProperties<ObjectRecord extends IRecord>(
record: ObjectRecord,
) {
private removeGraphQLAndNestedProperties<T extends ObjectRecord>(record: T) {
if (!record) {
return {};
}

View File

@@ -1,8 +1,8 @@
import {
ObjectRecordFilter,
ObjectRecordOrderBy,
OrderByDirection,
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import {
GraphqlQueryRunnerException,
@@ -11,14 +11,14 @@ import {
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
export const computeCursorArgFilter = (
cursor: Record<string, any>,
orderBy: RecordOrderBy,
fieldMetadataMap: FieldMetadataMap,
orderBy: ObjectRecordOrderBy,
fieldMetadataMapByName: FieldMetadataMap,
isForwardPagination = true,
): RecordFilter[] => {
): ObjectRecordFilter[] => {
const cursorKeys = Object.keys(cursor ?? {});
const cursorValues = Object.values(cursor ?? {});
@@ -39,7 +39,7 @@ export const computeCursorArgFilter = (
...buildWhereCondition(
cursorKeys[subConditionIndex],
cursorValues[subConditionIndex],
fieldMetadataMap,
fieldMetadataMapByName,
'eq',
),
};
@@ -68,18 +68,18 @@ export const computeCursorArgFilter = (
return {
...whereCondition,
...buildWhereCondition(key, value, fieldMetadataMap, operator),
} as RecordFilter;
...buildWhereCondition(key, value, fieldMetadataMapByName, operator),
} as ObjectRecordFilter;
});
};
const buildWhereCondition = (
key: string,
value: any,
fieldMetadataMap: FieldMetadataMap,
fieldMetadataMapByName: FieldMetadataMap,
operator: string,
): Record<string, any> => {
const fieldMetadata = fieldMetadataMap[key];
const fieldMetadata = fieldMetadataMapByName[key];
if (!fieldMetadata) {
throw new GraphqlQueryRunnerException(

View File

@@ -1,7 +1,7 @@
import {
Record as IRecord,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
ObjectRecord,
ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
@@ -24,9 +24,9 @@ export const decodeCursor = (cursor: string): CursorData => {
}
};
export const encodeCursor = <ObjectRecord extends IRecord = IRecord>(
objectRecord: ObjectRecord,
order: RecordOrderBy | undefined,
export const encodeCursor = <T extends ObjectRecord = ObjectRecord>(
objectRecord: T,
order: ObjectRecordOrderBy | undefined,
): string => {
const orderByValues: Record<string, any> = {};

View File

@@ -1,21 +0,0 @@
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
export const getObjectMetadataOrThrow = (
objectMetadataMap: Record<string, any>,
objectName: string,
): ObjectMetadataMapItem => {
const objectMetadata = objectMetadataMap[objectName];
if (!objectMetadata) {
throw new GraphqlQueryRunnerException(
`Object metadata not found for ${objectName}`,
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
return objectMetadata;
};

View File

@@ -1,7 +1,7 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import {
deduceRelationDirection,
RelationDirection,
@@ -9,7 +9,7 @@ import {
export const getRelationObjectMetadata = (
fieldMetadata: FieldMetadataInterface,
objectMetadataMap: ObjectMetadataMap,
objectMetadataMaps: ObjectMetadataMaps,
) => {
const relationMetadata = getRelationMetadata(fieldMetadata);
@@ -20,8 +20,8 @@ export const getRelationObjectMetadata = (
const referencedObjectMetadata =
relationDirection === RelationDirection.TO
? objectMetadataMap[relationMetadata.fromObjectMetadataId]
: objectMetadataMap[relationMetadata.toObjectMetadataId];
? objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId]
: objectMetadataMaps.byId[relationMetadata.toObjectMetadataId];
if (!referencedObjectMetadata) {
throw new Error(

View File

@@ -1,4 +1,4 @@
export interface Record {
export interface ObjectRecord {
id: string;
[key: string]: any;
createdAt: string;
@@ -6,8 +6,8 @@ export interface Record {
deletedAt: string | null;
}
export type RecordFilter = {
[Property in keyof Record]: any;
export type ObjectRecordFilter = {
[Property in keyof ObjectRecord]: any;
};
export enum OrderByDirection {
@@ -17,11 +17,11 @@ export enum OrderByDirection {
DescNullsLast = 'DescNullsLast',
}
export type RecordOrderBy = Array<{
[Property in keyof Record]?: OrderByDirection;
export type ObjectRecordOrderBy = Array<{
[Property in keyof ObjectRecord]?: OrderByDirection;
}>;
export interface RecordDuplicateCriteria {
export interface ObjectRecordDuplicateCriteria {
objectName: string;
columnNames: string[];
}

View File

@@ -1,12 +1,12 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
describe('QueryRunnerArgsFactory', () => {
const recordPositionFactory = {
@@ -14,13 +14,29 @@ describe('QueryRunnerArgsFactory', () => {
};
const workspaceId = 'workspaceId';
const options = {
fieldMetadataCollection: [
{ name: 'position', type: FieldMetadataType.POSITION },
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
] as FieldMetadataInterface[],
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
authContext: { workspace: { id: workspaceId } },
} as WorkspaceQueryRunnerOptions;
objectMetadataItemWithFieldMaps: {
isCustom: true,
nameSingular: 'testNumber',
fieldsByName: {
position: {
type: FieldMetadataType.POSITION,
isCustom: true,
nameSingular: 'position',
},
testNumber: {
type: FieldMetadataType.NUMBER,
isCustom: true,
nameSingular: 'testNumber',
},
otherField: {
type: FieldMetadataType.TEXT,
isCustom: true,
nameSingular: 'otherField',
},
} as unknown as FieldMetadataMap,
},
} as unknown as WorkspaceQueryRunnerOptions;
let factory: QueryRunnerArgsFactory;
@@ -61,7 +77,7 @@ describe('QueryRunnerArgsFactory', () => {
it('createMany type should override data position and number', async () => {
const args = {
id: 'uuid',
data: [{ position: 'last', testNumber: '1' }],
data: [{ position: 'last', testNumber: 1 }],
};
const result = await factory.create(
@@ -72,7 +88,7 @@ describe('QueryRunnerArgsFactory', () => {
expect(recordPositionFactory.create).toHaveBeenCalledWith(
'last',
{ isCustom: true, nameSingular: 'test' },
{ isCustom: true, nameSingular: 'testNumber' },
workspaceId,
0,
);
@@ -85,7 +101,7 @@ describe('QueryRunnerArgsFactory', () => {
it('createMany type should override position if not present', async () => {
const args = {
id: 'uuid',
data: [{ testNumber: '1' }],
data: [{ testNumber: 1 }],
};
const result = await factory.create(
@@ -96,7 +112,7 @@ describe('QueryRunnerArgsFactory', () => {
expect(recordPositionFactory.create).toHaveBeenCalledWith(
'first',
{ isCustom: true, nameSingular: 'test' },
{ isCustom: true, nameSingular: 'testNumber' },
workspaceId,
0,
);
@@ -109,7 +125,7 @@ describe('QueryRunnerArgsFactory', () => {
it('findMany type should override data position and number', async () => {
const args = {
id: 'uuid',
filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } },
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
};
const result = await factory.create(
@@ -127,7 +143,7 @@ describe('QueryRunnerArgsFactory', () => {
it('findOne type should override number in filter', async () => {
const args = {
id: 'uuid',
filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } },
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
};
const result = await factory.create(
@@ -143,23 +159,14 @@ describe('QueryRunnerArgsFactory', () => {
});
it('findDuplicates type should override number in data and id', async () => {
const optionsDuplicate = {
fieldMetadataCollection: [
{ name: 'id', type: FieldMetadataType.NUMBER },
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
] as FieldMetadataInterface[],
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
authContext: { workspace: { id: workspaceId } },
} as WorkspaceQueryRunnerOptions;
const args = {
ids: ['123'],
data: [{ testNumber: '1', otherField: 'test' }],
ids: [123],
data: [{ testNumber: 1, otherField: 'test' }],
};
const result = await factory.create(
args,
optionsDuplicate,
options,
ResolverArgsType.FindDuplicates,
);

View File

@@ -1,6 +1,9 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import {
ObjectRecord,
ObjectRecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import {
CreateManyResolverArgs,
@@ -10,13 +13,11 @@ import {
ResolverArgs,
ResolverArgsType,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
Record,
RecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { hasPositionField } from 'src/engine/metadata-modules/object-metadata/utils/has-position-field.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { RecordPositionFactory } from './record-position.factory';
@@ -34,27 +35,28 @@ export class QueryRunnerArgsFactory {
options: WorkspaceQueryRunnerOptions,
resolverArgsType: ResolverArgsType,
) {
const fieldMetadataCollection = options.fieldMetadataCollection;
const fieldMetadataMapByNameByName =
options.objectMetadataItemWithFieldMaps.fieldsByName;
const fieldMetadataMap = new Map(
fieldMetadataCollection.map((fieldMetadata) => [
fieldMetadata.name,
fieldMetadata,
]),
const shouldBackfillPosition = hasPositionField(
options.objectMetadataItemWithFieldMaps,
);
const shouldBackfillPosition = hasPositionField(options.objectMetadataItem);
switch (resolverArgsType) {
case ResolverArgsType.CreateMany:
return {
...args,
data: await Promise.all(
(args as CreateManyResolverArgs).data?.map((arg, index) =>
this.overrideDataByFieldMetadata(arg, options, fieldMetadataMap, {
argIndex: index,
shouldBackfillPosition,
}),
this.overrideDataByFieldMetadata(
arg,
options,
fieldMetadataMapByNameByName,
{
argIndex: index,
shouldBackfillPosition,
},
),
) ?? [],
),
} satisfies CreateManyResolverArgs;
@@ -63,7 +65,7 @@ export class QueryRunnerArgsFactory {
...args,
filter: await this.overrideFilterByFieldMetadata(
(args as FindOneResolverArgs).filter,
fieldMetadataMap,
fieldMetadataMapByNameByName,
),
};
case ResolverArgsType.FindMany:
@@ -71,7 +73,7 @@ export class QueryRunnerArgsFactory {
...args,
filter: await this.overrideFilterByFieldMetadata(
(args as FindManyResolverArgs).filter,
fieldMetadataMap,
fieldMetadataMapByNameByName,
),
};
@@ -80,15 +82,24 @@ export class QueryRunnerArgsFactory {
...args,
ids: (await Promise.all(
(args as FindDuplicatesResolverArgs).ids?.map((id) =>
this.overrideValueByFieldMetadata('id', id, fieldMetadataMap),
this.overrideValueByFieldMetadata(
'id',
id,
fieldMetadataMapByNameByName,
),
) ?? [],
)) as string[],
data: await Promise.all(
(args as FindDuplicatesResolverArgs).data?.map((arg, index) =>
this.overrideDataByFieldMetadata(arg, options, fieldMetadataMap, {
argIndex: index,
shouldBackfillPosition,
}),
this.overrideDataByFieldMetadata(
arg,
options,
fieldMetadataMapByNameByName,
{
argIndex: index,
shouldBackfillPosition,
},
),
) ?? [],
),
} satisfies FindDuplicatesResolverArgs;
@@ -98,9 +109,9 @@ export class QueryRunnerArgsFactory {
}
private async overrideDataByFieldMetadata(
data: Partial<Record> | undefined,
data: Partial<ObjectRecord> | undefined,
options: WorkspaceQueryRunnerOptions,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
fieldMetadataMapByNameByName: Record<string, FieldMetadataInterface>,
argPositionBackfillInput: ArgPositionBackfillInput,
) {
if (!data) {
@@ -111,7 +122,7 @@ export class QueryRunnerArgsFactory {
const createArgPromiseByArgKey = Object.entries(data).map(
async ([key, value]) => {
const fieldMetadata = fieldMetadataMap.get(key);
const fieldMetadata = fieldMetadataMapByNameByName[key];
if (!fieldMetadata) {
return [key, await Promise.resolve(value)];
@@ -126,8 +137,9 @@ export class QueryRunnerArgsFactory {
await this.recordPositionFactory.create(
value,
{
isCustom: options.objectMetadataItem.isCustom,
nameSingular: options.objectMetadataItem.nameSingular,
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
nameSingular:
options.objectMetadataItemWithFieldMaps.nameSingular,
},
options.authContext.workspace.id,
argPositionBackfillInput.argIndex,
@@ -154,8 +166,9 @@ export class QueryRunnerArgsFactory {
await this.recordPositionFactory.create(
'first',
{
isCustom: options.objectMetadataItem.isCustom,
nameSingular: options.objectMetadataItem.nameSingular,
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
nameSingular:
options.objectMetadataItemWithFieldMaps.nameSingular,
},
options.authContext.workspace.id,
argPositionBackfillInput.argIndex,
@@ -168,23 +181,27 @@ export class QueryRunnerArgsFactory {
}
private overrideFilterByFieldMetadata(
filter: RecordFilter | undefined,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
filter: ObjectRecordFilter | undefined,
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
) {
if (!filter) {
return;
}
const overrideFilter = (filterObject: RecordFilter) => {
const overrideFilter = (filterObject: ObjectRecordFilter) => {
return Object.entries(filterObject).reduce((acc, [key, value]) => {
if (key === 'and' || key === 'or') {
acc[key] = value.map((nestedFilter: RecordFilter) =>
acc[key] = value.map((nestedFilter: ObjectRecordFilter) =>
overrideFilter(nestedFilter),
);
} else if (key === 'not') {
acc[key] = overrideFilter(value);
} else {
acc[key] = this.transformValueByType(key, value, fieldMetadataMap);
acc[key] = this.transformValueByType(
key,
value,
fieldMetadataMapByName,
);
}
return acc;
@@ -197,9 +214,9 @@ export class QueryRunnerArgsFactory {
private transformValueByType(
key: string,
value: any,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
fieldMetadataMapByName: FieldMetadataMap,
) {
const fieldMetadata = fieldMetadataMap.get(key);
const fieldMetadata = fieldMetadataMapByName[key];
if (!fieldMetadata) {
return value;
@@ -226,9 +243,9 @@ export class QueryRunnerArgsFactory {
private async overrideValueByFieldMetadata(
key: string,
value: any,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
fieldMetadataMapByName: FieldMetadataMap,
) {
const fieldMetadata = fieldMetadataMap.get(key);
const fieldMetadata = fieldMetadataMapByName[key];
if (!fieldMetadata) {
return value;

View File

@@ -1,15 +0,0 @@
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
export interface PGGraphQLResponse<Data = any> {
resolve: {
data: Data;
errors: any[];
};
}
export type PGGraphQLResult<Data = any> = [PGGraphQLResponse<Data>];
export interface PGGraphQLMutation<Record = IRecord> {
affectedRows: number;
records: Record[];
}

View File

@@ -1,20 +1,12 @@
import { GraphQLResolveInfo } from 'graphql';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import {
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
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';
export interface WorkspaceQueryRunnerOptions {
authContext: AuthContext;
info: GraphQLResolveInfo;
objectMetadataItem: ObjectMetadataInterface;
fieldMetadataCollection: FieldMetadataInterface[];
objectMetadataCollection: ObjectMetadataInterface[];
objectMetadataMap: ObjectMetadataMap;
objectMetadataMapItem: ObjectMetadataMapItem;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
}

View File

@@ -1,12 +1,12 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type';
import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
@Injectable()
export class TelemetryListener {

View File

@@ -1,8 +1,8 @@
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { isDefined } from 'src/utils/is-defined';
export const withSoftDeleted = <T extends RecordFilter>(
export const withSoftDeleted = <T extends ObjectRecordFilter>(
filter: T | undefined | null,
): boolean => {
if (!isDefined(filter)) {

View File

@@ -31,22 +31,23 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
if (indexNameMatch) {
const indexName = indexNameMatch[1];
const deletedAtFieldMetadata = context.objectMetadataItem.fields.find(
(field) => field.name === 'deletedAt',
);
const deletedAtFieldMetadata =
context.objectMetadataItemWithFieldMaps.fieldsByName['deletedAt'];
const affectedColumns = context.objectMetadataItem.indexMetadatas
.find((index) => index.name === indexName)
?.indexFieldMetadatas?.filter(
(field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id,
)
.map((indexField) => {
const fieldMetadata = context.objectMetadataItem.fields.find(
(objectField) => indexField.fieldMetadataId === objectField.id,
);
const affectedColumns =
context.objectMetadataItemWithFieldMaps.indexMetadatas
.find((index) => index.name === indexName)
?.indexFieldMetadatas?.filter(
(field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id,
)
.map((indexField) => {
const fieldMetadata =
context.objectMetadataItemWithFieldMaps.fieldsById[
indexField.fieldMetadataId
];
return fieldMetadata?.label;
});
return fieldMetadata?.label;
});
const columnNames = affectedColumns?.join(', ');

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import merge from 'lodash.merge';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
@@ -53,13 +53,13 @@ export class WorkspaceQueryHookService {
public async executePostQueryHooks<
T extends WorkspaceResolverBuilderMethodNames,
Record extends IRecord = IRecord,
U extends ObjectRecord = ObjectRecord,
>(
authContext: AuthContext,
// TODO: We should allow wildcard for object name
objectName: string,
methodName: T,
payload: Record[],
payload: U[],
): Promise<void> {
const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`;
const postHookInstances =

View File

@@ -30,12 +30,10 @@ export class CreateManyResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.createMany(args, options);

View File

@@ -30,12 +30,10 @@ export class CreateOneResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.createOne(args, options);

View File

@@ -30,12 +30,10 @@ export class DeleteManyResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.deleteMany(args, options);

View File

@@ -30,12 +30,10 @@ export class DeleteOneResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.deleteOne(args, options);

View File

@@ -30,12 +30,10 @@ export class DestroyManyResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.destroyMany(args, options);

View File

@@ -30,12 +30,10 @@ export class DestroyOneResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphQLQueryRunnerService.destroyOne(args, options);

View File

@@ -30,12 +30,10 @@ export class FindDuplicatesResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.findDuplicates(

View File

@@ -30,12 +30,10 @@ export class FindManyResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.findMany(args, options);

View File

@@ -30,12 +30,10 @@ export class FindOneResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.findOne(args, options);

View File

@@ -30,12 +30,10 @@ export class RestoreManyResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.restoreMany(args, options);

View File

@@ -28,12 +28,10 @@ export class SearchResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.search(args, options);

View File

@@ -30,12 +30,10 @@ export class UpdateManyResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.updateMany(args, options);

View File

@@ -30,12 +30,10 @@ export class UpdateOneResolverFactory
try {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
objectMetadataMap: internalContext.objectMetadataMap,
objectMetadataMapItem: internalContext.objectMetadataMapItem,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.updateOne(args, options);

View File

@@ -1,14 +0,0 @@
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
export interface PGGraphQLResponse<Data = any> {
resolve: {
data: Data;
};
}
export type PGGraphQLResult<Data = any> = [PGGraphQLResponse<Data>];
export interface PGGraphQLMutation<Record = IRecord> {
affectedRows: number;
records: Record[];
}

View File

@@ -1,10 +1,10 @@
import { GraphQLFieldResolver } from 'graphql';
import {
Record,
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
ObjectRecord,
ObjectRecordFilter,
ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
@@ -26,8 +26,8 @@ export enum ResolverArgsType {
}
export interface FindManyResolverArgs<
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
Filter extends ObjectRecordFilter = ObjectRecordFilter,
OrderBy extends ObjectRecordOrderBy = ObjectRecordOrderBy,
> {
first?: number;
last?: number;
@@ -42,14 +42,14 @@ export interface FindOneResolverArgs<Filter = any> {
}
export interface FindDuplicatesResolverArgs<
Data extends Partial<Record> = Partial<Record>,
Data extends Partial<ObjectRecord> = Partial<ObjectRecord>,
> {
ids?: string[];
data?: Data[];
}
export interface SearchResolverArgs<
Filter extends RecordFilter = RecordFilter,
Filter extends ObjectRecordFilter = ObjectRecordFilter,
> {
searchInput?: string;
filter?: Filter;
@@ -57,28 +57,28 @@ export interface SearchResolverArgs<
}
export interface CreateOneResolverArgs<
Data extends Partial<Record> = Partial<Record>,
Data extends Partial<ObjectRecord> = Partial<ObjectRecord>,
> {
data: Data;
upsert?: boolean;
}
export interface CreateManyResolverArgs<
Data extends Partial<Record> = Partial<Record>,
Data extends Partial<ObjectRecord> = Partial<ObjectRecord>,
> {
data: Data[];
upsert?: boolean;
}
export interface UpdateOneResolverArgs<
Data extends Partial<Record> = Partial<Record>,
Data extends Partial<ObjectRecord> = Partial<ObjectRecord>,
> {
id: string;
data: Data;
}
export interface UpdateManyResolverArgs<
Data extends Partial<Record> = Partial<Record>,
Data extends Partial<ObjectRecord> = Partial<ObjectRecord>,
Filter = any,
> {
filter: Filter;

View File

@@ -2,8 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { IResolvers } from '@graphql-tools/utils';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
@@ -11,7 +9,7 @@ import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-res
import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory';
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
import { CreateManyResolverFactory } from './factories/create-many-resolver.factory';
@@ -49,8 +47,7 @@ export class WorkspaceResolverFactory {
async create(
authContext: AuthContext,
objectMetadataCollection: ObjectMetadataInterface[],
objectMetadataMap: ObjectMetadataMap,
objectMetadataMaps: ObjectMetadataMaps,
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
): Promise<IResolvers> {
const factories = new Map<
@@ -76,7 +73,7 @@ export class WorkspaceResolverFactory {
Mutation: {},
};
for (const objectMetadata of objectMetadataCollection) {
for (const objectMetadata of Object.values(objectMetadataMaps.byId)) {
// Generate query resolvers
for (const methodName of workspaceResolverBuilderMethods.queries) {
const resolverName = getResolverName(objectMetadata, methodName);
@@ -94,11 +91,8 @@ export class WorkspaceResolverFactory {
resolvers.Query[resolverName] = resolverFactory.create({
authContext,
objectMetadataItem: objectMetadata,
fieldMetadataCollection: objectMetadata.fields,
objectMetadataCollection,
objectMetadataMap,
objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular],
objectMetadataMaps,
objectMetadataItemWithFieldMaps: objectMetadata,
});
}
@@ -119,11 +113,8 @@ export class WorkspaceResolverFactory {
resolvers.Mutation[resolverName] = resolverFactory.create({
authContext,
objectMetadataItem: objectMetadata,
fieldMetadataCollection: objectMetadata.fields,
objectMetadataCollection,
objectMetadataMap,
objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular],
objectMetadataMaps,
objectMetadataItemWithFieldMaps: objectMetadata,
});
}
}

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import {
AggregationField,
getAvailableAggregationsFromObjectFields,
} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
type AggregationGraphQLType = Pick<AggregationField, 'type' | 'description'>;
@Injectable()
export class AggregationTypeFactory {
public create(
objectMetadata: ObjectMetadataInterface,
): Record<string, AggregationGraphQLType> {
const availableAggregations = getAvailableAggregationsFromObjectFields(
objectMetadata.fields,
);
return Object.entries(availableAggregations).reduce<
Record<string, AggregationGraphQLType>
>((acc, [key, agg]) => {
acc[key] = {
type: agg.type,
description: agg.description,
};
return acc;
}, {});
}
}

View File

@@ -1,17 +1,18 @@
import { Injectable } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLInt, GraphQLObjectType } from 'graphql';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { AggregationTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory';
import { pascalCase } from 'src/utils/pascal-case';
import { ConnectionTypeFactory } from './connection-type.factory';
import {
ObjectTypeDefinition,
ObjectTypeDefinitionKind,
} from './object-type-definition.factory';
import { ConnectionTypeFactory } from './connection-type.factory';
export enum ConnectionTypeDefinitionKind {
Edge = 'Edge',
@@ -20,7 +21,10 @@ export enum ConnectionTypeDefinitionKind {
@Injectable()
export class ConnectionTypeDefinitionFactory {
constructor(private readonly connectionTypeFactory: ConnectionTypeFactory) {}
constructor(
private readonly connectionTypeFactory: ConnectionTypeFactory,
private readonly aggregationTypeFactory: AggregationTypeFactory,
) {}
public create(
objectMetadata: ObjectMetadataInterface,
@@ -45,6 +49,10 @@ export class ConnectionTypeDefinitionFactory {
): GraphQLFieldConfigMap<any, any> {
const fields: GraphQLFieldConfigMap<any, any> = {};
const aggregatedFields = this.aggregationTypeFactory.create(objectMetadata);
Object.assign(fields, aggregatedFields);
fields.edges = {
type: this.connectionTypeFactory.create(
objectMetadata,
@@ -69,11 +77,6 @@ export class ConnectionTypeDefinitionFactory {
),
};
fields.totalCount = {
type: GraphQLInt,
description: 'Total number of records in the connection',
};
return fields;
}
}

View File

@@ -1,23 +1,24 @@
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { AggregationTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory';
import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory';
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
import { ArgsFactory } from './args.factory';
import { InputTypeFactory } from './input-type.factory';
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
import { ConnectionTypeFactory } from './connection-type.factory';
import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory';
import { EdgeTypeFactory } from './edge-type.factory';
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
import { InputTypeDefinitionFactory } from './input-type-definition.factory';
import { InputTypeFactory } from './input-type.factory';
import { MutationTypeFactory } from './mutation-type.factory';
import { ObjectTypeDefinitionFactory } from './object-type-definition.factory';
import { OrphanedTypesFactory } from './orphaned-types.factory';
import { OutputTypeFactory } from './output-type.factory';
import { QueryTypeFactory } from './query-type.factory';
import { RootTypeFactory } from './root-type.factory';
import { ConnectionTypeFactory } from './connection-type.factory';
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
import { EdgeTypeFactory } from './edge-type.factory';
import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory';
import { MutationTypeFactory } from './mutation-type.factory';
import { RelationTypeFactory } from './relation-type.factory';
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
import { OrphanedTypesFactory } from './orphaned-types.factory';
import { RootTypeFactory } from './root-type.factory';
export const workspaceSchemaBuilderFactories = [
ArgsFactory,
@@ -39,4 +40,5 @@ export const workspaceSchemaBuilderFactories = [
QueryTypeFactory,
MutationTypeFactory,
OrphanedTypesFactory,
AggregationTypeFactory,
];

View File

@@ -1,38 +0,0 @@
import { GraphQLScalarType } from 'graphql';
import { Kind } from 'graphql/language';
export const DateTimeScalarType = new GraphQLScalarType({
name: 'DateTime',
description: 'A custom scalar that represents a datetime in ISO format',
serialize(value: string): string {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new Error('Invalid date format, expected ISO date string');
}
return date.toISOString();
},
parseValue(value: string): Date {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new Error('Invalid date format, expected ISO date string');
}
return date;
},
parseLiteral(ast): Date {
if (ast.kind !== Kind.STRING) {
throw new Error('Invalid date format, expected ISO date string');
}
const date = new Date(ast.value);
if (isNaN(date.getTime())) {
throw new Error('Invalid date format, expected ISO date string');
}
return date;
},
});

View File

@@ -1,10 +1,9 @@
import { RawJSONScalar } from './raw-json.scalar';
import { PositionScalarType } from './position.scalar';
import { CursorScalarType } from './cursor.scalar';
import { BigFloatScalarType } from './big-float.scalar';
import { BigIntScalarType } from './big-int.scalar';
import { CursorScalarType } from './cursor.scalar';
import { DateScalarType } from './date.scalar';
import { DateTimeScalarType } from './date-time.scalar';
import { PositionScalarType } from './position.scalar';
import { RawJSONScalar } from './raw-json.scalar';
import { TimeScalarType } from './time.scalar';
import { UUIDScalarType } from './uuid.scalar';
@@ -12,7 +11,6 @@ export * from './big-float.scalar';
export * from './big-int.scalar';
export * from './cursor.scalar';
export * from './date.scalar';
export * from './date-time.scalar';
export * from './time.scalar';
export * from './uuid.scalar';
@@ -20,7 +18,6 @@ export const scalars = [
BigFloatScalarType,
BigIntScalarType,
DateScalarType,
DateTimeScalarType,
TimeScalarType,
UUIDScalarType,
CursorScalarType,

View File

@@ -1,17 +1,9 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import {
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
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';
export interface WorkspaceSchemaBuilderContext {
authContext: AuthContext;
fieldMetadataCollection: FieldMetadataInterface[];
objectMetadataCollection: ObjectMetadataInterface[];
objectMetadataItem: ObjectMetadataInterface;
objectMetadataMap: ObjectMetadataMap;
objectMetadataMapItem: ObjectMetadataMapItem;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}

View File

@@ -0,0 +1,84 @@
import { GraphQLISODateTime } from '@nestjs/graphql';
import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
enum AGGREGATION_OPERATIONS {
min = 'MIN',
max = 'MAX',
avg = 'AVG',
sum = 'SUM',
count = 'COUNT',
}
export type AggregationField = {
type: GraphQLScalarType;
description: string;
fromField: string;
aggregationOperation: AGGREGATION_OPERATIONS;
};
export const getAvailableAggregationsFromObjectFields = (
fields: FieldMetadataInterface[],
): Record<string, AggregationField> => {
return fields.reduce<Record<string, AggregationField>>((acc, field) => {
acc['totalCount'] = {
type: GraphQLInt,
description: `Total number of records in the connection`,
fromField: 'id',
aggregationOperation: AGGREGATION_OPERATIONS.count,
};
if (field.type === FieldMetadataType.DATE_TIME) {
acc[`min${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Oldest date contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.min,
};
acc[`max${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Most recent date contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.max,
};
}
if (field.type === FieldMetadataType.NUMBER) {
acc[`min${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Minimum amount contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.min,
};
acc[`max${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Maximum amount contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.max,
};
acc[`avg${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Average amount contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.avg,
};
acc[`sum${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Sum of amounts contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.sum,
};
}
return acc;
}, {});
};

View File

@@ -7,10 +7,10 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad
import { TypeDefinitionsGenerator } from './type-definitions.generator';
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface';
import { QueryTypeFactory } from './factories/query-type.factory';
import { MutationTypeFactory } from './factories/mutation-type.factory';
import { OrphanedTypesFactory } from './factories/orphaned-types.factory';
import { QueryTypeFactory } from './factories/query-type.factory';
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface';
@Injectable()
export class WorkspaceGraphQLSchemaFactory {

View File

@@ -56,13 +56,13 @@ export class WorkspaceSchemaFactory {
);
}
const objectMetadataMap =
await this.workspaceCacheStorageService.getObjectMetadataMap(
const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMaps(
authContext.workspace.id,
currentCacheVersion,
);
if (!objectMetadataMap) {
if (!objectMetadataMaps) {
await this.workspaceMetadataCacheService.recomputeMetadataCache({
workspaceId: authContext.workspace.id,
});
@@ -72,10 +72,10 @@ export class WorkspaceSchemaFactory {
);
}
const objectMetadataCollection = Object.values(objectMetadataMap).map(
const objectMetadataCollection = Object.values(objectMetadataMaps.byId).map(
(objectMetadataItem) => ({
...objectMetadataItem,
fields: Object.values(objectMetadataItem.fields),
fields: objectMetadataItem.fields,
indexes: objectMetadataItem.indexMetadatas,
}),
);
@@ -117,8 +117,7 @@ export class WorkspaceSchemaFactory {
const autoGeneratedResolvers = await this.workspaceResolverFactory.create(
authContext,
objectMetadataCollection,
objectMetadataMap,
objectMetadataMaps,
workspaceResolverBuilderMethodNames,
);
const scalarsResolvers =

View File

@@ -1,6 +1,6 @@
import { BadRequestException } from '@nestjs/common';
import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@@ -9,7 +9,7 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target
export const checkArrayFields = (
objectMetadata: ObjectMetadataInterface,
fields: Array<Partial<Record>>,
fields: Array<Partial<ObjectRecord>>,
): void => {
const fieldMetadataNames = objectMetadata.fields
.map((field) => {

View File

@@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';

View File

@@ -3,9 +3,9 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import {
ObjectRecordOrderBy,
OrderByDirection,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { checkArrayFields } from 'src/engine/api/rest/core/query-builder/utils/check-order-by.utils';
@@ -13,7 +13,7 @@ export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
@Injectable()
export class OrderByInputFactory {
create(request: Request, objectMetadata): RecordOrderBy {
create(request: Request, objectMetadata): ObjectRecordOrderBy {
const orderByQuery = request.query.order_by;
if (typeof orderByQuery !== 'string') {

View File

@@ -1,4 +1,4 @@
import { RecordDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecordDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
/**
* objectName: directly reference the name of the object from the metadata tables.
@@ -6,7 +6,7 @@ import { RecordDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-
* So if we need to reference a custom field, we should directly add the column name like `_customColumn`.
* If we need to terence a composite field, we should add all children of the composite like `nameFirstName` and `nameLastName`
*/
export const DUPLICATE_CRITERIA_COLLECTION: RecordDuplicateCriteria[] = [
export const DUPLICATE_CRITERIA_COLLECTION: ObjectRecordDuplicateCriteria[] = [
{
objectName: 'company',
columnNames: ['domainName'],

View File

@@ -1,11 +1,13 @@
import deepEqual from 'deep-equal';
import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
export const objectRecordChangedProperties = <
PRecord extends Partial<Record | BaseWorkspaceEntity> = Partial<Record>,
PRecord extends Partial<
ObjectRecord | BaseWorkspaceEntity
> = Partial<ObjectRecord>,
>(
oldRecord: PRecord,
newRecord: PRecord,

View File

@@ -1,23 +1,19 @@
import deepEqual from 'deep-equal';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const objectRecordChangedValues = (
oldRecord: Partial<IRecord>,
newRecord: Partial<IRecord>,
oldRecord: Partial<ObjectRecord>,
newRecord: Partial<ObjectRecord>,
updatedKeys: string[] | undefined,
objectMetadata: ObjectMetadataInterface,
objectMetadataItem: ObjectMetadataInterface,
) => {
const fieldsByKey = new Map(
objectMetadata.fields.map((field) => [field.name, field]),
);
return Object.keys(newRecord).reduce(
(acc, key) => {
const field = fieldsByKey.get(key);
const field = objectMetadataItem.fields.find((f) => f.name === key);
const oldRecordValue = oldRecord[key];
const newRecordValue = newRecord[key];

View File

@@ -8,12 +8,11 @@ export enum FeatureFlagKey {
IsFunctionSettingsEnabled = 'IS_FUNCTION_SETTINGS_ENABLED',
IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED',
IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED',
IsQueryRunnerTwentyORMEnabled = 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED',
IsWorkspaceFavoriteEnabled = 'IS_WORKSPACE_FAVORITE_ENABLED',
IsSSOEnabled = 'IS_SSO_ENABLED',
IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED',
IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED',
IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED',
IsMicrosoftSyncEnabled = 'IS_MICROSOFT_SYNC_ENABLED',
IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED',
IsAggregateQueryEnabled = 'IS_AGGREGATE_QUERY_ENABLED',
}

View File

@@ -1,4 +1,4 @@
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils';
import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';

View File

@@ -1,6 +1,6 @@
import { OpenAPIV3_1 } from 'openapi-types';
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils';
import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';

View File

@@ -467,13 +467,13 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
);
}
const objectMetadataMap =
await this.workspaceCacheStorageService.getObjectMetadataMap(
const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMaps(
workspaceId,
metadataVersion,
);
if (!objectMetadataMap) {
if (!objectMetadataMaps) {
throw new NotFoundException(
`Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`,
);
@@ -481,9 +481,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
const mappedResult = fieldMetadataItems.map((fieldMetadataItem) => {
const objectMetadata =
objectMetadataMap[fieldMetadataItem.objectMetadataId];
objectMetadataMaps.byId[fieldMetadataItem.objectMetadataId];
const fieldMetadata = objectMetadata.fields[fieldMetadataItem.id];
const fieldMetadata = objectMetadata.fieldsById[fieldMetadataItem.id];
const relationMetadata =
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
@@ -495,18 +495,18 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
}
const fromObjectMetadata =
objectMetadataMap[relationMetadata.fromObjectMetadataId];
objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId];
const toObjectMetadata =
objectMetadataMap[relationMetadata.toObjectMetadataId];
objectMetadataMaps.byId[relationMetadata.toObjectMetadataId];
const fromFieldMetadata =
objectMetadataMap[fromObjectMetadata.id].fields[
objectMetadataMaps.byId[fromObjectMetadata.id].fieldsById[
relationMetadata.fromFieldMetadataId
];
const toFieldMetadata =
objectMetadataMap[toObjectMetadata.id].fields[
objectMetadataMaps.byId[toObjectMetadata.id].fieldsById[
relationMetadata.toFieldMetadataId
];

View File

@@ -0,0 +1,3 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
export type FieldMetadataMap = Record<string, FieldMetadataInterface>;

View File

@@ -0,0 +1,8 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
export type ObjectMetadataItemWithFieldMaps = ObjectMetadataInterface & {
fieldsById: FieldMetadataMap;
fieldsByName: FieldMetadataMap;
};

View File

@@ -0,0 +1,7 @@
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export type ObjectMetadataMaps = {
byId: Record<string, ObjectMetadataItemWithFieldMaps>;
byNameSingular: Record<string, ObjectMetadataItemWithFieldMaps>;
byNamePlural: Record<string, ObjectMetadataItemWithFieldMaps>;
};

View File

@@ -1,36 +0,0 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
export type FieldMetadataMap = Record<string, FieldMetadataInterface>;
export type ObjectMetadataMapItem = Omit<ObjectMetadataInterface, 'fields'> & {
fields: FieldMetadataMap;
};
export type ObjectMetadataMap = Record<string, ObjectMetadataMapItem>;
export const generateObjectMetadataMap = (
objectMetadataCollection: ObjectMetadataInterface[],
): ObjectMetadataMap => {
const objectMetadataMap: ObjectMetadataMap = {};
for (const objectMetadata of objectMetadataCollection) {
const fieldsMap: FieldMetadataMap = {};
for (const fieldMetadata of objectMetadata.fields) {
fieldsMap[fieldMetadata.name] = fieldMetadata;
fieldsMap[fieldMetadata.id] = fieldMetadata;
}
const processedObjectMetadata: ObjectMetadataMapItem = {
...objectMetadata,
fields: fieldsMap,
};
objectMetadataMap[objectMetadata.id] = processedObjectMetadata;
objectMetadataMap[objectMetadata.nameSingular] = processedObjectMetadata;
objectMetadataMap[objectMetadata.namePlural] = processedObjectMetadata;
}
return objectMetadataMap;
};

View File

@@ -0,0 +1,39 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export const generateObjectMetadataMaps = (
objectMetadataCollection: ObjectMetadataInterface[],
): ObjectMetadataMaps => {
const objectMetadataMaps: ObjectMetadataMaps = {
byId: {},
byNameSingular: {},
byNamePlural: {},
};
for (const objectMetadata of objectMetadataCollection) {
const fieldsByIdMap: FieldMetadataMap = {};
const fieldsByNameMap: FieldMetadataMap = {};
for (const fieldMetadata of objectMetadata.fields) {
fieldsByNameMap[fieldMetadata.name] = fieldMetadata;
fieldsByIdMap[fieldMetadata.id] = fieldMetadata;
}
const processedObjectMetadata: ObjectMetadataItemWithFieldMaps = {
...objectMetadata,
fieldsById: fieldsByIdMap,
fieldsByName: fieldsByNameMap,
};
objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata;
objectMetadataMaps.byNameSingular[objectMetadata.nameSingular] =
processedObjectMetadata;
objectMetadataMaps.byNamePlural[objectMetadata.namePlural] =
processedObjectMetadata;
}
return objectMetadataMaps;
};

View File

@@ -1,12 +1,14 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import console from 'console';
import { Repository } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
import {
WorkspaceMetadataCacheException,
WorkspaceMetadataCacheExceptionCode,
@@ -85,15 +87,15 @@ export class WorkspaceMetadataCacheService {
console.timeEnd('fetching object metadata');
console.time('generating object metadata map');
const freshObjectMetadataMap =
generateObjectMetadataMap(objectMetadataItems);
const freshObjectMetadataMaps =
generateObjectMetadataMaps(objectMetadataItems);
console.timeEnd('generating object metadata map');
await this.workspaceCacheStorageService.setObjectMetadataMap(
await this.workspaceCacheStorageService.setObjectMetadataMaps(
workspaceId,
currentDatabaseVersion,
freshObjectMetadataMap,
freshObjectMetadataMaps,
);
await this.workspaceCacheStorageService.removeObjectMetadataOngoingCachingLock(

View File

@@ -10,7 +10,7 @@ import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-me
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
@@ -20,10 +20,10 @@ type EntitySchemaColumnMap = {
@Injectable()
export class EntitySchemaColumnFactory {
create(fieldMetadataMap: FieldMetadataMap): EntitySchemaColumnMap {
create(fieldMetadataMapByName: FieldMetadataMap): EntitySchemaColumnMap {
let entitySchemaColumnMap: EntitySchemaColumnMap = {};
const fieldMetadataCollection = Object.values(fieldMetadataMap);
const fieldMetadataCollection = Object.values(fieldMetadataMapByName);
for (const fieldMetadata of fieldMetadataCollection) {
const key = fieldMetadata.name;

View File

@@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common';
import { EntitySchemaRelationOptions } from 'typeorm';
import {
FieldMetadataMap,
ObjectMetadataMap,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
@@ -18,12 +16,12 @@ export class EntitySchemaRelationFactory {
constructor() {}
async create(
fieldMetadataMap: FieldMetadataMap,
objectMetadataMap: ObjectMetadataMap,
fieldMetadataMapByName: FieldMetadataMap,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<EntitySchemaRelationMap> {
const entitySchemaRelationMap: EntitySchemaRelationMap = {};
const fieldMetadataCollection = Object.values(fieldMetadataMap);
const fieldMetadataCollection = Object.values(fieldMetadataMapByName);
for (const fieldMetadata of fieldMetadataCollection) {
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
@@ -42,7 +40,7 @@ export class EntitySchemaRelationFactory {
const relationDetails = await determineRelationDetails(
fieldMetadata,
relationMetadata,
objectMetadataMap,
objectMetadataMaps,
);
entitySchemaRelationMap[fieldMetadata.name] = {

View File

@@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common';
import { EntitySchema } from 'typeorm';
import {
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
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 { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory';
import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory';
import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage';
@@ -20,17 +18,17 @@ export class EntitySchemaFactory {
async create(
workspaceId: string,
metadataVersion: number,
objectMetadata: ObjectMetadataMapItem,
objectMetadataMap: ObjectMetadataMap,
_metadataVersion: number,
objectMetadata: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<EntitySchema> {
const columns = this.entitySchemaColumnFactory.create(
objectMetadata.fields,
objectMetadata.fieldsByName,
);
const relations = await this.entitySchemaRelationFactory.create(
objectMetadata.fields,
objectMetadataMap,
objectMetadata.fieldsByName,
objectMetadataMaps,
);
const entitySchema = new EntitySchema({

View File

@@ -86,13 +86,13 @@ export class WorkspaceDatasourceFactory {
let cachedEntitySchemas: EntitySchema[];
const cachedObjectMetadataMap =
await this.workspaceCacheStorageService.getObjectMetadataMap(
const cachedObjectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMaps(
workspaceId,
cachedWorkspaceMetadataVersion,
);
if (!cachedObjectMetadataMap) {
if (!cachedObjectMetadataMaps) {
throw new TwentyORMException(
`Workspace Schema not found for workspace ${workspaceId}`,
TwentyORMExceptionCode.METADATA_COLLECTION_NOT_FOUND,
@@ -105,13 +105,14 @@ export class WorkspaceDatasourceFactory {
);
} else {
const entitySchemas = await Promise.all(
Object.values(cachedObjectMetadataMap).map((objectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
cachedWorkspaceMetadataVersion,
objectMetadata,
cachedObjectMetadataMap,
),
Object.values(cachedObjectMetadataMaps.byId).map(
(objectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
cachedWorkspaceMetadataVersion,
objectMetadata,
cachedObjectMetadataMaps,
),
),
);
@@ -127,7 +128,7 @@ export class WorkspaceDatasourceFactory {
const workspaceDataSource = new WorkspaceDataSource(
{
workspaceId,
objectMetadataMap: cachedObjectMetadataMap,
objectMetadataMaps: cachedObjectMetadataMaps,
},
{
url:

View File

@@ -1,6 +1,6 @@
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export interface WorkspaceInternalContext {
workspaceId: string;
objectMetadataMap: ObjectMetadataMap;
objectMetadataMaps: ObjectMetadataMaps;
}

View File

@@ -22,7 +22,7 @@ import { UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@@ -631,13 +631,15 @@ export class WorkspaceRepository<
}
const objectMetadata =
this.internalContext.objectMetadataMap[objectMetadataName];
this.internalContext.objectMetadataMaps.byNameSingular[
objectMetadataName
];
if (!objectMetadata) {
throw new Error(
`Object metadata for object "${objectMetadataName}" is missing ` +
`in workspace "${this.internalContext.workspaceId}" ` +
`with object metadata collection length: ${this.internalContext.objectMetadataMap.length}`,
`with object metadata collection length: ${this.internalContext.objectMetadataMaps.byNameSingular.length}`,
);
}
@@ -666,12 +668,12 @@ export class WorkspaceRepository<
async formatResult<T>(
data: T,
objectMetadata?: ObjectMetadataMapItem,
objectMetadata?: ObjectMetadataItemWithFieldMaps,
): Promise<T> {
objectMetadata ??= await this.getObjectMetadataFromTarget();
const objectMetadataMap = this.internalContext.objectMetadataMap;
const objectMetadataMaps = this.internalContext.objectMetadataMaps;
return formatResult(data, objectMetadata, objectMetadataMap) as T;
return formatResult(data, objectMetadata, objectMetadataMaps) as T;
}
}

View File

@@ -3,7 +3,7 @@ import { RelationType } from 'typeorm/metadata/types/RelationTypes';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
interface RelationDetails {
@@ -16,22 +16,25 @@ interface RelationDetails {
export async function determineRelationDetails(
fieldMetadata: FieldMetadataInterface,
relationMetadata: RelationMetadataEntity,
objectMetadataMap: ObjectMetadataMap,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<RelationDetails> {
const relationType = computeRelationType(fieldMetadata, relationMetadata);
const fromObjectMetadata = objectMetadataMap[fieldMetadata.objectMetadataId];
let toObjectMetadata = objectMetadataMap[relationMetadata.toObjectMetadataId];
const fromObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.objectMetadataId];
let toObjectMetadata =
objectMetadataMaps.byId[relationMetadata.toObjectMetadataId];
// RelationMetadata always store the relation from the perspective of the `from` object, MANY_TO_ONE relations are not stored yet
if (relationType === 'many-to-one') {
toObjectMetadata = objectMetadataMap[relationMetadata.fromObjectMetadataId];
toObjectMetadata =
objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId];
}
if (!fromObjectMetadata || !toObjectMetadata) {
throw new Error('Object metadata not found');
}
const toFieldMetadata = Object.values(toObjectMetadata.fields).find(
const toFieldMetadata = Object.values(toObjectMetadata.fieldsById).find(
(field) =>
relationType === 'many-to-one'
? field.id === relationMetadata.fromFieldMetadataId

View File

@@ -3,26 +3,28 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { capitalize } from 'src/utils/capitalize';
export function formatData<T>(
data: T,
objectMetadata: ObjectMetadataMapItem,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): T {
if (!data) {
return data;
}
if (Array.isArray(data)) {
return data.map((item) => formatData(item, objectMetadata)) as T;
return data.map((item) =>
formatData(item, objectMetadataItemWithFieldMaps),
) as T;
}
const newData: Record<string, any> = {};
for (const [key, value] of Object.entries(data)) {
const fieldMetadata = objectMetadata.fields[key];
const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsByName[key];
if (!fieldMetadata) {
throw new Error(

View File

@@ -6,18 +6,16 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import {
ObjectMetadataMap,
ObjectMetadataMapItem,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
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 { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
export function formatResult<T>(
data: T,
objectMetadata: ObjectMetadataMapItem,
objectMetadataMap: ObjectMetadataMap,
ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): T {
if (!data) {
return data;
@@ -25,7 +23,7 @@ export function formatResult<T>(
if (Array.isArray(data)) {
return data.map((item) =>
formatResult(item, objectMetadata, objectMetadataMap),
formatResult(item, ObjectMetadataItemWithFieldMaps, objectMetadataMaps),
) as T;
}
@@ -33,12 +31,13 @@ export function formatResult<T>(
return data;
}
if (!objectMetadata) {
if (!ObjectMetadataItemWithFieldMaps) {
throw new Error('Object metadata is missing');
}
const compositeFieldMetadataCollection =
getCompositeFieldMetadataCollection(objectMetadata);
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
ObjectMetadataItemWithFieldMaps,
);
const compositeFieldMetadataMap = new Map(
compositeFieldMetadataCollection.flatMap((fieldMetadata) => {
@@ -58,7 +57,7 @@ export function formatResult<T>(
);
const relationMetadataMap = new Map(
Object.values(objectMetadata.fields)
Object.values(ObjectMetadataItemWithFieldMaps.fieldsById)
.filter(({ type }) => isRelationFieldMetadataType(type))
.map((fieldMetadata) => [
fieldMetadata.name,
@@ -75,6 +74,8 @@ export function formatResult<T>(
]),
);
const newData: object = {};
const objectMetadaItemFieldsByName =
objectMetadataMaps.byId[ObjectMetadataItemWithFieldMaps.id]?.fieldsByName;
for (const [key, value] of Object.entries(data)) {
const compositePropertyArgs = compositeFieldMetadataMap.get(key);
@@ -83,11 +84,15 @@ export function formatResult<T>(
if (!compositePropertyArgs && !relationMetadata) {
if (isPlainObject(value)) {
newData[key] = formatResult(value, objectMetadata, objectMetadataMap);
} else if (objectMetadata.fields[key]) {
newData[key] = formatResult(
value,
ObjectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
} else if (objectMetadaItemFieldsByName[key]) {
newData[key] = formatFieldMetadataValue(
value,
objectMetadata.fields[key],
objectMetadaItemFieldsByName[key],
);
} else {
newData[key] = value;
@@ -98,10 +103,10 @@ export function formatResult<T>(
if (relationMetadata) {
const toObjectMetadata =
objectMetadataMap[relationMetadata.toObjectMetadataId];
objectMetadataMaps.byId[relationMetadata.toObjectMetadataId];
const fromObjectMetadata =
objectMetadataMap[relationMetadata.fromObjectMetadataId];
objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId];
if (!toObjectMetadata) {
throw new Error(
@@ -118,7 +123,7 @@ export function formatResult<T>(
newData[key] = formatResult(
value,
relationType === 'one-to-many' ? toObjectMetadata : fromObjectMetadata,
objectMetadataMap,
objectMetadataMaps,
);
continue;
}

View File

@@ -1,12 +1,14 @@
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export function getCompositeFieldMetadataCollection(
objectMetadata: ObjectMetadataMapItem,
ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
const compositeFieldMetadataCollection = Object.values(
objectMetadata.fields,
).filter((fieldMetadata) => isCompositeFieldMetadataType(fieldMetadata.type));
ObjectMetadataItemWithFieldMaps.fieldsById,
).filter((fieldMetadataItem) =>
isCompositeFieldMetadataType(fieldMetadataItem.type),
);
return compositeFieldMetadataCollection;
}

View File

@@ -5,14 +5,14 @@ import { EntitySchemaOptions } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export enum WorkspaceCacheKeys {
GraphQLTypeDefs = 'graphql:type-defs',
GraphQLUsedScalarNames = 'graphql:used-scalar-names',
GraphQLOperations = 'graphql:operations',
ORMEntitySchemas = 'orm:entity-schemas',
MetadataObjectMetadataMap = 'metadata:object-metadata-map',
MetadataObjectMetadataMaps = 'metadata:object-metadata-maps',
MetadataObjectMetadataOngoingCachingLock = 'metadata:object-metadata-ongoing-caching-lock',
MetadataVersion = 'metadata:workspace-metadata-version',
}
@@ -88,23 +88,23 @@ export class WorkspaceCacheStorageService {
);
}
setObjectMetadataMap(
setObjectMetadataMaps(
workspaceId: string,
metadataVersion: number,
objectMetadataMap: ObjectMetadataMap,
objectMetadataMaps: ObjectMetadataMaps,
) {
return this.cacheStorageService.set<ObjectMetadataMap>(
`${WorkspaceCacheKeys.MetadataObjectMetadataMap}:${workspaceId}:${metadataVersion}`,
objectMetadataMap,
return this.cacheStorageService.set<ObjectMetadataMaps>(
`${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`,
objectMetadataMaps,
);
}
getObjectMetadataMap(
getObjectMetadataMaps(
workspaceId: string,
metadataVersion: number,
): Promise<ObjectMetadataMap | undefined> {
return this.cacheStorageService.get<ObjectMetadataMap>(
`${WorkspaceCacheKeys.MetadataObjectMetadataMap}:${workspaceId}:${metadataVersion}`,
): Promise<ObjectMetadataMaps | undefined> {
return this.cacheStorageService.get<ObjectMetadataMaps>(
`${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`,
);
}
@@ -150,7 +150,7 @@ export class WorkspaceCacheStorageService {
async flush(workspaceId: string, metadataVersion: number): Promise<void> {
await this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataObjectMetadataMap}:${workspaceId}:${metadataVersion}`,
`${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`,
);
await this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataVersion}:${workspaceId}:${metadataVersion}`,

View File

@@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { objectRecordDiffMerge } from 'src/engine/core-modules/event-emitter/utils/object-record-diff-merge';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Injectable()
export class TimelineActivityRepository {
@@ -15,7 +15,7 @@ export class TimelineActivityRepository {
async upsertOne(
name: string,
properties: Partial<Record>,
properties: Partial<ObjectRecord>,
objectName: string,
recordId: string,
workspaceId: string,
@@ -105,7 +105,7 @@ export class TimelineActivityRepository {
private async updateTimelineActivity(
dataSourceSchema: string,
id: string,
properties: Partial<Record>,
properties: Partial<ObjectRecord>,
workspaceMemberId: string | undefined,
workspaceId: string,
) {
@@ -121,7 +121,7 @@ export class TimelineActivityRepository {
private async insertTimelineActivity(
dataSourceSchema: string,
name: string,
properties: Partial<Record>,
properties: Partial<ObjectRecord>,
objectName: string,
recordId: string,
workspaceMemberId: string | undefined,
@@ -151,7 +151,7 @@ export class TimelineActivityRepository {
objectName: string,
activities: {
name: string;
properties: Partial<Record> | null;
properties: Partial<ObjectRecord> | null;
workspaceMemberId: string | undefined;
recordId: string | null;
linkedRecordCachedName: string;

View File

@@ -1,12 +1,12 @@
import { v4 } from 'uuid';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
export const generateFakeObjectRecordEvent = <Entity>(
objectMetadataEntity: ObjectMetadataEntity,

View File

@@ -123,10 +123,6 @@ Creates resolver functions for querying and mutating the GraphQL schema.
Each factory in this directory is responsible for producing a distinct resolver type, such as the `FindManyResolverFactory`, designed for adaptable application across various tables.
### Workspace Query Builder
Includes factories that generate `pg_graphql` queries.
### Workspace Query Runner
Runs the generated queries on the database and parses the result.