feat: drop target column map (#4670)

This PR is dropping the column `targetColumnMap` of fieldMetadata
entities.
The goal of this column was to properly map field to their respecting
column in the table.
We decide to drop it and instead compute the column name on the fly when
we need it, as it's more easier to support.
Some parts of the code has been refactored to try making implementation
of composite type more easier to understand and maintain.

Fix #3760

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2024-04-08 16:00:28 +02:00
committed by GitHub
parent 84f8c14e52
commit 5019b5febc
72 changed files with 1432 additions and 1853 deletions

View File

@ -0,0 +1,84 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { pascalCase } from 'src/utils/pascal-case';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
InputTypeDefinition,
InputTypeDefinitionKind,
} from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { InputTypeFactory } from './input-type.factory';
@Injectable()
export class CompositeInputTypeDefinitionFactory {
private readonly logger = new Logger(
CompositeInputTypeDefinitionFactory.name,
);
constructor(private readonly inputTypeFactory: InputTypeFactory) {}
public create(
compositeType: CompositeType,
kind: InputTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions,
): InputTypeDefinition {
const name = pascalCase(compositeType.type.toString().toLowerCase());
return {
target: compositeType.type.toString(),
kind,
type: new GraphQLInputObjectType({
name: `${pascalCase(name)}${kind.toString()}Input`,
fields: this.generateFields(compositeType, kind, options),
}),
};
}
private generateFields(
compositeType: CompositeType,
kind: InputTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
for (const property of compositeType.properties) {
// Relation fields are not supported in composite types
if (isRelationFieldMetadataType(property.type)) {
this.logger.error(
'Relation fields are not supported in composite types',
{ compositeType, property },
);
throw new Error('Relation fields are not supported in composite types');
}
// Skip hidden fields
if (property.hidden === true || property.hidden === 'input') {
continue;
}
const type = this.inputTypeFactory.create(
property.name,
property.type,
kind,
options,
{
nullable: !property.isRequired,
isArray: property.type === FieldMetadataType.MULTI_SELECT,
},
);
fields[property.name] = {
type,
description: property.description,
};
}
return fields;
}
}

View File

@ -0,0 +1,84 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { pascalCase } from 'src/utils/pascal-case';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { OutputTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory';
import {
ObjectTypeDefinition,
ObjectTypeDefinitionKind,
} from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
@Injectable()
export class CompositeObjectTypeDefinitionFactory {
private readonly logger = new Logger(
CompositeObjectTypeDefinitionFactory.name,
);
constructor(private readonly outputTypeFactory: OutputTypeFactory) {}
public create(
compositeType: CompositeType,
options: WorkspaceBuildSchemaOptions,
): ObjectTypeDefinition {
const name = pascalCase(compositeType.type.toString().toLowerCase());
const kind = ObjectTypeDefinitionKind.Plain;
return {
target: compositeType.type.toString(),
kind,
type: new GraphQLObjectType({
name: `${name}${kind.toString()}`,
fields: this.generateFields(compositeType, kind, options),
}),
};
}
private generateFields(
compositeType: CompositeType,
kind: ObjectTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions,
): GraphQLFieldConfigMap<any, any> {
const fields: GraphQLFieldConfigMap<any, any> = {};
for (const property of compositeType.properties) {
// Relation fields are not supported in composite types
if (isRelationFieldMetadataType(property.type)) {
this.logger.error(
'Relation fields are not supported in composite types',
{ compositeType, property },
);
throw new Error('Relation fields are not supported in composite types');
}
// Skip hidden fields
if (property.hidden === true || property.hidden === 'output') {
continue;
}
const type = this.outputTypeFactory.create(
property.name,
property.type,
kind,
options,
{
nullable: !property.isRequired,
isArray: property.type === FieldMetadataType.MULTI_SELECT,
},
);
fields[property.name] = {
type,
description: property.description,
};
}
return fields;
}
}

View File

@ -1,4 +1,6 @@
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 { ArgsFactory } from './args.factory';
import { InputTypeFactory } from './input-type.factory';
@ -7,15 +9,11 @@ import { ObjectTypeDefinitionFactory } from './object-type-definition.factory';
import { OutputTypeFactory } from './output-type.factory';
import { QueryTypeFactory } from './query-type.factory';
import { RootTypeFactory } from './root-type.factory';
import { FilterTypeFactory } from './filter-type.factory';
import { FilterTypeDefinitionFactory } from './filter-type-definition.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 { OrderByTypeFactory } from './order-by-type.factory';
import { OrderByTypeDefinitionFactory } from './order-by-type-definition.factory';
import { RelationTypeFactory } from './relation-type.factory';
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
import { OrphanedTypesFactory } from './orphaned-types.factory';
@ -24,15 +22,13 @@ export const workspaceSchemaBuilderFactories = [
ArgsFactory,
InputTypeFactory,
InputTypeDefinitionFactory,
CompositeInputTypeDefinitionFactory,
OutputTypeFactory,
ObjectTypeDefinitionFactory,
CompositeObjectTypeDefinitionFactory,
EnumTypeDefinitionFactory,
RelationTypeFactory,
ExtendObjectTypeDefinitionFactory,
FilterTypeFactory,
FilterTypeDefinitionFactory,
OrderByTypeFactory,
OrderByTypeDefinitionFactory,
ConnectionTypeFactory,
ConnectionTypeDefinitionFactory,
EdgeTypeFactory,

View File

@ -1,91 +0,0 @@
import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } 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 { pascalCase } from 'src/utils/pascal-case';
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { FilterTypeFactory } from './filter-type.factory';
import {
InputTypeDefinition,
InputTypeDefinitionKind,
} from './input-type-definition.factory';
@Injectable()
export class FilterTypeDefinitionFactory {
constructor(
private readonly filterTypeFactory: FilterTypeFactory,
private readonly typeMapperService: TypeMapperService,
) {}
public create(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): InputTypeDefinition {
const kind = InputTypeDefinitionKind.Filter;
const filterInputType = new GraphQLInputObjectType({
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
description: objectMetadata.description,
fields: () => {
const andOrType = this.typeMapperService.mapToGqlType(filterInputType, {
isArray: true,
arrayDepth: 1,
nullable: true,
});
return {
...this.generateFields(objectMetadata, options),
and: {
type: andOrType,
},
or: {
type: andOrType,
},
not: {
type: this.typeMapperService.mapToGqlType(filterInputType, {
nullable: true,
}),
},
};
},
});
return {
target: objectMetadata.id,
kind,
type: filterInputType,
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
for (const fieldMetadata of objectMetadata.fields) {
// Relation types are generated during extension of object type definition
if (isRelationFieldMetadataType(fieldMetadata.type)) {
continue;
}
const type = this.filterTypeFactory.create(fieldMetadata, options, {
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
});
fields[fieldMetadata.name] = {
type,
description: fieldMetadata.description,
// TODO: Add default value
defaultValue: undefined,
};
}
return fields;
}
}

View File

@ -1,102 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import {
GraphQLInputObjectType,
GraphQLInputType,
GraphQLList,
GraphQLScalarType,
} from 'graphql';
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 {
TypeMapperService,
TypeOptions,
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
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 { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
import { InputTypeDefinitionKind } from './input-type-definition.factory';
@Injectable()
export class FilterTypeFactory {
private readonly logger = new Logger(FilterTypeFactory.name);
constructor(
private readonly typeMapperService: TypeMapperService,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
fieldMetadata: FieldMetadataInterface,
buildOptions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLInputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let filterType: GraphQLInputObjectType | GraphQLScalarType | undefined =
undefined;
if (isEnumFieldMetadataType(fieldMetadata.type)) {
filterType = this.createEnumFilterType(fieldMetadata);
} else {
filterType = this.typeMapperService.mapToFilterType(
fieldMetadata.type,
buildOptions.dateScalarMode,
buildOptions.numberScalarMode,
);
filterType ??= this.typeDefinitionsStorage.getInputTypeByKey(
target,
InputTypeDefinitionKind.Filter,
);
}
if (!filterType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata,
buildOptions,
typeOptions,
});
throw new Error(`Could not find a GraphQL type for ${target}`);
}
return this.typeMapperService.mapToGqlType(filterType, typeOptions);
}
private createEnumFilterType(
fieldMetadata: FieldMetadataInterface,
): GraphQLInputObjectType {
const enumType = this.typeDefinitionsStorage.getEnumTypeByKey(
fieldMetadata.id,
);
if (!enumType) {
this.logger.error(
`Could not find a GraphQL enum type for ${fieldMetadata.id}`,
{
fieldMetadata,
},
);
throw new Error(
`Could not find a GraphQL enum type for ${fieldMetadata.id}`,
);
}
return new GraphQLInputObjectType({
name: `${enumType.name}Filter`,
fields: () => ({
eq: { type: enumType },
neq: { type: enumType },
in: { type: new GraphQLList(enumType) },
is: { type: FilterIs },
}),
});
}
}

View File

@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable, forwardRef } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
@ -8,6 +8,8 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad
import { pascalCase } from 'src/utils/pascal-case';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
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 { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { InputTypeFactory } from './input-type.factory';
@ -26,23 +28,60 @@ export interface InputTypeDefinition {
@Injectable()
export class InputTypeDefinitionFactory {
constructor(private readonly inputTypeFactory: InputTypeFactory) {}
constructor(
@Inject(forwardRef(() => InputTypeFactory))
private readonly inputTypeFactory: InputTypeFactory,
private readonly typeMapperService: TypeMapperService,
) {}
public create(
objectMetadata: ObjectMetadataInterface,
kind: InputTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions,
): InputTypeDefinition {
const inputType = new GraphQLInputObjectType({
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
description: objectMetadata.description,
fields: () => {
switch (kind) {
/**
* Filter input type has additional fields for filtering and is self referencing
*/
case InputTypeDefinitionKind.Filter: {
const andOrType = this.typeMapperService.mapToGqlType(inputType, {
isArray: true,
arrayDepth: 1,
nullable: true,
});
return {
...this.generateFields(objectMetadata, kind, options),
and: {
type: andOrType,
},
or: {
type: andOrType,
},
not: {
type: this.typeMapperService.mapToGqlType(inputType, {
nullable: true,
}),
},
};
}
/**
* Other input types are generated with fields only
*/
default:
return this.generateFields(objectMetadata, kind, options);
}
},
});
return {
target: objectMetadata.id,
kind,
type: new GraphQLInputObjectType({
name: `${pascalCase(
objectMetadata.nameSingular,
)}${kind.toString()}Input`,
description: objectMetadata.description,
fields: this.generateFields(objectMetadata, kind, options),
}),
type: inputType,
};
}
@ -59,17 +98,25 @@ export class InputTypeDefinitionFactory {
continue;
}
const type = this.inputTypeFactory.create(fieldMetadata, kind, options, {
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
});
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
const type = this.inputTypeFactory.create(
target,
fieldMetadata.type,
kind,
options,
{
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
},
);
fields[fieldMetadata.name] = {
type,
description: fieldMetadata.description,
// TODO: Add default value
defaultValue: undefined,
};
}

View File

@ -1,16 +1,17 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLInputType } from 'graphql';
import { GraphQLInputObjectType, GraphQLInputType, GraphQLList } from 'graphql';
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 {
TypeMapperService,
TypeOptions,
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { InputTypeDefinitionKind } from './input-type-definition.factory';
@ -24,30 +25,60 @@ export class InputTypeFactory {
) {}
public create(
fieldMetadata: FieldMetadataInterface,
target: string,
type: FieldMetadataType,
kind: InputTypeDefinitionKind,
buildOtions: WorkspaceBuildSchemaOptions,
buildOptions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLInputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let inputType: GraphQLInputType | undefined =
this.typeMapperService.mapToScalarType(
fieldMetadata.type,
buildOtions.dateScalarMode,
buildOtions.numberScalarMode,
);
let inputType: GraphQLInputType | undefined;
switch (kind) {
/**
* Create and Update input types are classic scalar types
*/
case InputTypeDefinitionKind.Create:
case InputTypeDefinitionKind.Update:
inputType = this.typeMapperService.mapToScalarType(
type,
buildOptions.dateScalarMode,
buildOptions.numberScalarMode,
);
break;
/**
* Filter input maps to special filter type
*/
case InputTypeDefinitionKind.Filter: {
if (isEnumFieldMetadataType(type)) {
inputType = this.createEnumFilterType(target);
} else {
inputType = this.typeMapperService.mapToFilterType(
type,
buildOptions.dateScalarMode,
buildOptions.numberScalarMode,
);
}
break;
}
/**
* OrderBy input maps to special order by type
*/
case InputTypeDefinitionKind.OrderBy:
inputType = this.typeMapperService.mapToOrderByType(type);
break;
}
/**
* If input type is not scalar, we're looking for it in the type definitions storage as it can be an object type
*/
inputType ??= this.typeDefinitionsStorage.getInputTypeByKey(target, kind);
inputType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
if (!inputType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata,
type,
kind,
buildOtions,
buildOptions,
typeOptions,
});
@ -56,4 +87,24 @@ export class InputTypeFactory {
return this.typeMapperService.mapToGqlType(inputType, typeOptions);
}
private createEnumFilterType(target: string): GraphQLInputObjectType {
const enumType = this.typeDefinitionsStorage.getEnumTypeByKey(target);
if (!enumType) {
this.logger.error(`Could not find a GraphQL enum type for ${target}`);
throw new Error(`Could not find a GraphQL enum type for ${target}`);
}
return new GraphQLInputObjectType({
name: `${enumType.name}Filter`,
fields: () => ({
eq: { type: enumType },
neq: { type: enumType },
in: { type: new GraphQLList(enumType) },
is: { type: FilterIs },
}),
});
}
}

View File

@ -8,6 +8,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad
import { pascalCase } from 'src/utils/pascal-case';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
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 { OutputTypeFactory } from './output-type.factory';
@ -56,10 +57,20 @@ export class ObjectTypeDefinitionFactory {
continue;
}
const type = this.outputTypeFactory.create(fieldMetadata, kind, options, {
nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
});
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
const type = this.outputTypeFactory.create(
target,
fieldMetadata.type,
kind,
options,
{
nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
},
);
fields[fieldMetadata.name] = {
type,

View File

@ -1,66 +0,0 @@
import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } 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 { pascalCase } from 'src/utils/pascal-case';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import {
InputTypeDefinition,
InputTypeDefinitionKind,
} from './input-type-definition.factory';
import { OrderByTypeFactory } from './order-by-type.factory';
@Injectable()
export class OrderByTypeDefinitionFactory {
constructor(private readonly orderByTypeFactory: OrderByTypeFactory) {}
public create(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): InputTypeDefinition {
const kind = InputTypeDefinitionKind.OrderBy;
return {
target: objectMetadata.id,
kind,
type: new GraphQLInputObjectType({
name: `${pascalCase(
objectMetadata.nameSingular,
)}${kind.toString()}Input`,
description: objectMetadata.description,
fields: this.generateFields(objectMetadata, options),
}),
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
for (const fieldMetadata of objectMetadata.fields) {
// Relation field types are generated during extension of object type definition
if (isRelationFieldMetadataType(fieldMetadata.type)) {
continue;
}
const type = this.orderByTypeFactory.create(fieldMetadata, options, {
nullable: fieldMetadata.isNullable,
});
fields[fieldMetadata.name] = {
type,
description: fieldMetadata.description,
// TODO: Add default value
defaultValue: undefined,
};
}
return fields;
}
}

View File

@ -1,55 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLInputType } from 'graphql';
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 {
TypeMapperService,
TypeOptions,
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { InputTypeDefinitionKind } from './input-type-definition.factory';
@Injectable()
export class OrderByTypeFactory {
private readonly logger = new Logger(OrderByTypeFactory.name);
constructor(
private readonly typeMapperService: TypeMapperService,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
fieldMetadata: FieldMetadataInterface,
buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLInputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let orderByType = this.typeMapperService.mapToOrderByType(
fieldMetadata.type,
);
orderByType ??= this.typeDefinitionsStorage.getInputTypeByKey(
target,
InputTypeDefinitionKind.OrderBy,
);
if (!orderByType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata,
buildOtions,
typeOptions,
});
throw new Error(`Could not find a GraphQL type for ${target}`);
}
return this.typeMapperService.mapToGqlType(orderByType, typeOptions);
}
}

View File

@ -3,14 +3,13 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql';
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 {
TypeMapperService,
TypeOptions,
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
@ -24,28 +23,24 @@ export class OutputTypeFactory {
) {}
public create(
fieldMetadata: FieldMetadataInterface,
target: string,
type: FieldMetadataType,
kind: ObjectTypeDefinitionKind,
buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLOutputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let gqlType: GraphQLOutputType | undefined =
this.typeMapperService.mapToScalarType(
fieldMetadata.type,
type,
buildOtions.dateScalarMode,
buildOtions.numberScalarMode,
);
gqlType ??= this.typeDefinitionsStorage.getObjectTypeByKey(target, kind);
gqlType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
gqlType ??= this.typeDefinitionsStorage.getOutputTypeByKey(target, kind);
if (!gqlType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata,
type,
buildOtions,
typeOptions,
});

View File

@ -57,7 +57,6 @@ export class TypeMapperService {
const numberScalar =
numberScalarMode === 'float' ? GraphQLFloat : GraphQLInt;
// LINK and CURRENCY are handled in the factories because they are objects
const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([
[FieldMetadataType.UUID, GraphQLID],
[FieldMetadataType.TEXT, GraphQLString],
@ -86,7 +85,6 @@ export class TypeMapperService {
const numberScalar =
numberScalarMode === 'float' ? FloatFilterType : IntFilterType;
// LINK and CURRENCY are handled in the factories because they are objects
const typeFilterMapping = new Map<
FieldMetadataType,
GraphQLInputObjectType | GraphQLScalarType
@ -111,7 +109,6 @@ export class TypeMapperService {
mapToOrderByType(
fieldMetadataType: FieldMetadataType,
): GraphQLInputType | undefined {
// LINK and CURRENCY are handled in the factories because they are objects
const typeOrderByMapping = new Map<FieldMetadataType, GraphQLEnumType>([
[FieldMetadataType.UUID, OrderByDirectionType],
[FieldMetadataType.TEXT, OrderByDirectionType],

View File

@ -17,7 +17,12 @@ import {
ObjectTypeDefinitionKind,
} from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
// Must be scoped on REQUEST level
export type GqlInputType = InputTypeDefinition | EnumTypeDefinition;
export type GqlOutputType = ObjectTypeDefinition | EnumTypeDefinition;
// Must be scoped on REQUEST level, because we need to recreate it for each workspaces
// TODO: Implement properly durable by workspace
@Injectable({ scope: Scope.REQUEST, durable: true })
export class TypeDefinitionsStorage {
private readonly enumTypeDefinitions = new Map<string, EnumTypeDefinition>();
@ -68,10 +73,27 @@ export class TypeDefinitionsStorage {
getInputTypeByKey(
target: string,
kind: InputTypeDefinitionKind,
): GraphQLInputObjectType | undefined {
return this.inputTypeDefinitions.get(
this.generateCompositeKey(target, kind),
)?.type;
): GraphQLInputObjectType | GraphQLEnumType | undefined {
const key = this.generateCompositeKey(target, kind);
let definition: GqlInputType | undefined;
definition ??= this.inputTypeDefinitions.get(key);
definition ??= this.enumTypeDefinitions.get(target);
return definition?.type;
}
getOutputTypeByKey(
target: string,
kind: ObjectTypeDefinitionKind,
): GraphQLObjectType | GraphQLEnumType | undefined {
const key = this.generateCompositeKey(target, kind);
let definition: GqlOutputType | undefined;
definition ??= this.objectTypeDefinitions.get(key);
definition ??= this.enumTypeDefinitions.get(target);
return definition?.type;
}
getEnumTypeByKey(target: string): GraphQLEnumType | undefined {

View File

@ -1,15 +1,12 @@
import { Injectable, Logger } from '@nestjs/common';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { customTableDefaultColumns } from 'src/engine/workspace-manager/workspace-migration-runner/utils/custom-table-default-column.util';
import { fullNameObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
import { currencyObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
import { linkObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
import { addressObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
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 { TypeDefinitionsStorage } from './storages/type-definitions.storage';
import {
@ -20,24 +17,12 @@ import {
InputTypeDefinitionFactory,
InputTypeDefinitionKind,
} from './factories/input-type-definition.factory';
import { getFieldMetadataType } from './utils/get-field-metadata-type.util';
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface';
import { FilterTypeDefinitionFactory } from './factories/filter-type-definition.factory';
import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
import { EdgeTypeDefinitionFactory } from './factories/edge-type-definition.factory';
import { OrderByTypeDefinitionFactory } from './factories/order-by-type-definition.factory';
import { ExtendObjectTypeDefinitionFactory } from './factories/extend-object-type-definition.factory';
import { objectContainsRelationField } from './utils/object-contains-relation-field';
// Create a default field for each custom table default column
const defaultFields = customTableDefaultColumns.map((column) => {
return {
type: getFieldMetadataType(column.type),
name: column.name,
isNullable: true,
} as FieldMetadataEntity;
});
@Injectable()
export class TypeDefinitionsGenerator {
private readonly logger = new Logger(TypeDefinitionsGenerator.name);
@ -45,10 +30,10 @@ export class TypeDefinitionsGenerator {
constructor(
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory,
private readonly compositeObjectTypeDefinitionFactory: CompositeObjectTypeDefinitionFactory,
private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory,
private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory,
private readonly filterTypeDefintionFactory: FilterTypeDefinitionFactory,
private readonly orderByTypeDefinitionFactory: OrderByTypeDefinitionFactory,
private readonly compositeInputTypeDefinitionFactory: CompositeInputTypeDefinitionFactory,
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory,
private readonly extendObjectTypeDefinitionFactory: ExtendObjectTypeDefinitionFactory,
@ -58,38 +43,96 @@ export class TypeDefinitionsGenerator {
objectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions,
) {
// Generate static objects first because they can be used in dynamic objects
this.generateStaticObjectTypeDefs(options);
// Generate dynamic objects
this.generateDynamicObjectTypeDefs(objectMetadataCollection, options);
// Generate composite type objects first because they can be used in dynamic objects
this.generateCompositeTypeDefs(options);
// Generate metadata objects
this.generateMetadataTypeDefs(objectMetadataCollection, options);
}
private generateStaticObjectTypeDefs(options: WorkspaceBuildSchemaOptions) {
const staticObjectMetadataCollection = [
currencyObjectDefinition,
linkObjectDefinition,
fullNameObjectDefinition,
addressObjectDefinition,
] satisfies ObjectMetadataInterface[];
/**
* GENERATE COMPOSITE TYPE OBJECTS
*/
private generateCompositeTypeDefs(options: WorkspaceBuildSchemaOptions) {
const compositeTypeCollection = [...compositeTypeDefintions.values()];
this.logger.log(
`Generating staticObjects: [${staticObjectMetadataCollection
.map((object) => object.nameSingular)
`Generating composite type objects: [${compositeTypeCollection
.map((compositeType) => compositeType.type)
.join(', ')}]`,
);
// Generate static objects first because they can be used in dynamic objects
this.generateEnumTypeDefs(staticObjectMetadataCollection, options);
this.generateObjectTypeDefs(staticObjectMetadataCollection, options);
this.generateInputTypeDefs(staticObjectMetadataCollection, options);
// Generate composite types first because they can be used in metadata objects
this.generateCompositeObjectTypeDefs(compositeTypeCollection, options);
this.generateCompositeInputTypeDefs(compositeTypeCollection, options);
}
private generateDynamicObjectTypeDefs(
private generateCompositeObjectTypeDefs(
compositeTypes: CompositeType[],
options: WorkspaceBuildSchemaOptions,
) {
const compositeObjectTypeDefs = compositeTypes.map((compositeType) =>
this.compositeObjectTypeDefinitionFactory.create(compositeType, options),
);
this.typeDefinitionsStorage.addObjectTypes(compositeObjectTypeDefs);
}
private generateCompositeInputTypeDefs(
compisteTypes: CompositeType[],
options: WorkspaceBuildSchemaOptions,
) {
const inputTypeDefs = compisteTypes
.map((compositeType) => {
const optionalExtendedObjectMetadata = {
...compositeType,
properties: compositeType.properties.map((property) => ({
...property,
isRequired: false,
})),
};
return [
// Input type for create
this.compositeInputTypeDefinitionFactory.create(
compositeType,
InputTypeDefinitionKind.Create,
options,
),
// Input type for update
this.compositeInputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata,
InputTypeDefinitionKind.Update,
options,
),
// Filter input type
this.compositeInputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata,
InputTypeDefinitionKind.Filter,
options,
),
// OrderBy input type
this.compositeInputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata,
InputTypeDefinitionKind.OrderBy,
options,
),
];
})
.flat();
this.typeDefinitionsStorage.addInputTypes(inputTypeDefs);
}
/**
* GENERATE METADATA OBJECTS
*/
private generateMetadataTypeDefs(
dynamicObjectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions,
) {
this.logger.log(
`Generating dynamicObjects: [${dynamicObjectMetadataCollection
`Generating metadata objects: [${dynamicObjectMetadataCollection
.map((object) => object.nameSingular)
.join(', ')}]`,
);
@ -106,22 +149,16 @@ export class TypeDefinitionsGenerator {
}
private generateObjectTypeDefs(
objectMetadataCollection: ObjectMetadataInterface[],
objectMetadataCollection: ObjectMetadataInterface[] | CompositeType[],
options: WorkspaceBuildSchemaOptions,
) {
const objectTypeDefs = objectMetadataCollection.map((objectMetadata) => {
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
const extendedObjectMetadata = {
...objectMetadata,
fields,
};
return this.objectTypeDefinitionFactory.create(
extendedObjectMetadata,
const objectTypeDefs = objectMetadataCollection.map((objectMetadata) =>
this.objectTypeDefinitionFactory.create(
objectMetadata,
ObjectTypeDefinitionKind.Plain,
options,
);
});
),
);
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs);
}
@ -130,35 +167,15 @@ export class TypeDefinitionsGenerator {
objectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions,
) {
const edgeTypeDefs = objectMetadataCollection.map((objectMetadata) => {
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
const extendedObjectMetadata = {
...objectMetadata,
fields,
};
return this.edgeTypeDefinitionFactory.create(
extendedObjectMetadata,
options,
);
});
const edgeTypeDefs = objectMetadataCollection.map((objectMetadata) =>
this.edgeTypeDefinitionFactory.create(objectMetadata, options),
);
this.typeDefinitionsStorage.addObjectTypes(edgeTypeDefs);
// Connection type defs are using edge type defs
const connectionTypeDefs = objectMetadataCollection.map(
(objectMetadata) => {
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
const extendedObjectMetadata = {
...objectMetadata,
fields,
};
return this.connectionTypeDefinitionFactory.create(
extendedObjectMetadata,
options,
);
},
const connectionTypeDefs = objectMetadataCollection.map((objectMetadata) =>
this.connectionTypeDefinitionFactory.create(objectMetadata, options),
);
this.typeDefinitionsStorage.addObjectTypes(connectionTypeDefs);
@ -170,20 +187,18 @@ export class TypeDefinitionsGenerator {
) {
const inputTypeDefs = objectMetadataCollection
.map((objectMetadata) => {
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
const requiredExtendedObjectMetadata = {
...objectMetadata,
fields,
};
const optionalExtendedObjectMetadata = {
...objectMetadata,
fields: fields.map((field) => ({ ...field, isNullable: true })),
fields: objectMetadata.fields.map((field) => ({
...field,
isNullable: true,
})),
};
return [
// Input type for create
this.inputTypeDefinitionFactory.create(
requiredExtendedObjectMetadata,
objectMetadata,
InputTypeDefinitionKind.Create,
options,
),
@ -194,13 +209,15 @@ export class TypeDefinitionsGenerator {
options,
),
// Filter input type
this.filterTypeDefintionFactory.create(
this.inputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata,
InputTypeDefinitionKind.Filter,
options,
),
// OrderBy input type
this.orderByTypeDefinitionFactory.create(
this.inputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata,
InputTypeDefinitionKind.OrderBy,
options,
),
];
@ -237,16 +254,4 @@ export class TypeDefinitionsGenerator {
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs);
}
private mergeFieldsWithDefaults(
fields: FieldMetadataInterface[],
): FieldMetadataInterface[] {
const fieldNames = new Set(fields.map((field) => field.name));
const uniqueDefaultFields = defaultFields.filter(
(defaultField) => !fieldNames.has(defaultField.name),
);
return [...fields, ...uniqueDefaultFields];
}
}

View File

@ -12,10 +12,10 @@ import { TypeMapperService } from './services/type-mapper.service';
@Module({
imports: [ObjectMetadataModule],
providers: [
...workspaceSchemaBuilderFactories,
TypeDefinitionsGenerator,
TypeDefinitionsStorage,
TypeMapperService,
...workspaceSchemaBuilderFactories,
TypeDefinitionsGenerator,
WorkspaceGraphQLSchemaFactory,
],
exports: [WorkspaceGraphQLSchemaFactory],