From 59e53ba72d9faa947da0eeb2c317a73436f3986e Mon Sep 17 00:00:00 2001 From: martmull Date: Thu, 23 Nov 2023 15:26:59 +0100 Subject: [PATCH] Fix microAmount (#2654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix microAmount * Code review returns * Parse currency values as string * Jeremy's returns * fix: scalars not properly implemented * fix: filters not working on big float scalar --------- Co-authored-by: Jérémy Magrin --- .../field-metadata/field-metadata.entity.ts | 1 + .../field-metadata-default-value.interface.ts | 1 + ...rt-field-metadata-to-column-action.util.ts | 16 +++++- .../utils/generate-target-column-map.util.ts | 1 + ...lidate-default-value-based-on-type.util.ts | 1 + .../migrations/1697618009-addCompanyTable.ts | 2 +- .../1697618021-addOpportunityTable.ts | 2 +- .../services/ScalarsExplorer.service.ts | 50 +++++++++++++++++++ .../input/big-float-filter.input-type.ts | 22 ++++---- .../graphql-types/scalars/big-float.scalar.ts | 15 +++--- .../currency.object-definition.ts | 2 +- .../services/type-mapper.service.ts | 5 ++ .../workspace-schema-storage.module.ts | 6 +++ .../workspace-schema-storage.service.ts | 21 ++++++++ server/src/workspace/workspace.factory.ts | 20 ++++++-- server/src/workspace/workspace.module.ts | 3 +- 16 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 server/src/workspace/services/ScalarsExplorer.service.ts diff --git a/server/src/metadata/field-metadata/field-metadata.entity.ts b/server/src/metadata/field-metadata/field-metadata.entity.ts index 1f17832e2..addcc1cba 100644 --- a/server/src/metadata/field-metadata/field-metadata.entity.ts +++ b/server/src/metadata/field-metadata/field-metadata.entity.ts @@ -25,6 +25,7 @@ export enum FieldMetadataType { DATE_TIME = 'DATE_TIME', BOOLEAN = 'BOOLEAN', NUMBER = 'NUMBER', + NUMERIC = 'NUMERIC', PROBABILITY = 'PROBABILITY', ENUM = 'ENUM', LINK = 'LINK', diff --git a/server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts b/server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts index 468a19b69..63bb66d0a 100644 --- a/server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts +++ b/server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts @@ -53,6 +53,7 @@ type FieldMetadataDefaultValueMapping = { [FieldMetadataType.DATE_TIME]: FieldMetadataDefaultValueDateTime; [FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean; [FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber; + [FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString; [FieldMetadataType.PROBABILITY]: FieldMetadataDefaultValueNumber; [FieldMetadataType.ENUM]: FieldMetadataDefaultValueString; [FieldMetadataType.LINK]: FieldMetadataDefaultValueLink; diff --git a/server/src/metadata/field-metadata/utils/convert-field-metadata-to-column-action.util.ts b/server/src/metadata/field-metadata/utils/convert-field-metadata-to-column-action.util.ts index d519903f0..9c519e513 100644 --- a/server/src/metadata/field-metadata/utils/convert-field-metadata-to-column-action.util.ts +++ b/server/src/metadata/field-metadata/utils/convert-field-metadata-to-column-action.util.ts @@ -56,6 +56,19 @@ export function convertFieldMetadataToColumnActions( }, ]; } + case FieldMetadataType.NUMERIC: { + const defaultValue = + fieldMetadata.defaultValue as FieldMetadataDefaultValue; + + return [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: fieldMetadata.targetColumnMap.value, + columnType: 'numeric', + defaultValue: serializeDefaultValue(defaultValue?.value), + }, + ]; + } case FieldMetadataType.NUMBER: case FieldMetadataType.PROBABILITY: { const defaultValue = @@ -117,6 +130,7 @@ export function convertFieldMetadataToColumnActions( }, ]; } + case FieldMetadataType.CURRENCY: { const defaultValue = fieldMetadata.defaultValue as FieldMetadataDefaultValue; @@ -125,7 +139,7 @@ export function convertFieldMetadataToColumnActions( { action: WorkspaceMigrationColumnActionType.CREATE, columnName: fieldMetadata.targetColumnMap.amountMicros, - columnType: 'integer', + columnType: 'numeric', defaultValue: serializeDefaultValue(defaultValue?.amountMicros), }, { diff --git a/server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts b/server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts index 083a2d3fb..37c109c5a 100644 --- a/server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts +++ b/server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts @@ -24,6 +24,7 @@ export function generateTargetColumnMap( case FieldMetadataType.PHONE: case FieldMetadataType.EMAIL: case FieldMetadataType.NUMBER: + case FieldMetadataType.NUMERIC: case FieldMetadataType.PROBABILITY: case FieldMetadataType.BOOLEAN: case FieldMetadataType.DATE_TIME: diff --git a/server/src/metadata/field-metadata/utils/validate-default-value-based-on-type.util.ts b/server/src/metadata/field-metadata/utils/validate-default-value-based-on-type.util.ts index 2d1f9d543..091e158fd 100644 --- a/server/src/metadata/field-metadata/utils/validate-default-value-based-on-type.util.ts +++ b/server/src/metadata/field-metadata/utils/validate-default-value-based-on-type.util.ts @@ -26,6 +26,7 @@ export const validateDefaultValueBasedOnType = ( case FieldMetadataType.PHONE: case FieldMetadataType.EMAIL: case FieldMetadataType.ENUM: + case FieldMetadataType.NUMERIC: return ( typeof defaultValue === 'object' && 'value' in defaultValue && diff --git a/server/src/metadata/workspace-migration/migrations/1697618009-addCompanyTable.ts b/server/src/metadata/workspace-migration/migrations/1697618009-addCompanyTable.ts index 7ae155ae8..5e52a2ee5 100644 --- a/server/src/metadata/workspace-migration/migrations/1697618009-addCompanyTable.ts +++ b/server/src/metadata/workspace-migration/migrations/1697618009-addCompanyTable.ts @@ -55,7 +55,7 @@ export const addCompanyTable: WorkspaceMigrationTableAction[] = [ }, { columnName: 'annualRecurringRevenueAmountMicros', - columnType: 'integer', + columnType: 'numeric', action: WorkspaceMigrationColumnActionType.CREATE, }, { diff --git a/server/src/metadata/workspace-migration/migrations/1697618021-addOpportunityTable.ts b/server/src/metadata/workspace-migration/migrations/1697618021-addOpportunityTable.ts index 5d0b6bc2c..02a6c28bf 100644 --- a/server/src/metadata/workspace-migration/migrations/1697618021-addOpportunityTable.ts +++ b/server/src/metadata/workspace-migration/migrations/1697618021-addOpportunityTable.ts @@ -14,7 +14,7 @@ export const addOpportunityTable: WorkspaceMigrationTableAction[] = [ columns: [ { columnName: 'amountAmountMicros', - columnType: 'integer', + columnType: 'numeric', action: WorkspaceMigrationColumnActionType.CREATE, }, { diff --git a/server/src/workspace/services/ScalarsExplorer.service.ts b/server/src/workspace/services/ScalarsExplorer.service.ts new file mode 100644 index 000000000..31ca6c08e --- /dev/null +++ b/server/src/workspace/services/ScalarsExplorer.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; + +import { GraphQLScalarType, GraphQLSchema, isScalarType } from 'graphql'; + +import { scalars } from 'src/workspace/workspace-schema-builder/graphql-types/scalars'; + +@Injectable() +export class ScalarsExplorerService { + private scalarImplementations: Record; + + constructor() { + this.scalarImplementations = scalars.reduce((acc, scalar) => { + acc[scalar.name] = scalar; + return acc; + }, {}); + } + + getScalarImplementation(scalarName: string): GraphQLScalarType | undefined { + return this.scalarImplementations[scalarName]; + } + + getUsedScalarNames(schema: GraphQLSchema): string[] { + const typeMap = schema.getTypeMap(); + const usedScalarNames: string[] = []; + + for (const typeName in typeMap) { + const type = typeMap[typeName]; + if (isScalarType(type) && !typeName.startsWith('__')) { + usedScalarNames.push(type.name); + } + } + + return usedScalarNames; + } + + getScalarResolvers( + usedScalarNames: string[], + ): Record { + const scalarResolvers: Record = {}; + + for (const scalarName of usedScalarNames) { + const scalarImplementation = this.getScalarImplementation(scalarName); + if (scalarImplementation) { + scalarResolvers[scalarName] = scalarImplementation; + } + } + + return scalarResolvers; + } +} diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/big-float-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/big-float-filter.input-type.ts index 245993626..3ec8d355d 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/big-float-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/big-float-filter.input-type.ts @@ -1,22 +1,18 @@ -import { - GraphQLInputObjectType, - GraphQLList, - GraphQLNonNull, - GraphQLFloat, -} from 'graphql'; +import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { BigFloatScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars'; export const BigFloatFilterType = new GraphQLInputObjectType({ name: 'BigFloatFilter', fields: { - eq: { type: GraphQLFloat }, - gt: { type: GraphQLFloat }, - gte: { type: GraphQLFloat }, - in: { type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)) }, - lt: { type: GraphQLFloat }, - lte: { type: GraphQLFloat }, - neq: { type: GraphQLFloat }, + eq: { type: BigFloatScalarType }, + gt: { type: BigFloatScalarType }, + gte: { type: BigFloatScalarType }, + in: { type: new GraphQLList(new GraphQLNonNull(BigFloatScalarType)) }, + lt: { type: BigFloatScalarType }, + lte: { type: BigFloatScalarType }, + neq: { type: BigFloatScalarType }, is: { type: FilterIsNullable }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/scalars/big-float.scalar.ts b/server/src/workspace/workspace-schema-builder/graphql-types/scalars/big-float.scalar.ts index 0bccac1ee..17025f41f 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/scalars/big-float.scalar.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/scalars/big-float.scalar.ts @@ -5,17 +5,16 @@ export const BigFloatScalarType = new GraphQLScalarType({ name: 'BigFloat', description: 'A custom scalar type for representing big floating point numbers', - serialize(value: number): string { - return String(value); - }, - parseValue(value: string): number { + serialize(value: string): number { return parseFloat(value); }, - parseLiteral(ast): number | null { - if (ast.kind === Kind.FLOAT) { - return parseFloat(ast.value); + parseValue(value: number): string { + return String(value); + }, + parseLiteral(ast): string | null { + if (ast.kind === Kind.FLOAT || ast.kind === Kind.INT) { + return String(ast.value); } - return null; }, }); diff --git a/server/src/workspace/workspace-schema-builder/object-definitions/currency.object-definition.ts b/server/src/workspace/workspace-schema-builder/object-definitions/currency.object-definition.ts index 86677b38a..4b565ca26 100644 --- a/server/src/workspace/workspace-schema-builder/object-definitions/currency.object-definition.ts +++ b/server/src/workspace/workspace-schema-builder/object-definitions/currency.object-definition.ts @@ -13,7 +13,7 @@ export const currencyObjectDefinition = { fields: [ { id: 'amountMicros', - type: FieldMetadataType.NUMBER, + type: FieldMetadataType.NUMERIC, objectMetadataId: FieldMetadataType.CURRENCY.toString(), name: 'amountMicros', label: 'AmountMicros', diff --git a/server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts b/server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts index 480c62640..332b48b9e 100644 --- a/server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts +++ b/server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts @@ -30,8 +30,10 @@ import { FloatFilterType, IntFilterType, BooleanFilterType, + BigFloatFilterType, } from 'src/workspace/workspace-schema-builder/graphql-types/input'; import { OrderByDirectionType } from 'src/workspace/workspace-schema-builder/graphql-types/enum'; +import { BigFloatScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars'; export interface TypeOptions { nullable?: boolean; @@ -61,6 +63,7 @@ export class TypeMapperService { [FieldMetadataType.DATE_TIME, dateScalar], [FieldMetadataType.BOOLEAN, GraphQLBoolean], [FieldMetadataType.NUMBER, numberScalar], + [FieldMetadataType.NUMERIC, BigFloatScalarType], [FieldMetadataType.PROBABILITY, GraphQLFloat], [FieldMetadataType.RELATION, GraphQLID], ]); @@ -90,6 +93,7 @@ export class TypeMapperService { [FieldMetadataType.DATE_TIME, dateFilter], [FieldMetadataType.BOOLEAN, BooleanFilterType], [FieldMetadataType.NUMBER, numberScalar], + [FieldMetadataType.NUMERIC, BigFloatFilterType], [FieldMetadataType.PROBABILITY, FloatFilterType], [FieldMetadataType.RELATION, UUIDFilterType], ]); @@ -109,6 +113,7 @@ export class TypeMapperService { [FieldMetadataType.DATE_TIME, OrderByDirectionType], [FieldMetadataType.BOOLEAN, OrderByDirectionType], [FieldMetadataType.NUMBER, OrderByDirectionType], + [FieldMetadataType.NUMERIC, OrderByDirectionType], [FieldMetadataType.PROBABILITY, OrderByDirectionType], ]); diff --git a/server/src/workspace/workspace-schema-storage/workspace-schema-storage.module.ts b/server/src/workspace/workspace-schema-storage/workspace-schema-storage.module.ts index 5728f15b0..5c307b5fe 100644 --- a/server/src/workspace/workspace-schema-storage/workspace-schema-storage.module.ts +++ b/server/src/workspace/workspace-schema-storage/workspace-schema-storage.module.ts @@ -24,6 +24,12 @@ import { WorkspaceSchemaStorageService } from 'src/workspace/workspace-schema-st type: MemoryStorageType.Local, options: {}, }), + MemoryStorageModule.forRoot({ + identifier: 'usedScalarNames', + type: MemoryStorageType.Local, + options: {}, + serializer: new MemoryStorageJsonSerializer(), + }), MemoryStorageModule.forRoot({ identifier: 'cacheVersion', type: MemoryStorageType.Local, diff --git a/server/src/workspace/workspace-schema-storage/workspace-schema-storage.service.ts b/server/src/workspace/workspace-schema-storage/workspace-schema-storage.service.ts index f661d0e18..37688900e 100644 --- a/server/src/workspace/workspace-schema-storage/workspace-schema-storage.service.ts +++ b/server/src/workspace/workspace-schema-storage/workspace-schema-storage.service.ts @@ -14,6 +14,10 @@ export class WorkspaceSchemaStorageService { >, @InjectMemoryStorage('typeDefs') private readonly typeDefsMemoryStorageService: MemoryStorageService, + @InjectMemoryStorage('usedScalarNames') + private readonly usedScalarNamesMemoryStorageService: MemoryStorageService< + string[] + >, @InjectMemoryStorage('cacheVersion') private readonly cacheVersionMemoryStorageService: MemoryStorageService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, @@ -71,8 +75,25 @@ export class WorkspaceSchemaStorageService { }); } + setUsedScalarNames( + workspaceId: string, + scalarsUsed: string[], + ): Promise { + return this.usedScalarNamesMemoryStorageService.write({ + key: workspaceId, + data: scalarsUsed, + }); + } + + getUsedScalarNames(workspaceId: string): Promise { + return this.usedScalarNamesMemoryStorageService.read({ + key: workspaceId, + }); + } + async invalidateCache(workspaceId: string): Promise { await this.objectMetadataMemoryStorageService.delete({ key: workspaceId }); await this.typeDefsMemoryStorageService.delete({ key: workspaceId }); + await this.usedScalarNamesMemoryStorageService.delete({ key: workspaceId }); } } diff --git a/server/src/workspace/workspace.factory.ts b/server/src/workspace/workspace.factory.ts index 3b1051e99..e637529a2 100644 --- a/server/src/workspace/workspace.factory.ts +++ b/server/src/workspace/workspace.factory.ts @@ -7,6 +7,7 @@ import { gql } from 'graphql-tag'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { WorkspaceSchemaStorageService } from 'src/workspace/workspace-schema-storage/workspace-schema-storage.service'; import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; +import { ScalarsExplorerService } from 'src/workspace/services/ScalarsExplorer.service'; import { WorkspaceGraphQLSchemaFactory } from './workspace-schema-builder/workspace-graphql-schema.factory'; import { workspaceResolverBuilderMethodNames } from './workspace-resolver-builder/factories/factories'; @@ -17,6 +18,7 @@ export class WorkspaceFactory { constructor( private readonly dataSourceService: DataSourceService, private readonly objectMetadataService: ObjectMetadataService, + private readonly scalarsExplorerService: ScalarsExplorerService, private readonly workspaceGraphQLSchemaFactory: WorkspaceGraphQLSchemaFactory, private readonly workspaceResolverFactory: WorkspaceResolverFactory, private readonly workspaceSchemaStorageService: WorkspaceSchemaStorageService, @@ -63,20 +65,28 @@ export class WorkspaceFactory { let typeDefs = await this.workspaceSchemaStorageService.getTypeDefs( workspaceId, ); + let usedScalarNames = + await this.workspaceSchemaStorageService.getUsedScalarNames(workspaceId); // If typeDefs are not cached, generate them - if (!typeDefs) { + if (!typeDefs || !usedScalarNames) { const autoGeneratedSchema = await this.workspaceGraphQLSchemaFactory.create( objectMetadataCollection, workspaceResolverBuilderMethodNames, ); + usedScalarNames = + this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema); typeDefs = printSchema(autoGeneratedSchema); await this.workspaceSchemaStorageService.setTypeDefs( workspaceId, typeDefs, ); + await this.workspaceSchemaStorageService.setUsedScalarNames( + workspaceId, + usedScalarNames, + ); } const autoGeneratedResolvers = await this.workspaceResolverFactory.create( @@ -84,13 +94,17 @@ export class WorkspaceFactory { objectMetadataCollection, workspaceResolverBuilderMethodNames, ); + const scalarsResolvers = + this.scalarsExplorerService.getScalarResolvers(usedScalarNames); - // TODO: Cache the generate type definitions const executableSchema = makeExecutableSchema({ typeDefs: gql` ${typeDefs} `, - resolvers: autoGeneratedResolvers, + resolvers: { + ...scalarsResolvers, + ...autoGeneratedResolvers, + }, }); return executableSchema; diff --git a/server/src/workspace/workspace.module.ts b/server/src/workspace/workspace.module.ts index 1fc388c45..29b148b0c 100644 --- a/server/src/workspace/workspace.module.ts +++ b/server/src/workspace/workspace.module.ts @@ -4,6 +4,7 @@ import { MetadataModule } from 'src/metadata/metadata.module'; import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; import { WorkspaceSchemaStorageModule } from 'src/workspace/workspace-schema-storage/workspace-schema-storage.module'; import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; +import { ScalarsExplorerService } from 'src/workspace/services/ScalarsExplorer.service'; import { WorkspaceFactory } from './workspace.factory'; @@ -19,7 +20,7 @@ import { WorkspaceResolverBuilderModule } from './workspace-resolver-builder/wor WorkspaceResolverBuilderModule, WorkspaceSchemaStorageModule, ], - providers: [WorkspaceFactory], + providers: [WorkspaceFactory, ScalarsExplorerService], exports: [WorkspaceFactory], }) export class WorkspaceModule {}