Fix microAmount (#2654)

* 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 <jeremy.magrin@gmail.com>
This commit is contained in:
martmull 2023-11-23 15:26:59 +01:00 committed by GitHub
parent 0194f30dd8
commit 59e53ba72d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 139 additions and 29 deletions

View File

@ -25,6 +25,7 @@ export enum FieldMetadataType {
DATE_TIME = 'DATE_TIME',
BOOLEAN = 'BOOLEAN',
NUMBER = 'NUMBER',
NUMERIC = 'NUMERIC',
PROBABILITY = 'PROBABILITY',
ENUM = 'ENUM',
LINK = 'LINK',

View File

@ -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;

View File

@ -56,6 +56,19 @@ export function convertFieldMetadataToColumnActions(
},
];
}
case FieldMetadataType.NUMERIC: {
const defaultValue =
fieldMetadata.defaultValue as FieldMetadataDefaultValue<FieldMetadataType.NUMERIC>;
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<FieldMetadataType.CURRENCY>;
@ -125,7 +139,7 @@ export function convertFieldMetadataToColumnActions(
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.amountMicros,
columnType: 'integer',
columnType: 'numeric',
defaultValue: serializeDefaultValue(defaultValue?.amountMicros),
},
{

View File

@ -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:

View File

@ -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 &&

View File

@ -55,7 +55,7 @@ export const addCompanyTable: WorkspaceMigrationTableAction[] = [
},
{
columnName: 'annualRecurringRevenueAmountMicros',
columnType: 'integer',
columnType: 'numeric',
action: WorkspaceMigrationColumnActionType.CREATE,
},
{

View File

@ -14,7 +14,7 @@ export const addOpportunityTable: WorkspaceMigrationTableAction[] = [
columns: [
{
columnName: 'amountAmountMicros',
columnType: 'integer',
columnType: 'numeric',
action: WorkspaceMigrationColumnActionType.CREATE,
},
{

View File

@ -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<string, GraphQLScalarType>;
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<string, GraphQLScalarType> {
const scalarResolvers: Record<string, GraphQLScalarType> = {};
for (const scalarName of usedScalarNames) {
const scalarImplementation = this.getScalarImplementation(scalarName);
if (scalarImplementation) {
scalarResolvers[scalarName] = scalarImplementation;
}
}
return scalarResolvers;
}
}

View File

@ -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 },
},
});

View File

@ -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;
},
});

View File

@ -13,7 +13,7 @@ export const currencyObjectDefinition = {
fields: [
{
id: 'amountMicros',
type: FieldMetadataType.NUMBER,
type: FieldMetadataType.NUMERIC,
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
name: 'amountMicros',
label: 'AmountMicros',

View File

@ -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<T = any> {
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],
]);

View File

@ -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<string[]>(),
}),
MemoryStorageModule.forRoot({
identifier: 'cacheVersion',
type: MemoryStorageType.Local,

View File

@ -14,6 +14,10 @@ export class WorkspaceSchemaStorageService {
>,
@InjectMemoryStorage('typeDefs')
private readonly typeDefsMemoryStorageService: MemoryStorageService<string>,
@InjectMemoryStorage('usedScalarNames')
private readonly usedScalarNamesMemoryStorageService: MemoryStorageService<
string[]
>,
@InjectMemoryStorage('cacheVersion')
private readonly cacheVersionMemoryStorageService: MemoryStorageService<string>,
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
@ -71,8 +75,25 @@ export class WorkspaceSchemaStorageService {
});
}
setUsedScalarNames(
workspaceId: string,
scalarsUsed: string[],
): Promise<void> {
return this.usedScalarNamesMemoryStorageService.write({
key: workspaceId,
data: scalarsUsed,
});
}
getUsedScalarNames(workspaceId: string): Promise<string[] | null> {
return this.usedScalarNamesMemoryStorageService.read({
key: workspaceId,
});
}
async invalidateCache(workspaceId: string): Promise<void> {
await this.objectMetadataMemoryStorageService.delete({ key: workspaceId });
await this.typeDefsMemoryStorageService.delete({ key: workspaceId });
await this.usedScalarNamesMemoryStorageService.delete({ key: workspaceId });
}
}

View File

@ -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;

View File

@ -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 {}