Connect - Update Gql schema generation (#13001)
To test :
```
mutation testMutation {
createPerson(
data: {company: {connect: {where: {domainName: {primaryLinkUrl: "airbnb.com"}}}}}
) {
id
}
}
```
closes https://github.com/twentyhq/core-team-issues/issues/1167
This commit is contained in:
@ -4,6 +4,7 @@ import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/work
|
|||||||
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-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 { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||||
import { ExtendObjectTypeDefinitionV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory';
|
import { ExtendObjectTypeDefinitionV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory';
|
||||||
|
import { RelationConnectInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory';
|
||||||
import { RelationTypeV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-type-v2.factory';
|
import { RelationTypeV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-type-v2.factory';
|
||||||
|
|
||||||
import { ArgsFactory } from './args.factory';
|
import { ArgsFactory } from './args.factory';
|
||||||
@ -25,6 +26,7 @@ export const workspaceSchemaBuilderFactories = [
|
|||||||
InputTypeFactory,
|
InputTypeFactory,
|
||||||
InputTypeDefinitionFactory,
|
InputTypeDefinitionFactory,
|
||||||
CompositeInputTypeDefinitionFactory,
|
CompositeInputTypeDefinitionFactory,
|
||||||
|
RelationConnectInputTypeDefinitionFactory,
|
||||||
OutputTypeFactory,
|
OutputTypeFactory,
|
||||||
ObjectTypeDefinitionFactory,
|
ObjectTypeDefinitionFactory,
|
||||||
CompositeObjectTypeDefinitionFactory,
|
CompositeObjectTypeDefinitionFactory,
|
||||||
|
|||||||
@ -32,11 +32,17 @@ export class InputTypeDefinitionFactory {
|
|||||||
private readonly typeMapperService: TypeMapperService,
|
private readonly typeMapperService: TypeMapperService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public create(
|
public create({
|
||||||
objectMetadata: ObjectMetadataInterface,
|
objectMetadata,
|
||||||
kind: InputTypeDefinitionKind,
|
kind,
|
||||||
options: WorkspaceBuildSchemaOptions,
|
options,
|
||||||
): InputTypeDefinition {
|
isRelationConnectEnabled = false,
|
||||||
|
}: {
|
||||||
|
objectMetadata: ObjectMetadataInterface;
|
||||||
|
kind: InputTypeDefinitionKind;
|
||||||
|
options: WorkspaceBuildSchemaOptions;
|
||||||
|
isRelationConnectEnabled?: boolean;
|
||||||
|
}): InputTypeDefinition {
|
||||||
// @ts-expect-error legacy noImplicitAny
|
// @ts-expect-error legacy noImplicitAny
|
||||||
const inputType = new GraphQLInputObjectType({
|
const inputType = new GraphQLInputObjectType({
|
||||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
|
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
|
||||||
@ -55,12 +61,12 @@ export class InputTypeDefinitionFactory {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...generateFields(
|
...generateFields({
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
kind,
|
kind,
|
||||||
options,
|
options,
|
||||||
this.inputTypeFactory,
|
typeFactory: this.inputTypeFactory,
|
||||||
),
|
}),
|
||||||
and: {
|
and: {
|
||||||
type: andOrType,
|
type: andOrType,
|
||||||
},
|
},
|
||||||
@ -78,12 +84,13 @@ export class InputTypeDefinitionFactory {
|
|||||||
* Other input types are generated with fields only
|
* Other input types are generated with fields only
|
||||||
*/
|
*/
|
||||||
default:
|
default:
|
||||||
return generateFields(
|
return generateFields({
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
kind,
|
kind,
|
||||||
options,
|
options,
|
||||||
this.inputTypeFactory,
|
typeFactory: this.inputTypeFactory,
|
||||||
);
|
isRelationConnectEnabled,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -44,6 +44,9 @@ export class InputTypeFactory {
|
|||||||
*/
|
*/
|
||||||
case InputTypeDefinitionKind.Create:
|
case InputTypeDefinitionKind.Create:
|
||||||
case InputTypeDefinitionKind.Update:
|
case InputTypeDefinitionKind.Update:
|
||||||
|
//if it's a relation connect field, type is in storage
|
||||||
|
if (typeOptions.isRelationConnectField) break;
|
||||||
|
|
||||||
inputType = this.typeMapperService.mapToScalarType(
|
inputType = this.typeMapperService.mapToScalarType(
|
||||||
type,
|
type,
|
||||||
typeOptions.settings,
|
typeOptions.settings,
|
||||||
|
|||||||
@ -37,12 +37,12 @@ export class ObjectTypeDefinitionFactory {
|
|||||||
type: new GraphQLObjectType({
|
type: new GraphQLObjectType({
|
||||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||||
description: objectMetadata.description,
|
description: objectMetadata.description,
|
||||||
fields: generateFields(
|
fields: generateFields({
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
kind,
|
kind,
|
||||||
options,
|
options,
|
||||||
this.outputTypeFactory,
|
typeFactory: this.outputTypeFactory,
|
||||||
),
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,150 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GraphQLInputFieldConfig,
|
||||||
|
GraphQLInputObjectType,
|
||||||
|
GraphQLInputType,
|
||||||
|
GraphQLString,
|
||||||
|
} 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 {
|
||||||
|
InputTypeDefinition,
|
||||||
|
InputTypeDefinitionKind,
|
||||||
|
} from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
|
||||||
|
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||||
|
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 { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util';
|
||||||
|
import { pascalCase } from 'src/utils/pascal-case';
|
||||||
|
|
||||||
|
export const formatRelationConnectInputTarget = (objectMetadataId: string) =>
|
||||||
|
`${objectMetadataId}-connect-input`;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RelationConnectInputTypeDefinitionFactory {
|
||||||
|
constructor(private readonly typeMapperService: TypeMapperService) {}
|
||||||
|
|
||||||
|
public create(
|
||||||
|
objectMetadata: ObjectMetadataInterface,
|
||||||
|
): InputTypeDefinition[] {
|
||||||
|
const fields = this.generateRelationConnectInputType(objectMetadata);
|
||||||
|
const target = formatRelationConnectInputTarget(objectMetadata.id);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
target,
|
||||||
|
kind: InputTypeDefinitionKind.Create,
|
||||||
|
type: fields,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target,
|
||||||
|
kind: InputTypeDefinitionKind.Update,
|
||||||
|
type: fields,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateRelationConnectInputType(
|
||||||
|
objectMetadata: ObjectMetadataInterface,
|
||||||
|
): GraphQLInputObjectType {
|
||||||
|
return new GraphQLInputObjectType({
|
||||||
|
name: `${pascalCase(objectMetadata.nameSingular)}RelationInput`,
|
||||||
|
fields: () => ({
|
||||||
|
connect: {
|
||||||
|
type: new GraphQLInputObjectType({
|
||||||
|
name: `${pascalCase(objectMetadata.nameSingular)}ConnectInput`,
|
||||||
|
fields: this.generateRelationWhereInputType(objectMetadata),
|
||||||
|
}),
|
||||||
|
description: `Connect to a ${objectMetadata.nameSingular} record`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateRelationWhereInputType(
|
||||||
|
objectMetadata: ObjectMetadataInterface,
|
||||||
|
): Record<string, GraphQLInputFieldConfig> {
|
||||||
|
const uniqueConstraints = getUniqueConstraintsFields(objectMetadata);
|
||||||
|
|
||||||
|
const fields: Record<
|
||||||
|
string,
|
||||||
|
{ type: GraphQLInputType; description: string }
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
uniqueConstraints.forEach((constraint) => {
|
||||||
|
constraint.forEach((field) => {
|
||||||
|
if (isCompositeFieldMetadataType(field.type)) {
|
||||||
|
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||||
|
|
||||||
|
if (!compositeType) {
|
||||||
|
throw new Error(
|
||||||
|
`Composite type definition not found for field type ${field.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueProperties = compositeType.properties.filter(
|
||||||
|
(property) => property.isIncludedInUniqueConstraint,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uniqueProperties.length > 0) {
|
||||||
|
const compositeFields: Record<
|
||||||
|
string,
|
||||||
|
{ type: GraphQLInputType; description: string }
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
uniqueProperties.forEach((property) => {
|
||||||
|
const scalarType = this.typeMapperService.mapToScalarType(
|
||||||
|
property.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
compositeFields[property.name] = {
|
||||||
|
type: scalarType || GraphQLString,
|
||||||
|
description: `Connect by ${property.name}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const compositeInputType = new GraphQLInputObjectType({
|
||||||
|
name: `${pascalCase(objectMetadata.nameSingular)}${pascalCase(field.name)}WhereInput`,
|
||||||
|
fields: () => compositeFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
fields[field.name] = {
|
||||||
|
type: compositeInputType,
|
||||||
|
description: `Connect by ${field.label || field.name}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const scalarType = this.typeMapperService.mapToScalarType(
|
||||||
|
field.type,
|
||||||
|
field.settings,
|
||||||
|
field.name === 'id',
|
||||||
|
);
|
||||||
|
|
||||||
|
fields[field.name] = {
|
||||||
|
type: scalarType || GraphQLString,
|
||||||
|
description: `Connect by ${field.label || field.name}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
where: {
|
||||||
|
type: new GraphQLInputObjectType({
|
||||||
|
name: `${pascalCase(objectMetadata.nameSingular)}WhereUniqueInput`,
|
||||||
|
fields: () => fields,
|
||||||
|
}),
|
||||||
|
description: `Find a ${objectMetadata.nameSingular} record based on its unique fields: ${this.formatConstraints(uniqueConstraints)}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatConstraints(constraints: FieldMetadataInterface[][]) {
|
||||||
|
return constraints
|
||||||
|
.map((constraint) => constraint.map((field) => field.name).join(' and '))
|
||||||
|
.join(' or ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,4 +13,10 @@ export interface WorkspaceBuildSchemaOptions {
|
|||||||
* @default 'float'
|
* @default 'float'
|
||||||
*/
|
*/
|
||||||
numberScalarMode?: NumberScalarMode;
|
numberScalarMode?: NumberScalarMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workspace ID - used to relation connect feature flag check
|
||||||
|
* TODO: remove once IS_RELATION_CONNECT_ENABLED is removed
|
||||||
|
*/
|
||||||
|
workspaceId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export interface TypeOptions<T = any> {
|
|||||||
defaultValue?: T;
|
defaultValue?: T;
|
||||||
settings?: FieldMetadataSettings<FieldMetadataType>;
|
settings?: FieldMetadataSettings<FieldMetadataType>;
|
||||||
isIdField?: boolean;
|
isIdField?: boolean;
|
||||||
|
isRelationConnectField?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StringArrayScalarType = new GraphQLList(GraphQLString);
|
const StringArrayScalarType = new GraphQLList(GraphQLString);
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
@ -8,6 +10,9 @@ import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/work
|
|||||||
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-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 { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||||
import { ExtendObjectTypeDefinitionV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory';
|
import { ExtendObjectTypeDefinitionV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory';
|
||||||
|
import { RelationConnectInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory';
|
||||||
|
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 { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||||
|
|
||||||
import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
|
import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
|
||||||
@ -39,6 +44,8 @@ export class TypeDefinitionsGenerator {
|
|||||||
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
|
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
|
||||||
private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory,
|
private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory,
|
||||||
private readonly extendObjectTypeDefinitionV2Factory: ExtendObjectTypeDefinitionV2Factory,
|
private readonly extendObjectTypeDefinitionV2Factory: ExtendObjectTypeDefinitionV2Factory,
|
||||||
|
private readonly relationConnectInputTypeDefinitionFactory: RelationConnectInputTypeDefinitionFactory,
|
||||||
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generate(
|
async generate(
|
||||||
@ -49,6 +56,8 @@ export class TypeDefinitionsGenerator {
|
|||||||
await this.generateCompositeTypeDefs(options);
|
await this.generateCompositeTypeDefs(options);
|
||||||
// Generate metadata objects
|
// Generate metadata objects
|
||||||
await this.generateMetadataTypeDefs(objectMetadataCollection, options);
|
await this.generateMetadataTypeDefs(objectMetadataCollection, options);
|
||||||
|
|
||||||
|
this.generateRelationConnectInputTypeDefs(objectMetadataCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,10 +105,10 @@ export class TypeDefinitionsGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private generateCompositeInputTypeDefs(
|
private generateCompositeInputTypeDefs(
|
||||||
compisteTypes: CompositeType[],
|
compositeTypes: CompositeType[],
|
||||||
options: WorkspaceBuildSchemaOptions,
|
options: WorkspaceBuildSchemaOptions,
|
||||||
) {
|
) {
|
||||||
const inputTypeDefs = compisteTypes
|
const inputTypeDefs = compositeTypes
|
||||||
.map((compositeType) => {
|
.map((compositeType) => {
|
||||||
const optionalExtendedObjectMetadata = {
|
const optionalExtendedObjectMetadata = {
|
||||||
...compositeType,
|
...compositeType,
|
||||||
@ -159,7 +168,7 @@ export class TypeDefinitionsGenerator {
|
|||||||
this.generateEnumTypeDefs(dynamicObjectMetadataCollection, options);
|
this.generateEnumTypeDefs(dynamicObjectMetadataCollection, options);
|
||||||
this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options);
|
this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options);
|
||||||
this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options);
|
this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options);
|
||||||
this.generateInputTypeDefs(dynamicObjectMetadataCollection, options);
|
await this.generateInputTypeDefs(dynamicObjectMetadataCollection, options);
|
||||||
await this.generateExtendedObjectTypeDefs(
|
await this.generateExtendedObjectTypeDefs(
|
||||||
dynamicObjectMetadataCollection,
|
dynamicObjectMetadataCollection,
|
||||||
options,
|
options,
|
||||||
@ -200,10 +209,17 @@ export class TypeDefinitionsGenerator {
|
|||||||
this.typeDefinitionsStorage.addObjectTypes(connectionTypeDefs);
|
this.typeDefinitionsStorage.addObjectTypes(connectionTypeDefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateInputTypeDefs(
|
private async generateInputTypeDefs(
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
objectMetadataCollection: ObjectMetadataInterface[],
|
||||||
options: WorkspaceBuildSchemaOptions,
|
options: WorkspaceBuildSchemaOptions,
|
||||||
) {
|
) {
|
||||||
|
const isRelationConnectEnabled = isDefined(options.workspaceId)
|
||||||
|
? await this.featureFlagService.isFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_RELATION_CONNECT_ENABLED,
|
||||||
|
options.workspaceId,
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
const inputTypeDefs = objectMetadataCollection
|
const inputTypeDefs = objectMetadataCollection
|
||||||
.map((objectMetadata) => {
|
.map((objectMetadata) => {
|
||||||
const optionalExtendedObjectMetadata = {
|
const optionalExtendedObjectMetadata = {
|
||||||
@ -216,29 +232,31 @@ export class TypeDefinitionsGenerator {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
// Input type for create
|
// Input type for create
|
||||||
this.inputTypeDefinitionFactory.create(
|
this.inputTypeDefinitionFactory.create({
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
InputTypeDefinitionKind.Create,
|
kind: InputTypeDefinitionKind.Create,
|
||||||
options,
|
options,
|
||||||
),
|
isRelationConnectEnabled,
|
||||||
|
}),
|
||||||
// Input type for update
|
// Input type for update
|
||||||
this.inputTypeDefinitionFactory.create(
|
this.inputTypeDefinitionFactory.create({
|
||||||
optionalExtendedObjectMetadata,
|
objectMetadata: optionalExtendedObjectMetadata,
|
||||||
InputTypeDefinitionKind.Update,
|
kind: InputTypeDefinitionKind.Update,
|
||||||
|
isRelationConnectEnabled,
|
||||||
options,
|
options,
|
||||||
),
|
}),
|
||||||
// Filter input type
|
// Filter input type
|
||||||
this.inputTypeDefinitionFactory.create(
|
this.inputTypeDefinitionFactory.create({
|
||||||
optionalExtendedObjectMetadata,
|
objectMetadata: optionalExtendedObjectMetadata,
|
||||||
InputTypeDefinitionKind.Filter,
|
kind: InputTypeDefinitionKind.Filter,
|
||||||
options,
|
options,
|
||||||
),
|
}),
|
||||||
// OrderBy input type
|
// OrderBy input type
|
||||||
this.inputTypeDefinitionFactory.create(
|
this.inputTypeDefinitionFactory.create({
|
||||||
optionalExtendedObjectMetadata,
|
objectMetadata: optionalExtendedObjectMetadata,
|
||||||
InputTypeDefinitionKind.OrderBy,
|
kind: InputTypeDefinitionKind.OrderBy,
|
||||||
options,
|
options,
|
||||||
),
|
}),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
.flat();
|
.flat();
|
||||||
@ -283,4 +301,16 @@ export class TypeDefinitionsGenerator {
|
|||||||
|
|
||||||
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs);
|
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private generateRelationConnectInputTypeDefs(
|
||||||
|
objectMetadataCollection: ObjectMetadataInterface[],
|
||||||
|
) {
|
||||||
|
const relationWhereInputTypeDefs = objectMetadataCollection
|
||||||
|
.map((objectMetadata) =>
|
||||||
|
this.relationConnectInputTypeDefinitionFactory.create(objectMetadata),
|
||||||
|
)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
this.typeDefinitionsStorage.addInputTypes(relationWhereInputTypeDefs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,16 @@ import {
|
|||||||
GraphQLOutputType,
|
GraphQLOutputType,
|
||||||
} from 'graphql';
|
} from 'graphql';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||||
|
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 { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
|
||||||
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
|
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
|
||||||
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
|
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
|
||||||
|
import { formatRelationConnectInputTarget } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory';
|
||||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||||
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||||
|
|
||||||
@ -30,6 +33,7 @@ type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> =
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
settings: any;
|
settings: any;
|
||||||
isIdField: boolean;
|
isIdField: boolean;
|
||||||
|
isRelationConnectField?: boolean;
|
||||||
},
|
},
|
||||||
) => T extends InputTypeDefinitionKind
|
) => T extends InputTypeDefinitionKind
|
||||||
? GraphQLInputType
|
? GraphQLInputType
|
||||||
@ -38,87 +42,189 @@ type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> =
|
|||||||
|
|
||||||
export const generateFields = <
|
export const generateFields = <
|
||||||
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
||||||
>(
|
>({
|
||||||
objectMetadata: ObjectMetadataInterface,
|
objectMetadata,
|
||||||
kind: T,
|
kind,
|
||||||
options: WorkspaceBuildSchemaOptions,
|
options,
|
||||||
typeFactory: TypeFactory<T>,
|
typeFactory,
|
||||||
): T extends InputTypeDefinitionKind
|
isRelationConnectEnabled = false,
|
||||||
|
}: {
|
||||||
|
objectMetadata: ObjectMetadataInterface;
|
||||||
|
kind: T;
|
||||||
|
options: WorkspaceBuildSchemaOptions;
|
||||||
|
typeFactory: TypeFactory<T>;
|
||||||
|
isRelationConnectEnabled?: boolean;
|
||||||
|
}): T extends InputTypeDefinitionKind
|
||||||
? GraphQLInputFieldConfigMap
|
? GraphQLInputFieldConfigMap
|
||||||
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
GraphQLFieldConfigMap<any, any> => {
|
GraphQLFieldConfigMap<any, any> => {
|
||||||
const fields = {};
|
const allGeneratedFields = {};
|
||||||
|
|
||||||
for (const fieldMetadata of objectMetadata.fields) {
|
for (const fieldMetadata of objectMetadata.fields) {
|
||||||
|
let generatedField;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isFieldMetadataInterfaceOfType(
|
isFieldMetadataInterfaceOfType(fieldMetadata, FieldMetadataType.RELATION)
|
||||||
fieldMetadata,
|
|
||||||
FieldMetadataType.RELATION,
|
|
||||||
) &&
|
|
||||||
fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE
|
|
||||||
) {
|
) {
|
||||||
continue;
|
generatedField = generateRelationField({
|
||||||
|
fieldMetadata,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
typeFactory,
|
||||||
|
isRelationConnectEnabled,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
generatedField = generateField({
|
||||||
|
fieldMetadata,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
typeFactory,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
Object.assign(allGeneratedFields, generatedField);
|
||||||
? fieldMetadata.type.toString()
|
}
|
||||||
: fieldMetadata.id;
|
|
||||||
|
|
||||||
const typeFactoryOptions = isInputTypeDefinitionKind(kind)
|
return allGeneratedFields;
|
||||||
? {
|
};
|
||||||
nullable: fieldMetadata.isNullable,
|
|
||||||
defaultValue: fieldMetadata.defaultValue,
|
|
||||||
isArray:
|
|
||||||
kind !== InputTypeDefinitionKind.Filter &&
|
|
||||||
fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
|
||||||
settings: fieldMetadata.settings,
|
|
||||||
isIdField: fieldMetadata.name === 'id',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
nullable: fieldMetadata.isNullable,
|
|
||||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
|
||||||
settings: fieldMetadata.settings,
|
|
||||||
// Scalar type is already defined in the entity itself.
|
|
||||||
isIdField: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const type = typeFactory.create(
|
const getTarget = <T extends FieldMetadataType>(
|
||||||
target,
|
fieldMetadata: FieldMetadataInterface<T>,
|
||||||
|
) => {
|
||||||
|
return isCompositeFieldMetadataType(fieldMetadata.type)
|
||||||
|
? fieldMetadata.type.toString()
|
||||||
|
: fieldMetadata.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeFactoryOptions = <T extends FieldMetadataType>(
|
||||||
|
fieldMetadata: FieldMetadataInterface<T>,
|
||||||
|
kind: InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
||||||
|
) => {
|
||||||
|
return isInputTypeDefinitionKind(kind)
|
||||||
|
? {
|
||||||
|
nullable: fieldMetadata.isNullable,
|
||||||
|
defaultValue: fieldMetadata.defaultValue,
|
||||||
|
isArray:
|
||||||
|
kind !== InputTypeDefinitionKind.Filter &&
|
||||||
|
fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||||
|
settings: fieldMetadata.settings,
|
||||||
|
isIdField: fieldMetadata.name === 'id',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
nullable: fieldMetadata.isNullable,
|
||||||
|
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||||
|
settings: fieldMetadata.settings,
|
||||||
|
// Scalar type is already defined in the entity itself.
|
||||||
|
isIdField: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateField = <
|
||||||
|
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
||||||
|
>({
|
||||||
|
fieldMetadata,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
typeFactory,
|
||||||
|
}: {
|
||||||
|
fieldMetadata: FieldMetadataInterface;
|
||||||
|
kind: T;
|
||||||
|
options: WorkspaceBuildSchemaOptions;
|
||||||
|
typeFactory: TypeFactory<T>;
|
||||||
|
}) => {
|
||||||
|
const target = getTarget(fieldMetadata);
|
||||||
|
|
||||||
|
const typeFactoryOptions = getTypeFactoryOptions(fieldMetadata, kind);
|
||||||
|
|
||||||
|
const type = typeFactory.create(
|
||||||
|
target,
|
||||||
|
fieldMetadata.type,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
typeFactoryOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
[fieldMetadata.name]: {
|
||||||
|
type,
|
||||||
|
description: fieldMetadata.description,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateRelationField = <
|
||||||
|
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
||||||
|
>({
|
||||||
|
fieldMetadata,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
typeFactory,
|
||||||
|
isRelationConnectEnabled,
|
||||||
|
}: {
|
||||||
|
fieldMetadata: FieldMetadataInterface<FieldMetadataType.RELATION>;
|
||||||
|
kind: T;
|
||||||
|
options: WorkspaceBuildSchemaOptions;
|
||||||
|
typeFactory: TypeFactory<T>;
|
||||||
|
isRelationConnectEnabled: boolean;
|
||||||
|
}) => {
|
||||||
|
const relationField = {};
|
||||||
|
|
||||||
|
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) {
|
||||||
|
return relationField;
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinColumnName = fieldMetadata.settings?.joinColumnName;
|
||||||
|
|
||||||
|
if (!joinColumnName) {
|
||||||
|
throw new Error('Join column name is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = getTarget(fieldMetadata);
|
||||||
|
const typeFactoryOptions = getTypeFactoryOptions(fieldMetadata, kind);
|
||||||
|
|
||||||
|
let type = typeFactory.create(
|
||||||
|
target,
|
||||||
|
fieldMetadata.type,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
typeFactoryOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-expect-error legacy noImplicitAny
|
||||||
|
relationField[joinColumnName] = {
|
||||||
|
type,
|
||||||
|
description: fieldMetadata.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
[InputTypeDefinitionKind.Create, InputTypeDefinitionKind.Update].includes(
|
||||||
|
kind as InputTypeDefinitionKind,
|
||||||
|
) &&
|
||||||
|
isDefined(fieldMetadata.relationTargetObjectMetadataId) &&
|
||||||
|
isRelationConnectEnabled
|
||||||
|
) {
|
||||||
|
type = typeFactory.create(
|
||||||
|
formatRelationConnectInputTarget(
|
||||||
|
fieldMetadata.relationTargetObjectMetadataId,
|
||||||
|
),
|
||||||
fieldMetadata.type,
|
fieldMetadata.type,
|
||||||
kind,
|
kind,
|
||||||
options,
|
options,
|
||||||
typeFactoryOptions,
|
{
|
||||||
|
...typeFactoryOptions,
|
||||||
|
isRelationConnectField: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
|
||||||
isFieldMetadataInterfaceOfType(
|
|
||||||
fieldMetadata,
|
|
||||||
FieldMetadataType.RELATION,
|
|
||||||
) &&
|
|
||||||
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE
|
|
||||||
) {
|
|
||||||
const joinColumnName = fieldMetadata.settings?.joinColumnName;
|
|
||||||
|
|
||||||
if (!joinColumnName) {
|
|
||||||
throw new Error('Join column name is not defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error legacy noImplicitAny
|
|
||||||
fields[joinColumnName] = {
|
|
||||||
type,
|
|
||||||
description: fieldMetadata.description,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error legacy noImplicitAny
|
|
||||||
fields[fieldMetadata.name] = {
|
|
||||||
type,
|
|
||||||
description: fieldMetadata.description,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields;
|
// @ts-expect-error legacy noImplicitAny
|
||||||
|
relationField[fieldMetadata.name] = {
|
||||||
|
type: type,
|
||||||
|
description: fieldMetadata.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
return relationField;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Type guard
|
// Type guard
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module';
|
import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module';
|
||||||
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||||
|
|
||||||
import { TypeDefinitionsGenerator } from './type-definitions.generator';
|
import { TypeDefinitionsGenerator } from './type-definitions.generator';
|
||||||
@ -11,7 +12,11 @@ import { TypeMapperService } from './services/type-mapper.service';
|
|||||||
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ObjectMetadataModule, WorkspaceResolverBuilderModule],
|
imports: [
|
||||||
|
ObjectMetadataModule,
|
||||||
|
WorkspaceResolverBuilderModule,
|
||||||
|
FeatureFlagModule,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
TypeDefinitionsStorage,
|
TypeDefinitionsStorage,
|
||||||
TypeMapperService,
|
TypeMapperService,
|
||||||
|
|||||||
@ -81,7 +81,9 @@ export class WorkspaceSchemaFactory {
|
|||||||
await this.workspaceGraphQLSchemaFactory.create(
|
await this.workspaceGraphQLSchemaFactory.create(
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
workspaceResolverBuilderMethodNames,
|
workspaceResolverBuilderMethodNames,
|
||||||
{},
|
{
|
||||||
|
workspaceId: authContext.workspace.id,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
usedScalarNames =
|
usedScalarNames =
|
||||||
|
|||||||
@ -7,4 +7,5 @@ export enum FeatureFlagKey {
|
|||||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||||
IS_IMAP_ENABLED = 'IS_IMAP_ENABLED',
|
IS_IMAP_ENABLED = 'IS_IMAP_ENABLED',
|
||||||
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
|
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
|
||||||
|
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,151 @@
|
|||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
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 { IndexFieldMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface';
|
||||||
|
import { IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
|
||||||
|
|
||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
|
import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util';
|
||||||
|
|
||||||
|
describe('getUniqueConstraintsFields', () => {
|
||||||
|
const mockIdField: FieldMetadataInterface = {
|
||||||
|
id: 'field-id-1',
|
||||||
|
name: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
type: FieldMetadataType.UUID,
|
||||||
|
objectMetadataId: 'object-id-1',
|
||||||
|
isNullable: false,
|
||||||
|
isUnique: false,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: true,
|
||||||
|
isActive: true,
|
||||||
|
isLabelSyncedWithName: false,
|
||||||
|
createdAt: new Date('2024-01-01'),
|
||||||
|
updatedAt: new Date('2024-01-01'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockEmailField: FieldMetadataInterface = {
|
||||||
|
id: 'field-id-2',
|
||||||
|
name: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
type: FieldMetadataType.EMAILS,
|
||||||
|
objectMetadataId: 'object-id-1',
|
||||||
|
isNullable: true,
|
||||||
|
isUnique: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
isActive: true,
|
||||||
|
isLabelSyncedWithName: false,
|
||||||
|
createdAt: new Date('2024-01-01'),
|
||||||
|
updatedAt: new Date('2024-01-01'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockNameField: FieldMetadataInterface = {
|
||||||
|
id: 'field-id-3',
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
objectMetadataId: 'object-id-1',
|
||||||
|
isNullable: true,
|
||||||
|
isUnique: false,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
isActive: true,
|
||||||
|
isLabelSyncedWithName: false,
|
||||||
|
createdAt: new Date('2024-01-01'),
|
||||||
|
updatedAt: new Date('2024-01-01'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMockIndexFieldMetadata = (
|
||||||
|
fieldMetadataId: string,
|
||||||
|
indexMetadataId: string,
|
||||||
|
order = 0,
|
||||||
|
): IndexFieldMetadataInterface =>
|
||||||
|
({
|
||||||
|
id: `index-field-${fieldMetadataId}-${indexMetadataId}`,
|
||||||
|
indexMetadataId,
|
||||||
|
fieldMetadataId,
|
||||||
|
order,
|
||||||
|
createdAt: new Date('2024-01-01'),
|
||||||
|
updatedAt: new Date('2024-01-01'),
|
||||||
|
}) as IndexFieldMetadataInterface;
|
||||||
|
|
||||||
|
const createMockIndexMetadata = (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
isUnique: boolean,
|
||||||
|
indexFieldMetadatas: IndexFieldMetadataInterface[],
|
||||||
|
): IndexMetadataInterface => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
isUnique,
|
||||||
|
indexFieldMetadatas,
|
||||||
|
createdAt: new Date('2024-01-01'),
|
||||||
|
updatedAt: new Date('2024-01-01'),
|
||||||
|
indexWhereClause: null,
|
||||||
|
indexType: IndexType.BTREE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMockObjectMetadata = (
|
||||||
|
fields: FieldMetadataInterface[],
|
||||||
|
indexMetadatas: IndexMetadataInterface[] = [],
|
||||||
|
): ObjectMetadataInterface => ({
|
||||||
|
id: 'object-id-1',
|
||||||
|
workspaceId: 'workspace-id-1',
|
||||||
|
nameSingular: 'person',
|
||||||
|
namePlural: 'people',
|
||||||
|
labelSingular: 'Person',
|
||||||
|
labelPlural: 'People',
|
||||||
|
description: 'A person object',
|
||||||
|
icon: 'IconUser',
|
||||||
|
targetTableName: 'person',
|
||||||
|
fields,
|
||||||
|
indexMetadatas,
|
||||||
|
isSystem: false,
|
||||||
|
isCustom: false,
|
||||||
|
isActive: true,
|
||||||
|
isRemote: false,
|
||||||
|
isAuditLogged: true,
|
||||||
|
isSearchable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the primary key constraint field if no unique indexes are present', () => {
|
||||||
|
const objectMetadata = createMockObjectMetadata([
|
||||||
|
mockIdField,
|
||||||
|
mockNameField,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = getUniqueConstraintsFields(objectMetadata);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toHaveLength(1);
|
||||||
|
expect(result[0][0]).toEqual(mockIdField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the primary key constraint field and the unique indexes fields if unique indexes are present', () => {
|
||||||
|
const emailIndexFieldMetadata = createMockIndexFieldMetadata(
|
||||||
|
'field-id-2',
|
||||||
|
'index-id-1',
|
||||||
|
);
|
||||||
|
const emailIndex = createMockIndexMetadata(
|
||||||
|
'index-id-1',
|
||||||
|
'unique_email_index',
|
||||||
|
true,
|
||||||
|
[emailIndexFieldMetadata],
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectMetadata = createMockObjectMetadata(
|
||||||
|
[mockIdField, mockEmailField, mockNameField],
|
||||||
|
[emailIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = getUniqueConstraintsFields(objectMetadata);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toHaveLength(1);
|
||||||
|
expect(result[0][0]).toEqual(mockIdField);
|
||||||
|
expect(result[1]).toHaveLength(1);
|
||||||
|
expect(result[1][0]).toEqual(mockEmailField);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
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 const getUniqueConstraintsFields = (
|
||||||
|
objectMetadata: ObjectMetadataInterface,
|
||||||
|
): FieldMetadataInterface[][] => {
|
||||||
|
const uniqueIndexes = objectMetadata.indexMetadatas.filter(
|
||||||
|
(index) => index.isUnique,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldsMapById = new Map(
|
||||||
|
objectMetadata.fields.map((field) => [field.id, field]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const primaryKeyConstraintField = objectMetadata.fields.find(
|
||||||
|
(field) => field.name === 'id',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDefined(primaryKeyConstraintField)) {
|
||||||
|
throw new Error(
|
||||||
|
`Primary key constraint field not found for object metadata ${objectMetadata.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherUniqueConstraintsFields = uniqueIndexes.map((index) =>
|
||||||
|
index.indexFieldMetadatas.map((field) => {
|
||||||
|
const indexField = fieldsMapById.get(field.fieldMetadataId);
|
||||||
|
|
||||||
|
if (!isDefined(indexField)) {
|
||||||
|
throw new Error(
|
||||||
|
`Index field not found for field id ${field.fieldMetadataId} in index metadata ${index.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexField;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [[primaryKeyConstraintField], ...otherUniqueConstraintsFields];
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user