feat: refactor schema builder and resolver builder (#2215)

* feat: wip refactor schema builder

* feat: wip store types and first queries generation

* feat: refactor schema-builder and resolver-builder

* fix: clean & small type fix

* fix: avoid breaking change

* fix: remove util from pg-graphql classes

* fix: required default fields

* Refactor frontend accordingly

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2023-11-03 17:16:37 +01:00
committed by GitHub
parent aba3fd454b
commit 1ed4965a95
216 changed files with 3215 additions and 2028 deletions

View File

@ -30,7 +30,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'name',
label: 'Name',
targetColumnMap: {
@ -45,7 +45,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'domainName',
label: 'Domain Name',
targetColumnMap: {
@ -60,7 +60,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'address',
label: 'Address',
targetColumnMap: {
@ -75,7 +75,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'employees',
label: 'Employees',
targetColumnMap: {
@ -91,7 +91,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'name',
label: 'Name',
targetColumnMap: {
@ -106,7 +106,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'objectId',
label: 'Object Id',
targetColumnMap: {
@ -121,7 +121,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'type',
label: 'Type',
targetColumnMap: {
@ -137,7 +137,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'fieldId',
label: 'Field Id',
targetColumnMap: {
@ -152,7 +152,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'viewId',
label: 'View Id',
targetColumnMap: {
@ -167,7 +167,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'boolean',
type: 'BOOLEAN',
name: 'isVisible',
label: 'Visible',
targetColumnMap: {
@ -182,7 +182,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'number',
type: 'NUMBER',
name: 'size',
label: 'Size',
targetColumnMap: {
@ -197,7 +197,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'number',
type: 'NUMBER',
name: 'position',
label: 'Position',
targetColumnMap: {
@ -213,7 +213,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'fieldId',
label: 'Field Id',
targetColumnMap: {
@ -228,7 +228,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'viewId',
label: 'View Id',
targetColumnMap: {
@ -243,7 +243,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'operand',
label: 'Operand',
targetColumnMap: {
@ -258,7 +258,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'value',
label: 'Value',
targetColumnMap: {
@ -273,7 +273,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'displayValue',
label: 'Display Value',
targetColumnMap: {
@ -289,7 +289,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'fieldId',
label: 'Field Id',
targetColumnMap: {
@ -304,7 +304,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'viewId',
label: 'View Id',
targetColumnMap: {
@ -319,7 +319,7 @@ export const seedFieldMetadata = async (
isCustom: false,
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
isActive: true,
type: 'text',
type: 'TEXT',
name: 'direction',
label: 'Direction',
targetColumnMap: {

View File

@ -14,6 +14,9 @@ import { EnvironmentService } from './integrations/environment/environment.servi
const bootstrap = async () => {
const app = await NestFactory.create(AppModule, {
cors: true,
logger: process.env.DEBUG_MODE
? ['error', 'warn', 'log', 'verbose', 'debug']
: ['error', 'warn', 'log'],
});
// Apply validation pipes globally

View File

@ -8,6 +8,8 @@ import {
IsUUID,
} from 'class-validator';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
@InputType()
export class CreateFieldInput {
@IsString()
@ -20,20 +22,10 @@ export class CreateFieldInput {
@Field()
label: string;
// Todo: use a type enum and share with typeorm entity
@IsEnum([
'text',
'phone',
'email',
'number',
'boolean',
'date',
'url',
'money',
])
@IsEnum(FieldMetadataType)
@IsNotEmpty()
@Field()
type: string;
@Field(() => FieldMetadataType)
type: FieldMetadataType;
@IsUUID()
@Field()

View File

@ -1,4 +1,4 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql';
import {
Column,
@ -17,13 +17,31 @@ import {
QueryOptions,
} from '@ptc-org/nestjs-query-graphql';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
import { BeforeCreateOneField } from './hooks/before-create-one-field.hook';
import { FieldMetadataTargetColumnMap } from './interfaces/field-metadata-target-column-map.interface';
export enum FieldMetadataType {
UUID = 'uuid',
TEXT = 'TEXT',
PHONE = 'PHONE',
EMAIL = 'EMAIL',
DATE = 'DATE',
BOOLEAN = 'BOOLEAN',
NUMBER = 'NUMBER',
ENUM = 'ENUM',
URL = 'URL',
MONEY = 'MONEY',
}
registerEnumType(FieldMetadataType, {
name: 'FieldMetadataType',
description: 'Type of the field',
});
export type FieldMetadataTargetColumnMap = {
[key: string]: string;
};
@Entity('field_metadata')
@ObjectType('field')
@BeforeCreateOne(BeforeCreateOneField)
@ -43,7 +61,7 @@ export type FieldMetadataTargetColumnMap = {
'objectId',
'workspaceId',
])
export class FieldMetadata {
export class FieldMetadata implements FieldMetadataInterface {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@ -51,9 +69,9 @@ export class FieldMetadata {
@Column({ nullable: false, name: 'object_id' })
objectId: string;
@Field()
@Field(() => FieldMetadataType)
@Column({ nullable: false })
type: string;
type: FieldMetadataType;
@Field()
@Column({ nullable: false })

View File

@ -0,0 +1,35 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export interface FieldMetadataTargetColumnMapValue {
value: string;
}
export interface FieldMetadataTargetColumnMapUrl {
text: string;
link: string;
}
export interface FieldMetadataTargetColumnMapMoney {
value: number;
currency: string;
}
type AllFieldMetadataTypes = {
[key: string]: any;
};
type FieldMetadataTypeMapping = {
[FieldMetadataType.URL]: FieldMetadataTargetColumnMapUrl;
[FieldMetadataType.MONEY]: FieldMetadataTargetColumnMapMoney;
};
type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> =
T extends keyof FieldMetadataTypeMapping
? FieldMetadataTypeMapping[T]
: T extends 'default'
? AllFieldMetadataTypes
: FieldMetadataTargetColumnMapValue;
export type FieldMetadataTargetColumnMap<
T extends FieldMetadataType | 'default' = 'default',
> = TypeByFieldMetadata<T>;

View File

@ -1,9 +1,11 @@
import { v4 } from 'uuid';
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { uuidToBase36 } from 'src/metadata/data-source/data-source.util';
import {
FieldMetadata,
FieldMetadataTargetColumnMap,
FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';
import { TenantMigrationColumnAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
@ -25,24 +27,24 @@ export function generateColumnName(name: string): string {
* @returns FieldMetadataTargetColumnMap
*/
export function generateTargetColumnMap(
type: string,
type: FieldMetadataType,
): FieldMetadataTargetColumnMap {
switch (type) {
case 'text':
case 'phone':
case 'email':
case 'number':
case 'boolean':
case 'date':
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
case FieldMetadataType.NUMBER:
case FieldMetadataType.BOOLEAN:
case FieldMetadataType.DATE:
return {
value: `column_${uuidToBase36(v4())}`,
};
case 'url':
case FieldMetadataType.URL:
return {
text: `column_${uuidToBase36(v4())}`,
link: `column_${uuidToBase36(v4())}`,
};
case 'money':
case FieldMetadataType.MONEY:
return {
amount: `column_${uuidToBase36(v4())}`,
currency: `column_${uuidToBase36(v4())}`,
@ -56,7 +58,7 @@ export function convertFieldMetadataToColumnActions(
fieldMetadata: FieldMetadata,
): TenantMigrationColumnAction[] {
switch (fieldMetadata.type) {
case 'text':
case FieldMetadataType.TEXT:
return [
{
name: fieldMetadata.targetColumnMap.value,
@ -64,8 +66,8 @@ export function convertFieldMetadataToColumnActions(
type: 'text',
},
];
case 'phone':
case 'email':
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
return [
{
name: fieldMetadata.targetColumnMap.value,
@ -73,7 +75,7 @@ export function convertFieldMetadataToColumnActions(
type: 'varchar',
},
];
case 'number':
case FieldMetadataType.NUMBER:
return [
{
name: fieldMetadata.targetColumnMap.value,
@ -81,7 +83,7 @@ export function convertFieldMetadataToColumnActions(
type: 'integer',
},
];
case 'boolean':
case FieldMetadataType.BOOLEAN:
return [
{
name: fieldMetadata.targetColumnMap.value,
@ -89,7 +91,7 @@ export function convertFieldMetadataToColumnActions(
type: 'boolean',
},
];
case 'date':
case FieldMetadataType.DATE:
return [
{
name: fieldMetadata.targetColumnMap.value,
@ -97,7 +99,7 @@ export function convertFieldMetadataToColumnActions(
type: 'timestamp',
},
];
case 'url':
case FieldMetadataType.URL:
return [
{
name: fieldMetadata.targetColumnMap.text,
@ -110,7 +112,7 @@ export function convertFieldMetadataToColumnActions(
type: 'varchar',
},
];
case 'money':
case FieldMetadataType.MONEY:
return [
{
name: fieldMetadata.targetColumnMap.amount,
@ -127,24 +129,3 @@ export function convertFieldMetadataToColumnActions(
throw new Error(`Unknown type ${fieldMetadata.type}`);
}
}
// Deprecated with target_column_name deprecation
export function convertMetadataTypeToColumnType(type: string) {
switch (type) {
case 'text':
case 'url':
case 'phone':
case 'email':
return 'text';
case 'number':
return 'int';
case 'boolean':
return 'boolean';
case 'date':
return 'timestamp';
case 'money':
return 'integer';
default:
throw new Error('Invalid type');
}
}

View File

@ -0,0 +1,25 @@
import { TableColumnOptions } from 'typeorm';
export const customTableDefaultColumns: TableColumnOptions[] = [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'public.uuid_generate_v4()',
},
{
name: 'createdAt',
type: 'timestamp',
default: 'now()',
},
{
name: 'updatedAt',
type: 'timestamp',
default: 'now()',
},
{
name: 'deletedAt',
type: 'timestamp',
isNullable: true,
},
];

View File

@ -9,6 +9,8 @@ import {
} from 'src/metadata/tenant-migration/tenant-migration.entity';
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
import { customTableDefaultColumns } from './custom-table-default-column.util';
@Injectable()
export class MigrationRunnerService {
constructor(
@ -114,29 +116,7 @@ export class MigrationRunnerService {
new Table({
name: tableName,
schema: schemaName,
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'public.uuid_generate_v4()',
},
{
name: 'createdAt',
type: 'timestamp',
default: 'now()',
},
{
name: 'updatedAt',
type: 'timestamp',
default: 'now()',
},
{
name: 'deletedAt',
type: 'timestamp',
isNullable: true,
},
],
columns: customTableDefaultColumns,
}),
true,
);

View File

@ -17,6 +17,8 @@ import {
QueryOptions,
} from '@ptc-org/nestjs-query-graphql';
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook';
@ -41,7 +43,7 @@ import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook';
'workspaceId',
])
@Unique('IndexOnNamePluralAndWorkspaceIdUnique', ['namePlural', 'workspaceId'])
export class ObjectMetadata {
export class ObjectMetadata implements ObjectMetadataInterface {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;

View File

@ -70,6 +70,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadata> {
return createdObjectMetadata;
}
public async getObjectMetadataFromWorkspaceId(workspaceId: string) {
return this.objectMetadataRepository.find({
where: { workspaceId },
relations: ['fields'],
});
}
public async getObjectMetadataFromDataSourceId(dataSourceId: string) {
return this.objectMetadataRepository.find({
where: { dataSourceId },

View File

@ -8,7 +8,7 @@ const companiesMetadata = {
icon: 'IconBuildingSkyscraper',
fields: [
{
type: 'text',
type: 'TEXT',
name: 'name',
label: 'Name',
targetColumnMap: {
@ -19,7 +19,7 @@ const companiesMetadata = {
isNullable: false,
},
{
type: 'text',
type: 'TEXT',
name: 'domainName',
label: 'Domain Name',
targetColumnMap: {
@ -30,7 +30,7 @@ const companiesMetadata = {
isNullable: true,
},
{
type: 'text',
type: 'TEXT',
name: 'address',
label: 'Address',
targetColumnMap: {
@ -41,7 +41,7 @@ const companiesMetadata = {
isNullable: true,
},
{
type: 'number',
type: 'NUMBER',
name: 'employees',
label: 'Employees',
targetColumnMap: {

View File

@ -8,7 +8,7 @@ const viewFieldsMetadata = {
icon: 'IconColumns3',
fields: [
{
type: 'text',
type: 'TEXT',
name: 'fieldId',
label: 'Field Id',
targetColumnMap: {
@ -19,7 +19,7 @@ const viewFieldsMetadata = {
isNullable: false,
},
{
type: 'text',
type: 'TEXT',
name: 'viewId',
label: 'View Id',
targetColumnMap: {
@ -41,7 +41,7 @@ const viewFieldsMetadata = {
isNullable: false,
},
{
type: 'number',
type: 'NUMBER',
name: 'size',
label: 'Size',
targetColumnMap: {
@ -52,7 +52,7 @@ const viewFieldsMetadata = {
isNullable: false,
},
{
type: 'number',
type: 'NUMBER',
name: 'position',
label: 'Position',
targetColumnMap: {

View File

@ -8,7 +8,7 @@ const viewFiltersMetadata = {
icon: 'IconFilterBolt',
fields: [
{
type: 'text',
type: 'TEXT',
name: 'fieldId',
label: 'Field Id',
targetColumnMap: {
@ -19,7 +19,7 @@ const viewFiltersMetadata = {
isNullable: true,
},
{
type: 'text',
type: 'TEXT',
name: 'viewId',
label: 'View Id',
targetColumnMap: {
@ -30,7 +30,7 @@ const viewFiltersMetadata = {
isNullable: false,
},
{
type: 'text',
type: 'TEXT',
name: 'operand',
label: 'Operand',
targetColumnMap: {
@ -41,7 +41,7 @@ const viewFiltersMetadata = {
isNullable: false,
},
{
type: 'text',
type: 'TEXT',
name: 'value',
label: 'Value',
targetColumnMap: {
@ -52,7 +52,7 @@ const viewFiltersMetadata = {
isNullable: false,
},
{
type: 'text',
type: 'TEXT',
name: 'displayValue',
label: 'Display Value',
targetColumnMap: {

View File

@ -8,7 +8,7 @@ const viewSortsMetadata = {
icon: 'IconArrowsSort',
fields: [
{
type: 'text',
type: 'TEXT',
name: 'fieldId',
label: 'Field Id',
targetColumnMap: {
@ -19,7 +19,7 @@ const viewSortsMetadata = {
isNullable: false,
},
{
type: 'text',
type: 'TEXT',
name: 'viewId',
label: 'View Id',
targetColumnMap: {
@ -30,7 +30,7 @@ const viewSortsMetadata = {
isNullable: false,
},
{
type: 'text',
type: 'TEXT',
name: 'direction',
label: 'Direction',
targetColumnMap: {

View File

@ -8,7 +8,7 @@ const viewsMetadata = {
icon: 'IconLayoutCollage',
fields: [
{
type: 'text',
type: 'TEXT',
name: 'name',
label: 'Name',
targetColumnMap: {
@ -19,7 +19,7 @@ const viewsMetadata = {
isNullable: false,
},
{
type: 'text',
type: 'TEXT',
name: 'objectId',
label: 'Object Id',
targetColumnMap: {
@ -30,7 +30,7 @@ const viewsMetadata = {
isNullable: false,
},
{
type: 'text',
type: 'TEXT',
name: 'type',
label: 'Type',
targetColumnMap: {

View File

@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { EntityResolverService } from './entity-resolver.service';
@Module({
imports: [DataSourceModule],
providers: [EntityResolverService],
exports: [EntityResolverService],
})
export class EntityResolverModule {}

View File

@ -1,27 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { EntityResolverService } from './entity-resolver.service';
describe('EntityResolverService', () => {
let service: EntityResolverService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EntityResolverService,
{
provide: DataSourceService,
useValue: {},
},
],
}).compile();
service = module.get<EntityResolverService>(EntityResolverService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,106 +0,0 @@
import { Injectable } from '@nestjs/common';
import { GraphQLResolveInfo } from 'graphql';
import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { PGGraphQLQueryRunner } from './pg-graphql/pg-graphql-query-runner.util';
@Injectable()
export class EntityResolverService {
constructor(private readonly dataSourceService: DataSourceService) {}
async findMany(
args: {
first?: number;
last?: number;
before?: string;
after?: string;
filter?: any;
orderBy?: any;
},
context: SchemaBuilderContext,
info: GraphQLResolveInfo,
) {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
tableName: context.tableName,
workspaceId: context.workspaceId,
info,
fields: context.fields,
});
return runner.findMany(args);
}
async findOne(
args: { filter?: any },
context: SchemaBuilderContext,
info: GraphQLResolveInfo,
) {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
tableName: context.tableName,
workspaceId: context.workspaceId,
info,
fields: context.fields,
});
return runner.findOne(args);
}
async createOne(
args: { data: any },
context: SchemaBuilderContext,
info: GraphQLResolveInfo,
) {
const records = await this.createMany({ data: [args.data] }, context, info);
return records?.[0];
}
async createMany(
args: { data: any[] },
context: SchemaBuilderContext,
info: GraphQLResolveInfo,
) {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
tableName: context.tableName,
workspaceId: context.workspaceId,
info,
fields: context.fields,
});
return runner.createMany(args);
}
async updateOne(
args: { id: string; data: any },
context: SchemaBuilderContext,
info: GraphQLResolveInfo,
) {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
tableName: context.tableName,
workspaceId: context.workspaceId,
info,
fields: context.fields,
});
return runner.updateOne(args);
}
async deleteOne(
args: { id: string },
context: SchemaBuilderContext,
info: GraphQLResolveInfo,
) {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
tableName: context.tableName,
workspaceId: context.workspaceId,
info,
fields: context.fields,
});
return runner.deleteOne(args);
}
}

View File

@ -1,135 +0,0 @@
import { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields';
import { v4 as uuidv4 } from 'uuid';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { stringifyWithoutKeyQuote } from 'src/tenant/entity-resolver/utils/stringify-without-key-quote.util';
import { convertFieldsToGraphQL } from 'src/tenant/entity-resolver/utils/convert-fields-to-graphql.util';
import { convertArguments } from 'src/tenant/entity-resolver/utils/convert-arguments.util';
import { generateArgsInput } from 'src/tenant/entity-resolver/utils/generate-args-input.util';
type CommandArgs = {
findMany: {
first?: number;
last?: number;
before?: string;
after?: string;
filter?: any;
};
findOne: { filter?: any };
createMany: { data: any[] };
updateOne: { id: string; data: any };
deleteOne: { id: string };
};
export interface PGGraphQLQueryBuilderOptions {
tableName: string;
info: GraphQLResolveInfo;
fields: FieldMetadata[];
}
export class PGGraphQLQueryBuilder {
private options: PGGraphQLQueryBuilderOptions;
constructor(options: PGGraphQLQueryBuilderOptions) {
this.options = options;
}
private getFieldsString(): string {
const select = graphqlFields(this.options.info);
return convertFieldsToGraphQL(select, this.options.fields);
}
// Define command setters
findMany(args?: CommandArgs['findMany']) {
const { tableName } = this.options;
const fieldsString = this.getFieldsString();
const convertedArgs = convertArguments(args, this.options.fields);
const argsString = generateArgsInput(convertedArgs);
return `
query {
${tableName}Collection${argsString ? `(${argsString})` : ''} {
${fieldsString}
}
}
`;
}
findOne(args: CommandArgs['findOne']) {
const { tableName } = this.options;
const fieldsString = this.getFieldsString();
const convertedArgs = convertArguments(args, this.options.fields);
const argsString = generateArgsInput(convertedArgs);
return `
query {
${tableName}Collection${argsString ? `(${argsString})` : ''} {
edges {
node {
${fieldsString}
}
}
}
}
`;
}
createMany(initialArgs: CommandArgs['createMany']) {
const { tableName } = this.options;
const fieldsString = this.getFieldsString();
const args = convertArguments(initialArgs, this.options.fields);
return `
mutation {
insertInto${tableName}Collection(objects: ${stringifyWithoutKeyQuote(
args.data.map((datum) => ({
id: uuidv4(),
...datum,
})),
)}) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
updateOne(initialArgs: CommandArgs['updateOne']) {
const { tableName } = this.options;
const fieldsString = this.getFieldsString();
const args = convertArguments(initialArgs, this.options.fields);
return `
mutation {
update${tableName}Collection(set: ${stringifyWithoutKeyQuote(
args.data,
)}, filter: { id: { eq: "${args.id}" } }) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
deleteOne(args: CommandArgs['deleteOne']) {
const { tableName } = this.options;
const fieldsString = this.getFieldsString();
return `
mutation {
deleteFrom${tableName}Collection(filter: { id: { eq: "${args.id}" } }) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

@ -1,113 +0,0 @@
import { BadRequestException } from '@nestjs/common';
import { GraphQLResolveInfo } from 'graphql';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { parseResult } from 'src/tenant/entity-resolver/utils/parse-result.util';
import { PGGraphQLQueryBuilder } from './pg-graphql-query-builder.util';
interface QueryRunnerOptions {
tableName: string;
workspaceId: string;
info: GraphQLResolveInfo;
fields: FieldMetadata[];
}
export class PGGraphQLQueryRunner {
private queryBuilder: PGGraphQLQueryBuilder;
private options: QueryRunnerOptions;
constructor(
private dataSourceService: DataSourceService,
options: QueryRunnerOptions,
) {
this.queryBuilder = new PGGraphQLQueryBuilder({
tableName: options.tableName,
info: options.info,
fields: options.fields,
});
this.options = options;
}
private async execute(query: string, workspaceId: string): Promise<any> {
const workspaceDataSource =
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
await workspaceDataSource?.query(`
SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)};
`);
return workspaceDataSource?.query(`
SELECT graphql.resolve($$
${query}
$$);
`);
}
private parseResult(graphqlResult: any, command: string): any {
const tableName = this.options.tableName;
const entityKey = `${command}${tableName}Collection`;
const result = graphqlResult?.[0]?.resolve?.data?.[entityKey];
if (!result) {
throw new BadRequestException('Malformed result from GraphQL query');
}
return parseResult(result);
}
async findMany(args: {
first?: number;
last?: number;
before?: string;
after?: string;
filter?: any;
orderBy?: any;
}): Promise<any[]> {
const query = this.queryBuilder.findMany(args);
const result = await this.execute(query, this.options.workspaceId);
return this.parseResult(result, '');
}
async findOne(args: { filter?: any }): Promise<any> {
if (!args.filter || Object.keys(args.filter).length === 0) {
throw new BadRequestException('Missing filter argument');
}
const query = this.queryBuilder.findOne(args);
const result = await this.execute(query, this.options.workspaceId);
const parsedResult = this.parseResult(result, '');
return parsedResult?.edges?.[0]?.node;
}
async createMany(args: { data: any[] }): Promise<any[]> {
const query = this.queryBuilder.createMany(args);
const result = await this.execute(query, this.options.workspaceId);
return this.parseResult(result, 'insertInto')?.records;
}
async createOne(args: { data: any }): Promise<any> {
const records = await this.createMany({ data: [args.data] });
return records?.[0];
}
async updateOne(args: { id: string; data: any }): Promise<any> {
const query = this.queryBuilder.updateOne(args);
const result = await this.execute(query, this.options.workspaceId);
return this.parseResult(result, 'update')?.records?.[0];
}
async deleteOne(args: { id: string }): Promise<any> {
const query = this.queryBuilder.deleteOne(args);
const result = await this.execute(query, this.options.workspaceId);
return this.parseResult(result, 'deleteFrom')?.records?.[0];
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import {
CreateManyResolverArgs,
Resolver,
} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface';
import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner';
@Injectable()
export class CreateManyResolverFactory implements FactoryInterface {
public static methodName = 'createMany' as const;
constructor(private readonly dataSourceService: DataSourceService) {}
create(context: SchemaBuilderContext): Resolver<CreateManyResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
return runner.createMany(args);
};
}
}

View File

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import {
CreateOneResolverArgs,
Resolver,
} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface';
import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
@Injectable()
export class CreateOneResolverFactory implements FactoryInterface {
public static methodName = 'createOne' as const;
constructor(private readonly dataSourceService: DataSourceService) {}
create(context: SchemaBuilderContext): Resolver<CreateOneResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection:
internalContext.fieldMetadataCollection as FieldMetadata[],
});
return runner.createOne(args);
};
}
}

View File

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import {
DeleteOneResolverArgs,
Resolver,
} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface';
import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
@Injectable()
export class DeleteOneResolverFactory implements FactoryInterface {
public static methodName = 'deleteOne' as const;
constructor(private readonly dataSourceService: DataSourceService) {}
create(context: SchemaBuilderContext): Resolver<DeleteOneResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection:
internalContext.fieldMetadataCollection as FieldMetadata[],
});
return runner.deleteOne(args);
};
}
}

View File

@ -0,0 +1,28 @@
import { FindManyResolverFactory } from './find-many-resolver.factory';
import { FindOneResolverFactory } from './find-one-resolver.factory';
import { CreateManyResolverFactory } from './create-many-resolver.factory';
import { CreateOneResolverFactory } from './create-one-resolver.factory';
import { UpdateOneResolverFactory } from './update-one-resolver.factory';
import { DeleteOneResolverFactory } from './delete-one-resolver.factory';
export const resolverBuilderFactories = [
FindManyResolverFactory,
FindOneResolverFactory,
CreateManyResolverFactory,
CreateOneResolverFactory,
UpdateOneResolverFactory,
DeleteOneResolverFactory,
];
export const resolverBuilderMethodNames = {
queries: [
FindManyResolverFactory.methodName,
FindOneResolverFactory.methodName,
],
mutations: [
CreateManyResolverFactory.methodName,
CreateOneResolverFactory.methodName,
UpdateOneResolverFactory.methodName,
DeleteOneResolverFactory.methodName,
],
} as const;

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import {
FindManyResolverArgs,
Resolver,
} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface';
import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner';
@Injectable()
export class FindManyResolverFactory implements FactoryInterface {
public static methodName = 'findMany' as const;
constructor(private readonly dataSourceService: DataSourceService) {}
create(context: SchemaBuilderContext): Resolver<FindManyResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
return runner.findMany(args);
};
}
}

View File

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import {
FindOneResolverArgs,
Resolver,
} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface';
import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
@Injectable()
export class FindOneResolverFactory implements FactoryInterface {
public static methodName = 'findOne' as const;
constructor(private readonly dataSourceService: DataSourceService) {}
create(context: SchemaBuilderContext): Resolver<FindOneResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection:
internalContext.fieldMetadataCollection as FieldMetadata[],
});
return runner.findOne(args);
};
}
}

View File

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import {
Resolver,
UpdateOneResolverArgs,
} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface';
import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
@Injectable()
export class UpdateOneResolverFactory implements FactoryInterface {
public static methodName = 'updateOne' as const;
constructor(private readonly dataSourceService: DataSourceService) {}
create(context: SchemaBuilderContext): Resolver<UpdateOneResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection:
internalContext.fieldMetadataCollection as FieldMetadata[],
});
return runner.updateOne(args);
};
}
}

View File

@ -0,0 +1,7 @@
import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface';
import { Resolver } from './resolvers-builder.interface';
export interface FactoryInterface {
create(context: SchemaBuilderContext): Resolver;
}

View File

@ -0,0 +1,14 @@
import { Record as IRecord } from './record.interface';
export interface PGGraphQLResponse<Data = any> {
resolve: {
data: Data;
};
}
export type PGGraphQLResult<Data = any> = [PGGraphQLResponse<Data>];
export interface PGGraphQLMutation<Record = IRecord> {
affectedRows: number;
records: Record[];
}

View File

@ -0,0 +1,21 @@
export interface Record {
id?: string;
[key: string]: any;
createdAt?: Date;
updatedAt?: Date;
}
export type RecordFilter = {
[Property in keyof Record]: any;
};
export enum OrderByDirection {
AscNullsFirst = 'AscNullsFirst',
AscNullsLast = 'AscNullsLast',
DescNullsFirst = 'DescNullsFirst',
DescNullsLast = 'DescNullsLast',
}
export type RecordOrderBy = {
[Property in keyof Record]: OrderByDirection;
};

View File

@ -0,0 +1,55 @@
import { GraphQLFieldResolver } from 'graphql';
import { resolverBuilderMethodNames } from 'src/tenant/resolver-builder/factories/factories';
import { Record, RecordFilter, RecordOrderBy } from './record.interface';
export type Resolver<Args = any> = GraphQLFieldResolver<any, any, Args>;
export interface FindManyResolverArgs<
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
> {
first?: number;
last?: number;
before?: string;
after?: string;
filter?: Filter;
orderBy?: OrderBy;
}
export interface FindOneResolverArgs<Filter = any> {
filter?: Filter;
}
export interface CreateOneResolverArgs<Data extends Record = Record> {
data: Data;
}
export interface CreateManyResolverArgs<Data extends Record = Record> {
data: Data[];
}
export interface UpdateOneResolverArgs<Data extends Record = Record> {
id: string;
data: Data;
}
export interface DeleteOneResolverArgs {
id: string;
}
export type ResolverBuilderQueryMethodNames =
(typeof resolverBuilderMethodNames.queries)[number];
export type ResolverBuilderMutationMethodNames =
(typeof resolverBuilderMethodNames.mutations)[number];
export type ResolverBuilderMethodNames =
| ResolverBuilderQueryMethodNames
| ResolverBuilderMutationMethodNames;
export interface ResolverBuilderMethods {
readonly queries: readonly ResolverBuilderQueryMethodNames[];
readonly mutations: readonly ResolverBuilderMutationMethodNames[];
}

View File

@ -1,13 +1,12 @@
import { GraphQLResolveInfo } from 'graphql';
import {
FieldMetadata,
FieldMetadataTargetColumnMap,
} from 'src/metadata/field-metadata/field-metadata.entity';
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import {
PGGraphQLQueryBuilder,
PGGraphQLQueryBuilderOptions,
} from 'src/tenant/entity-resolver/pg-graphql/pg-graphql-query-builder.util';
} from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-builder';
const testUUID = '123e4567-e89b-12d3-a456-426614174001';
@ -34,7 +33,7 @@ describe('PGGraphQLQueryBuilder', () => {
let mockOptions: PGGraphQLQueryBuilderOptions;
beforeEach(() => {
const fields = [
const fieldMetadataCollection = [
{
name: 'name',
targetColumnMap: {
@ -57,9 +56,9 @@ describe('PGGraphQLQueryBuilder', () => {
] as FieldMetadata[];
mockOptions = {
tableName: 'TestTable',
targetTableName: 'TestTable',
info: {} as GraphQLResolveInfo,
fields,
fieldMetadataCollection,
};
queryBuilder = new PGGraphQLQueryBuilder(mockOptions);

View File

@ -0,0 +1,149 @@
import { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields';
import { v4 as uuidv4 } from 'uuid';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
import {
CreateManyResolverArgs,
DeleteOneResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
UpdateOneResolverArgs,
} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import {
Record as IRecord,
RecordFilter,
RecordOrderBy,
} from 'src/tenant/resolver-builder/interfaces/record.interface';
import { stringifyWithoutKeyQuote } from 'src/tenant/resolver-builder/utils/stringify-without-key-quote.util';
import { convertFieldsToGraphQL } from 'src/tenant/resolver-builder/utils/convert-fields-to-graphql.util';
import { convertArguments } from 'src/tenant/resolver-builder/utils/convert-arguments.util';
import { generateArgsInput } from 'src/tenant/resolver-builder/utils/generate-args-input.util';
export interface PGGraphQLQueryBuilderOptions {
targetTableName: string;
info: GraphQLResolveInfo;
fieldMetadataCollection: FieldMetadataInterface[];
}
export class PGGraphQLQueryBuilder<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
> {
private options: PGGraphQLQueryBuilderOptions;
constructor(options: PGGraphQLQueryBuilderOptions) {
this.options = options;
}
private getFieldsString(): string {
const select = graphqlFields(this.options.info);
return convertFieldsToGraphQL(select, this.options.fieldMetadataCollection);
}
findMany(args?: FindManyResolverArgs<Filter, OrderBy>): string {
const { targetTableName } = this.options;
const fieldsString = this.getFieldsString();
const convertedArgs = convertArguments(
args,
this.options.fieldMetadataCollection,
);
const argsString = generateArgsInput(convertedArgs);
return `
query {
${targetTableName}Collection${argsString ? `(${argsString})` : ''} {
${fieldsString}
}
}
`;
}
findOne(args: FindOneResolverArgs<Filter>): string {
const { targetTableName } = this.options;
const fieldsString = this.getFieldsString();
const convertedArgs = convertArguments(
args,
this.options.fieldMetadataCollection,
);
const argsString = generateArgsInput(convertedArgs);
return `
query {
${targetTableName}Collection${argsString ? `(${argsString})` : ''} {
edges {
node {
${fieldsString}
}
}
}
}
`;
}
createMany(initialArgs: CreateManyResolverArgs<Record>): string {
const { targetTableName } = this.options;
const fieldsString = this.getFieldsString();
const args = convertArguments(
initialArgs,
this.options.fieldMetadataCollection,
);
return `
mutation {
insertInto${targetTableName}Collection(objects: ${stringifyWithoutKeyQuote(
args.data.map((datum) => ({
id: uuidv4(),
...datum,
})),
)}) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
updateOne(initialArgs: UpdateOneResolverArgs<Record>): string {
const { targetTableName } = this.options;
const fieldsString = this.getFieldsString();
const args = convertArguments(
initialArgs,
this.options.fieldMetadataCollection,
);
return `
mutation {
update${targetTableName}Collection(set: ${stringifyWithoutKeyQuote(
args.data,
)}, filter: { id: { eq: "${args.id}" } }) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
deleteOne(args: DeleteOneResolverArgs): string {
const { targetTableName } = this.options;
const fieldsString = this.getFieldsString();
return `
mutation {
deleteFrom${targetTableName}Collection(filter: { id: { eq: "${args.id}" } }) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

@ -0,0 +1,148 @@
import { BadRequestException } from '@nestjs/common';
import { GraphQLResolveInfo } from 'graphql';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
import {
CreateManyResolverArgs,
CreateOneResolverArgs,
DeleteOneResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
UpdateOneResolverArgs,
} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import {
Record as IRecord,
RecordFilter,
RecordOrderBy,
} from 'src/tenant/resolver-builder/interfaces/record.interface';
import { IConnection } from 'src/utils/pagination/interfaces/connection.interface';
import {
PGGraphQLMutation,
PGGraphQLResult,
} from 'src/tenant/resolver-builder/interfaces/pg-graphql.interface';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { parseResult } from 'src/tenant/resolver-builder/utils/parse-result.util';
import { PGGraphQLQueryBuilder } from './pg-graphql-query-builder';
interface QueryRunnerOptions {
targetTableName: string;
workspaceId: string;
info: GraphQLResolveInfo;
fieldMetadataCollection: FieldMetadataInterface[];
}
export class PGGraphQLQueryRunner<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
> {
private queryBuilder: PGGraphQLQueryBuilder;
private options: QueryRunnerOptions;
constructor(
private dataSourceService: DataSourceService,
options: QueryRunnerOptions,
) {
this.queryBuilder = new PGGraphQLQueryBuilder({
targetTableName: options.targetTableName,
info: options.info,
fieldMetadataCollection: options.fieldMetadataCollection,
});
this.options = options;
}
private async execute(
query: string,
workspaceId: string,
): Promise<PGGraphQLResult | undefined> {
const workspaceDataSource =
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
await workspaceDataSource?.query(`
SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)};
`);
return workspaceDataSource?.query<PGGraphQLResult>(`
SELECT graphql.resolve($$
${query}
$$);
`);
}
private parseResult<Result>(
graphqlResult: PGGraphQLResult | undefined,
command: string,
): Result {
const tableName = this.options.targetTableName;
const entityKey = `${command}${tableName}Collection`;
const result = graphqlResult?.[0]?.resolve?.data?.[entityKey];
if (!result) {
throw new BadRequestException('Malformed result from GraphQL query');
}
return parseResult(result);
}
async findMany(
args: FindManyResolverArgs<Filter, OrderBy>,
): Promise<IConnection<Record> | undefined> {
const query = this.queryBuilder.findMany(args);
const result = await this.execute(query, this.options.workspaceId);
return this.parseResult<IConnection<Record>>(result, '');
}
async findOne(
args: FindOneResolverArgs<Filter>,
): Promise<Record | undefined> {
if (!args.filter || Object.keys(args.filter).length === 0) {
throw new BadRequestException('Missing filter argument');
}
const query = this.queryBuilder.findOne(args);
const result = await this.execute(query, this.options.workspaceId);
const parsedResult = this.parseResult<IConnection<Record>>(result, '');
return parsedResult?.edges?.[0]?.node;
}
async createMany(
args: CreateManyResolverArgs<Record>,
): Promise<Record[] | undefined> {
const query = this.queryBuilder.createMany(args);
const result = await this.execute(query, this.options.workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(result, 'insertInto')
?.records;
}
async createOne(
args: CreateOneResolverArgs<Record>,
): Promise<Record | undefined> {
const records = await this.createMany({ data: [args.data] });
return records?.[0];
}
async updateOne(
args: UpdateOneResolverArgs<Record>,
): Promise<Record | undefined> {
const query = this.queryBuilder.updateOne(args);
const result = await this.execute(query, this.options.workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(result, 'update')
?.records?.[0];
}
async deleteOne(args: DeleteOneResolverArgs): Promise<Record | undefined> {
const query = this.queryBuilder.deleteOne(args);
const result = await this.execute(query, this.options.workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(result, 'deleteFrom')
?.records?.[0];
}
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { ResolverFactory } from './resolver.factory';
import { resolverBuilderFactories } from './factories/factories';
@Module({
imports: [DataSourceModule],
providers: [...resolverBuilderFactories, ResolverFactory],
exports: [ResolverFactory],
})
export class ResolverBuilderModule {}

View File

@ -0,0 +1,100 @@
import { Injectable, Logger } from '@nestjs/common';
import { IResolvers } from '@graphql-tools/utils';
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { getResolverName } from 'src/tenant/utils/get-resolver-name.util';
import { FindManyResolverFactory } from './factories/find-many-resolver.factory';
import { FindOneResolverFactory } from './factories/find-one-resolver.factory';
import { CreateManyResolverFactory } from './factories/create-many-resolver.factory';
import { CreateOneResolverFactory } from './factories/create-one-resolver.factory';
import { UpdateOneResolverFactory } from './factories/update-one-resolver.factory';
import { DeleteOneResolverFactory } from './factories/delete-one-resolver.factory';
import {
ResolverBuilderMethodNames,
ResolverBuilderMethods,
} from './interfaces/resolvers-builder.interface';
import { FactoryInterface } from './interfaces/factory.interface';
@Injectable()
export class ResolverFactory {
private readonly logger = new Logger(ResolverFactory.name);
constructor(
private readonly findManyResolverFactory: FindManyResolverFactory,
private readonly findOneResolverFactory: FindOneResolverFactory,
private readonly createManyResolverFactory: CreateManyResolverFactory,
private readonly createOneResolverFactory: CreateOneResolverFactory,
private readonly updateOneResolverFactory: UpdateOneResolverFactory,
private readonly deleteOneResolverFactory: DeleteOneResolverFactory,
) {}
async create(
workspaceId: string,
objectMetadataCollection: ObjectMetadataInterface[],
resolverBuilderMethods: ResolverBuilderMethods,
): Promise<IResolvers> {
const factories = new Map<ResolverBuilderMethodNames, FactoryInterface>([
['findMany', this.findManyResolverFactory],
['findOne', this.findOneResolverFactory],
['createMany', this.createManyResolverFactory],
['createOne', this.createOneResolverFactory],
['updateOne', this.updateOneResolverFactory],
['deleteOne', this.deleteOneResolverFactory],
]);
const resolvers: IResolvers = {
Query: {},
Mutation: {},
};
for (const objectMetadata of objectMetadataCollection) {
// Generate query resolvers
for (const methodName of resolverBuilderMethods.queries) {
const resolverName = getResolverName(objectMetadata, methodName);
const resolverFactory = factories.get(methodName);
if (!resolverFactory) {
this.logger.error(`Unknown query resolver type: ${methodName}`, {
objectMetadata,
methodName,
resolverName,
});
throw new Error(`Unknown query resolver type: ${methodName}`);
}
resolvers.Query[resolverName] = resolverFactory.create({
workspaceId,
targetTableName: objectMetadata.targetTableName,
fieldMetadataCollection: objectMetadata.fields,
});
}
// Generate mutation resolvers
for (const methodName of resolverBuilderMethods.mutations) {
const resolverName = getResolverName(objectMetadata, methodName);
const resolverFactory = factories.get(methodName);
if (!resolverFactory) {
this.logger.error(`Unknown mutation resolver type: ${methodName}`, {
objectMetadata,
methodName,
resolverName,
});
throw new Error(`Unknown mutation resolver type: ${methodName}`);
}
resolvers.Mutation[resolverName] = resolverFactory.create({
workspaceId,
targetTableName: objectMetadata.targetTableName,
fieldMetadataCollection: objectMetadata.fields,
});
}
}
return resolvers;
}
}

View File

@ -1,8 +1,10 @@
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
import {
FieldMetadata,
FieldMetadataTargetColumnMap,
FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';
import { convertArguments } from 'src/tenant/entity-resolver/utils/convert-arguments.util';
import { convertArguments } from 'src/tenant/resolver-builder/utils/convert-arguments.util';
describe('convertArguments', () => {
let fields;
@ -14,14 +16,14 @@ describe('convertArguments', () => {
targetColumnMap: {
value: 'column_1randomFirstNameKey',
} as FieldMetadataTargetColumnMap,
type: 'text',
type: FieldMetadataType.TEXT,
},
{
name: 'age',
targetColumnMap: {
value: 'column_randomAgeKey',
} as FieldMetadataTargetColumnMap,
type: 'text',
type: FieldMetadataType.TEXT,
},
{
name: 'website',
@ -29,7 +31,7 @@ describe('convertArguments', () => {
link: 'column_randomLinkKey',
text: 'column_randomTex7Key',
} as FieldMetadataTargetColumnMap,
type: 'url',
type: FieldMetadataType.URL,
},
] as FieldMetadata[];
});

View File

@ -1,8 +1,7 @@
import {
FieldMetadata,
FieldMetadataTargetColumnMap,
} from 'src/metadata/field-metadata/field-metadata.entity';
import { convertFieldsToGraphQL } from 'src/tenant/entity-resolver/utils/convert-fields-to-graphql.util';
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { convertFieldsToGraphQL } from 'src/tenant/resolver-builder/utils/convert-fields-to-graphql.util';
const normalizeWhitespace = (str) => str.replace(/\s+/g, ' ').trim();

View File

@ -1,4 +1,4 @@
import { generateArgsInput } from 'src/tenant/entity-resolver/utils/generate-args-input.util';
import { generateArgsInput } from 'src/tenant/resolver-builder/utils/generate-args-input.util';
const normalizeWhitespace = (str) => str.replace(/\s+/g, '');

View File

@ -2,7 +2,7 @@ import {
isSpecialKey,
handleSpecialKey,
parseResult,
} from 'src/tenant/entity-resolver/utils/parse-result.util';
} from 'src/tenant/resolver-builder/utils/parse-result.util';
describe('isSpecialKey', () => {
test('should return true if the key starts with "___"', () => {

View File

@ -1,4 +1,4 @@
import { stringifyWithoutKeyQuote } from 'src/tenant/entity-resolver/utils/stringify-without-key-quote.util';
import { stringifyWithoutKeyQuote } from 'src/tenant/resolver-builder/utils/stringify-without-key-quote.util';
describe('stringifyWithoutKeyQuote', () => {
test('should stringify object correctly without quotes around keys', () => {

View File

@ -1,6 +1,9 @@
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
export const convertArguments = (args: any, fields: FieldMetadata[]): any => {
export const convertArguments = (
args: any,
fields: FieldMetadataInterface[],
): any => {
const fieldsMap = new Map(
fields.map((metadata) => [metadata.name, metadata]),
);

View File

@ -1,10 +1,10 @@
import isEmpty from 'lodash.isempty';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
export const convertFieldsToGraphQL = (
select: any,
fields: FieldMetadata[],
fields: FieldMetadataInterface[],
acc = '',
) => {
const fieldsMap = new Map(

View File

@ -0,0 +1,98 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigArgumentMap } from 'graphql';
import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ArgsMetadata } from 'src/tenant/schema-builder/interfaces/param-metadata.interface';
import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage';
import { TypeMapperService } from 'src/tenant/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: BuildSchemaOptions,
): 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, {
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 { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/tenant/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: BuildSchemaOptions,
): 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: BuildSchemaOptions,
): 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 { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/tenant/schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage';
import { PageInfoType } from 'src/tenant/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: BuildSchemaOptions,
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 { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/tenant/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: BuildSchemaOptions,
): 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: BuildSchemaOptions,
): 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 { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/tenant/schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage';
import { CursorScalarType } from 'src/tenant/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: BuildSchemaOptions,
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,35 @@
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';
export const schemaBuilderFactories = [
ArgsFactory,
InputTypeFactory,
InputTypeDefinitionFactory,
OutputTypeFactory,
ObjectTypeDefinitionFactory,
FilterTypeFactory,
FilterTypeDefinitionFactory,
OrderByTypeFactory,
OrderByTypeDefinitionFactory,
ConnectionTypeFactory,
ConnectionTypeDefinitionFactory,
EdgeTypeFactory,
EdgeTypeDefinitionFactory,
RootTypeFactory,
QueryTypeFactory,
MutationTypeFactory,
];

View File

@ -0,0 +1,85 @@
import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { TypeMapperService } from 'src/tenant/schema-builder/services/type-mapper.service';
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: BuildSchemaOptions,
): 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: BuildSchemaOptions,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
objectMetadata.fields.forEach((fieldMetadata: FieldMetadata) => {
const type = this.filterTypeFactory.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,60 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLInputType } from 'graphql';
import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/tenant/schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/tenant/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: BuildSchemaOptions,
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,70 @@
import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
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: BuildSchemaOptions,
): 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: BuildSchemaOptions,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
objectMetadata.fields.forEach((fieldMetadata: FieldMetadata) => {
const type = this.inputTypeFactory.create(fieldMetadata, kind, options, {
nullable: fieldMetadata.isNullable,
});
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 { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/tenant/schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/tenant/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: BuildSchemaOptions,
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 { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ResolverBuilderMutationMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { ObjectMetadataInterface } from 'src/tenant/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[],
resolverMethodNames: ResolverBuilderMutationMethodNames[],
options: BuildSchemaOptions,
): GraphQLObjectType {
return this.rootTypeFactory.create(
objectMetadataCollection,
resolverMethodNames,
ObjectTypeName.Mutation,
options,
);
}
}

View File

@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { pascalCase } from 'src/utils/pascal-case';
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: BuildSchemaOptions,
): 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: BuildSchemaOptions,
): GraphQLFieldConfigMap<any, any> {
const fields: GraphQLFieldConfigMap<any, any> = {};
objectMetadata.fields.forEach((fieldMetadata: FieldMetadata) => {
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,61 @@
import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
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: BuildSchemaOptions,
): 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: BuildSchemaOptions,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
objectMetadata.fields.forEach((fieldMetadata: FieldMetadata) => {
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 { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/tenant/schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/tenant/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: BuildSchemaOptions,
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,62 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql';
import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/tenant/schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/tenant/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: BuildSchemaOptions,
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 { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ResolverBuilderQueryMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { ObjectMetadataInterface } from 'src/tenant/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[],
resolverMethodNames: ResolverBuilderQueryMethodNames[],
options: BuildSchemaOptions,
): GraphQLObjectType {
return this.rootTypeFactory.create(
objectMetadataCollection,
resolverMethodNames,
ObjectTypeName.Query,
options,
);
}
}

View File

@ -0,0 +1,112 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { ResolverBuilderMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage';
import { getResolverName } from 'src/tenant/utils/get-resolver-name.util';
import { getResolverArgs } from 'src/tenant/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[],
resolverMethodNames: ResolverBuilderMethodNames[],
objectTypeName: ObjectTypeName,
options: BuildSchemaOptions,
): GraphQLObjectType {
if (resolverMethodNames.length === 0) {
this.logger.error(
`No resolver methods were found for ${objectTypeName.toString()}`,
{
resolverMethodNames,
objectTypeName,
options,
},
);
throw new Error(
`No resolvers were found for ${objectTypeName.toString()}`,
);
}
return new GraphQLObjectType({
name: objectTypeName.toString(),
fields: this.generateFields(
objectMetadataCollection,
resolverMethodNames,
options,
),
});
}
private generateFields<T = any, U = any>(
objectMetadataCollection: ObjectMetadataInterface[],
resolverMethodNames: ResolverBuilderMethodNames[],
options: BuildSchemaOptions,
): GraphQLFieldConfigMap<T, U> {
const fieldConfigMap: GraphQLFieldConfigMap<T, U> = {};
for (const objectMetadata of objectMetadataCollection) {
for (const methodName of resolverMethodNames) {
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,51 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLSchema } from 'graphql';
import { ResolverBuilderMethods } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface';
import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service';
import { TypeDefinitionsGenerator } from './type-definitions.generator';
import { BuildSchemaOptions } from './interfaces/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';
@Injectable()
export class GraphQLSchemaFactory {
private readonly logger = new Logger(GraphQLSchemaFactory.name);
constructor(
private readonly objectMetadataService: ObjectMetadataService,
private readonly typeDefinitionsGenerator: TypeDefinitionsGenerator,
private readonly queryTypeFactory: QueryTypeFactory,
private readonly mutationTypeFactory: MutationTypeFactory,
) {}
async create(
objectMetadataCollection: ObjectMetadataInterface[],
resolverBuilderMethods: ResolverBuilderMethods,
options: BuildSchemaOptions = {},
): Promise<GraphQLSchema> {
// Generate type definitions
this.typeDefinitionsGenerator.generate(objectMetadataCollection, options);
// Generate schema
const schema = new GraphQLSchema({
query: this.queryTypeFactory.create(
objectMetadataCollection,
[...resolverBuilderMethods.queries],
options,
),
mutation: this.mutationTypeFactory.create(
objectMetadataCollection,
[...resolverBuilderMethods.mutations],
options,
),
});
return schema;
}
}

View File

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

View File

@ -5,10 +5,8 @@ import {
GraphQLFloat,
} from 'graphql';
import { FilterIsEnumType } from './filter-is-enum-filter.type';
export const BigFloatFilterType = new GraphQLInputObjectType({
name: 'BigFloatType',
name: 'BigFloatFilter',
fields: {
eq: { type: GraphQLFloat },
gt: { type: GraphQLFloat },
@ -17,6 +15,5 @@ export const BigFloatFilterType = new GraphQLInputObjectType({
lt: { type: GraphQLFloat },
lte: { type: GraphQLFloat },
neq: { type: GraphQLFloat },
is: { type: FilterIsEnumType },
},
});

View File

@ -5,8 +5,6 @@ import {
GraphQLInt,
} from 'graphql';
import { FilterIsEnumType } from './filter-is-enum-filter.type';
export const BigIntFilterType = new GraphQLInputObjectType({
name: 'BigIntFilter',
fields: {
@ -17,6 +15,5 @@ export const BigIntFilterType = new GraphQLInputObjectType({
lt: { type: GraphQLInt },
lte: { type: GraphQLInt },
neq: { type: GraphQLInt },
is: { type: FilterIsEnumType },
},
});

View File

@ -1,8 +1,6 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
import { DateScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date.scalar';
import { FilterIsEnumType } from './filter-is-enum-filter.type';
import { DateScalarType } from 'src/tenant/schema-builder/graphql-types/scalars';
export const DateFilterType = new GraphQLInputObjectType({
name: 'DateFilter',
@ -14,6 +12,5 @@ export const DateFilterType = new GraphQLInputObjectType({
lt: { type: DateScalarType },
lte: { type: DateScalarType },
neq: { type: DateScalarType },
is: { type: FilterIsEnumType },
},
});

View File

@ -1,8 +1,6 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date-time.scalar';
import { FilterIsEnumType } from './filter-is-enum-filter.type';
import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars';
export const DatetimeFilterType = new GraphQLInputObjectType({
name: 'DateTimeFilter',
@ -14,6 +12,5 @@ export const DatetimeFilterType = new GraphQLInputObjectType({
lt: { type: DateTimeScalarType },
lte: { type: DateTimeScalarType },
neq: { type: DateTimeScalarType },
is: { type: FilterIsEnumType },
},
});

View File

@ -1,9 +0,0 @@
import { GraphQLEnumType } from 'graphql';
export const FilterIsEnumType = new GraphQLEnumType({
name: 'FilterIs',
values: {
PENDING: { value: 'PENDING' },
RELEASED: { value: 'RELEASED' },
},
});

View File

@ -5,9 +5,7 @@ import {
GraphQLNonNull,
} from 'graphql';
import { FilterIsEnumType } from './filter-is-enum-filter.type';
export const FloatFilter = new GraphQLInputObjectType({
export const FloatFilterType = new GraphQLInputObjectType({
name: 'FloatFilter',
fields: {
eq: { type: GraphQLFloat },
@ -17,6 +15,5 @@ export const FloatFilter = new GraphQLInputObjectType({
lt: { type: GraphQLFloat },
lte: { type: GraphQLFloat },
neq: { type: GraphQLFloat },
is: { type: FilterIsEnumType },
},
});

View File

@ -0,0 +1,9 @@
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';

View File

@ -5,9 +5,7 @@ import {
GraphQLInt,
} from 'graphql';
import { FilterIsEnumType } from './filter-is-enum-filter.type';
export const IntFilter = new GraphQLInputObjectType({
export const IntFilterType = new GraphQLInputObjectType({
name: 'IntFilter',
fields: {
eq: { type: GraphQLInt },
@ -17,6 +15,5 @@ export const IntFilter = new GraphQLInputObjectType({
lt: { type: GraphQLInt },
lte: { type: GraphQLInt },
neq: { type: GraphQLInt },
is: { type: FilterIsEnumType },
},
});

View File

@ -1,12 +0,0 @@
import { GraphQLInputObjectType } from 'graphql';
import { StringFilterType } from 'src/tenant/schema-builder/graphql-types/input/string-filter.type';
import { IntFilter } from 'src/tenant/schema-builder/graphql-types/input/int-filter.type';
export const MoneyFilterType = new GraphQLInputObjectType({
name: 'MoneyFilter',
fields: {
amount: { type: IntFilter },
currency: { type: StringFilterType },
},
});

View File

@ -5,8 +5,6 @@ import {
GraphQLString,
} from 'graphql';
import { FilterIsEnumType } from './filter-is-enum-filter.type';
export const StringFilterType = new GraphQLInputObjectType({
name: 'StringFilter',
fields: {
@ -17,7 +15,6 @@ export const StringFilterType = new GraphQLInputObjectType({
lt: { type: GraphQLString },
lte: { type: GraphQLString },
neq: { type: GraphQLString },
is: { type: FilterIsEnumType },
startsWith: { type: GraphQLString },
like: { type: GraphQLString },
ilike: { type: GraphQLString },

View File

@ -1,10 +1,8 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
import { TimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/time.scalar';
import { TimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars';
import { FilterIsEnumType } from './filter-is-enum-filter.type';
export const TimeFilter = new GraphQLInputObjectType({
export const TimeFilterType = new GraphQLInputObjectType({
name: 'TimeFilter',
fields: {
eq: { type: TimeScalarType },
@ -14,6 +12,5 @@ export const TimeFilter = new GraphQLInputObjectType({
lt: { type: TimeScalarType },
lte: { type: TimeScalarType },
neq: { type: TimeScalarType },
is: { type: FilterIsEnumType },
},
});

View File

@ -1,11 +0,0 @@
import { GraphQLInputObjectType } from 'graphql';
import { StringFilterType } from 'src/tenant/schema-builder/graphql-types/input/string-filter.type';
export const UrlFilterType = new GraphQLInputObjectType({
name: 'UrlFilter',
fields: {
text: { type: StringFilterType },
link: { type: StringFilterType },
},
});

View File

@ -1,8 +1,6 @@
import { GraphQLInputObjectType, GraphQLList } from 'graphql';
import { UUIDScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/uuid.scalar';
import { FilterIsEnumType } from './filter-is-enum-filter.type';
import { UUIDScalarType } from 'src/tenant/schema-builder/graphql-types/scalars';
export const UUIDFilterType = new GraphQLInputObjectType({
name: 'UUIDFilter',
@ -10,6 +8,5 @@ export const UUIDFilterType = new GraphQLInputObjectType({
eq: { type: UUIDScalarType },
in: { type: new GraphQLList(UUIDScalarType) },
neq: { type: UUIDScalarType },
is: { type: FilterIsEnumType },
},
});

View File

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

View File

@ -6,6 +6,14 @@ 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,

View File

@ -0,0 +1,16 @@
export type DateScalarMode = 'isoDate' | 'timestamp';
export type NumberScalarMode = 'float' | 'integer';
export interface BuildSchemaOptions {
/**
* Date scalar mode
* @default 'isoDate'
*/
dateScalarMode?: DateScalarMode;
/**
* Number scalar mode
* @default 'float'
*/
numberScalarMode?: NumberScalarMode;
}

View File

@ -0,0 +1,15 @@
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export interface FieldMetadataInterface<
T extends FieldMetadataType | 'default' = 'default',
> {
id: string;
type: FieldMetadataType;
name: string;
label: string;
targetColumnMap: FieldMetadataTargetColumnMap<T>;
description?: string;
isNullable?: boolean;
}

View File

@ -0,0 +1,12 @@
import { FieldMetadataInterface } from './field-metadata.interface';
export interface ObjectMetadataInterface {
id: string;
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string;
targetTableName: string;
fields: FieldMetadataInterface[];
}

View File

@ -0,0 +1,18 @@
import { InputTypeDefinitionKind } from 'src/tenant/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 {
kind?: InputTypeDefinitionKind;
type?: FieldMetadataType;
isNullable?: boolean;
isArray?: boolean;
}
export interface ArgsMetadata {
args: {
[key: string]: ArgMetadata;
};
objectMetadata: ObjectMetadataInterface;
}

View File

@ -1,7 +1,7 @@
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { FieldMetadataInterface } from './field-metadata.interface';
export interface SchemaBuilderContext {
tableName: string;
workspaceId: string;
fields: FieldMetadata[];
targetTableName: string;
fieldMetadataCollection: FieldMetadataInterface[];
}

View File

@ -0,0 +1,28 @@
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const moneyObjectDefinition = {
id: FieldMetadataType.MONEY.toString(),
nameSingular: 'Money',
namePlural: 'Money',
labelSingular: 'Money',
labelPlural: 'Money',
targetTableName: 'money',
fields: [
{
id: 'amount',
type: FieldMetadataType.NUMBER,
name: 'amount',
label: 'Amount',
targetColumnMap: { value: 'amount' },
},
{
id: 'currency',
type: FieldMetadataType.TEXT,
name: 'currency',
label: 'Currency',
targetColumnMap: { value: 'currency' },
},
],
} as ObjectMetadataInterface;

View File

@ -0,0 +1,28 @@
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const urlObjectDefinition = {
id: FieldMetadataType.URL.toString(),
nameSingular: 'Url',
namePlural: 'Url',
labelSingular: 'Url',
labelPlural: 'Url',
targetTableName: 'url',
fields: [
{
id: 'text',
type: FieldMetadataType.TEXT,
name: 'text',
label: 'Text',
targetColumnMap: { value: 'text' },
},
{
id: 'link',
type: FieldMetadataType.TEXT,
name: 'link',
label: 'Link',
targetColumnMap: { value: 'link' },
},
],
} as ObjectMetadataInterface;

View File

@ -1,13 +1,25 @@
import { Module } from '@nestjs/common';
import { EntityResolverModule } from 'src/tenant/entity-resolver/entity-resolver.module';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
import { SchemaBuilderService } from './schema-builder.service';
import { TypeDefinitionsGenerator } from './type-definitions.generator';
import { GraphQLSchemaFactory } from './graphql-schema.factory';
import { schemaBuilderFactories } from './factories/factories';
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
import { TypeMapperService } from './services/type-mapper.service';
@Module({
imports: [EntityResolverModule],
providers: [SchemaBuilderService, JwtAuthGuard],
exports: [SchemaBuilderService],
imports: [ObjectMetadataModule],
providers: [
...schemaBuilderFactories,
TypeDefinitionsGenerator,
TypeDefinitionsStorage,
TypeMapperService,
GraphQLSchemaFactory,
JwtAuthGuard,
],
exports: [GraphQLSchemaFactory],
})
export class SchemaBuilderModule {}

View File

@ -1,27 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EntityResolverService } from 'src/tenant/entity-resolver/entity-resolver.service';
import { SchemaBuilderService } from './schema-builder.service';
describe('SchemaBuilderService', () => {
let service: SchemaBuilderService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SchemaBuilderService,
{
provide: EntityResolverService,
useValue: {},
},
],
}).compile();
service = module.get<SchemaBuilderService>(SchemaBuilderService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,252 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
GraphQLFieldConfigMap,
GraphQLID,
GraphQLInputObjectType,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
} from 'graphql';
import upperFirst from 'lodash.upperfirst';
import { EntityResolverService } from 'src/tenant/entity-resolver/entity-resolver.service';
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
import { generateEdgeType } from './utils/generate-edge-type.util';
import { generateConnectionType } from './utils/generate-connection-type.util';
import { generateObjectType } from './utils/generate-object-type.util';
import { generateCreateInputType } from './utils/generate-create-input-type.util';
import { generateUpdateInputType } from './utils/generate-update-input-type.util';
import { SchemaBuilderContext } from './interfaces/schema-builder-context.interface';
import { cleanEntityName } from './utils/clean-entity-name.util';
import { scalars } from './graphql-types/scalars';
import { CursorScalarType } from './graphql-types/scalars/cursor.scalar';
import { generateFilterInputType } from './utils/generate-filter-input-type.util';
import { generateOrderByInputType } from './utils/generate-order-by-input-type.util';
@Injectable()
export class SchemaBuilderService {
workspaceId: string;
constructor(private readonly entityResolverService: EntityResolverService) {}
private generateQueryFieldForEntity(
entityName: {
singular: string;
plural: string;
},
tableName: string,
ObjectType: GraphQLObjectType,
objectDefinition: ObjectMetadata,
) {
const schemaBuilderContext: SchemaBuilderContext = {
tableName,
workspaceId: this.workspaceId,
fields: objectDefinition.fields,
};
const EdgeType = generateEdgeType(ObjectType);
const ConnectionType = generateConnectionType(EdgeType);
const FilterInputType = generateFilterInputType(
entityName.singular,
objectDefinition.fields,
);
const OrderByInputType = generateOrderByInputType(
entityName.singular,
objectDefinition.fields,
);
return {
[`${entityName.plural}`]: {
type: ConnectionType,
args: {
first: { type: GraphQLInt },
last: { type: GraphQLInt },
before: { type: CursorScalarType },
after: { type: CursorScalarType },
filter: { type: FilterInputType },
orderBy: { type: OrderByInputType },
},
resolve: async (root, args, context, info) => {
return this.entityResolverService.findMany(
args,
schemaBuilderContext,
info,
);
},
},
[`${entityName.singular}`]: {
type: ObjectType,
args: {
filter: { type: FilterInputType },
},
resolve: (root, args, context, info) => {
return this.entityResolverService.findOne(
args,
schemaBuilderContext,
info,
);
},
},
} as GraphQLFieldConfigMap<any, any>;
}
private generateMutationFieldForEntity(
entityName: {
singular: string;
plural: string;
},
tableName: string,
ObjectType: GraphQLObjectType,
CreateInputType: GraphQLInputObjectType,
UpdateInputType: GraphQLInputObjectType,
objectDefinition: ObjectMetadata,
) {
const schemaBuilderContext: SchemaBuilderContext = {
tableName,
workspaceId: this.workspaceId,
fields: objectDefinition.fields,
};
return {
[`createOne${upperFirst(entityName.singular)}`]: {
type: new GraphQLNonNull(ObjectType),
args: {
data: { type: new GraphQLNonNull(CreateInputType) },
},
resolve: (root, args, context, info) => {
return this.entityResolverService.createOne(
args,
schemaBuilderContext,
info,
);
},
},
[`createMany${upperFirst(entityName.singular)}`]: {
type: new GraphQLList(ObjectType),
args: {
data: {
type: new GraphQLNonNull(
new GraphQLList(new GraphQLNonNull(CreateInputType)),
),
},
},
resolve: (root, args, context, info) => {
return this.entityResolverService.createMany(
args,
schemaBuilderContext,
info,
);
},
},
[`updateOne${upperFirst(entityName.singular)}`]: {
type: new GraphQLNonNull(ObjectType),
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
data: { type: new GraphQLNonNull(UpdateInputType) },
},
resolve: (root, args, context, info) => {
return this.entityResolverService.updateOne(
args,
schemaBuilderContext,
info,
);
},
},
[`deleteOne${upperFirst(entityName.singular)}`]: {
type: new GraphQLNonNull(ObjectType),
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
},
resolve: (root, args, context, info) => {
return this.entityResolverService.deleteOne(
args,
schemaBuilderContext,
info,
);
},
},
} as GraphQLFieldConfigMap<any, any>;
}
private generateQueryAndMutationTypes(objectMetadata: ObjectMetadata[]): {
query: GraphQLObjectType;
mutation: GraphQLObjectType;
} {
const queryFields: any = {};
const mutationFields: any = {};
for (const objectDefinition of objectMetadata) {
const entityName = {
singular: cleanEntityName(objectDefinition.nameSingular),
plural: cleanEntityName(objectDefinition.namePlural),
};
const tableName = objectDefinition?.targetTableName ?? '';
const ObjectType = generateObjectType(
entityName.singular,
objectDefinition.fields,
);
const CreateInputType = generateCreateInputType(
entityName.singular,
objectDefinition.fields,
);
const UpdateInputType = generateUpdateInputType(
entityName.singular,
objectDefinition.fields,
);
Object.assign(
queryFields,
this.generateQueryFieldForEntity(
entityName,
tableName,
ObjectType,
objectDefinition,
),
);
Object.assign(
mutationFields,
this.generateMutationFieldForEntity(
entityName,
tableName,
ObjectType,
CreateInputType,
UpdateInputType,
objectDefinition,
),
);
}
return {
query: new GraphQLObjectType({
name: 'Query',
fields: queryFields,
}),
mutation: new GraphQLObjectType({
name: 'Mutation',
fields: mutationFields,
}),
};
}
async generateSchema(
workspaceId: string,
objectMetadata: ObjectMetadata[],
): Promise<GraphQLSchema> {
this.workspaceId = workspaceId;
const { query, mutation } =
this.generateQueryAndMutationTypes(objectMetadata);
return new GraphQLSchema({
query,
mutation,
types: [...scalars],
});
}
}

View File

@ -0,0 +1,152 @@
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/tenant/schema-builder/interfaces/build-schema-optionts.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
UUIDFilterType,
StringFilterType,
DatetimeFilterType,
DateFilterType,
FloatFilterType,
IntFilterType,
} from 'src/tenant/schema-builder/graphql-types/input';
import { OrderByDirectionType } from 'src/tenant/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;
// URL and MONEY 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],
]);
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;
// URL and MONEY 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, GraphQLBoolean],
[FieldMetadataType.NUMBER, numberScalar],
]);
return typeFilterMapping.get(fieldMetadataType);
}
mapToOrderByType(
fieldMetadataType: FieldMetadataType,
): GraphQLInputType | undefined {
// URL and MONEY 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],
]);
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) {
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,76 @@
import { Injectable } from '@nestjs/common';
import { GraphQLInputObjectType, GraphQLObjectType } from 'graphql';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
InputTypeDefinition,
InputTypeDefinitionKind,
} from 'src/tenant/schema-builder/factories/input-type-definition.factory';
import {
ObjectTypeDefinition,
ObjectTypeDefinitionKind,
} from 'src/tenant/schema-builder/factories/object-type-definition.factory';
@Injectable()
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,208 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { customTableDefaultColumns } from 'src/metadata/migration-runner/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 { BuildSchemaOptions } from './interfaces/build-schema-optionts.interface';
import { moneyObjectDefinition } from './object-definitions/money.object-definition';
import { urlObjectDefinition } from './object-definitions/url.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';
// 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 FieldMetadata;
});
@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,
) {}
generate(
objectMetadataCollection: ObjectMetadataInterface[],
options: BuildSchemaOptions,
) {
// 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: BuildSchemaOptions) {
const staticObjectMetadataCollection = [
moneyObjectDefinition,
urlObjectDefinition,
];
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: BuildSchemaOptions,
) {
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);
}
private generateObjectTypeDefs(
objectMetadataCollection: ObjectMetadataInterface[],
options: BuildSchemaOptions,
) {
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: BuildSchemaOptions,
) {
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: BuildSchemaOptions,
) {
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 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

@ -1,52 +0,0 @@
import {
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLString,
} from 'graphql';
import { PageInfoType } from 'src/tenant/schema-builder/graphql-types/object/page-into.type';
import { generateConnectionType } from 'src/tenant/schema-builder/utils/generate-connection-type.util';
describe('generateConnectionType', () => {
// Create a mock EdgeType for testing
const mockEdgeType = new GraphQLObjectType({
name: 'MockEdge',
fields: {
node: { type: GraphQLString },
cursor: { type: GraphQLString },
},
});
// Generate a connection type using the mock
const MockConnectionType = generateConnectionType(mockEdgeType);
test('should generate a GraphQLObjectType', () => {
expect(MockConnectionType).toBeInstanceOf(GraphQLObjectType);
});
test('should generate a type with the correct name', () => {
expect(MockConnectionType.name).toBe('MockConnection');
});
test('should include the correct fields', () => {
const fields = MockConnectionType.getFields();
expect(fields).toHaveProperty('edges');
if (
fields.edges.type instanceof GraphQLList ||
fields.edges.type instanceof GraphQLNonNull
) {
expect(fields.edges.type.ofType).toBe(mockEdgeType);
} else {
fail('edges.type is not an instance of GraphQLList or GraphQLNonNull');
}
expect(fields).toHaveProperty('pageInfo');
if (fields.pageInfo.type instanceof GraphQLNonNull) {
expect(fields.pageInfo.type.ofType).toBe(PageInfoType);
} else {
fail('pageInfo.type is not an instance of GraphQLNonNull');
}
});
});

View File

@ -1,56 +0,0 @@
import {
GraphQLID,
GraphQLInputObjectType,
GraphQLInt,
GraphQLNonNull,
GraphQLString,
} from 'graphql';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { generateCreateInputType } from 'src/tenant/schema-builder/utils/generate-create-input-type.util';
describe('generateCreateInputType', () => {
test('should generate a GraphQLInputObjectType with correct name', () => {
const columns = [];
const name = 'testType';
const inputType = generateCreateInputType(name, columns);
expect(inputType).toBeInstanceOf(GraphQLInputObjectType);
expect(inputType.name).toBe('TestTypeCreateInput');
});
test('should include default id field', () => {
const columns = [];
const name = 'testType';
const inputType = generateCreateInputType(name, columns);
const fields = inputType.getFields();
expect(fields.id).toBeDefined();
expect(fields.id.type).toBe(GraphQLID);
});
test('should generate fields with correct types and descriptions', () => {
const columns = [
{
name: 'firstName',
type: 'text',
isNullable: false,
},
{
name: 'age',
type: 'number',
isNullable: true,
},
] as FieldMetadata[];
const name = 'testType';
const inputType = generateCreateInputType(name, columns);
const fields = inputType.getFields();
if (fields.firstName.type instanceof GraphQLNonNull) {
expect(fields.firstName.type.ofType).toBe(GraphQLString);
} else {
fail('firstName type is not an instance of GraphQLNonNull');
}
expect(fields.age.type).toBe(GraphQLInt);
});
});

View File

@ -1,38 +0,0 @@
import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql';
import { generateEdgeType } from 'src/tenant/schema-builder/utils/generate-edge-type.util';
describe('generateEdgeType', () => {
// Mock GraphQLObjectType for testing
const mockObjectType = new GraphQLObjectType({
name: 'MockItem',
fields: {
sampleField: { type: GraphQLString },
},
});
test('should generate a GraphQLObjectType', () => {
const edgeType = generateEdgeType(mockObjectType);
expect(edgeType).toBeInstanceOf(GraphQLObjectType);
});
test('should generate a type with the correct name', () => {
const edgeType = generateEdgeType(mockObjectType);
expect(edgeType.name).toBe('MockItemEdge');
});
test('should have a "node" field of the provided ObjectType', () => {
const edgeType = generateEdgeType(mockObjectType);
const fields = edgeType.getFields();
expect(fields.node.type).toBe(mockObjectType);
});
test('should have a "cursor" field of type GraphQLNonNull(GraphQLString)', () => {
const edgeType = generateEdgeType(mockObjectType);
const fields = edgeType.getFields();
expect(fields.cursor.type).toBeInstanceOf(GraphQLNonNull);
if (fields.cursor.type instanceof GraphQLNonNull) {
expect(fields.cursor.type.ofType).toBe(GraphQLString);
}
});
});

View File

@ -1,53 +0,0 @@
import { GraphQLList, GraphQLNonNull, GraphQLInputObjectType } from 'graphql';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { generateFilterInputType } from 'src/tenant/schema-builder/utils/generate-filter-input-type.util';
import { mapColumnTypeToFilterType } from 'src/tenant/schema-builder/utils/map-column-type-to-filter-type.util';
describe('generateFilterInputType', () => {
it('handles empty columns array', () => {
const FilterInputType = generateFilterInputType('EmptyTest', []);
expect(FilterInputType.name).toBe('EmptyTestFilterInput');
expect(FilterInputType.getFields()).toHaveProperty('id');
expect(FilterInputType.getFields()).toHaveProperty('createdAt');
expect(FilterInputType.getFields()).toHaveProperty('updatedAt');
expect(FilterInputType.getFields()).toHaveProperty('and');
expect(FilterInputType.getFields()).toHaveProperty('or');
expect(FilterInputType.getFields()).toHaveProperty('not');
});
it('handles various column types', () => {
const columns = [
{ name: 'stringField', type: 'text' },
{ name: 'intField', type: 'number' },
{ name: 'booleanField', type: 'boolean' },
] as FieldMetadata[];
const FilterInputType = generateFilterInputType('MultiTypeTest', columns);
columns.forEach((column) => {
const expectedType = mapColumnTypeToFilterType(column);
expect(FilterInputType.getFields()[column.name].type).toBe(expectedType);
});
});
it('handles nested logical fields', () => {
const FilterInputType = generateFilterInputType('NestedTest', []);
const andFieldType = FilterInputType.getFields().and.type;
const orFieldType = FilterInputType.getFields().or.type;
const notFieldType = FilterInputType.getFields().not.type;
expect(andFieldType).toBeInstanceOf(GraphQLList);
expect(orFieldType).toBeInstanceOf(GraphQLList);
if (notFieldType instanceof GraphQLNonNull) {
expect(notFieldType.ofType).toBe(FilterInputType);
} else {
expect(notFieldType).toBeInstanceOf(GraphQLInputObjectType);
}
});
});

View File

@ -1,73 +0,0 @@
import {
GraphQLID,
GraphQLInt,
GraphQLNonNull,
GraphQLObjectType,
GraphQLString,
} from 'graphql';
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
import { generateObjectType } from 'src/tenant/schema-builder/utils/generate-object-type.util';
import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date-time.scalar';
describe('generateObjectType', () => {
test('should generate a GraphQLObjectType with correct name', () => {
const columns = [];
const name = 'testType';
const objectType = generateObjectType(name, columns);
expect(objectType).toBeInstanceOf(GraphQLObjectType);
expect(objectType.name).toBe('TestType');
});
test('should include default fields', () => {
const columns = [];
const name = 'testType';
const objectType = generateObjectType(name, columns);
const fields = objectType.getFields();
if (fields.id.type instanceof GraphQLNonNull) {
expect(fields.id.type.ofType).toBe(GraphQLID);
} else {
fail('id.type is not an instance of GraphQLNonNull');
}
if (fields.createdAt.type instanceof GraphQLNonNull) {
expect(fields.createdAt.type.ofType).toBe(DateTimeScalarType);
} else {
fail('createdAt.type is not an instance of GraphQLNonNull');
}
if (fields.updatedAt.type instanceof GraphQLNonNull) {
expect(fields.updatedAt.type.ofType).toBe(DateTimeScalarType);
} else {
fail('updatedAt.type is not an instance of GraphQLNonNull');
}
});
test('should generate fields based on provided columns', () => {
const columns = [
{
name: 'firstName',
type: 'text',
isNullable: false,
},
{
name: 'age',
type: 'number',
isNullable: true,
},
] as FieldMetadata[];
const name = 'testType';
const objectType = generateObjectType(name, columns);
const fields = objectType.getFields();
if (fields.firstName.type instanceof GraphQLNonNull) {
expect(fields.firstName.type.ofType).toBe(GraphQLString);
} else {
fail('firstName.type is not an instance of GraphQLNonNull');
}
expect(fields.age.type).toBe(GraphQLInt);
});
});

Some files were not shown because too many files have changed in this diff Show More