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:
@ -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: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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>;
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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 {}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
28
server/src/tenant/resolver-builder/factories/factories.ts
Normal file
28
server/src/tenant/resolver-builder/factories/factories.ts
Normal 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;
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -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[];
|
||||
}
|
||||
@ -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);
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
100
server/src/tenant/resolver-builder/resolver.factory.ts
Normal file
100
server/src/tenant/resolver-builder/resolver.factory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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[];
|
||||
});
|
||||
@ -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();
|
||||
|
||||
@ -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, '');
|
||||
|
||||
@ -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 "___"', () => {
|
||||
@ -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', () => {
|
||||
@ -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]),
|
||||
);
|
||||
@ -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(
|
||||
98
server/src/tenant/schema-builder/factories/args.factory.ts
Normal file
98
server/src/tenant/schema-builder/factories/args.factory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
35
server/src/tenant/schema-builder/factories/factories.ts
Normal file
35
server/src/tenant/schema-builder/factories/factories.ts
Normal 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,
|
||||
];
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
112
server/src/tenant/schema-builder/factories/root-type.factory.ts
Normal file
112
server/src/tenant/schema-builder/factories/root-type.factory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
51
server/src/tenant/schema-builder/graphql-schema.factory.ts
Normal file
51
server/src/tenant/schema-builder/graphql-schema.factory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './order-by-direction.enum-type';
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -1,9 +0,0 @@
|
||||
import { GraphQLEnumType } from 'graphql';
|
||||
|
||||
export const FilterIsEnumType = new GraphQLEnumType({
|
||||
name: 'FilterIs',
|
||||
values: {
|
||||
PENDING: { value: 'PENDING' },
|
||||
RELEASED: { value: 'RELEASED' },
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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';
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export * from './page-into.object-type';
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 {}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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],
|
||||
});
|
||||
}
|
||||
}
|
||||
152
server/src/tenant/schema-builder/services/type-mapper.service.ts
Normal file
152
server/src/tenant/schema-builder/services/type-mapper.service.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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()}`;
|
||||
}
|
||||
}
|
||||
208
server/src/tenant/schema-builder/type-definitions.generator.ts
Normal file
208
server/src/tenant/schema-builder/type-definitions.generator.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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
Reference in New Issue
Block a user