mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-27 14:24:06 +03:00
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:
parent
0194f30dd8
commit
59e53ba72d
@ -25,6 +25,7 @@ export enum FieldMetadataType {
|
||||
DATE_TIME = 'DATE_TIME',
|
||||
BOOLEAN = 'BOOLEAN',
|
||||
NUMBER = 'NUMBER',
|
||||
NUMERIC = 'NUMERIC',
|
||||
PROBABILITY = 'PROBABILITY',
|
||||
ENUM = 'ENUM',
|
||||
LINK = 'LINK',
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
},
|
||||
{
|
||||
|
@ -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:
|
||||
|
@ -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 &&
|
||||
|
@ -55,7 +55,7 @@ export const addCompanyTable: WorkspaceMigrationTableAction[] = [
|
||||
},
|
||||
{
|
||||
columnName: 'annualRecurringRevenueAmountMicros',
|
||||
columnType: 'integer',
|
||||
columnType: 'numeric',
|
||||
action: WorkspaceMigrationColumnActionType.CREATE,
|
||||
},
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ export const addOpportunityTable: WorkspaceMigrationTableAction[] = [
|
||||
columns: [
|
||||
{
|
||||
columnName: 'amountAmountMicros',
|
||||
columnType: 'integer',
|
||||
columnType: 'numeric',
|
||||
action: WorkspaceMigrationColumnActionType.CREATE,
|
||||
},
|
||||
{
|
||||
|
50
server/src/workspace/services/ScalarsExplorer.service.ts
Normal file
50
server/src/workspace/services/ScalarsExplorer.service.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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 },
|
||||
},
|
||||
});
|
||||
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ export const currencyObjectDefinition = {
|
||||
fields: [
|
||||
{
|
||||
id: 'amountMicros',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
type: FieldMetadataType.NUMERIC,
|
||||
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
|
||||
name: 'amountMicros',
|
||||
label: 'AmountMicros',
|
||||
|
@ -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],
|
||||
]);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {}
|
||||
|
Loading…
Reference in New Issue
Block a user