Setup relations for remote objects (#5149)

New strategy:
- add settings field on FieldMetadata. Contains a boolean isIdField and
for numbers, a precision
- if idField, the graphql scalar returned will be a GraphQL id. This
will allow the app to work even for ids that are not uuid
- remove globals dateScalar and numberScalar modes. These were not used
- set limit as Integer
- check manually in query runner mutations that we send a valid id

Todo left:
- remove WorkspaceBuildSchemaOptions since this is not used anymore.
Will do in another PR

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Thomas Trompette
2024-04-26 14:37:34 +02:00
committed by GitHub
parent dc576d0818
commit 224c8d361b
71 changed files with 616 additions and 223 deletions

View File

@ -32,27 +32,7 @@ export class ArgsFactory {
// Argument is a scalar type
if (arg.type) {
const fieldType = this.typeMapperService.mapToScalarType(
arg.type,
options.dateScalarMode,
options.numberScalarMode,
);
if (!fieldType) {
this.logger.error(
`Could not find a GraphQL type for ${arg.type.toString()}`,
{
arg,
options,
},
);
throw new Error(
`Could not find a GraphQL type for ${arg.type.toString()}`,
);
}
const gqlType = this.typeMapperService.mapToGqlType(fieldType, {
const gqlType = this.typeMapperService.mapToGqlType(arg.type, {
defaultValue: arg.defaultValue,
nullable: arg.isNullable,
isArray: arg.isArray,

View File

@ -102,6 +102,8 @@ export class InputTypeDefinitionFactory {
? fieldMetadata.type.toString()
: fieldMetadata.id;
const isIdField = fieldMetadata.name === 'id';
const type = this.inputTypeFactory.create(
target,
fieldMetadata.type,
@ -111,6 +113,8 @@ export class InputTypeDefinitionFactory {
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
isIdField,
},
);

View File

@ -41,8 +41,8 @@ export class InputTypeFactory {
case InputTypeDefinitionKind.Update:
inputType = this.typeMapperService.mapToScalarType(
type,
buildOptions.dateScalarMode,
buildOptions.numberScalarMode,
typeOptions.settings,
typeOptions.isIdField,
);
break;
/**
@ -54,8 +54,8 @@ export class InputTypeFactory {
} else {
inputType = this.typeMapperService.mapToFilterType(
type,
buildOptions.dateScalarMode,
buildOptions.numberScalarMode,
typeOptions.settings,
typeOptions.isIdField,
);
}

View File

@ -69,6 +69,9 @@ export class ObjectTypeDefinitionFactory {
{
nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
// Scalar type is already defined in the entity itself.
isIdField: false,
},
);

View File

@ -32,8 +32,8 @@ export class OutputTypeFactory {
let gqlType: GraphQLOutputType | undefined =
this.typeMapperService.mapToScalarType(
type,
buildOtions.dateScalarMode,
buildOtions.numberScalarMode,
typeOptions.settings,
typeOptions.isIdField,
);
gqlType ??= this.typeDefinitionsStorage.getOutputTypeByKey(target, kind);

View File

@ -0,0 +1,17 @@
import { GraphQLID, GraphQLInputObjectType, GraphQLList } from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const IDFilterType = new GraphQLInputObjectType({
name: 'IDFilter',
fields: {
eq: { type: GraphQLID },
gt: { type: GraphQLID },
gte: { type: GraphQLID },
in: { type: new GraphQLList(GraphQLID) },
lt: { type: GraphQLID },
lte: { type: GraphQLID },
neq: { type: GraphQLID },
is: { type: FilterIs },
},
});

View File

@ -1,9 +1,10 @@
import { GraphQLScalarType } from 'graphql';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export interface ArgMetadata<T = any> {
kind?: InputTypeDefinitionKind;
type?: FieldMetadataType;
type?: GraphQLScalarType;
isNullable?: boolean;
isArray?: boolean;
defaultValue?: T;

View File

@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common';
import { GraphQLISODateTime, GraphQLTimestamp } from '@nestjs/graphql';
import { GraphQLISODateTime } from '@nestjs/graphql';
import {
GraphQLBoolean,
GraphQLEnumType,
GraphQLFloat,
GraphQLID,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLInt,
@ -15,22 +16,17 @@ import {
GraphQLType,
} from 'graphql';
import {
DateScalarMode,
NumberScalarMode,
} from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
UUIDFilterType,
StringFilterType,
DatetimeFilterType,
DateFilterType,
FloatFilterType,
IntFilterType,
BooleanFilterType,
BigFloatFilterType,
RawJsonFilterType,
IntFilterType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input';
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
import {
@ -39,38 +35,46 @@ import {
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { PositionScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/position.scalar';
import { JsonScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar';
import { IDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/id-filter.input-type';
export interface TypeOptions<T = any> {
nullable?: boolean;
isArray?: boolean;
arrayDepth?: number;
defaultValue?: T;
settings?: FieldMetadataSettings<FieldMetadataType | 'default'>;
isIdField?: boolean;
}
@Injectable()
export class TypeMapperService {
mapToScalarType(
fieldMetadataType: FieldMetadataType,
dateScalarMode: DateScalarMode = 'isoDate',
numberScalarMode: NumberScalarMode = 'float',
settings?: FieldMetadataSettings<FieldMetadataType | 'default'>,
isIdField?: boolean,
): GraphQLScalarType | undefined {
const dateScalar =
dateScalarMode === 'timestamp' ? GraphQLTimestamp : GraphQLISODateTime;
if (isIdField || settings?.isForeignKey) {
return GraphQLID;
}
const numberScalar =
numberScalarMode === 'float' ? GraphQLFloat : GraphQLInt;
fieldMetadataType === FieldMetadataType.NUMBER &&
(settings as FieldMetadataSettings<FieldMetadataType.NUMBER>)
?.precision === 0
? GraphQLInt
: GraphQLFloat;
const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([
[FieldMetadataType.UUID, UUIDScalarType],
[FieldMetadataType.TEXT, GraphQLString],
[FieldMetadataType.PHONE, GraphQLString],
[FieldMetadataType.EMAIL, GraphQLString],
[FieldMetadataType.DATE_TIME, dateScalar],
[FieldMetadataType.DATE, dateScalar],
[FieldMetadataType.DATE_TIME, GraphQLISODateTime],
[FieldMetadataType.DATE, GraphQLISODateTime],
[FieldMetadataType.BOOLEAN, GraphQLBoolean],
[FieldMetadataType.NUMBER, numberScalar],
[FieldMetadataType.NUMERIC, BigFloatScalarType],
[FieldMetadataType.PROBABILITY, GraphQLFloat],
[FieldMetadataType.RELATION, UUIDScalarType],
[FieldMetadataType.POSITION, PositionScalarType],
[FieldMetadataType.RAW_JSON, JsonScalarType],
]);
@ -80,29 +84,34 @@ export class TypeMapperService {
mapToFilterType(
fieldMetadataType: FieldMetadataType,
dateScalarMode: DateScalarMode = 'isoDate',
numberScalarMode: NumberScalarMode = 'float',
settings?: FieldMetadataSettings<FieldMetadataType | 'default'>,
isIdField?: boolean,
): GraphQLInputObjectType | GraphQLScalarType | undefined {
const dateFilter =
dateScalarMode === 'timestamp' ? DatetimeFilterType : DateFilterType;
if (isIdField || settings?.isForeignKey) {
return IDFilterType;
}
const numberScalar =
numberScalarMode === 'float' ? FloatFilterType : IntFilterType;
fieldMetadataType === FieldMetadataType.NUMBER &&
(settings as FieldMetadataSettings<FieldMetadataType.NUMBER>)
?.precision === 0
? IntFilterType
: FloatFilterType;
const typeFilterMapping = new Map<
FieldMetadataType,
GraphQLInputObjectType | GraphQLScalarType
>([
[FieldMetadataType.UUID, UUIDFilterType],
[FieldMetadataType.UUID, IDFilterType],
[FieldMetadataType.TEXT, StringFilterType],
[FieldMetadataType.PHONE, StringFilterType],
[FieldMetadataType.EMAIL, StringFilterType],
[FieldMetadataType.DATE_TIME, dateFilter],
[FieldMetadataType.DATE_TIME, DateFilterType],
[FieldMetadataType.DATE, DateFilterType],
[FieldMetadataType.BOOLEAN, BooleanFilterType],
[FieldMetadataType.NUMBER, numberScalar],
[FieldMetadataType.NUMERIC, BigFloatFilterType],
[FieldMetadataType.PROBABILITY, FloatFilterType],
[FieldMetadataType.RELATION, UUIDFilterType],
[FieldMetadataType.POSITION, FloatFilterType],
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
]);

View File

@ -1,18 +1,20 @@
import { GraphQLID, GraphQLInt, GraphQLString } from 'graphql';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
describe('getResolverArgs', () => {
const expectedOutputs = {
findMany: {
first: { type: FieldMetadataType.NUMBER, isNullable: true },
last: { type: FieldMetadataType.NUMBER, isNullable: true },
before: { type: FieldMetadataType.TEXT, isNullable: true },
after: { type: FieldMetadataType.TEXT, isNullable: true },
first: { type: GraphQLInt, isNullable: true },
last: { type: GraphQLInt, isNullable: true },
before: { type: GraphQLString, isNullable: true },
after: { type: GraphQLString, isNullable: true },
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: true },
orderBy: { kind: InputTypeDefinitionKind.OrderBy, isNullable: true },
limit: { type: GraphQLInt, isNullable: true },
},
findOne: {
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
@ -28,14 +30,14 @@ describe('getResolverArgs', () => {
data: { kind: InputTypeDefinitionKind.Create, isNullable: false },
},
updateOne: {
id: { type: FieldMetadataType.UUID, isNullable: false },
id: { type: GraphQLID, isNullable: false },
data: { kind: InputTypeDefinitionKind.Update, isNullable: false },
},
deleteOne: {
id: { type: FieldMetadataType.UUID, isNullable: false },
id: { type: GraphQLID, isNullable: false },
},
executeQuickActionOnOne: {
id: { type: FieldMetadataType.UUID, isNullable: false },
id: { type: GraphQLID, isNullable: false },
},
};

View File

@ -1,7 +1,8 @@
import { GraphQLString, GraphQLInt, GraphQLID } from 'graphql';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ArgMetadata } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
export const getResolverArgs = (
@ -11,19 +12,23 @@ export const getResolverArgs = (
case 'findMany':
return {
first: {
type: FieldMetadataType.NUMBER,
type: GraphQLInt,
isNullable: true,
},
last: {
type: FieldMetadataType.NUMBER,
type: GraphQLInt,
isNullable: true,
},
before: {
type: FieldMetadataType.TEXT,
type: GraphQLString,
isNullable: true,
},
after: {
type: FieldMetadataType.TEXT,
type: GraphQLString,
isNullable: true,
},
limit: {
type: GraphQLInt,
isNullable: true,
},
filter: {
@ -61,7 +66,7 @@ export const getResolverArgs = (
case 'updateOne':
return {
id: {
type: FieldMetadataType.UUID,
type: GraphQLID,
isNullable: false,
},
data: {
@ -72,7 +77,7 @@ export const getResolverArgs = (
case 'findDuplicates':
return {
id: {
type: FieldMetadataType.UUID,
type: GraphQLID,
isNullable: true,
},
data: {
@ -83,14 +88,14 @@ export const getResolverArgs = (
case 'deleteOne':
return {
id: {
type: FieldMetadataType.UUID,
type: GraphQLID,
isNullable: false,
},
};
case 'executeQuickActionOnOne':
return {
id: {
type: FieldMetadataType.UUID,
type: GraphQLID,
isNullable: false,
},
};