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 { 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 { 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 { ArgsFactory } from './args.factory';
|
||||
@ -25,6 +26,7 @@ export const workspaceSchemaBuilderFactories = [
|
||||
InputTypeFactory,
|
||||
InputTypeDefinitionFactory,
|
||||
CompositeInputTypeDefinitionFactory,
|
||||
RelationConnectInputTypeDefinitionFactory,
|
||||
OutputTypeFactory,
|
||||
ObjectTypeDefinitionFactory,
|
||||
CompositeObjectTypeDefinitionFactory,
|
||||
|
||||
@ -32,11 +32,17 @@ export class InputTypeDefinitionFactory {
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
public create({
|
||||
objectMetadata,
|
||||
kind,
|
||||
options,
|
||||
isRelationConnectEnabled = false,
|
||||
}: {
|
||||
objectMetadata: ObjectMetadataInterface;
|
||||
kind: InputTypeDefinitionKind;
|
||||
options: WorkspaceBuildSchemaOptions;
|
||||
isRelationConnectEnabled?: boolean;
|
||||
}): InputTypeDefinition {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
const inputType = new GraphQLInputObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
|
||||
@ -55,12 +61,12 @@ export class InputTypeDefinitionFactory {
|
||||
});
|
||||
|
||||
return {
|
||||
...generateFields(
|
||||
...generateFields({
|
||||
objectMetadata,
|
||||
kind,
|
||||
options,
|
||||
this.inputTypeFactory,
|
||||
),
|
||||
typeFactory: this.inputTypeFactory,
|
||||
}),
|
||||
and: {
|
||||
type: andOrType,
|
||||
},
|
||||
@ -78,12 +84,13 @@ export class InputTypeDefinitionFactory {
|
||||
* Other input types are generated with fields only
|
||||
*/
|
||||
default:
|
||||
return generateFields(
|
||||
return generateFields({
|
||||
objectMetadata,
|
||||
kind,
|
||||
options,
|
||||
this.inputTypeFactory,
|
||||
);
|
||||
typeFactory: this.inputTypeFactory,
|
||||
isRelationConnectEnabled,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -44,6 +44,9 @@ export class InputTypeFactory {
|
||||
*/
|
||||
case InputTypeDefinitionKind.Create:
|
||||
case InputTypeDefinitionKind.Update:
|
||||
//if it's a relation connect field, type is in storage
|
||||
if (typeOptions.isRelationConnectField) break;
|
||||
|
||||
inputType = this.typeMapperService.mapToScalarType(
|
||||
type,
|
||||
typeOptions.settings,
|
||||
|
||||
@ -37,12 +37,12 @@ export class ObjectTypeDefinitionFactory {
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: generateFields(
|
||||
fields: generateFields({
|
||||
objectMetadata,
|
||||
kind,
|
||||
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'
|
||||
*/
|
||||
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;
|
||||
settings?: FieldMetadataSettings<FieldMetadataType>;
|
||||
isIdField?: boolean;
|
||||
isRelationConnectField?: boolean;
|
||||
}
|
||||
|
||||
const StringArrayScalarType = new GraphQLList(GraphQLString);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
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 { 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 { 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 { 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 { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
|
||||
@ -39,6 +44,8 @@ export class TypeDefinitionsGenerator {
|
||||
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
|
||||
private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory,
|
||||
private readonly extendObjectTypeDefinitionV2Factory: ExtendObjectTypeDefinitionV2Factory,
|
||||
private readonly relationConnectInputTypeDefinitionFactory: RelationConnectInputTypeDefinitionFactory,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async generate(
|
||||
@ -49,6 +56,8 @@ export class TypeDefinitionsGenerator {
|
||||
await this.generateCompositeTypeDefs(options);
|
||||
// Generate metadata objects
|
||||
await this.generateMetadataTypeDefs(objectMetadataCollection, options);
|
||||
|
||||
this.generateRelationConnectInputTypeDefs(objectMetadataCollection);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,10 +105,10 @@ export class TypeDefinitionsGenerator {
|
||||
}
|
||||
|
||||
private generateCompositeInputTypeDefs(
|
||||
compisteTypes: CompositeType[],
|
||||
compositeTypes: CompositeType[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
) {
|
||||
const inputTypeDefs = compisteTypes
|
||||
const inputTypeDefs = compositeTypes
|
||||
.map((compositeType) => {
|
||||
const optionalExtendedObjectMetadata = {
|
||||
...compositeType,
|
||||
@ -159,7 +168,7 @@ export class TypeDefinitionsGenerator {
|
||||
this.generateEnumTypeDefs(dynamicObjectMetadataCollection, options);
|
||||
this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options);
|
||||
this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options);
|
||||
this.generateInputTypeDefs(dynamicObjectMetadataCollection, options);
|
||||
await this.generateInputTypeDefs(dynamicObjectMetadataCollection, options);
|
||||
await this.generateExtendedObjectTypeDefs(
|
||||
dynamicObjectMetadataCollection,
|
||||
options,
|
||||
@ -200,10 +209,17 @@ export class TypeDefinitionsGenerator {
|
||||
this.typeDefinitionsStorage.addObjectTypes(connectionTypeDefs);
|
||||
}
|
||||
|
||||
private generateInputTypeDefs(
|
||||
private async generateInputTypeDefs(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
) {
|
||||
const isRelationConnectEnabled = isDefined(options.workspaceId)
|
||||
? await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_RELATION_CONNECT_ENABLED,
|
||||
options.workspaceId,
|
||||
)
|
||||
: false;
|
||||
|
||||
const inputTypeDefs = objectMetadataCollection
|
||||
.map((objectMetadata) => {
|
||||
const optionalExtendedObjectMetadata = {
|
||||
@ -216,29 +232,31 @@ export class TypeDefinitionsGenerator {
|
||||
|
||||
return [
|
||||
// Input type for create
|
||||
this.inputTypeDefinitionFactory.create(
|
||||
this.inputTypeDefinitionFactory.create({
|
||||
objectMetadata,
|
||||
InputTypeDefinitionKind.Create,
|
||||
kind: InputTypeDefinitionKind.Create,
|
||||
options,
|
||||
),
|
||||
isRelationConnectEnabled,
|
||||
}),
|
||||
// Input type for update
|
||||
this.inputTypeDefinitionFactory.create(
|
||||
optionalExtendedObjectMetadata,
|
||||
InputTypeDefinitionKind.Update,
|
||||
this.inputTypeDefinitionFactory.create({
|
||||
objectMetadata: optionalExtendedObjectMetadata,
|
||||
kind: InputTypeDefinitionKind.Update,
|
||||
isRelationConnectEnabled,
|
||||
options,
|
||||
),
|
||||
}),
|
||||
// Filter input type
|
||||
this.inputTypeDefinitionFactory.create(
|
||||
optionalExtendedObjectMetadata,
|
||||
InputTypeDefinitionKind.Filter,
|
||||
this.inputTypeDefinitionFactory.create({
|
||||
objectMetadata: optionalExtendedObjectMetadata,
|
||||
kind: InputTypeDefinitionKind.Filter,
|
||||
options,
|
||||
),
|
||||
}),
|
||||
// OrderBy input type
|
||||
this.inputTypeDefinitionFactory.create(
|
||||
optionalExtendedObjectMetadata,
|
||||
InputTypeDefinitionKind.OrderBy,
|
||||
this.inputTypeDefinitionFactory.create({
|
||||
objectMetadata: optionalExtendedObjectMetadata,
|
||||
kind: InputTypeDefinitionKind.OrderBy,
|
||||
options,
|
||||
),
|
||||
}),
|
||||
];
|
||||
})
|
||||
.flat();
|
||||
@ -283,4 +301,16 @@ export class TypeDefinitionsGenerator {
|
||||
|
||||
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,
|
||||
} from 'graphql';
|
||||
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 { 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 { 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 { 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 { 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
|
||||
settings: any;
|
||||
isIdField: boolean;
|
||||
isRelationConnectField?: boolean;
|
||||
},
|
||||
) => T extends InputTypeDefinitionKind
|
||||
? GraphQLInputType
|
||||
@ -38,87 +42,189 @@ type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> =
|
||||
|
||||
export const generateFields = <
|
||||
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
||||
>(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: T,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
typeFactory: TypeFactory<T>,
|
||||
): T extends InputTypeDefinitionKind
|
||||
>({
|
||||
objectMetadata,
|
||||
kind,
|
||||
options,
|
||||
typeFactory,
|
||||
isRelationConnectEnabled = false,
|
||||
}: {
|
||||
objectMetadata: ObjectMetadataInterface;
|
||||
kind: T;
|
||||
options: WorkspaceBuildSchemaOptions;
|
||||
typeFactory: TypeFactory<T>;
|
||||
isRelationConnectEnabled?: boolean;
|
||||
}): T extends InputTypeDefinitionKind
|
||||
? GraphQLInputFieldConfigMap
|
||||
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
GraphQLFieldConfigMap<any, any> => {
|
||||
const fields = {};
|
||||
const allGeneratedFields = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
let generatedField;
|
||||
|
||||
if (
|
||||
isFieldMetadataInterfaceOfType(
|
||||
fieldMetadata,
|
||||
FieldMetadataType.RELATION,
|
||||
) &&
|
||||
fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE
|
||||
isFieldMetadataInterfaceOfType(fieldMetadata, FieldMetadataType.RELATION)
|
||||
) {
|
||||
continue;
|
||||
generatedField = generateRelationField({
|
||||
fieldMetadata,
|
||||
kind,
|
||||
options,
|
||||
typeFactory,
|
||||
isRelationConnectEnabled,
|
||||
});
|
||||
} else {
|
||||
generatedField = generateField({
|
||||
fieldMetadata,
|
||||
kind,
|
||||
options,
|
||||
typeFactory,
|
||||
});
|
||||
}
|
||||
|
||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
||||
? fieldMetadata.type.toString()
|
||||
: fieldMetadata.id;
|
||||
Object.assign(allGeneratedFields, generatedField);
|
||||
}
|
||||
|
||||
const typeFactoryOptions = 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,
|
||||
};
|
||||
return allGeneratedFields;
|
||||
};
|
||||
|
||||
const type = typeFactory.create(
|
||||
target,
|
||||
const getTarget = <T extends FieldMetadataType>(
|
||||
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,
|
||||
kind,
|
||||
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
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
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 { TypeDefinitionsGenerator } from './type-definitions.generator';
|
||||
@ -11,7 +12,11 @@ import { TypeMapperService } from './services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
||||
|
||||
@Module({
|
||||
imports: [ObjectMetadataModule, WorkspaceResolverBuilderModule],
|
||||
imports: [
|
||||
ObjectMetadataModule,
|
||||
WorkspaceResolverBuilderModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
TypeDefinitionsStorage,
|
||||
TypeMapperService,
|
||||
|
||||
@ -81,7 +81,9 @@ export class WorkspaceSchemaFactory {
|
||||
await this.workspaceGraphQLSchemaFactory.create(
|
||||
objectMetadataCollection,
|
||||
workspaceResolverBuilderMethodNames,
|
||||
{},
|
||||
{
|
||||
workspaceId: authContext.workspace.id,
|
||||
},
|
||||
);
|
||||
|
||||
usedScalarNames =
|
||||
|
||||
@ -7,4 +7,5 @@ export enum FeatureFlagKey {
|
||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||
IS_IMAP_ENABLED = 'IS_IMAP_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