feat: rename tenant into workspace (#2553)
* feat: rename tenant into workspace * fix: missing some files and reset not working * fix: wrong import * Use link in company seeds * Use link in company seeds --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -0,0 +1,99 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigArgumentMap } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ArgsMetadata } from 'src/workspace/workspace-schema-builder/interfaces/param-metadata.interface';
|
||||
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { TypeMapperService } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
|
||||
@Injectable()
|
||||
export class ArgsFactory {
|
||||
private readonly logger = new Logger(ArgsFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
{ args, objectMetadata }: ArgsMetadata,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigArgumentMap {
|
||||
const fieldConfigMap: GraphQLFieldConfigArgumentMap = {};
|
||||
|
||||
for (const key in args) {
|
||||
if (!args.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
const arg = args[key];
|
||||
|
||||
// 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, {
|
||||
defaultValue: arg.defaultValue,
|
||||
nullable: arg.isNullable,
|
||||
isArray: arg.isArray,
|
||||
});
|
||||
|
||||
fieldConfigMap[key] = {
|
||||
type: gqlType,
|
||||
};
|
||||
}
|
||||
|
||||
// Argument is an input type
|
||||
if (arg.kind) {
|
||||
const inputType = this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
objectMetadata.id,
|
||||
arg.kind,
|
||||
);
|
||||
|
||||
if (!inputType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL input type for ${objectMetadata.id}`,
|
||||
{
|
||||
objectMetadata,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL input type for ${objectMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const gqlType = this.typeMapperService.mapToGqlType(inputType, {
|
||||
nullable: arg.isNullable,
|
||||
isArray: arg.isArray,
|
||||
});
|
||||
|
||||
fieldConfigMap[key] = {
|
||||
type: gqlType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return fieldConfigMap;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import {
|
||||
ObjectTypeDefinition,
|
||||
ObjectTypeDefinitionKind,
|
||||
} from './object-type-definition.factory';
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
|
||||
export enum ConnectionTypeDefinitionKind {
|
||||
Edge = 'Edge',
|
||||
PageInfo = 'PageInfo',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(ConnectionTypeDefinitionFactory.name);
|
||||
|
||||
constructor(private readonly connectionTypeFactory: ConnectionTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
const kind = ObjectTypeDefinitionKind.Connection;
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
fields.edges = {
|
||||
type: this.connectionTypeFactory.create(
|
||||
objectMetadata,
|
||||
ConnectionTypeDefinitionKind.Edge,
|
||||
options,
|
||||
{
|
||||
isArray: true,
|
||||
arrayDepth: 1,
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
fields.pageInfo = {
|
||||
type: this.connectionTypeFactory.create(
|
||||
objectMetadata,
|
||||
ConnectionTypeDefinitionKind.PageInfo,
|
||||
options,
|
||||
{
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { PageInfoType } from 'src/workspace/workspace-schema-builder/graphql-types/object';
|
||||
|
||||
import { ConnectionTypeDefinitionKind } from './connection-type-definition.factory';
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionTypeFactory {
|
||||
private readonly logger = new Logger(ConnectionTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ConnectionTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
if (kind === ConnectionTypeDefinitionKind.PageInfo) {
|
||||
return this.typeMapperService.mapToGqlType(PageInfoType, typeOptions);
|
||||
}
|
||||
|
||||
const edgeType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
kind as unknown as ObjectTypeDefinitionKind,
|
||||
);
|
||||
|
||||
if (!edgeType) {
|
||||
this.logger.error(
|
||||
`Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
{
|
||||
objectMetadata,
|
||||
buildOtions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(edgeType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import {
|
||||
ObjectTypeDefinition,
|
||||
ObjectTypeDefinitionKind,
|
||||
} from './object-type-definition.factory';
|
||||
import { EdgeTypeFactory } from './edge-type.factory';
|
||||
|
||||
export enum EdgeTypeDefinitionKind {
|
||||
Node = 'Node',
|
||||
Cursor = 'Cursor',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EdgeTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(EdgeTypeDefinitionFactory.name);
|
||||
|
||||
constructor(private readonly edgeTypeFactory: EdgeTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
const kind = ObjectTypeDefinitionKind.Edge;
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
fields.node = {
|
||||
type: this.edgeTypeFactory.create(
|
||||
objectMetadata,
|
||||
EdgeTypeDefinitionKind.Node,
|
||||
options,
|
||||
{
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
fields.cursor = {
|
||||
type: this.edgeTypeFactory.create(
|
||||
objectMetadata,
|
||||
EdgeTypeDefinitionKind.Cursor,
|
||||
options,
|
||||
{
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { CursorScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
import { EdgeTypeDefinitionKind } from './edge-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class EdgeTypeFactory {
|
||||
private readonly logger = new Logger(EdgeTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: EdgeTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
if (kind === EdgeTypeDefinitionKind.Cursor) {
|
||||
return this.typeMapperService.mapToGqlType(CursorScalarType, typeOptions);
|
||||
}
|
||||
|
||||
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
ObjectTypeDefinitionKind.Plain,
|
||||
);
|
||||
|
||||
if (!objectType) {
|
||||
this.logger.error(
|
||||
`Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
{
|
||||
objectMetadata,
|
||||
buildOtions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(objectType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,176 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
GraphQLFieldConfigArgumentMap,
|
||||
GraphQLFieldConfigMap,
|
||||
GraphQLObjectType,
|
||||
} from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { objectContainsCompositeField } from 'src/workspace/workspace-schema-builder/utils/object-contains-composite-field';
|
||||
import { getResolverArgs } from 'src/workspace/workspace-schema-builder/utils/get-resolver-args.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
import {
|
||||
RelationDirection,
|
||||
deduceRelationDirection,
|
||||
} from 'src/workspace/utils/deduce-relation-direction.util';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
import { RelationTypeFactory } from './relation-type.factory';
|
||||
import { ArgsFactory } from './args.factory';
|
||||
|
||||
export enum ObjectTypeDefinitionKind {
|
||||
Connection = 'Connection',
|
||||
Edge = 'Edge',
|
||||
Plain = '',
|
||||
}
|
||||
|
||||
export interface ObjectTypeDefinition {
|
||||
target: string;
|
||||
kind: ObjectTypeDefinitionKind;
|
||||
type: GraphQLObjectType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ExtendObjectTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(ExtendObjectTypeDefinitionFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly relationTypeFactory: RelationTypeFactory,
|
||||
private readonly argsFactory: ArgsFactory,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
const kind = ObjectTypeDefinitionKind.Plain;
|
||||
const gqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
kind,
|
||||
);
|
||||
const containsCompositeField = objectContainsCompositeField(objectMetadata);
|
||||
|
||||
if (!gqlType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
|
||||
{
|
||||
objectMetadata,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Security check to avoid extending an object that does not need to be extended
|
||||
if (!containsCompositeField) {
|
||||
this.logger.error(
|
||||
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
|
||||
{
|
||||
objectMetadata,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Extract current object config to extend it
|
||||
const config = gqlType.toConfig();
|
||||
|
||||
// Recreate the same object type with the new fields
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
...config,
|
||||
fields: () => ({
|
||||
...config.fields,
|
||||
...this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Ignore non composite fields as they are already defined
|
||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.RELATION: {
|
||||
const relationMetadata =
|
||||
fieldMetadata.fromRelationMetadata ??
|
||||
fieldMetadata.toRelationMetadata;
|
||||
|
||||
if (!relationMetadata) {
|
||||
this.logger.error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationDirection = deduceRelationDirection(
|
||||
fieldMetadata.objectMetadataId,
|
||||
relationMetadata,
|
||||
);
|
||||
const relationType = this.relationTypeFactory.create(
|
||||
fieldMetadata,
|
||||
relationMetadata,
|
||||
relationDirection,
|
||||
);
|
||||
let argsType: GraphQLFieldConfigArgumentMap | undefined = undefined;
|
||||
|
||||
// Args are only needed when relation is of kind `oneToMany` and the relation direction is `from`
|
||||
if (
|
||||
relationMetadata.relationType ===
|
||||
RelationMetadataType.ONE_TO_MANY &&
|
||||
relationDirection === RelationDirection.FROM
|
||||
) {
|
||||
const args = getResolverArgs('findMany');
|
||||
|
||||
argsType = this.argsFactory.create(
|
||||
{
|
||||
args,
|
||||
objectMetadata: relationMetadata.toObjectMetadata,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type: relationType,
|
||||
args: argsType,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { ArgsFactory } from './args.factory';
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
import { InputTypeDefinitionFactory } from './input-type-definition.factory';
|
||||
import { ObjectTypeDefinitionFactory } from './object-type-definition.factory';
|
||||
import { OutputTypeFactory } from './output-type.factory';
|
||||
import { QueryTypeFactory } from './query-type.factory';
|
||||
import { RootTypeFactory } from './root-type.factory';
|
||||
import { FilterTypeFactory } from './filter-type.factory';
|
||||
import { FilterTypeDefinitionFactory } from './filter-type-definition.factory';
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
|
||||
import { EdgeTypeFactory } from './edge-type.factory';
|
||||
import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory';
|
||||
import { MutationTypeFactory } from './mutation-type.factory';
|
||||
import { OrderByTypeFactory } from './order-by-type.factory';
|
||||
import { OrderByTypeDefinitionFactory } from './order-by-type-definition.factory';
|
||||
import { RelationTypeFactory } from './relation-type.factory';
|
||||
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
|
||||
import { OrphanedTypesFactory } from './orphaned-types.factory';
|
||||
|
||||
export const workspaceSchemaBuilderFactories = [
|
||||
ArgsFactory,
|
||||
InputTypeFactory,
|
||||
InputTypeDefinitionFactory,
|
||||
OutputTypeFactory,
|
||||
ObjectTypeDefinitionFactory,
|
||||
RelationTypeFactory,
|
||||
ExtendObjectTypeDefinitionFactory,
|
||||
FilterTypeFactory,
|
||||
FilterTypeDefinitionFactory,
|
||||
OrderByTypeFactory,
|
||||
OrderByTypeDefinitionFactory,
|
||||
ConnectionTypeFactory,
|
||||
ConnectionTypeDefinitionFactory,
|
||||
EdgeTypeFactory,
|
||||
EdgeTypeDefinitionFactory,
|
||||
RootTypeFactory,
|
||||
QueryTypeFactory,
|
||||
MutationTypeFactory,
|
||||
OrphanedTypesFactory,
|
||||
];
|
||||
@ -0,0 +1,91 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { TypeMapperService } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { FilterTypeFactory } from './filter-type.factory';
|
||||
import {
|
||||
InputTypeDefinition,
|
||||
InputTypeDefinitionKind,
|
||||
} from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FilterTypeDefinitionFactory {
|
||||
constructor(
|
||||
private readonly filterTypeFactory: FilterTypeFactory,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
const kind = InputTypeDefinitionKind.Filter;
|
||||
const filterInputType = new GraphQLInputObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => {
|
||||
const andOrType = this.typeMapperService.mapToGqlType(filterInputType, {
|
||||
isArray: true,
|
||||
arrayDepth: 1,
|
||||
nullable: true,
|
||||
});
|
||||
|
||||
return {
|
||||
...this.generateFields(objectMetadata, options),
|
||||
and: {
|
||||
type: andOrType,
|
||||
},
|
||||
or: {
|
||||
type: andOrType,
|
||||
},
|
||||
not: {
|
||||
type: this.typeMapperService.mapToGqlType(filterInputType, {
|
||||
nullable: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: filterInputType,
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Composite field types are generated during extension of object type definition
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
//continue;
|
||||
}
|
||||
|
||||
const type = this.filterTypeFactory.create(fieldMetadata, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
defaultValue: fieldMetadata.defaultValue,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
// TODO: Add default value
|
||||
defaultValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FilterTypeFactory {
|
||||
private readonly logger = new Logger(FilterTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLInputType {
|
||||
let filterType = this.typeMapperService.mapToFilterType(
|
||||
fieldMetadata.type,
|
||||
buildOtions.dateScalarMode,
|
||||
buildOtions.numberScalarMode,
|
||||
);
|
||||
|
||||
if (!filterType) {
|
||||
filterType = this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
fieldMetadata.type.toString(),
|
||||
InputTypeDefinitionKind.Filter,
|
||||
);
|
||||
|
||||
if (!filterType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(filterType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
|
||||
export enum InputTypeDefinitionKind {
|
||||
Create = 'Create',
|
||||
Update = 'Update',
|
||||
Filter = 'Filter',
|
||||
OrderBy = 'OrderBy',
|
||||
}
|
||||
|
||||
export interface InputTypeDefinition {
|
||||
target: string;
|
||||
kind: InputTypeDefinitionKind;
|
||||
type: GraphQLInputObjectType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class InputTypeDefinitionFactory {
|
||||
constructor(private readonly inputTypeFactory: InputTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLInputObjectType({
|
||||
name: `${pascalCase(
|
||||
objectMetadata.nameSingular,
|
||||
)}${kind.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, kind, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Composite field types are generated during extension of object type definition
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
//continue;
|
||||
}
|
||||
|
||||
const type = this.inputTypeFactory.create(fieldMetadata, kind, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
defaultValue: fieldMetadata.defaultValue,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
// TODO: Add default value
|
||||
defaultValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class InputTypeFactory {
|
||||
private readonly logger = new Logger(InputTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLInputType {
|
||||
let inputType: GraphQLInputType | undefined =
|
||||
this.typeMapperService.mapToScalarType(
|
||||
fieldMetadata.type,
|
||||
buildOtions.dateScalarMode,
|
||||
buildOtions.numberScalarMode,
|
||||
);
|
||||
|
||||
if (!inputType) {
|
||||
inputType = this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
fieldMetadata.type.toString(),
|
||||
kind,
|
||||
);
|
||||
|
||||
if (!inputType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
kind,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(inputType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { WorkspaceResolverBuilderMutationMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
|
||||
|
||||
@Injectable()
|
||||
export class MutationTypeFactory {
|
||||
constructor(private readonly rootTypeFactory: RootTypeFactory) {}
|
||||
create(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderMutationMethodNames[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLObjectType {
|
||||
return this.rootTypeFactory.create(
|
||||
objectMetadataCollection,
|
||||
workspaceResolverMethodNames,
|
||||
ObjectTypeName.Mutation,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { OutputTypeFactory } from './output-type.factory';
|
||||
|
||||
export enum ObjectTypeDefinitionKind {
|
||||
Connection = 'Connection',
|
||||
Edge = 'Edge',
|
||||
Plain = '',
|
||||
}
|
||||
|
||||
export interface ObjectTypeDefinition {
|
||||
target: string;
|
||||
kind: ObjectTypeDefinitionKind;
|
||||
type: GraphQLObjectType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ObjectTypeDefinitionFactory {
|
||||
constructor(private readonly outputTypeFactory: OutputTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, kind, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Composite field types are generated during extension of object type definition
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.outputTypeFactory.create(fieldMetadata, kind, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import {
|
||||
InputTypeDefinition,
|
||||
InputTypeDefinitionKind,
|
||||
} from './input-type-definition.factory';
|
||||
import { OrderByTypeFactory } from './order-by-type.factory';
|
||||
|
||||
@Injectable()
|
||||
export class OrderByTypeDefinitionFactory {
|
||||
constructor(private readonly orderByTypeFactory: OrderByTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
const kind = InputTypeDefinitionKind.OrderBy;
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLInputObjectType({
|
||||
name: `${pascalCase(
|
||||
objectMetadata.nameSingular,
|
||||
)}${kind.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Composite field types are generated during extension of object type definition
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.orderByTypeFactory.create(fieldMetadata, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
// TODO: Add default value
|
||||
defaultValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class OrderByTypeFactory {
|
||||
private readonly logger = new Logger(OrderByTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLInputType {
|
||||
let orderByType = this.typeMapperService.mapToOrderByType(
|
||||
fieldMetadata.type,
|
||||
);
|
||||
|
||||
if (!orderByType) {
|
||||
orderByType = this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
fieldMetadata.type.toString(),
|
||||
InputTypeDefinitionKind.OrderBy,
|
||||
);
|
||||
|
||||
if (!orderByType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(orderByType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLNamedType } from 'graphql';
|
||||
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
@Injectable()
|
||||
export class OrphanedTypesFactory {
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(): GraphQLNamedType[] {
|
||||
const objectTypeDefs =
|
||||
this.typeDefinitionsStorage.getAllObjectTypeDefinitions();
|
||||
const inputTypeDefs =
|
||||
this.typeDefinitionsStorage.getAllInputTypeDefinitions();
|
||||
const classTypeDefs = [...objectTypeDefs, ...inputTypeDefs];
|
||||
|
||||
return [...classTypeDefs.map(({ type }) => type)];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class OutputTypeFactory {
|
||||
private readonly logger = new Logger(OutputTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
let gqlType: GraphQLOutputType | undefined =
|
||||
this.typeMapperService.mapToScalarType(
|
||||
fieldMetadata.type,
|
||||
buildOtions.dateScalarMode,
|
||||
buildOtions.numberScalarMode,
|
||||
);
|
||||
|
||||
if (!gqlType) {
|
||||
gqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
fieldMetadata.type.toString(),
|
||||
kind,
|
||||
);
|
||||
|
||||
if (!gqlType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(gqlType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { WorkspaceResolverBuilderQueryMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
|
||||
|
||||
@Injectable()
|
||||
export class QueryTypeFactory {
|
||||
constructor(private readonly rootTypeFactory: RootTypeFactory) {}
|
||||
create(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderQueryMethodNames[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLObjectType {
|
||||
return this.rootTypeFactory.create(
|
||||
objectMetadataCollection,
|
||||
workspaceResolverMethodNames,
|
||||
ObjectTypeName.Query,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
import { RelationMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface';
|
||||
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { RelationDirection } from 'src/workspace/utils/deduce-relation-direction.util';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class RelationTypeFactory {
|
||||
private readonly logger = new Logger(RelationTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
relationMetadata: RelationMetadataInterface,
|
||||
relationDirection: RelationDirection,
|
||||
): GraphQLOutputType {
|
||||
let relationQqlType: GraphQLOutputType | undefined = undefined;
|
||||
|
||||
if (
|
||||
relationDirection === RelationDirection.FROM &&
|
||||
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY
|
||||
) {
|
||||
relationQqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
relationMetadata.toObjectMetadataId,
|
||||
ObjectTypeDefinitionKind.Connection,
|
||||
);
|
||||
} else {
|
||||
const relationObjectId =
|
||||
relationDirection === RelationDirection.FROM
|
||||
? relationMetadata.toObjectMetadataId
|
||||
: relationMetadata.fromObjectMetadataId;
|
||||
|
||||
relationQqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
relationObjectId,
|
||||
ObjectTypeDefinitionKind.Plain,
|
||||
);
|
||||
}
|
||||
|
||||
if (!relationQqlType) {
|
||||
this.logger.error(
|
||||
`Could not find a relation type for ${fieldMetadata.id}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(`Could not find a relation type for ${fieldMetadata.id}`);
|
||||
}
|
||||
|
||||
return relationQqlType;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { getResolverName } from 'src/workspace/utils/get-resolver-name.util';
|
||||
import { getResolverArgs } from 'src/workspace/workspace-schema-builder/utils/get-resolver-args.util';
|
||||
|
||||
import { ArgsFactory } from './args.factory';
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
export enum ObjectTypeName {
|
||||
Query = 'Query',
|
||||
Mutation = 'Mutation',
|
||||
Subscription = 'Subscription',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RootTypeFactory {
|
||||
private readonly logger = new Logger(RootTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
private readonly argsFactory: ArgsFactory,
|
||||
) {}
|
||||
|
||||
create(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderMethodNames[],
|
||||
objectTypeName: ObjectTypeName,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLObjectType {
|
||||
if (workspaceResolverMethodNames.length === 0) {
|
||||
this.logger.error(
|
||||
`No resolver methods were found for ${objectTypeName.toString()}`,
|
||||
{
|
||||
workspaceResolverMethodNames,
|
||||
objectTypeName,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`No resolvers were found for ${objectTypeName.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
return new GraphQLObjectType({
|
||||
name: objectTypeName.toString(),
|
||||
fields: this.generateFields(
|
||||
objectMetadataCollection,
|
||||
workspaceResolverMethodNames,
|
||||
options,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private generateFields<T = any, U = any>(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderMethodNames[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<T, U> {
|
||||
const fieldConfigMap: GraphQLFieldConfigMap<T, U> = {};
|
||||
|
||||
for (const objectMetadata of objectMetadataCollection) {
|
||||
for (const methodName of workspaceResolverMethodNames) {
|
||||
const name = getResolverName(objectMetadata, methodName);
|
||||
const args = getResolverArgs(methodName);
|
||||
const outputType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
methodName === 'findMany'
|
||||
? ObjectTypeDefinitionKind.Connection
|
||||
: ObjectTypeDefinitionKind.Plain,
|
||||
);
|
||||
const argsType = this.argsFactory.create(
|
||||
{
|
||||
args,
|
||||
objectMetadata,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
if (!outputType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`,
|
||||
{
|
||||
objectMetadata,
|
||||
methodName,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`,
|
||||
);
|
||||
}
|
||||
|
||||
fieldConfigMap[name] = {
|
||||
type: outputType,
|
||||
args: argsType,
|
||||
resolve: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return fieldConfigMap;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './order-by-direction.enum-type';
|
||||
@ -0,0 +1,24 @@
|
||||
import { GraphQLEnumType } from 'graphql';
|
||||
|
||||
export const OrderByDirectionType = new GraphQLEnumType({
|
||||
name: 'OrderByDirection',
|
||||
description: 'This enum is used to specify the order of results',
|
||||
values: {
|
||||
AscNullsFirst: {
|
||||
value: 'AscNullsFirst',
|
||||
description: 'Ascending order, nulls first',
|
||||
},
|
||||
AscNullsLast: {
|
||||
value: 'AscNullsLast',
|
||||
description: 'Ascending order, nulls last',
|
||||
},
|
||||
DescNullsFirst: {
|
||||
value: 'DescNullsFirst',
|
||||
description: 'Descending order, nulls first',
|
||||
},
|
||||
DescNullsLast: {
|
||||
value: 'DescNullsLast',
|
||||
description: 'Descending order, nulls last',
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
import {
|
||||
GraphQLInputObjectType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLFloat,
|
||||
} from 'graphql';
|
||||
|
||||
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 },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
import {
|
||||
GraphQLInputObjectType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLInt,
|
||||
} from 'graphql';
|
||||
|
||||
export const BigIntFilterType = new GraphQLInputObjectType({
|
||||
name: 'BigIntFilter',
|
||||
fields: {
|
||||
eq: { type: GraphQLInt },
|
||||
gt: { type: GraphQLInt },
|
||||
gte: { type: GraphQLInt },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(GraphQLInt)) },
|
||||
lt: { type: GraphQLInt },
|
||||
lte: { type: GraphQLInt },
|
||||
neq: { type: GraphQLInt },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { GraphQLBoolean, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
export const BooleanFilterType = new GraphQLInputObjectType({
|
||||
name: 'BooleanFilter',
|
||||
fields: {
|
||||
eq: { type: GraphQLBoolean },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
|
||||
|
||||
import { DateScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
export const DateFilterType = new GraphQLInputObjectType({
|
||||
name: 'DateFilter',
|
||||
fields: {
|
||||
eq: { type: DateScalarType },
|
||||
gt: { type: DateScalarType },
|
||||
gte: { type: DateScalarType },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(DateScalarType)) },
|
||||
lt: { type: DateScalarType },
|
||||
lte: { type: DateScalarType },
|
||||
neq: { type: DateScalarType },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
|
||||
|
||||
import { DateTimeScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
export const DatetimeFilterType = new GraphQLInputObjectType({
|
||||
name: 'DateTimeFilter',
|
||||
fields: {
|
||||
eq: { type: DateTimeScalarType },
|
||||
gt: { type: DateTimeScalarType },
|
||||
gte: { type: DateTimeScalarType },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(DateTimeScalarType)) },
|
||||
lt: { type: DateTimeScalarType },
|
||||
lte: { type: DateTimeScalarType },
|
||||
neq: { type: DateTimeScalarType },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
import {
|
||||
GraphQLInputObjectType,
|
||||
GraphQLFloat,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
} from 'graphql';
|
||||
|
||||
export const FloatFilterType = new GraphQLInputObjectType({
|
||||
name: 'FloatFilter',
|
||||
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 },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
export * from './big-float-filter.input-type';
|
||||
export * from './big-int-filter.input-type';
|
||||
export * from './date-filter.input-type';
|
||||
export * from './date-time-filter.input-type';
|
||||
export * from './float-filter.input-type';
|
||||
export * from './int-filter.input-type';
|
||||
export * from './string-filter.input-type';
|
||||
export * from './time-filter.input-type';
|
||||
export * from './uuid-filter.input-type';
|
||||
export * from './boolean-filter.input-type';
|
||||
@ -0,0 +1,19 @@
|
||||
import {
|
||||
GraphQLInputObjectType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLInt,
|
||||
} from 'graphql';
|
||||
|
||||
export const IntFilterType = new GraphQLInputObjectType({
|
||||
name: 'IntFilter',
|
||||
fields: {
|
||||
eq: { type: GraphQLInt },
|
||||
gt: { type: GraphQLInt },
|
||||
gte: { type: GraphQLInt },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(GraphQLInt)) },
|
||||
lt: { type: GraphQLInt },
|
||||
lte: { type: GraphQLInt },
|
||||
neq: { type: GraphQLInt },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import {
|
||||
GraphQLInputObjectType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLString,
|
||||
} from 'graphql';
|
||||
|
||||
export const StringFilterType = new GraphQLInputObjectType({
|
||||
name: 'StringFilter',
|
||||
fields: {
|
||||
eq: { type: GraphQLString },
|
||||
gt: { type: GraphQLString },
|
||||
gte: { type: GraphQLString },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(GraphQLString)) },
|
||||
lt: { type: GraphQLString },
|
||||
lte: { type: GraphQLString },
|
||||
neq: { type: GraphQLString },
|
||||
startsWith: { type: GraphQLString },
|
||||
like: { type: GraphQLString },
|
||||
ilike: { type: GraphQLString },
|
||||
regex: { type: GraphQLString },
|
||||
iregex: { type: GraphQLString },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
|
||||
|
||||
import { TimeScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
export const TimeFilterType = new GraphQLInputObjectType({
|
||||
name: 'TimeFilter',
|
||||
fields: {
|
||||
eq: { type: TimeScalarType },
|
||||
gt: { type: TimeScalarType },
|
||||
gte: { type: TimeScalarType },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(TimeScalarType)) },
|
||||
lt: { type: TimeScalarType },
|
||||
lte: { type: TimeScalarType },
|
||||
neq: { type: TimeScalarType },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
import { GraphQLInputObjectType, GraphQLList } from 'graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
export const UUIDFilterType = new GraphQLInputObjectType({
|
||||
name: 'UUIDFilter',
|
||||
fields: {
|
||||
eq: { type: UUIDScalarType },
|
||||
in: { type: new GraphQLList(UUIDScalarType) },
|
||||
neq: { type: UUIDScalarType },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export * from './page-into.object-type';
|
||||
@ -0,0 +1,15 @@
|
||||
import { ConnectionCursorScalar } from '@ptc-org/nestjs-query-graphql';
|
||||
import { GraphQLBoolean, GraphQLNonNull, GraphQLObjectType } from 'graphql';
|
||||
|
||||
/**
|
||||
* GraphQL PageInfo type.
|
||||
*/
|
||||
export const PageInfoType = new GraphQLObjectType({
|
||||
name: 'PageInfo',
|
||||
fields: {
|
||||
startCursor: { type: ConnectionCursorScalar },
|
||||
endCursor: { type: ConnectionCursorScalar },
|
||||
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
||||
hasPreviousPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { GraphQLScalarType } from 'graphql';
|
||||
import { Kind } from 'graphql/language';
|
||||
|
||||
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 {
|
||||
return parseFloat(value);
|
||||
},
|
||||
parseLiteral(ast): number | null {
|
||||
if (ast.kind === Kind.FLOAT) {
|
||||
return parseFloat(ast.value);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
import { GraphQLScalarType } from 'graphql';
|
||||
|
||||
export const BigIntScalarType = new GraphQLScalarType({
|
||||
name: 'BigInt',
|
||||
description:
|
||||
'The `BigInt` scalar type represents non-fractional signed whole numeric values.',
|
||||
serialize(value: bigint): string {
|
||||
return value.toString();
|
||||
},
|
||||
parseValue(value: string): bigint {
|
||||
return BigInt(value);
|
||||
},
|
||||
parseLiteral(ast): bigint | null {
|
||||
if (ast.kind === 'IntValue') {
|
||||
return BigInt(ast.value);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import { GraphQLScalarType, Kind } from 'graphql';
|
||||
|
||||
export const CursorScalarType = new GraphQLScalarType({
|
||||
name: 'Cursor',
|
||||
description: 'A custom scalar that represents a cursor for pagination',
|
||||
serialize(value) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Cursor must be a string');
|
||||
}
|
||||
return value;
|
||||
},
|
||||
parseValue(value) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Cursor must be a string');
|
||||
}
|
||||
return value;
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new Error('Cursor must be a string');
|
||||
}
|
||||
return ast.value;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
import { GraphQLScalarType } from 'graphql';
|
||||
import { Kind } from 'graphql/language';
|
||||
|
||||
export const DateTimeScalarType = new GraphQLScalarType({
|
||||
name: 'DateTime',
|
||||
description: 'A custom scalar that represents a datetime in ISO format',
|
||||
serialize(value: string): string {
|
||||
const date = new Date(value);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error('Invalid date format, expected ISO date string');
|
||||
}
|
||||
|
||||
return date.toISOString();
|
||||
},
|
||||
parseValue(value: string): Date {
|
||||
const date = new Date(value);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error('Invalid date format, expected ISO date string');
|
||||
}
|
||||
|
||||
return date;
|
||||
},
|
||||
parseLiteral(ast): Date {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new Error('Invalid date format, expected ISO date string');
|
||||
}
|
||||
|
||||
const date = new Date(ast.value);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error('Invalid date format, expected ISO date string');
|
||||
}
|
||||
|
||||
return date;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
import { GraphQLScalarType } from 'graphql';
|
||||
import { Kind } from 'graphql/language';
|
||||
|
||||
export const DateScalarType = new GraphQLScalarType({
|
||||
name: 'Date',
|
||||
description: 'Date custom scalar type',
|
||||
serialize(value: Date): number {
|
||||
return value.getTime();
|
||||
},
|
||||
parseValue(value: number): Date {
|
||||
return new Date(value);
|
||||
},
|
||||
parseLiteral(ast): Date | null {
|
||||
if (ast.kind === Kind.INT) {
|
||||
return new Date(parseInt(ast.value, 10));
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { BigFloatScalarType } from './big-float.scalar';
|
||||
import { BigIntScalarType } from './big-int.scalar';
|
||||
import { CursorScalarType } from './cursor.scalar';
|
||||
import { DateScalarType } from './date.scalar';
|
||||
import { DateTimeScalarType } from './date-time.scalar';
|
||||
import { TimeScalarType } from './time.scalar';
|
||||
import { UUIDScalarType } from './uuid.scalar';
|
||||
|
||||
export * from './big-float.scalar';
|
||||
export * from './big-int.scalar';
|
||||
export * from './cursor.scalar';
|
||||
export * from './date.scalar';
|
||||
export * from './date-time.scalar';
|
||||
export * from './time.scalar';
|
||||
export * from './uuid.scalar';
|
||||
|
||||
export const scalars = [
|
||||
BigFloatScalarType,
|
||||
BigIntScalarType,
|
||||
DateScalarType,
|
||||
DateTimeScalarType,
|
||||
TimeScalarType,
|
||||
UUIDScalarType,
|
||||
CursorScalarType,
|
||||
];
|
||||
@ -0,0 +1,24 @@
|
||||
import { GraphQLScalarType } from 'graphql';
|
||||
import { IntValueNode, Kind } from 'graphql/language';
|
||||
|
||||
export const TimeScalarType = new GraphQLScalarType({
|
||||
name: 'Time',
|
||||
description: 'Time custom scalar type',
|
||||
serialize(value: Date): number {
|
||||
return value.getTime();
|
||||
},
|
||||
parseValue(value: number): Date {
|
||||
return new Date(value);
|
||||
},
|
||||
parseLiteral(ast): Date {
|
||||
if (ast.kind === Kind.INT) {
|
||||
const intAst = ast as IntValueNode;
|
||||
|
||||
if (typeof intAst.value === 'number') {
|
||||
return new Date(intAst.value);
|
||||
}
|
||||
throw new Error(`Invalid timestamp value: ${ast.value}`);
|
||||
}
|
||||
throw new Error(`Invalid AST kind: ${ast.kind}`);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
import { GraphQLScalarType, Kind } from 'graphql';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
|
||||
const checkUUID = (value: any): string => {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('UUID must be a string');
|
||||
}
|
||||
if (!uuidValidate(value)) {
|
||||
throw new Error('Invalid UUID');
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const UUIDScalarType = new GraphQLScalarType({
|
||||
name: 'UUID',
|
||||
description: 'A UUID scalar type',
|
||||
serialize: checkUUID,
|
||||
parseValue: checkUUID,
|
||||
parseLiteral(ast): string {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new Error('UUID must be a string');
|
||||
}
|
||||
|
||||
return ast.value;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
|
||||
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
export interface FieldMetadataInterface<
|
||||
T extends FieldMetadataType | 'default' = 'default',
|
||||
> {
|
||||
id: string;
|
||||
type: FieldMetadataType;
|
||||
name: string;
|
||||
label: string;
|
||||
targetColumnMap: FieldMetadataTargetColumnMap<T>;
|
||||
defaultValue?: FieldMetadataDefaultValue<T>;
|
||||
objectMetadataId: string;
|
||||
description?: string;
|
||||
isNullable?: boolean;
|
||||
fromRelationMetadata?: RelationMetadataEntity;
|
||||
toRelationMetadata?: RelationMetadataEntity;
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import { FieldMetadataInterface } from './field-metadata.interface';
|
||||
import { RelationMetadataInterface } from './relation-metadata.interface';
|
||||
|
||||
export interface ObjectMetadataInterface {
|
||||
id: string;
|
||||
nameSingular: string;
|
||||
namePlural: string;
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
description?: string;
|
||||
targetTableName: string;
|
||||
fromRelations: RelationMetadataInterface[];
|
||||
toRelations: RelationMetadataInterface[];
|
||||
fields: FieldMetadataInterface[];
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { InputTypeDefinitionKind } from 'src/workspace/workspace-schema-builder/factories/input-type-definition.factory';
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
import { ObjectMetadataInterface } from './object-metadata.interface';
|
||||
|
||||
export interface ArgMetadata<T = any> {
|
||||
kind?: InputTypeDefinitionKind;
|
||||
type?: FieldMetadataType;
|
||||
isNullable?: boolean;
|
||||
isArray?: boolean;
|
||||
defaultValue?: T;
|
||||
}
|
||||
|
||||
export interface ArgsMetadata {
|
||||
args: {
|
||||
[key: string]: ArgMetadata;
|
||||
};
|
||||
objectMetadata: ObjectMetadataInterface;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
import { ObjectMetadataInterface } from './object-metadata.interface';
|
||||
import { FieldMetadataInterface } from './field-metadata.interface';
|
||||
|
||||
export interface RelationMetadataInterface {
|
||||
id: string;
|
||||
|
||||
relationType: RelationMetadataType;
|
||||
|
||||
fromObjectMetadataId: string;
|
||||
fromObjectMetadata: ObjectMetadataInterface;
|
||||
|
||||
toObjectMetadataId: string;
|
||||
toObjectMetadata: ObjectMetadataInterface;
|
||||
|
||||
fromFieldMetadataId: string;
|
||||
fromFieldMetadata: FieldMetadataInterface;
|
||||
|
||||
toFieldMetadataId: string;
|
||||
toFieldMetadata: FieldMetadataInterface;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
export type DateScalarMode = 'isoDate' | 'timestamp';
|
||||
export type NumberScalarMode = 'float' | 'integer';
|
||||
|
||||
export interface WorkspaceBuildSchemaOptions {
|
||||
/**
|
||||
* Date scalar mode
|
||||
* @default 'isoDate'
|
||||
*/
|
||||
dateScalarMode?: DateScalarMode;
|
||||
|
||||
/**
|
||||
* Number scalar mode
|
||||
* @default 'float'
|
||||
*/
|
||||
numberScalarMode?: NumberScalarMode;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { FieldMetadataInterface } from './field-metadata.interface';
|
||||
|
||||
export interface WorkspaceSchemaBuilderContext {
|
||||
workspaceId: string;
|
||||
targetTableName: string;
|
||||
fieldMetadataCollection: FieldMetadataInterface[];
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export const currencyObjectDefinition = {
|
||||
id: FieldMetadataType.CURRENCY.toString(),
|
||||
nameSingular: 'currency',
|
||||
namePlural: 'currency',
|
||||
labelSingular: 'Currency',
|
||||
labelPlural: 'Currency',
|
||||
targetTableName: '',
|
||||
fields: [
|
||||
{
|
||||
id: 'amountMicros',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
|
||||
name: 'amountMicros',
|
||||
label: 'AmountMicros',
|
||||
targetColumnMap: { value: 'amountMicros' },
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
{
|
||||
id: 'currencyCode',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
|
||||
name: 'currencyCode',
|
||||
label: 'Currency Code',
|
||||
targetColumnMap: { value: 'currencyCode' },
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
],
|
||||
fromRelations: [],
|
||||
toRelations: [],
|
||||
} satisfies ObjectMetadataInterface;
|
||||
@ -0,0 +1,35 @@
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export const linkObjectDefinition = {
|
||||
id: FieldMetadataType.LINK.toString(),
|
||||
nameSingular: 'link',
|
||||
namePlural: 'link',
|
||||
labelSingular: 'Link',
|
||||
labelPlural: 'Link',
|
||||
targetTableName: '',
|
||||
fields: [
|
||||
{
|
||||
id: 'label',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: FieldMetadataType.LINK.toString(),
|
||||
name: 'label',
|
||||
label: 'Label',
|
||||
targetColumnMap: { value: 'label' },
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
{
|
||||
id: 'url',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: FieldMetadataType.LINK.toString(),
|
||||
name: 'url',
|
||||
label: 'Url',
|
||||
targetColumnMap: { value: 'url' },
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
],
|
||||
fromRelations: [],
|
||||
toRelations: [],
|
||||
} satisfies ObjectMetadataInterface;
|
||||
@ -0,0 +1,158 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GraphQLISODateTime, GraphQLTimestamp } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLEnumType,
|
||||
GraphQLFloat,
|
||||
GraphQLID,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInputType,
|
||||
GraphQLInt,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLScalarType,
|
||||
GraphQLString,
|
||||
GraphQLType,
|
||||
} from 'graphql';
|
||||
|
||||
import {
|
||||
DateScalarMode,
|
||||
NumberScalarMode,
|
||||
} from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
UUIDFilterType,
|
||||
StringFilterType,
|
||||
DatetimeFilterType,
|
||||
DateFilterType,
|
||||
FloatFilterType,
|
||||
IntFilterType,
|
||||
BooleanFilterType,
|
||||
} from 'src/workspace/workspace-schema-builder/graphql-types/input';
|
||||
import { OrderByDirectionType } from 'src/workspace/workspace-schema-builder/graphql-types/enum';
|
||||
|
||||
export interface TypeOptions<T = any> {
|
||||
nullable?: boolean;
|
||||
isArray?: boolean;
|
||||
arrayDepth?: number;
|
||||
defaultValue?: T;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TypeMapperService {
|
||||
mapToScalarType(
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
dateScalarMode: DateScalarMode = 'isoDate',
|
||||
numberScalarMode: NumberScalarMode = 'float',
|
||||
): GraphQLScalarType | undefined {
|
||||
const dateScalar =
|
||||
dateScalarMode === 'timestamp' ? GraphQLTimestamp : GraphQLISODateTime;
|
||||
const numberScalar =
|
||||
numberScalarMode === 'float' ? GraphQLFloat : GraphQLInt;
|
||||
|
||||
// LINK and CURRENCY are handled in the factories because they are objects
|
||||
const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([
|
||||
[FieldMetadataType.UUID, GraphQLID],
|
||||
[FieldMetadataType.TEXT, GraphQLString],
|
||||
[FieldMetadataType.PHONE, GraphQLString],
|
||||
[FieldMetadataType.EMAIL, GraphQLString],
|
||||
[FieldMetadataType.DATE, dateScalar],
|
||||
[FieldMetadataType.BOOLEAN, GraphQLBoolean],
|
||||
[FieldMetadataType.NUMBER, numberScalar],
|
||||
[FieldMetadataType.PROBABILITY, GraphQLFloat],
|
||||
[FieldMetadataType.RELATION, GraphQLID],
|
||||
]);
|
||||
|
||||
return typeScalarMapping.get(fieldMetadataType);
|
||||
}
|
||||
|
||||
mapToFilterType(
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
dateScalarMode: DateScalarMode = 'isoDate',
|
||||
numberScalarMode: NumberScalarMode = 'float',
|
||||
): GraphQLInputObjectType | GraphQLScalarType<boolean, boolean> | undefined {
|
||||
const dateFilter =
|
||||
dateScalarMode === 'timestamp' ? DatetimeFilterType : DateFilterType;
|
||||
const numberScalar =
|
||||
numberScalarMode === 'float' ? FloatFilterType : IntFilterType;
|
||||
|
||||
// LINK and CURRENCY are handled in the factories because they are objects
|
||||
const typeFilterMapping = new Map<
|
||||
FieldMetadataType,
|
||||
GraphQLInputObjectType | GraphQLScalarType<boolean, boolean>
|
||||
>([
|
||||
[FieldMetadataType.UUID, UUIDFilterType],
|
||||
[FieldMetadataType.TEXT, StringFilterType],
|
||||
[FieldMetadataType.PHONE, StringFilterType],
|
||||
[FieldMetadataType.EMAIL, StringFilterType],
|
||||
[FieldMetadataType.DATE, dateFilter],
|
||||
[FieldMetadataType.BOOLEAN, BooleanFilterType],
|
||||
[FieldMetadataType.NUMBER, numberScalar],
|
||||
[FieldMetadataType.PROBABILITY, FloatFilterType],
|
||||
[FieldMetadataType.RELATION, UUIDFilterType],
|
||||
]);
|
||||
|
||||
return typeFilterMapping.get(fieldMetadataType);
|
||||
}
|
||||
|
||||
mapToOrderByType(
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
): GraphQLInputType | undefined {
|
||||
// LINK and CURRENCY are handled in the factories because they are objects
|
||||
const typeOrderByMapping = new Map<FieldMetadataType, GraphQLEnumType>([
|
||||
[FieldMetadataType.UUID, OrderByDirectionType],
|
||||
[FieldMetadataType.TEXT, OrderByDirectionType],
|
||||
[FieldMetadataType.PHONE, OrderByDirectionType],
|
||||
[FieldMetadataType.EMAIL, OrderByDirectionType],
|
||||
[FieldMetadataType.DATE, OrderByDirectionType],
|
||||
[FieldMetadataType.BOOLEAN, OrderByDirectionType],
|
||||
[FieldMetadataType.NUMBER, OrderByDirectionType],
|
||||
[FieldMetadataType.PROBABILITY, OrderByDirectionType],
|
||||
]);
|
||||
|
||||
return typeOrderByMapping.get(fieldMetadataType);
|
||||
}
|
||||
|
||||
mapToGqlType<T extends GraphQLType = GraphQLType>(
|
||||
typeRef: T,
|
||||
options: TypeOptions,
|
||||
): T {
|
||||
let graphqlType: T | GraphQLList<T> | GraphQLNonNull<T> = typeRef;
|
||||
|
||||
if (options.isArray) {
|
||||
graphqlType = this.mapToGqlList(
|
||||
graphqlType,
|
||||
options.arrayDepth ?? 1,
|
||||
options.nullable ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
if (!options.nullable && !options.defaultValue) {
|
||||
graphqlType = new GraphQLNonNull(graphqlType) as unknown as T;
|
||||
}
|
||||
|
||||
return graphqlType as T;
|
||||
}
|
||||
|
||||
private mapToGqlList<T extends GraphQLType = GraphQLType>(
|
||||
targetType: T,
|
||||
depth: number,
|
||||
nullable: boolean,
|
||||
): GraphQLList<T> {
|
||||
const targetTypeNonNull = nullable
|
||||
? targetType
|
||||
: new GraphQLNonNull(targetType);
|
||||
|
||||
if (depth === 0) {
|
||||
return targetType as GraphQLList<T>;
|
||||
}
|
||||
|
||||
return this.mapToGqlList<T>(
|
||||
new GraphQLList(targetTypeNonNull) as unknown as T,
|
||||
depth - 1,
|
||||
nullable,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import { Injectable, Scope } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputObjectType, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
InputTypeDefinition,
|
||||
InputTypeDefinitionKind,
|
||||
} from 'src/workspace/workspace-schema-builder/factories/input-type-definition.factory';
|
||||
import {
|
||||
ObjectTypeDefinition,
|
||||
ObjectTypeDefinitionKind,
|
||||
} from 'src/workspace/workspace-schema-builder/factories/object-type-definition.factory';
|
||||
|
||||
// Must be scoped on REQUEST level
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class TypeDefinitionsStorage {
|
||||
private readonly objectTypeDefinitions = new Map<
|
||||
string,
|
||||
ObjectTypeDefinition
|
||||
>();
|
||||
private readonly inputTypeDefinitions = new Map<
|
||||
string,
|
||||
InputTypeDefinition
|
||||
>();
|
||||
|
||||
addObjectTypes(objectDefs: ObjectTypeDefinition[]) {
|
||||
objectDefs.forEach((item) =>
|
||||
this.objectTypeDefinitions.set(
|
||||
this.generateCompositeKey(item.target, item.kind),
|
||||
item,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getObjectTypeByKey(
|
||||
target: string,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
): GraphQLObjectType | undefined {
|
||||
return this.objectTypeDefinitions.get(
|
||||
this.generateCompositeKey(target, kind),
|
||||
)?.type;
|
||||
}
|
||||
|
||||
getAllObjectTypeDefinitions(): ObjectTypeDefinition[] {
|
||||
return Array.from(this.objectTypeDefinitions.values());
|
||||
}
|
||||
|
||||
addInputTypes(inputDefs: InputTypeDefinition[]) {
|
||||
inputDefs.forEach((item) =>
|
||||
this.inputTypeDefinitions.set(
|
||||
this.generateCompositeKey(item.target, item.kind),
|
||||
item,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getInputTypeByKey(
|
||||
target: string,
|
||||
kind: InputTypeDefinitionKind,
|
||||
): GraphQLInputObjectType | undefined {
|
||||
return this.inputTypeDefinitions.get(
|
||||
this.generateCompositeKey(target, kind),
|
||||
)?.type;
|
||||
}
|
||||
|
||||
getAllInputTypeDefinitions(): InputTypeDefinition[] {
|
||||
return Array.from(this.inputTypeDefinitions.values());
|
||||
}
|
||||
|
||||
private generateCompositeKey(
|
||||
target: string | FieldMetadataType,
|
||||
kind: ObjectTypeDefinitionKind | InputTypeDefinitionKind,
|
||||
): string {
|
||||
return `${target.toString()}_${kind.toString()}`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,230 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { customTableDefaultColumns } from 'src/workspace/workspace-migration-runner/utils/custom-table-default-column.util';
|
||||
|
||||
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
||||
import {
|
||||
ObjectTypeDefinitionFactory,
|
||||
ObjectTypeDefinitionKind,
|
||||
} from './factories/object-type-definition.factory';
|
||||
import {
|
||||
InputTypeDefinitionFactory,
|
||||
InputTypeDefinitionKind,
|
||||
} from './factories/input-type-definition.factory';
|
||||
import { getFieldMetadataType } from './utils/get-field-metadata-type.util';
|
||||
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface';
|
||||
import { currencyObjectDefinition } from './object-definitions/currency.object-definition';
|
||||
import { linkObjectDefinition } from './object-definitions/link.object-definition';
|
||||
import { ObjectMetadataInterface } from './interfaces/object-metadata.interface';
|
||||
import { FieldMetadataInterface } from './interfaces/field-metadata.interface';
|
||||
import { FilterTypeDefinitionFactory } from './factories/filter-type-definition.factory';
|
||||
import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
|
||||
import { EdgeTypeDefinitionFactory } from './factories/edge-type-definition.factory';
|
||||
import { OrderByTypeDefinitionFactory } from './factories/order-by-type-definition.factory';
|
||||
import { ExtendObjectTypeDefinitionFactory } from './factories/extend-object-type-definition.factory';
|
||||
import { objectContainsCompositeField } from './utils/object-contains-composite-field';
|
||||
|
||||
// Create a default field for each custom table default column
|
||||
const defaultFields = customTableDefaultColumns.map((column) => {
|
||||
return {
|
||||
type: getFieldMetadataType(column.type),
|
||||
name: column.name,
|
||||
isNullable: true,
|
||||
} as FieldMetadataEntity;
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class TypeDefinitionsGenerator {
|
||||
private readonly logger = new Logger(TypeDefinitionsGenerator.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory,
|
||||
private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory,
|
||||
private readonly filterTypeDefintionFactory: FilterTypeDefinitionFactory,
|
||||
private readonly orderByTypeDefinitionFactory: OrderByTypeDefinitionFactory,
|
||||
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
|
||||
private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory,
|
||||
private readonly extendObjectTypeDefinitionFactory: ExtendObjectTypeDefinitionFactory,
|
||||
) {}
|
||||
|
||||
generate(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
) {
|
||||
// Generate static objects first because they can be used in dynamic objects
|
||||
this.generateStaticObjectTypeDefs(options);
|
||||
// Generate dynamic objects
|
||||
this.generateDynamicObjectTypeDefs(objectMetadataCollection, options);
|
||||
}
|
||||
|
||||
private generateStaticObjectTypeDefs(options: WorkspaceBuildSchemaOptions) {
|
||||
const staticObjectMetadataCollection = [
|
||||
currencyObjectDefinition,
|
||||
linkObjectDefinition,
|
||||
];
|
||||
|
||||
this.logger.log(
|
||||
`Generating staticObjects: [${staticObjectMetadataCollection
|
||||
.map((object) => object.nameSingular)
|
||||
.join(', ')}]`,
|
||||
);
|
||||
|
||||
// Generate static objects first because they can be used in dynamic objects
|
||||
this.generateObjectTypeDefs(staticObjectMetadataCollection, options);
|
||||
this.generateInputTypeDefs(staticObjectMetadataCollection, options);
|
||||
}
|
||||
|
||||
private generateDynamicObjectTypeDefs(
|
||||
dynamicObjectMetadataCollection: ObjectMetadataInterface[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
) {
|
||||
this.logger.log(
|
||||
`Generating dynamicObjects: [${dynamicObjectMetadataCollection
|
||||
.map((object) => object.nameSingular)
|
||||
.join(', ')}]`,
|
||||
);
|
||||
|
||||
// Generate dynamic objects
|
||||
this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options);
|
||||
this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options);
|
||||
this.generateInputTypeDefs(dynamicObjectMetadataCollection, options);
|
||||
this.generateExtendedObjectTypeDefs(
|
||||
dynamicObjectMetadataCollection,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
private generateObjectTypeDefs(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
) {
|
||||
const objectTypeDefs = objectMetadataCollection.map((objectMetadata) => {
|
||||
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
|
||||
const extendedObjectMetadata = {
|
||||
...objectMetadata,
|
||||
fields,
|
||||
};
|
||||
|
||||
return this.objectTypeDefinitionFactory.create(
|
||||
extendedObjectMetadata,
|
||||
ObjectTypeDefinitionKind.Plain,
|
||||
options,
|
||||
);
|
||||
});
|
||||
|
||||
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs);
|
||||
}
|
||||
|
||||
private generatePaginationTypeDefs(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
) {
|
||||
const edgeTypeDefs = objectMetadataCollection.map((objectMetadata) => {
|
||||
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
|
||||
const extendedObjectMetadata = {
|
||||
...objectMetadata,
|
||||
fields,
|
||||
};
|
||||
|
||||
return this.edgeTypeDefinitionFactory.create(
|
||||
extendedObjectMetadata,
|
||||
options,
|
||||
);
|
||||
});
|
||||
|
||||
this.typeDefinitionsStorage.addObjectTypes(edgeTypeDefs);
|
||||
|
||||
// Connection type defs are using edge type defs
|
||||
const connectionTypeDefs = objectMetadataCollection.map(
|
||||
(objectMetadata) => {
|
||||
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
|
||||
const extendedObjectMetadata = {
|
||||
...objectMetadata,
|
||||
fields,
|
||||
};
|
||||
|
||||
return this.connectionTypeDefinitionFactory.create(
|
||||
extendedObjectMetadata,
|
||||
options,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
this.typeDefinitionsStorage.addObjectTypes(connectionTypeDefs);
|
||||
}
|
||||
|
||||
private generateInputTypeDefs(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
) {
|
||||
const inputTypeDefs = objectMetadataCollection
|
||||
.map((objectMetadata) => {
|
||||
const fields = this.mergeFieldsWithDefaults(objectMetadata.fields);
|
||||
const requiredExtendedObjectMetadata = {
|
||||
...objectMetadata,
|
||||
fields,
|
||||
};
|
||||
const optionalExtendedObjectMetadata = {
|
||||
...objectMetadata,
|
||||
fields: fields.map((field) => ({ ...field, isNullable: true })),
|
||||
};
|
||||
|
||||
return [
|
||||
// Input type for create
|
||||
this.inputTypeDefinitionFactory.create(
|
||||
requiredExtendedObjectMetadata,
|
||||
InputTypeDefinitionKind.Create,
|
||||
options,
|
||||
),
|
||||
// Input type for update
|
||||
this.inputTypeDefinitionFactory.create(
|
||||
optionalExtendedObjectMetadata,
|
||||
InputTypeDefinitionKind.Update,
|
||||
options,
|
||||
),
|
||||
// Filter input type
|
||||
this.filterTypeDefintionFactory.create(
|
||||
optionalExtendedObjectMetadata,
|
||||
options,
|
||||
),
|
||||
// OrderBy input type
|
||||
this.orderByTypeDefinitionFactory.create(
|
||||
optionalExtendedObjectMetadata,
|
||||
options,
|
||||
),
|
||||
];
|
||||
})
|
||||
.flat();
|
||||
|
||||
this.typeDefinitionsStorage.addInputTypes(inputTypeDefs);
|
||||
}
|
||||
|
||||
private generateExtendedObjectTypeDefs(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
) {
|
||||
// Generate extended object type defs only for objects that contain composite fields
|
||||
const objectMetadataCollectionWithCompositeFields =
|
||||
objectMetadataCollection.filter(objectContainsCompositeField);
|
||||
const objectTypeDefs = objectMetadataCollectionWithCompositeFields.map(
|
||||
(objectMetadata) =>
|
||||
this.extendObjectTypeDefinitionFactory.create(objectMetadata, options),
|
||||
);
|
||||
|
||||
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs);
|
||||
}
|
||||
|
||||
private mergeFieldsWithDefaults(
|
||||
fields: FieldMetadataInterface[],
|
||||
): FieldMetadataInterface[] {
|
||||
const fieldNames = new Set(fields.map((field) => field.name));
|
||||
|
||||
const uniqueDefaultFields = defaultFields.filter(
|
||||
(defaultField) => !fieldNames.has(defaultField.name),
|
||||
);
|
||||
|
||||
return [...fields, ...uniqueDefaultFields];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { cleanEntityName } from 'src/workspace/workspace-schema-builder/utils/clean-entity-name.util';
|
||||
|
||||
describe('cleanEntityName', () => {
|
||||
test('should camelCase strings', () => {
|
||||
expect(cleanEntityName('hello world')).toBe('helloWorld');
|
||||
expect(cleanEntityName('my name is John')).toBe('myNameIsJohn');
|
||||
});
|
||||
|
||||
test('should remove numbers at the beginning', () => {
|
||||
expect(cleanEntityName('123hello')).toBe('hello');
|
||||
expect(cleanEntityName('456hello world')).toBe('helloWorld');
|
||||
});
|
||||
|
||||
test('should remove special characters', () => {
|
||||
expect(cleanEntityName('hello$world')).toBe('helloWorld');
|
||||
expect(cleanEntityName('some#special&chars')).toBe('someSpecialChars');
|
||||
});
|
||||
|
||||
test('should handle empty strings', () => {
|
||||
expect(cleanEntityName('')).toBe('');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { getFieldMetadataType } from 'src/workspace/workspace-schema-builder/utils/get-field-metadata-type.util';
|
||||
|
||||
describe('getFieldMetadataType', () => {
|
||||
it.each([
|
||||
['uuid', FieldMetadataType.UUID],
|
||||
['timestamp', FieldMetadataType.DATE],
|
||||
])(
|
||||
'should return correct FieldMetadataType for type %s',
|
||||
(type, expectedMetadataType) => {
|
||||
expect(getFieldMetadataType(type)).toBe(expectedMetadataType);
|
||||
},
|
||||
);
|
||||
|
||||
it('should throw an error for an unknown type', () => {
|
||||
const unknownType = 'unknownType';
|
||||
expect(() => getFieldMetadataType(unknownType)).toThrow(
|
||||
`Unknown type ${unknownType}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,55 @@
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { InputTypeDefinitionKind } from 'src/workspace/workspace-schema-builder/factories/input-type-definition.factory';
|
||||
import { getResolverArgs } from 'src/workspace/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 },
|
||||
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: true },
|
||||
orderBy: { kind: InputTypeDefinitionKind.OrderBy, isNullable: true },
|
||||
},
|
||||
findOne: {
|
||||
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
|
||||
},
|
||||
createMany: {
|
||||
data: {
|
||||
kind: InputTypeDefinitionKind.Create,
|
||||
isNullable: false,
|
||||
isArray: true,
|
||||
},
|
||||
},
|
||||
createOne: {
|
||||
data: { kind: InputTypeDefinitionKind.Create, isNullable: false },
|
||||
},
|
||||
updateOne: {
|
||||
id: { type: FieldMetadataType.UUID, isNullable: false },
|
||||
data: { kind: InputTypeDefinitionKind.Update, isNullable: false },
|
||||
},
|
||||
deleteOne: {
|
||||
id: { type: FieldMetadataType.UUID, isNullable: false },
|
||||
},
|
||||
};
|
||||
|
||||
// Test each resolver type
|
||||
Object.entries(expectedOutputs).forEach(([resolverType, expectedOutput]) => {
|
||||
it(`should return correct args for ${resolverType} resolver`, () => {
|
||||
expect(
|
||||
getResolverArgs(resolverType as WorkspaceResolverBuilderMethodNames),
|
||||
).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
// Test for an unknown resolver type
|
||||
it('should throw an error for an unknown resolver type', () => {
|
||||
const unknownType = 'unknownType';
|
||||
expect(() =>
|
||||
getResolverArgs(unknownType as WorkspaceResolverBuilderMethodNames),
|
||||
).toThrow(`Unknown resolver type: ${unknownType}`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
import { camelCase } from 'src/utils/camel-case';
|
||||
|
||||
export const cleanEntityName = (entityName: string) => {
|
||||
// Remove all leading numbers
|
||||
let camelCasedEntityName = entityName.replace(/^[0-9]+/, '');
|
||||
|
||||
// Trim the string
|
||||
camelCasedEntityName = camelCasedEntityName.trim();
|
||||
|
||||
// Camel case the string
|
||||
camelCasedEntityName = camelCase(camelCasedEntityName);
|
||||
|
||||
// Remove all special characters but keep alphabets and numbers
|
||||
camelCasedEntityName = camelCasedEntityName.replace(/[^a-zA-Z0-9]/g, '');
|
||||
|
||||
return camelCasedEntityName;
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const typeOrmTypeMapping = new Map<string, FieldMetadataType>([
|
||||
['uuid', FieldMetadataType.UUID],
|
||||
['timestamp', FieldMetadataType.DATE],
|
||||
// Add more types here if we need to support more than id, and createdAt/updatedAt/deletedAt
|
||||
]);
|
||||
|
||||
export const getFieldMetadataType = (type: string) => {
|
||||
const fieldType = typeOrmTypeMapping.get(type);
|
||||
|
||||
if (fieldType === undefined || fieldType === null) {
|
||||
throw new Error(`Unknown type ${type}`);
|
||||
}
|
||||
|
||||
return fieldType;
|
||||
};
|
||||
@ -0,0 +1,81 @@
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ArgMetadata } from 'src/workspace/workspace-schema-builder/interfaces/param-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { InputTypeDefinitionKind } from 'src/workspace/workspace-schema-builder/factories/input-type-definition.factory';
|
||||
|
||||
export const getResolverArgs = (
|
||||
type: WorkspaceResolverBuilderMethodNames,
|
||||
): { [key: string]: ArgMetadata } => {
|
||||
switch (type) {
|
||||
case 'findMany':
|
||||
return {
|
||||
first: {
|
||||
type: FieldMetadataType.NUMBER,
|
||||
isNullable: true,
|
||||
},
|
||||
last: {
|
||||
type: FieldMetadataType.NUMBER,
|
||||
isNullable: true,
|
||||
},
|
||||
before: {
|
||||
type: FieldMetadataType.TEXT,
|
||||
isNullable: true,
|
||||
},
|
||||
after: {
|
||||
type: FieldMetadataType.TEXT,
|
||||
isNullable: true,
|
||||
},
|
||||
filter: {
|
||||
kind: InputTypeDefinitionKind.Filter,
|
||||
isNullable: true,
|
||||
},
|
||||
orderBy: {
|
||||
kind: InputTypeDefinitionKind.OrderBy,
|
||||
isNullable: true,
|
||||
},
|
||||
};
|
||||
case 'findOne':
|
||||
return {
|
||||
filter: {
|
||||
kind: InputTypeDefinitionKind.Filter,
|
||||
isNullable: false,
|
||||
},
|
||||
};
|
||||
case 'createMany':
|
||||
return {
|
||||
data: {
|
||||
kind: InputTypeDefinitionKind.Create,
|
||||
isNullable: false,
|
||||
isArray: true,
|
||||
},
|
||||
};
|
||||
case 'createOne':
|
||||
return {
|
||||
data: {
|
||||
kind: InputTypeDefinitionKind.Create,
|
||||
isNullable: false,
|
||||
},
|
||||
};
|
||||
case 'updateOne':
|
||||
return {
|
||||
id: {
|
||||
type: FieldMetadataType.UUID,
|
||||
isNullable: false,
|
||||
},
|
||||
data: {
|
||||
kind: InputTypeDefinitionKind.Update,
|
||||
isNullable: false,
|
||||
},
|
||||
};
|
||||
case 'deleteOne':
|
||||
return {
|
||||
id: {
|
||||
type: FieldMetadataType.UUID,
|
||||
isNullable: false,
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown resolver type: ${type}`);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
export const objectContainsCompositeField = (
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
): boolean => {
|
||||
return objectMetadata.fields.some((field) =>
|
||||
isCompositeFieldMetadataType(field.type),
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLSchema } from 'graphql';
|
||||
|
||||
import { WorkspaceResolverBuilderMethods } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { TypeDefinitionsGenerator } from './type-definitions.generator';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface';
|
||||
import { QueryTypeFactory } from './factories/query-type.factory';
|
||||
import { MutationTypeFactory } from './factories/mutation-type.factory';
|
||||
import { ObjectMetadataInterface } from './interfaces/object-metadata.interface';
|
||||
import { OrphanedTypesFactory } from './factories/orphaned-types.factory';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceGraphQLSchemaFactory {
|
||||
private readonly logger = new Logger(WorkspaceGraphQLSchemaFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeDefinitionsGenerator: TypeDefinitionsGenerator,
|
||||
private readonly queryTypeFactory: QueryTypeFactory,
|
||||
private readonly mutationTypeFactory: MutationTypeFactory,
|
||||
private readonly orphanedTypesFactory: OrphanedTypesFactory,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
|
||||
options: WorkspaceBuildSchemaOptions = {},
|
||||
): Promise<GraphQLSchema> {
|
||||
// Generate type definitions
|
||||
this.typeDefinitionsGenerator.generate(objectMetadataCollection, options);
|
||||
|
||||
// Generate schema
|
||||
const schema = new GraphQLSchema({
|
||||
query: this.queryTypeFactory.create(
|
||||
objectMetadataCollection,
|
||||
[...workspaceResolverBuilderMethods.queries],
|
||||
options,
|
||||
),
|
||||
mutation: this.mutationTypeFactory.create(
|
||||
objectMetadataCollection,
|
||||
[...workspaceResolverBuilderMethods.mutations],
|
||||
options,
|
||||
),
|
||||
types: this.orphanedTypesFactory.create(),
|
||||
});
|
||||
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
|
||||
|
||||
import { TypeDefinitionsGenerator } from './type-definitions.generator';
|
||||
import { WorkspaceGraphQLSchemaFactory } from './workspace-graphql-schema.factory';
|
||||
|
||||
import { workspaceSchemaBuilderFactories } from './factories/factories';
|
||||
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
||||
import { TypeMapperService } from './services/type-mapper.service';
|
||||
|
||||
@Module({
|
||||
imports: [ObjectMetadataModule],
|
||||
providers: [
|
||||
...workspaceSchemaBuilderFactories,
|
||||
TypeDefinitionsGenerator,
|
||||
TypeDefinitionsStorage,
|
||||
TypeMapperService,
|
||||
WorkspaceGraphQLSchemaFactory,
|
||||
JwtAuthGuard,
|
||||
],
|
||||
exports: [WorkspaceGraphQLSchemaFactory],
|
||||
})
|
||||
export class WorkspaceSchemaBuilderModule {}
|
||||
Reference in New Issue
Block a user