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:
Jérémy M
2023-11-17 11:26:33 +01:00
committed by GitHub
parent bc579d64a6
commit b86ada6d2b
239 changed files with 1603 additions and 1618 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from './order-by-direction.enum-type';

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
import { GraphQLBoolean, GraphQLInputObjectType } from 'graphql';
export const BooleanFilterType = new GraphQLInputObjectType({
name: 'BooleanFilter',
fields: {
eq: { type: GraphQLBoolean },
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from './page-into.object-type';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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[];
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import { FieldMetadataInterface } from './field-metadata.interface';
export interface WorkspaceSchemaBuilderContext {
workspaceId: string;
targetTableName: string;
fieldMetadataCollection: FieldMetadataInterface[];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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