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:
@ -78,14 +78,16 @@ export class FindDuplicatesQueryFactory {
|
||||
}
|
||||
|
||||
buildQueryForExistingRecord(
|
||||
id: string,
|
||||
id: string | number,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
) {
|
||||
const idQueryField = typeof id === 'string' ? `"${id}"` : id;
|
||||
|
||||
return `
|
||||
query {
|
||||
${computeObjectTargetTable(
|
||||
options.objectMetadataItem,
|
||||
)}Collection(filter: { id: { eq: "${id}" }}){
|
||||
)}Collection(filter: { id: { eq: ${idQueryField} }}){
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
|
||||
@ -77,7 +77,7 @@ export class WorkspaceQueryBuilderFactory {
|
||||
}
|
||||
|
||||
findDuplicatesExistingRecord(
|
||||
id: string,
|
||||
id: string | number,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
): string {
|
||||
return this.findDuplicatesQueryFactory.buildQueryForExistingRecord(
|
||||
|
||||
@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@ -14,6 +15,7 @@ describe('QueryRunnerArgsFactory', () => {
|
||||
const options = {
|
||||
fieldMetadataCollection: [
|
||||
{ name: 'position', type: FieldMetadataType.POSITION },
|
||||
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
|
||||
] as FieldMetadataInterface[],
|
||||
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
|
||||
} as WorkspaceQueryRunnerOptions;
|
||||
@ -45,18 +47,92 @@ describe('QueryRunnerArgsFactory', () => {
|
||||
const args = {
|
||||
data: [],
|
||||
};
|
||||
const result = await factory.create(args, options);
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.CreateMany,
|
||||
);
|
||||
|
||||
expect(result).toEqual(args);
|
||||
});
|
||||
|
||||
it('should override args when of type array', async () => {
|
||||
const args = { data: [{ id: 1 }, { position: 'last' }] };
|
||||
it('createMany type should override data position and number', async () => {
|
||||
const args = {
|
||||
id: 'uuid',
|
||||
data: [{ position: 'last', testNumber: '1' }],
|
||||
};
|
||||
|
||||
const result = await factory.create(args, options);
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.CreateMany,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: [{ id: 1 }, { position: 2 }],
|
||||
id: 'uuid',
|
||||
data: [{ position: 2, testNumber: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('findMany type should override data position and number', async () => {
|
||||
const args = {
|
||||
id: 'uuid',
|
||||
filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } },
|
||||
};
|
||||
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.FindMany,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'uuid',
|
||||
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('findOne type should override number in filter', async () => {
|
||||
const args = {
|
||||
id: 'uuid',
|
||||
filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } },
|
||||
};
|
||||
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.FindOne,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'uuid',
|
||||
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('findDuplicates type should override number in data and id', async () => {
|
||||
const optionsDuplicate = {
|
||||
fieldMetadataCollection: [
|
||||
{ name: 'id', type: FieldMetadataType.NUMBER },
|
||||
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
|
||||
] as FieldMetadataInterface[],
|
||||
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
|
||||
} as WorkspaceQueryRunnerOptions;
|
||||
|
||||
const args = {
|
||||
id: '123',
|
||||
data: { testNumber: '1', otherField: 'test' },
|
||||
};
|
||||
|
||||
const result = await factory.create(
|
||||
args,
|
||||
optionsDuplicate,
|
||||
ResolverArgsType.FindDuplicates,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 123,
|
||||
data: { testNumber: 1, otherField: 'test' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,6 +2,15 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import {
|
||||
CreateManyResolverArgs,
|
||||
FindDuplicatesResolverArgs,
|
||||
FindManyResolverArgs,
|
||||
FindOneResolverArgs,
|
||||
ResolverArgs,
|
||||
ResolverArgsType,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
@ -12,8 +21,9 @@ export class QueryRunnerArgsFactory {
|
||||
constructor(private readonly recordPositionFactory: RecordPositionFactory) {}
|
||||
|
||||
async create(
|
||||
args: Record<string, any>,
|
||||
args: ResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
resolverArgsType: ResolverArgsType,
|
||||
) {
|
||||
const fieldMetadataCollection = options.fieldMetadataCollection;
|
||||
|
||||
@ -24,21 +34,62 @@ export class QueryRunnerArgsFactory {
|
||||
]),
|
||||
);
|
||||
|
||||
return {
|
||||
data: await Promise.all(
|
||||
args.data.map((arg) =>
|
||||
this.overrideArgByFieldMetadata(arg, options, fieldMetadataMap),
|
||||
),
|
||||
),
|
||||
};
|
||||
switch (resolverArgsType) {
|
||||
case ResolverArgsType.CreateMany:
|
||||
return {
|
||||
...args,
|
||||
data: await Promise.all(
|
||||
(args as CreateManyResolverArgs).data.map((arg) =>
|
||||
this.overrideDataByFieldMetadata(arg, options, fieldMetadataMap),
|
||||
),
|
||||
),
|
||||
} satisfies CreateManyResolverArgs;
|
||||
case ResolverArgsType.FindOne:
|
||||
return {
|
||||
...args,
|
||||
filter: await this.overrideFilterByFieldMetadata(
|
||||
(args as FindOneResolverArgs).filter,
|
||||
fieldMetadataMap,
|
||||
),
|
||||
};
|
||||
case ResolverArgsType.FindMany:
|
||||
return {
|
||||
...args,
|
||||
filter: await this.overrideFilterByFieldMetadata(
|
||||
(args as FindManyResolverArgs).filter,
|
||||
fieldMetadataMap,
|
||||
),
|
||||
};
|
||||
|
||||
case ResolverArgsType.FindDuplicates:
|
||||
return {
|
||||
...args,
|
||||
id: await this.overrideValueByFieldMetadata(
|
||||
'id',
|
||||
(args as FindDuplicatesResolverArgs).id,
|
||||
fieldMetadataMap,
|
||||
),
|
||||
data: await this.overrideDataByFieldMetadata(
|
||||
(args as FindDuplicatesResolverArgs).data,
|
||||
options,
|
||||
fieldMetadataMap,
|
||||
),
|
||||
};
|
||||
default:
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
private async overrideArgByFieldMetadata(
|
||||
arg: Record<string, any>,
|
||||
private async overrideDataByFieldMetadata(
|
||||
data: Record<string, any> | undefined,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
fieldMetadataMap: Map<string, FieldMetadataInterface>,
|
||||
) {
|
||||
const createArgPromiseByArgKey = Object.entries(arg).map(
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createArgPromiseByArgKey = Object.entries(data).map(
|
||||
async ([key, value]) => {
|
||||
const fieldMetadata = fieldMetadataMap.get(key);
|
||||
|
||||
@ -59,6 +110,8 @@ export class QueryRunnerArgsFactory {
|
||||
options.workspaceId,
|
||||
),
|
||||
];
|
||||
case FieldMetadataType.NUMBER:
|
||||
return [key, await Promise.resolve(Number(value))];
|
||||
default:
|
||||
return [key, await Promise.resolve(value)];
|
||||
}
|
||||
@ -69,4 +122,57 @@ export class QueryRunnerArgsFactory {
|
||||
|
||||
return Object.fromEntries(newArgEntries);
|
||||
}
|
||||
|
||||
private overrideFilterByFieldMetadata(
|
||||
filter: RecordFilter | undefined,
|
||||
fieldMetadataMap: Map<string, FieldMetadataInterface>,
|
||||
) {
|
||||
if (!filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createArgPromiseByArgKey = Object.entries(filter).map(
|
||||
([key, value]) => {
|
||||
const fieldMetadata = fieldMetadataMap.get(key);
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return [key, value];
|
||||
}
|
||||
|
||||
const createFilterByKey = Object.entries(value).map(
|
||||
([filterKey, filterValue]) => {
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.NUMBER:
|
||||
return [filterKey, Number(filterValue)];
|
||||
default:
|
||||
return [filterKey, filterValue];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return [key, Object.fromEntries(createFilterByKey)];
|
||||
},
|
||||
);
|
||||
|
||||
return Object.fromEntries(createArgPromiseByArgKey);
|
||||
}
|
||||
|
||||
private async overrideValueByFieldMetadata(
|
||||
key: string,
|
||||
value: any,
|
||||
fieldMetadataMap: Map<string, FieldMetadataInterface>,
|
||||
) {
|
||||
const fieldMetadata = fieldMetadataMap.get(key);
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return value;
|
||||
}
|
||||
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.NUMBER:
|
||||
return Number(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
export const assertIsValidUuid = (value: string) => {
|
||||
const isValid =
|
||||
/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
|
||||
value,
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new BadRequestException(`Value "${value}" is not a valid UUID`);
|
||||
}
|
||||
};
|
||||
@ -22,6 +22,7 @@ import {
|
||||
FindDuplicatesResolverArgs,
|
||||
FindManyResolverArgs,
|
||||
FindOneResolverArgs,
|
||||
ResolverArgsType,
|
||||
UpdateManyResolverArgs,
|
||||
UpdateOneResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
@ -48,6 +49,7 @@ import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-r
|
||||
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assertIsValidUuid.util';
|
||||
|
||||
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
|
||||
import {
|
||||
@ -83,9 +85,15 @@ export class WorkspaceQueryRunnerService {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const start = performance.now();
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.findMany(
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.FindMany,
|
||||
)) as FindManyResolverArgs<Filter, OrderBy>;
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.findMany(
|
||||
computedArgs,
|
||||
options,
|
||||
);
|
||||
|
||||
await this.workspacePreQueryHookService.executePreHooks(
|
||||
@ -123,9 +131,16 @@ export class WorkspaceQueryRunnerService {
|
||||
throw new BadRequestException('Missing filter argument');
|
||||
}
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const query = await this.workspaceQueryBuilderFactory.findOne(
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.FindOne,
|
||||
)) as FindOneResolverArgs<Filter>;
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.findOne(
|
||||
computedArgs,
|
||||
options,
|
||||
);
|
||||
|
||||
await this.workspacePreQueryHookService.executePreHooks(
|
||||
@ -164,12 +179,18 @@ export class WorkspaceQueryRunnerService {
|
||||
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.FindDuplicates,
|
||||
)) as FindDuplicatesResolverArgs<TRecord>;
|
||||
|
||||
let existingRecord: Record<string, unknown> | undefined;
|
||||
|
||||
if (args.id) {
|
||||
if (computedArgs.id) {
|
||||
const existingRecordQuery =
|
||||
this.workspaceQueryBuilderFactory.findDuplicatesExistingRecord(
|
||||
args.id,
|
||||
computedArgs.id,
|
||||
options,
|
||||
);
|
||||
|
||||
@ -192,7 +213,7 @@ export class WorkspaceQueryRunnerService {
|
||||
}
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.findDuplicates(
|
||||
args,
|
||||
computedArgs,
|
||||
options,
|
||||
existingRecord,
|
||||
);
|
||||
@ -202,7 +223,7 @@ export class WorkspaceQueryRunnerService {
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findDuplicates',
|
||||
args,
|
||||
computedArgs,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
@ -222,10 +243,17 @@ export class WorkspaceQueryRunnerService {
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const computedArgs = await this.queryRunnerArgsFactory.create(
|
||||
args.data.forEach((record) => {
|
||||
if (record.id) {
|
||||
assertIsValidUuid(record.id);
|
||||
}
|
||||
});
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
options,
|
||||
);
|
||||
ResolverArgsType.CreateMany,
|
||||
)) as CreateManyResolverArgs<Record>;
|
||||
|
||||
await this.workspacePreQueryHookService.executePreHooks(
|
||||
userId,
|
||||
@ -288,6 +316,7 @@ export class WorkspaceQueryRunnerService {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
assertIsValidUuid(args.id);
|
||||
|
||||
const existingRecord = await this.findOne(
|
||||
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs,
|
||||
@ -337,6 +366,7 @@ export class WorkspaceQueryRunnerService {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
assertIsValidUuid(args.data.id);
|
||||
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_RECORD_AFFECTED',
|
||||
|
||||
@ -10,6 +10,19 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work
|
||||
|
||||
export type Resolver<Args = any> = GraphQLFieldResolver<any, any, Args>;
|
||||
|
||||
export enum ResolverArgsType {
|
||||
FindMany = 'FindMany',
|
||||
FindOne = 'FindOne',
|
||||
FindDuplicates = 'FindDuplicates',
|
||||
CreateOne = 'CreateOne',
|
||||
CreateMany = 'CreateMany',
|
||||
UpdateOne = 'UpdateOne',
|
||||
UpdateMany = 'UpdateMany',
|
||||
DeleteOne = 'DeleteOne',
|
||||
DeleteMany = 'DeleteMany',
|
||||
ExecuteQuickActionOnOne = 'ExecuteQuickActionOnOne',
|
||||
}
|
||||
|
||||
export interface FindManyResolverArgs<
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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;
|
||||
|
||||
@ -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],
|
||||
]);
|
||||
|
||||
@ -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 },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@ -16,7 +16,7 @@ export class FindManyQueryFactory {
|
||||
$filter: ${objectNameSingular}FilterInput,
|
||||
$orderBy: ${objectNameSingular}OrderByInput,
|
||||
$lastCursor: String,
|
||||
$limit: Float = 60
|
||||
$limit: Int = 60
|
||||
) {
|
||||
${objectNamePlural}(
|
||||
filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor
|
||||
|
||||
Reference in New Issue
Block a user