Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,53 @@
import { Injectable } from '@nestjs/common';
import { GraphQLScalarType, GraphQLSchema, isScalarType } from 'graphql';
import { scalars } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
@Injectable()
export class ScalarsExplorerService {
private scalarImplementations: Record<string, GraphQLScalarType>;
constructor() {
this.scalarImplementations = scalars.reduce((acc, scalar) => {
acc[scalar.name] = scalar;
return acc;
}, {});
}
getScalarImplementation(scalarName: string): GraphQLScalarType | undefined {
return this.scalarImplementations[scalarName];
}
getUsedScalarNames(schema: GraphQLSchema): string[] {
const typeMap = schema.getTypeMap();
const usedScalarNames: string[] = [];
for (const typeName in typeMap) {
const type = typeMap[typeName];
if (isScalarType(type) && !typeName.startsWith('__')) {
usedScalarNames.push(type.name);
}
}
return usedScalarNames;
}
getScalarResolvers(
usedScalarNames: string[],
): Record<string, GraphQLScalarType> {
const scalarResolvers: Record<string, GraphQLScalarType> = {};
for (const scalarName of usedScalarNames) {
const scalarImplementation = this.getScalarImplementation(scalarName);
if (scalarImplementation) {
scalarResolvers[scalarName] = scalarImplementation;
}
}
return scalarResolvers;
}
}

View File

@ -0,0 +1,69 @@
import { RelationMetadataInterface } from 'src/metadata/field-metadata/interfaces/relation-metadata.interface';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import {
deduceRelationDirection,
RelationDirection,
} from 'src/workspace/utils/deduce-relation-direction.util';
describe('deduceRelationDirection', () => {
it('should return FROM when the current object Metadata ID matches fromObjectMetadataId', () => {
const currentObjectId = 'from_object_id';
const relationMetadata = {
id: 'relation_id',
fromObjectMetadataId: currentObjectId,
toObjectMetadataId: 'to_object_id',
fromFieldMetadataId: 'from_field_id',
toFieldMetadataId: 'to_field_id',
relationType: RelationMetadataType.ONE_TO_ONE,
};
const result = deduceRelationDirection(
currentObjectId,
relationMetadata as RelationMetadataInterface,
);
expect(result).toBe(RelationDirection.FROM);
});
it('should return TO when the current object Metadata ID matches toObjectMetadataId', () => {
// Arrange
const currentObjectId = 'to_object_id';
const relationMetadata = {
id: 'relation_id',
fromObjectMetadataId: 'from_object_id',
toObjectMetadataId: currentObjectId,
fromFieldMetadataId: 'from_field_id',
toFieldMetadataId: 'to_field_id',
relationType: RelationMetadataType.ONE_TO_ONE,
};
const result = deduceRelationDirection(
currentObjectId,
relationMetadata as RelationMetadataInterface,
);
expect(result).toBe(RelationDirection.TO);
});
it('should throw an error when the current object Metadata ID does not match any object metadata ID', () => {
const currentObjectId = 'unrelated_object_id';
const relationMetadata = {
id: 'relation_id',
fromObjectMetadataId: 'from_object_id',
toObjectMetadataId: 'to_object_id',
fromFieldMetadataId: 'from_field_id',
toFieldMetadataId: 'to_field_id',
relationType: RelationMetadataType.ONE_TO_ONE,
};
expect(() =>
deduceRelationDirection(
currentObjectId,
relationMetadata as RelationMetadataInterface,
),
).toThrow(
`Relation metadata ${relationMetadata.id} is not related to object ${currentObjectId}`,
);
});
});

View File

@ -0,0 +1,34 @@
import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { getResolverName } from 'src/workspace/utils/get-resolver-name.util';
describe('getResolverName', () => {
const metadata = {
nameSingular: 'entity',
namePlural: 'entities',
};
it.each([
['findMany', 'entities'],
['findOne', 'entity'],
['createMany', 'createEntities'],
['createOne', 'createEntity'],
['updateOne', 'updateEntity'],
['deleteOne', 'deleteEntity'],
])('should return correct name for %s resolver', (type, expectedResult) => {
expect(
getResolverName(metadata, type as WorkspaceResolverBuilderMethodNames),
).toBe(expectedResult);
});
it('should throw an error for an unknown resolver type', () => {
const unknownType = 'unknownType';
expect(() =>
getResolverName(
metadata,
unknownType as WorkspaceResolverBuilderMethodNames,
),
).toThrow(`Unknown resolver type: ${unknownType}`);
});
});

View File

@ -0,0 +1,23 @@
import { RelationMetadataInterface } from 'src/metadata/field-metadata/interfaces/relation-metadata.interface';
export enum RelationDirection {
FROM = 'from',
TO = 'to',
}
export const deduceRelationDirection = (
currentObjectId: string,
relationMetadata: RelationMetadataInterface,
): RelationDirection => {
if (relationMetadata.fromObjectMetadataId === currentObjectId) {
return RelationDirection.FROM;
}
if (relationMetadata.toObjectMetadataId === currentObjectId) {
return RelationDirection.TO;
}
throw new Error(
`Relation metadata ${relationMetadata.id} is not related to object ${currentObjectId}`,
);
};

View File

@ -0,0 +1,31 @@
import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { camelCase } from 'src/utils/camel-case';
import { pascalCase } from 'src/utils/pascal-case';
export const getResolverName = (
objectMetadata: Pick<ObjectMetadataInterface, 'namePlural' | 'nameSingular'>,
type: WorkspaceResolverBuilderMethodNames,
) => {
switch (type) {
case 'findMany':
return `${camelCase(objectMetadata.namePlural)}`;
case 'findOne':
return `${camelCase(objectMetadata.nameSingular)}`;
case 'createMany':
return `create${pascalCase(objectMetadata.namePlural)}`;
case 'createOne':
return `create${pascalCase(objectMetadata.nameSingular)}`;
case 'updateOne':
return `update${pascalCase(objectMetadata.nameSingular)}`;
case 'deleteOne':
return `delete${pascalCase(objectMetadata.nameSingular)}`;
case 'updateMany':
return `update${pascalCase(objectMetadata.namePlural)}`;
case 'deleteMany':
return `delete${pascalCase(objectMetadata.namePlural)}`;
default:
throw new Error(`Unknown resolver type: ${type}`);
}
};

View File

@ -0,0 +1,7 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const isRelationFieldMetadataType = (
type: FieldMetadataType,
): type is FieldMetadataType.RELATION => {
return type === FieldMetadataType.RELATION;
};

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { WorkspaceDataSourceService } from './workspace-datasource.service';
@Module({
imports: [DataSourceModule, TypeORMModule],
exports: [WorkspaceDataSourceService],
providers: [WorkspaceDataSourceService],
})
export class WorkspaceDataSourceModule {}

View File

@ -0,0 +1,101 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
@Injectable()
export class WorkspaceDataSourceService {
constructor(
private readonly dataSourceService: DataSourceService,
private readonly typeormService: TypeORMService,
) {}
/**
*
* Connect to the workspace data source
*
* @param workspaceId
* @returns
*/
public async connectToWorkspaceDataSource(
workspaceId: string,
): Promise<DataSource> {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const dataSource = await this.typeormService.connectToDataSource(
dataSourceMetadata,
);
if (!dataSource) {
throw new Error(
`Could not connect to workspace data source for workspace ${workspaceId}`,
);
}
return dataSource;
}
/**
*
* Create a new DB schema for a workspace
*
* @param workspaceId
* @returns
*/
public async createWorkspaceDBSchema(workspaceId: string): Promise<string> {
const schemaName = this.getSchemaName(workspaceId);
return await this.typeormService.createSchema(schemaName);
}
/**
*
* Delete a DB schema for a workspace
*
* @param workspaceId
* @returns
*/
public async deleteWorkspaceDBSchema(workspaceId: string): Promise<void> {
const schemaName = this.getSchemaName(workspaceId);
return await this.typeormService.deleteSchema(schemaName);
}
/**
*
* Get the schema name for a workspace
*
* @param workspaceId
* @returns string
*/
public getSchemaName(workspaceId: string): string {
return `workspace_${this.uuidToBase36(workspaceId)}`;
}
/**
*
* Convert a uuid to base36
*
* @param uuid
* @returns string
*/
private uuidToBase36(uuid: string): string {
let devId = false;
if (uuid.startsWith('twenty-')) {
devId = true;
// Clean dev uuids (twenty-)
uuid = uuid.replace('twenty-', '');
}
const hexString = uuid.replace(/-/g, '');
const base10Number = BigInt('0x' + hexString);
const base36String = base10Number.toString(36);
return `${devId ? 'twenty_' : ''}${base36String}`;
}
}

View File

@ -0,0 +1,23 @@
import { EntityManager } from 'typeorm';
import { companiesDemo } from './companies-demo';
export const companyPrefillData = async (
entityManager: EntityManager,
schemaName: string,
) => {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.company`, [
'name',
'domainName',
'address',
'employees',
'linkedinLinkUrl',
])
.orIgnore()
.values(companiesDemo)
.returning('*')
.execute();
};

View File

@ -0,0 +1,43 @@
import { DataSource, EntityManager } from 'typeorm';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { viewPrefillData } from 'src/workspace/workspace-manager/demo-objects-prefill-data/view';
import { companyPrefillData } from 'src/workspace/workspace-manager/demo-objects-prefill-data/company';
import { personPrefillData } from 'src/workspace/workspace-manager/demo-objects-prefill-data/person';
import { pipelineStepPrefillData } from 'src/workspace/workspace-manager/demo-objects-prefill-data/pipeline-step';
import { workspaceMemberPrefillData } from 'src/workspace/workspace-manager/demo-objects-prefill-data/workspace-member';
import { seedDemoOpportunity } from 'src/workspace/workspace-manager/demo-objects-prefill-data/opportunity';
export const demoObjectsPrefillData = async (
workspaceDataSource: DataSource,
schemaName: string,
objectMetadata: ObjectMetadataEntity[],
) => {
const objectMetadataMap = objectMetadata.reduce((acc, object) => {
acc[object.nameSingular] = {
id: object.id,
fields: object.fields.reduce((acc, field) => {
acc[field.name] = field.id;
return acc;
}, {}),
};
return acc;
}, {});
// TODO: udnerstand why only with this createQueryRunner transaction below works
const queryRunner = workspaceDataSource.createQueryRunner();
await queryRunner.connect();
workspaceDataSource.transaction(async (entityManager: EntityManager) => {
await companyPrefillData(entityManager, schemaName);
await personPrefillData(entityManager, schemaName);
await viewPrefillData(entityManager, schemaName, objectMetadataMap);
await pipelineStepPrefillData(entityManager, schemaName);
await seedDemoOpportunity(entityManager, schemaName);
await workspaceMemberPrefillData(entityManager, schemaName);
});
};

View File

@ -0,0 +1,78 @@
import { EntityManager } from 'typeorm';
import { v4 } from 'uuid';
const tableName = 'opportunity';
const getRandomProbability = () => {
const firstDigit = Math.floor(Math.random() * 9) + 1;
return firstDigit / 10;
};
const getRandomPipelineStepId = (pipelineStepIds: { id: string }[]) =>
pipelineStepIds[Math.floor(Math.random() * pipelineStepIds.length)].id;
const generateRandomAmountMicros = () => {
const firstDigit = Math.floor(Math.random() * 9) + 1;
return firstDigit * 10000000000;
};
// Function to generate the array of opportunities
// companiesWithPeople - selecting from the db companies and 1 person related to the company.id to use companyId, pointOfContactId and personId
// pipelineStepIds - selecting from the db pipeline, getting random id from selected to use as pipelineStepId
const generateOpportunities = (
companies,
pipelineStepIds: { id: string }[],
) => {
return companies.map((company) => ({
id: v4(),
amountAmountMicros: generateRandomAmountMicros(),
amountCurrencyCode: 'USD',
closeDate: new Date(),
probability: getRandomProbability(),
pipelineStepId: getRandomPipelineStepId(pipelineStepIds),
pointOfContactId: company.personId,
personId: company.personId,
companyId: company.id,
}));
};
export const seedDemoOpportunity = async (
entityManager: EntityManager,
schemaName: string,
) => {
const companiesWithPeople = await entityManager?.query(
`SELECT company.*, person.id AS "personId"
FROM ${schemaName}.company
LEFT JOIN ${schemaName}.person ON company.id = "person"."companyId"
LIMIT 50`,
);
const pipelineStepIds = await entityManager?.query(
`SELECT id FROM ${schemaName}."pipelineStep"`,
);
const opportunities = generateOpportunities(
companiesWithPeople,
pipelineStepIds,
);
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.${tableName}`, [
'id',
'amountAmountMicros',
'amountCurrencyCode',
'closeDate',
'probability',
'pipelineStepId',
'pointOfContactId',
'personId',
'companyId',
])
.orIgnore()
.values(opportunities)
.execute();
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,42 @@
import { EntityManager } from 'typeorm';
import { peopleDemo } from './people-demo';
export const personPrefillData = async (
entityManager: EntityManager,
schemaName: string,
) => {
const companies = await entityManager?.query(
`SELECT * FROM ${schemaName}.company`,
);
// Iterate through the array and add a UUID for each person
const people = peopleDemo.map((person, index) => ({
nameFirstName: person.firstName,
nameLastName: person.lastName,
email: person.email,
linkedinLinkUrl: person.linkedinUrl,
jobTitle: person.jobTitle,
city: person.city,
avatarUrl: person.avatarUrl,
companyId: companies[Math.floor(index / 2)].id,
}));
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.person`, [
'nameFirstName',
'nameLastName',
'email',
'linkedinLinkUrl',
'jobTitle',
'city',
'avatarUrl',
'companyId',
])
.orIgnore()
.values(people)
.returning('*')
.execute();
};

View File

@ -0,0 +1,41 @@
import { EntityManager } from 'typeorm';
export const pipelineStepPrefillData = async (
entityManager: EntityManager,
schemaName: string,
) => {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.pipelineStep`, ['name', 'color', 'position'])
.orIgnore()
.values([
{
name: 'New',
color: 'red',
position: 0,
},
{
name: 'Screening',
color: 'purple',
position: 1,
},
{
name: 'Meeting',
color: 'sky',
position: 2,
},
{
name: 'Proposal',
color: 'turquoise',
position: 3,
},
{
name: 'Customer',
color: 'yellow',
position: 4,
},
])
.returning('*')
.execute();
};

View File

@ -0,0 +1,201 @@
import { EntityManager } from 'typeorm';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
export const viewPrefillData = async (
entityManager: EntityManager,
schemaName: string,
objectMetadataMap: Record<string, ObjectMetadataEntity>,
) => {
// Creating views
const createdViews = await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.view`, ['name', 'objectMetadataId', 'type'])
.orIgnore()
.values([
{
name: 'All Companies',
objectMetadataId: objectMetadataMap['company'].id,
type: 'table',
},
{
name: 'All People',
objectMetadataId: objectMetadataMap['person'].id,
type: 'table',
},
{
name: 'All Opportunities',
objectMetadataId: objectMetadataMap['opportunity'].id,
type: 'kanban',
},
])
.returning('*')
.execute();
const viewIdMap = createdViews.raw.reduce((acc, view) => {
acc[view.name] = view.id;
return acc;
}, {});
// Creating viewFields
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.viewField`, [
'fieldMetadataId',
'viewId',
'position',
'isVisible',
'size',
])
.orIgnore()
.values([
// Company
{
fieldMetadataId: objectMetadataMap['company'].fields['name'],
viewId: viewIdMap['All Companies'],
position: 0,
isVisible: true,
size: 180,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['domainName'],
viewId: viewIdMap['All Companies'],
position: 1,
isVisible: true,
size: 100,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['accountOwner'],
viewId: viewIdMap['All Companies'],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['createdAt'],
viewId: viewIdMap['All Companies'],
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['employees'],
viewId: viewIdMap['All Companies'],
position: 4,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['linkedinLink'],
viewId: viewIdMap['All Companies'],
position: 5,
isVisible: true,
size: 170,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['address'],
viewId: viewIdMap['All Companies'],
position: 6,
isVisible: true,
size: 170,
},
// Person
{
fieldMetadataId: objectMetadataMap['person'].fields['name'],
viewId: viewIdMap['All People'],
position: 0,
isVisible: true,
size: 210,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['email'],
viewId: viewIdMap['All People'],
position: 1,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['company'],
viewId: viewIdMap['All People'],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['phone'],
viewId: viewIdMap['All People'],
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['createdAt'],
viewId: viewIdMap['All People'],
position: 4,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['city'],
viewId: viewIdMap['All People'],
position: 5,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['jobTitle'],
viewId: viewIdMap['All People'],
position: 6,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['linkedinLink'],
viewId: viewIdMap['All People'],
position: 7,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['xLink'],
viewId: viewIdMap['All People'],
position: 8,
isVisible: true,
size: 150,
},
// Opportunity
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['amount'],
viewId: viewIdMap['All Opportunities'],
position: 0,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['closeDate'],
viewId: viewIdMap['All Opportunities'],
position: 1,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['probability'],
viewId: viewIdMap['All Opportunities'],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId:
objectMetadataMap['opportunity'].fields['pointOfContact'],
viewId: viewIdMap['All Opportunities'],
position: 3,
isVisible: true,
size: 150,
},
])
.execute();
};

View File

@ -0,0 +1,54 @@
import { EntityManager } from 'typeorm';
import { DemoSeedUserIds } from 'src/database/typeorm-seeds/core/demo/users';
const WorkspaceMemberIds = {
Noah: '20202020-0687-4c41-b707-ed1bfca972a7',
Hugo: '20202020-77d5-4cb6-b60a-f4a835a85d61',
Julia: '20202020-1553-45c6-a028-5a9064cce07f',
};
export const workspaceMemberPrefillData = async (
entityManager: EntityManager,
schemaName: string,
) => {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workspaceMember`, [
'id',
'nameFirstName',
'nameLastName',
'locale',
'colorScheme',
'userId',
])
.orIgnore()
.values([
{
id: WorkspaceMemberIds.Noah,
nameFirstName: 'Noah',
nameLastName: 'A',
locale: 'en',
colorScheme: 'Light',
userId: DemoSeedUserIds.Noah,
},
{
id: WorkspaceMemberIds.Hugo,
nameFirstName: 'Hugo',
nameLastName: 'I',
locale: 'en',
colorScheme: 'Light',
userId: DemoSeedUserIds.Hugo,
},
{
id: WorkspaceMemberIds.Julia,
nameFirstName: 'Julia',
nameLastName: 'S',
locale: 'en',
colorScheme: 'Light',
userId: DemoSeedUserIds.Julia,
},
])
.execute();
};

View File

@ -0,0 +1,51 @@
import { EntityManager } from 'typeorm';
export const companyPrefillData = async (
entityManager: EntityManager,
schemaName: string,
) => {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.company`, [
'name',
'domainName',
'address',
'employees',
])
.orIgnore()
.values([
{
name: 'Airbnb',
domainName: 'airbnb.com',
address: 'San Francisco',
employees: 5000,
},
{
name: 'Qonto',
domainName: 'qonto.com',
address: 'San Francisco',
employees: 800,
},
{
name: 'Stripe',
domainName: 'stripe.com',
address: 'San Francisco',
employees: 8000,
},
{
name: 'Figma',
domainName: 'figma.com',
address: 'San Francisco',
employees: 800,
},
{
name: 'Notion',
domainName: 'notion.com',
address: 'San Francisco',
employees: 400,
},
])
.returning('*')
.execute();
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
import { EntityManager } from 'typeorm';
export const pipelineStepPrefillData = async (
entityManager: EntityManager,
schemaName: string,
) => {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.pipelineStep`, ['name', 'color', 'position'])
.orIgnore()
.values([
{
name: 'New',
color: 'red',
position: 0,
},
{
name: 'Screening',
color: 'purple',
position: 1,
},
{
name: 'Meeting',
color: 'sky',
position: 2,
},
{
name: 'Proposal',
color: 'turquoise',
position: 3,
},
{
name: 'Customer',
color: 'yellow',
position: 4,
},
])
.returning('*')
.execute();
};

View File

@ -0,0 +1,33 @@
import { DataSource, EntityManager } from 'typeorm';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { viewPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/view';
import { companyPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/company';
import { personPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/person';
import { pipelineStepPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/pipeline-step';
export const standardObjectsPrefillData = async (
workspaceDataSource: DataSource,
schemaName: string,
objectMetadata: ObjectMetadataEntity[],
) => {
const objectMetadataMap = objectMetadata.reduce((acc, object) => {
acc[object.nameSingular] = {
id: object.id,
fields: object.fields.reduce((acc, field) => {
acc[field.name] = field.id;
return acc;
}, {}),
};
return acc;
}, {});
workspaceDataSource.transaction(async (entityManager: EntityManager) => {
await companyPrefillData(entityManager, schemaName);
await personPrefillData(entityManager, schemaName);
await viewPrefillData(entityManager, schemaName, objectMetadataMap);
await pipelineStepPrefillData(entityManager, schemaName);
});
};

View File

@ -0,0 +1,201 @@
import { EntityManager } from 'typeorm';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
export const viewPrefillData = async (
entityManager: EntityManager,
schemaName: string,
objectMetadataMap: Record<string, ObjectMetadataEntity>,
) => {
// Creating views
const createdViews = await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.view`, ['name', 'objectMetadataId', 'type'])
.orIgnore()
.values([
{
name: 'All Companies',
objectMetadataId: objectMetadataMap['company'].id,
type: 'table',
},
{
name: 'All People',
objectMetadataId: objectMetadataMap['person'].id,
type: 'table',
},
{
name: 'All Opportunities',
objectMetadataId: objectMetadataMap['opportunity'].id,
type: 'kanban',
},
])
.returning('*')
.execute();
const viewIdMap = createdViews.raw.reduce((acc, view) => {
acc[view.name] = view.id;
return acc;
}, {});
// Creating viewFields
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.viewField`, [
'fieldMetadataId',
'viewId',
'position',
'isVisible',
'size',
])
.orIgnore()
.values([
// Company
{
fieldMetadataId: objectMetadataMap['company'].fields['name'],
viewId: viewIdMap['All Companies'],
position: 0,
isVisible: true,
size: 180,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['domainName'],
viewId: viewIdMap['All Companies'],
position: 1,
isVisible: true,
size: 100,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['accountOwner'],
viewId: viewIdMap['All Companies'],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['createdAt'],
viewId: viewIdMap['All Companies'],
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['employees'],
viewId: viewIdMap['All Companies'],
position: 4,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['linkedinLink'],
viewId: viewIdMap['All Companies'],
position: 5,
isVisible: true,
size: 170,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['address'],
viewId: viewIdMap['All Companies'],
position: 6,
isVisible: true,
size: 170,
},
// Person
{
fieldMetadataId: objectMetadataMap['person'].fields['name'],
viewId: viewIdMap['All People'],
position: 0,
isVisible: true,
size: 210,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['email'],
viewId: viewIdMap['All People'],
position: 1,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['company'],
viewId: viewIdMap['All People'],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['phone'],
viewId: viewIdMap['All People'],
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['createdAt'],
viewId: viewIdMap['All People'],
position: 4,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['city'],
viewId: viewIdMap['All People'],
position: 5,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['jobTitle'],
viewId: viewIdMap['All People'],
position: 6,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['linkedinLink'],
viewId: viewIdMap['All People'],
position: 7,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['xLink'],
viewId: viewIdMap['All People'],
position: 8,
isVisible: true,
size: 150,
},
// Opportunity
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['amount'],
viewId: viewIdMap['All Opportunities'],
position: 0,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['closeDate'],
viewId: viewIdMap['All Opportunities'],
position: 1,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['probability'],
viewId: viewIdMap['All Opportunities'],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId:
objectMetadataMap['opportunity'].fields['pointOfContact'],
viewId: viewIdMap['All Opportunities'],
position: 3,
isVisible: true,
size: 150,
},
])
.execute();
};

View File

@ -0,0 +1,87 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const connectedAccountMetadata = {
nameSingular: 'connectedAccount',
namePlural: 'connectedAccounts',
labelSingular: 'Connected Account',
labelPlural: 'Connected Accounts',
targetTableName: 'connectedAccount',
description: 'A connected account',
icon: 'IconBuildingSkyscraper',
isActive: true,
isSystem: false,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'type',
label: 'type',
targetColumnMap: {
value: 'type',
},
description: 'The account type',
icon: 'IconSettings',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'accessToken',
label: 'accessToken',
targetColumnMap: {
value: 'accessToken',
},
description: 'Messaging provider access token',
icon: 'IconKey',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'refreshToken',
label: 'refreshToken',
targetColumnMap: {
value: 'refreshToken',
},
description: 'Messaging provider refresh token',
icon: 'IconKey',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT, // Should be an array of strings
name: 'externalScopes',
label: 'externalScopes',
targetColumnMap: {
value: 'externalScopes',
},
description: 'External scopes',
icon: 'IconCircle',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.BOOLEAN, // Should be an array of strings
name: 'hasEmailScope',
label: 'hasEmailScope',
targetColumnMap: {
value: 'hasEmailScope',
},
description: 'Has email scope',
icon: 'IconMail',
isNullable: false,
defaultValue: { value: '' },
},
],
};
export default connectedAccountMetadata;

View File

@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { WorkspaceSyncMetadataModule } from 'src/workspace/workspace-sync-metadata/worksapce-sync-metadata.module';
import { WorkspaceManagerService } from './workspace-manager.service';
@Module({
imports: [
WorkspaceDataSourceModule,
WorkspaceMigrationModule,
ObjectMetadataModule,
DataSourceModule,
WorkspaceSyncMetadataModule,
],
exports: [WorkspaceManagerService],
providers: [WorkspaceManagerService],
})
export class WorkspaceManagerModule {}

View File

@ -0,0 +1,172 @@
import { Injectable } from '@nestjs/common';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
import { standardObjectsPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data';
import { demoObjectsPrefillData } from 'src/workspace/workspace-manager/demo-objects-prefill-data/demo-objects-prefill-data';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync.metadata.service';
@Injectable()
export class WorkspaceManagerService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly dataSourceService: DataSourceService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
) {}
/**
* Init a workspace by creating a new data source and running all migrations
* @param workspaceId
* @returns Promise<void>
*/
public async init(workspaceId: string): Promise<void> {
const schemaName =
await this.workspaceDataSourceService.createWorkspaceDBSchema(
workspaceId,
);
const dataSourceMetadata =
await this.dataSourceService.createDataSourceMetadata(
workspaceId,
schemaName,
);
await this.setWorkspaceMaxRow(workspaceId, schemaName);
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
await this.prefillWorkspaceWithStandardObjects(
dataSourceMetadata,
workspaceId,
);
}
/**
* InitDemo a workspace by creating a new data source and running all migrations
* @param workspaceId
* @returns Promise<void>
*/
public async initDemo(workspaceId: string): Promise<void> {
const schemaName =
await this.workspaceDataSourceService.createWorkspaceDBSchema(
workspaceId,
);
const dataSourceMetadata =
await this.dataSourceService.createDataSourceMetadata(
workspaceId,
schemaName,
);
await this.setWorkspaceMaxRow(workspaceId, schemaName);
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId);
}
/**
*
* We are updating the pg_graphql max_rows from 30 (default value) to 60
*
* @params workspaceId, schemaName
* @param workspaceId
*/
private async setWorkspaceMaxRow(workspaceId, schemaName) {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
await workspaceDataSource.query(
`comment on schema ${schemaName} is e'@graphql({"max_rows": 60})'`,
);
}
/**
*
* We are prefilling a few standard objects with data to make it easier for the user to get started.
*
* @param dataSourceMetadata
* @param workspaceId
*/
private async prefillWorkspaceWithStandardObjects(
dataSourceMetadata: DataSourceEntity,
workspaceId: string,
) {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
if (!workspaceDataSource) {
throw new Error('Could not connect to workspace data source');
}
const createdObjectMetadata =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
await standardObjectsPrefillData(
workspaceDataSource,
dataSourceMetadata.schema,
createdObjectMetadata,
);
}
/**
*
* We are prefilling a few demo objects with data to make it easier for the user to get started.
*
* @param dataSourceMetadata
* @param workspaceId
*/
private async prefillWorkspaceWithDemoObjects(
dataSourceMetadata: DataSourceEntity,
workspaceId: string,
) {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
if (!workspaceDataSource) {
throw new Error('Could not connect to workspace data source');
}
const createdObjectMetadata =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
await demoObjectsPrefillData(
workspaceDataSource,
dataSourceMetadata.schema,
createdObjectMetadata,
);
}
/**
*
* Delete a workspace by deleting all metadata and the schema
*
* @param workspaceId
*/
public async delete(workspaceId: string): Promise<void> {
// Delete data from metadata tables
await this.objectMetadataService.deleteObjectsMetadata(workspaceId);
await this.workspaceMigrationService.delete(workspaceId);
await this.dataSourceService.delete(workspaceId);
// Delete schema
await this.workspaceDataSourceService.deleteWorkspaceDBSchema(workspaceId);
}
}

View File

@ -0,0 +1,45 @@
import { Command, CommandRunner, Option } from 'nest-commander';
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
// TODO: implement dry-run
interface RunWorkspaceMigrationsOptions {
workspaceId: string;
}
@Command({
name: 'workspace:migrate',
description: 'Run workspace migrations',
})
export class RunWorkspaceMigrationsCommand extends CommandRunner {
constructor(
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
) {
super();
}
async run(
_passedParam: string[],
options: RunWorkspaceMigrationsOptions,
): Promise<void> {
// TODO: run in a dedicated job + run queries in a transaction.
await this.workspaceMigrationService.insertStandardMigrations(
options.workspaceId,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
options.workspaceId,
);
}
// TODO: workspaceId should be optional and we should run migrations for all workspaces
@Option({
flags: '-w, --workspace-id [workspace_id]',
description: 'workspace id',
required: true,
})
parseWorkspaceId(value: string): string {
return value;
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
import { RunWorkspaceMigrationsCommand } from './run-workspace-migrations.command';
@Module({
imports: [WorkspaceMigrationModule, WorkspaceMigrationRunnerModule],
providers: [RunWorkspaceMigrationsCommand],
})
export class WorkspaceMigrationRunnerCommandsModule {}

View File

@ -0,0 +1,187 @@
import { Injectable } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
import { WorkspaceMigrationColumnAlter } from 'src/metadata/workspace-migration/workspace-migration.entity';
@Injectable()
export class WorkspaceMigrationEnumService {
async alterEnum(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnAlter,
) {
const columnDefinition = migrationColumn.alteredColumnDefinition;
const oldEnumTypeName = `${tableName}_${columnDefinition.columnName}_enum`;
const newEnumTypeName = `${tableName}_${columnDefinition.columnName}_enum_new`;
const enumValues =
columnDefinition.enum?.map((enumValue) => {
if (typeof enumValue === 'string') {
return enumValue;
}
return enumValue.to;
}) ?? [];
if (!columnDefinition.isNullable && !columnDefinition.defaultValue) {
columnDefinition.defaultValue = columnDefinition.enum?.[0];
}
// Create new enum type with new values
await this.createNewEnumType(
newEnumTypeName,
queryRunner,
schemaName,
enumValues,
);
// Temporarily change column type to text
await queryRunner.query(`
ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${columnDefinition.columnName}" TYPE TEXT
`);
// Migrate existing values to new values
await this.migrateEnumValues(
queryRunner,
schemaName,
tableName,
migrationColumn,
);
// Update existing rows to handle missing values
await this.handleMissingEnumValues(
queryRunner,
schemaName,
tableName,
migrationColumn,
enumValues,
);
// Alter column type to new enum
await this.updateColumnToNewEnum(
queryRunner,
schemaName,
tableName,
columnDefinition.columnName,
newEnumTypeName,
);
// Drop old enum type
await this.dropOldEnumType(queryRunner, schemaName, oldEnumTypeName);
// Rename new enum type to old enum type name
await this.renameEnumType(
queryRunner,
schemaName,
oldEnumTypeName,
newEnumTypeName,
);
}
private async createNewEnumType(
name: string,
queryRunner: QueryRunner,
schemaName: string,
newValues: string[],
) {
const enumValues = newValues
.map((value) => `'${value.replace(/'/g, "''")}'`)
.join(', ');
await queryRunner.query(
`CREATE TYPE "${schemaName}"."${name}" AS ENUM (${enumValues})`,
);
}
private async migrateEnumValues(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnAlter,
) {
const columnDefinition = migrationColumn.alteredColumnDefinition;
if (!columnDefinition.enum) {
return;
}
for (const enumValue of columnDefinition.enum) {
// Skip string values
if (typeof enumValue === 'string') {
continue;
}
await queryRunner.query(`
UPDATE "${schemaName}"."${tableName}"
SET "${columnDefinition.columnName}" = '${enumValue.to}'
WHERE "${columnDefinition.columnName}" = '${enumValue.from}'
`);
}
}
private async handleMissingEnumValues(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnAlter,
enumValues: string[],
) {
const columnDefinition = migrationColumn.alteredColumnDefinition;
// Set missing values to null or default value
let defaultValue = 'NULL';
if (columnDefinition.defaultValue) {
if (Array.isArray(columnDefinition.defaultValue)) {
defaultValue = `ARRAY[${columnDefinition.defaultValue
.map((e) => `'${e}'`)
.join(', ')}]`;
} else {
defaultValue = `'${columnDefinition.defaultValue}'`;
}
}
await queryRunner.query(`
UPDATE "${schemaName}"."${tableName}"
SET "${columnDefinition.columnName}" = ${defaultValue}
WHERE "${columnDefinition.columnName}" NOT IN (${enumValues
.map((e) => `'${e}'`)
.join(', ')})
`);
}
private async updateColumnToNewEnum(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
columnName: string,
newEnumTypeName: string,
) {
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE "${schemaName}"."${newEnumTypeName}" USING ("${columnName}"::text::"${schemaName}"."${newEnumTypeName}")`,
);
}
private async dropOldEnumType(
queryRunner: QueryRunner,
schemaName: string,
oldEnumTypeName: string,
) {
await queryRunner.query(
`DROP TYPE IF EXISTS "${schemaName}"."${oldEnumTypeName}"`,
);
}
private async renameEnumType(
queryRunner: QueryRunner,
schemaName: string,
oldEnumTypeName: string,
newEnumTypeName: string,
) {
await queryRunner.query(`
ALTER TYPE "${schemaName}"."${newEnumTypeName}"
RENAME TO "${oldEnumTypeName}"
`);
}
}

View File

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

View File

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { WorkspaceCacheVersionModule } from 'src/metadata/workspace-cache-version/workspace-cache-version.module';
import { WorkspaceMigrationEnumService } from 'src/workspace/workspace-migration-runner/services/workspace-migration-enum.service';
import { WorkspaceMigrationRunnerService } from './workspace-migration-runner.service';
@Module({
imports: [
WorkspaceDataSourceModule,
WorkspaceMigrationModule,
WorkspaceCacheVersionModule,
],
providers: [WorkspaceMigrationRunnerService, WorkspaceMigrationEnumService],
exports: [WorkspaceMigrationRunnerService],
})
export class WorkspaceMigrationRunnerModule {}

View File

@ -0,0 +1,306 @@
import { Injectable } from '@nestjs/common';
import {
QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableUnique,
} from 'typeorm';
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import {
WorkspaceMigrationTableAction,
WorkspaceMigrationColumnAction,
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate,
WorkspaceMigrationColumnRelation,
WorkspaceMigrationColumnAlter,
} from 'src/metadata/workspace-migration/workspace-migration.entity';
import { WorkspaceCacheVersionService } from 'src/metadata/workspace-cache-version/workspace-cache-version.service';
import { WorkspaceMigrationEnumService } from 'src/workspace/workspace-migration-runner/services/workspace-migration-enum.service';
import { customTableDefaultColumns } from './utils/custom-table-default-column.util';
@Injectable()
export class WorkspaceMigrationRunnerService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
private readonly workspaceMigrationEnumService: WorkspaceMigrationEnumService,
) {}
/**
* Executes pending migrations for a given workspace
*
* @param workspaceId string
* @returns Promise<WorkspaceMigrationTableAction[]>
*/
public async executeMigrationFromPendingMigrations(
workspaceId: string,
): Promise<WorkspaceMigrationTableAction[]> {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
if (!workspaceDataSource) {
throw new Error('Workspace data source not found');
}
const pendingMigrations =
await this.workspaceMigrationService.getPendingMigrations(workspaceId);
if (pendingMigrations.length === 0) {
return [];
}
const flattenedPendingMigrations: WorkspaceMigrationTableAction[] =
pendingMigrations.reduce((acc, pendingMigration) => {
return [...acc, ...pendingMigration.migrations];
}, []);
const queryRunner = workspaceDataSource?.createQueryRunner();
const schemaName =
this.workspaceDataSourceService.getSchemaName(workspaceId);
// Loop over each migration and create or update the table
// TODO: Should be done in a transaction
for (const migration of flattenedPendingMigrations) {
await this.handleTableChanges(queryRunner, schemaName, migration);
}
// Update appliedAt date for each migration
// TODO: Should be done after the migration is successful
for (const pendingMigration of pendingMigrations) {
await this.workspaceMigrationService.setAppliedAtForMigration(
workspaceId,
pendingMigration,
);
}
await queryRunner.release();
// Increment workspace cache version
await this.workspaceCacheVersionService.incrementVersion(workspaceId);
return flattenedPendingMigrations;
}
/**
* Handles table changes for a given migration
*
* @param queryRunner QueryRunner
* @param schemaName string
* @param tableMigration WorkspaceMigrationTableChange
*/
private async handleTableChanges(
queryRunner: QueryRunner,
schemaName: string,
tableMigration: WorkspaceMigrationTableAction,
) {
switch (tableMigration.action) {
case 'create':
await this.createTable(queryRunner, schemaName, tableMigration.name);
break;
case 'alter':
await this.handleColumnChanges(
queryRunner,
schemaName,
tableMigration.name,
tableMigration?.columns,
);
break;
default:
throw new Error(
`Migration table action ${tableMigration.action} not supported`,
);
}
}
/**
* Creates a table for a given schema and table name
*
* @param queryRunner QueryRunner
* @param schemaName string
* @param tableName string
*/
private async createTable(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
) {
await queryRunner.createTable(
new Table({
name: tableName,
schema: schemaName,
columns: customTableDefaultColumns,
}),
true,
);
}
/**
* Handles column changes for a given migration
*
* @param queryRunner QueryRunner
* @param schemaName string
* @param tableName string
* @param columnMigrations WorkspaceMigrationColumnAction[]
* @returns
*/
private async handleColumnChanges(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
columnMigrations?: WorkspaceMigrationColumnAction[],
) {
if (!columnMigrations || columnMigrations.length === 0) {
return;
}
for (const columnMigration of columnMigrations) {
switch (columnMigration.action) {
case WorkspaceMigrationColumnActionType.CREATE:
await this.createColumn(
queryRunner,
schemaName,
tableName,
columnMigration,
);
break;
case WorkspaceMigrationColumnActionType.ALTER:
await this.alterColumn(
queryRunner,
schemaName,
tableName,
columnMigration,
);
break;
case WorkspaceMigrationColumnActionType.RELATION:
await this.createForeignKey(
queryRunner,
schemaName,
tableName,
columnMigration,
);
break;
default:
throw new Error(`Migration column action not supported`);
}
}
}
/**
* Creates a column for a given schema, table name, and column migration
*
* @param queryRunner QueryRunner
* @param schemaName string
* @param tableName string
* @param migrationColumn WorkspaceMigrationColumnAction
*/
private async createColumn(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnCreate,
) {
const hasColumn = await queryRunner.hasColumn(
`${schemaName}.${tableName}`,
migrationColumn.columnName,
);
if (hasColumn) {
return;
}
await queryRunner.addColumn(
`${schemaName}.${tableName}`,
new TableColumn({
name: migrationColumn.columnName,
type: migrationColumn.columnType,
default: migrationColumn.defaultValue,
enum: migrationColumn.enum?.filter(
(value): value is string => typeof value === 'string',
),
isArray: migrationColumn.isArray,
isNullable: true,
}),
);
}
private async alterColumn(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnAlter,
) {
const enumValues = migrationColumn.alteredColumnDefinition.enum;
// TODO: Maybe we can do something better if we can recreate the old `TableColumn` object
if (enumValues) {
// This is returning the old enum values to avoid TypeORM droping the enum type
await this.workspaceMigrationEnumService.alterEnum(
queryRunner,
schemaName,
tableName,
migrationColumn,
);
}
await queryRunner.changeColumn(
`${schemaName}.${tableName}`,
new TableColumn({
name: migrationColumn.currentColumnDefinition.columnName,
type: migrationColumn.currentColumnDefinition.columnType,
default: migrationColumn.currentColumnDefinition.defaultValue,
enum: migrationColumn.currentColumnDefinition.enum?.filter(
(value): value is string => typeof value === 'string',
),
isArray: migrationColumn.currentColumnDefinition.isArray,
isNullable: migrationColumn.currentColumnDefinition.isNullable,
}),
new TableColumn({
name: migrationColumn.alteredColumnDefinition.columnName,
type: migrationColumn.alteredColumnDefinition.columnType,
default: migrationColumn.alteredColumnDefinition.defaultValue,
enum: migrationColumn.currentColumnDefinition.enum?.filter(
(value): value is string => typeof value === 'string',
),
isArray: migrationColumn.alteredColumnDefinition.isArray,
isNullable: migrationColumn.alteredColumnDefinition.isNullable,
}),
);
// }
}
private async createForeignKey(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnRelation,
) {
await queryRunner.createForeignKey(
`${schemaName}.${tableName}`,
new TableForeignKey({
columnNames: [migrationColumn.columnName],
referencedColumnNames: [migrationColumn.referencedTableColumnName],
referencedTableName: migrationColumn.referencedTableName,
onDelete: 'CASCADE',
}),
);
// Create unique constraint if for one to one relation
if (migrationColumn.isUnique) {
await queryRunner.createUniqueConstraint(
`${schemaName}.${tableName}`,
new TableUnique({
name: `UNIQUE_${tableName}_${migrationColumn.columnName}`,
columnNames: [migrationColumn.columnName],
}),
);
}
}
}

View File

@ -0,0 +1,237 @@
// import { GraphQLResolveInfo } from 'graphql';
// 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/resolver-builder/pg-graphql/pg-graphql-query-builder';
// const testUUID = '123e4567-e89b-12d3-a456-426614174001';
// const normalizeWhitespace = (str) => str.replace(/\s+/g, '');
// // Mocking dependencies
// jest.mock('uuid', () => ({
// v4: jest.fn(() => testUUID),
// }));
// jest.mock('graphql-fields', () =>
// jest.fn(() => ({
// name: true,
// age: true,
// complexField: {
// subField1: true,
// subField2: true,
// },
// })),
// );
// describe('PGGraphQLQueryBuilder', () => {
// let queryBuilder;
// let mockOptions: PGGraphQLQueryBuilderOptions;
// beforeEach(() => {
// const fieldMetadataCollection = [
// {
// name: 'name',
// targetColumnMap: {
// value: 'column_name',
// } as FieldMetadataTargetColumnMap,
// },
// {
// name: 'age',
// targetColumnMap: {
// value: 'column_age',
// } as FieldMetadataTargetColumnMap,
// },
// {
// name: 'complexField',
// targetColumnMap: {
// subField1: 'column_subField1',
// subField2: 'column_subField2',
// } as FieldMetadataTargetColumnMap,
// },
// ] as FieldMetadata[];
// mockOptions = {
// targetTableName: 'TestTable',
// info: {} as GraphQLResolveInfo,
// fieldMetadataCollection,
// };
// queryBuilder = new PGGraphQLQueryBuilder(mockOptions);
// });
// test('findMany generates correct query with no arguments', () => {
// const query = queryBuilder.findMany();
// expect(normalizeWhitespace(query)).toBe(
// normalizeWhitespace(`
// query {
// TestTableCollection {
// name: column_name
// age: column_age
// ___complexField_subField1: column_subField1
// ___complexField_subField2: column_subField2
// }
// }
// `),
// );
// });
// test('findMany generates correct query with filter parameters', () => {
// const args = {
// filter: {
// name: { eq: 'Alice' },
// age: { gt: 20 },
// },
// };
// const query = queryBuilder.findMany(args);
// expect(normalizeWhitespace(query)).toBe(
// normalizeWhitespace(`
// query {
// TestTableCollection(filter: { column_name: { eq: "Alice" }, column_age: { gt: 20 } }) {
// name: column_name
// age: column_age
// ___complexField_subField1: column_subField1
// ___complexField_subField2: column_subField2
// }
// }
// `),
// );
// });
// test('findMany generates correct query with combined pagination parameters', () => {
// const args = {
// first: 5,
// after: 'someCursor',
// before: 'anotherCursor',
// last: 3,
// };
// const query = queryBuilder.findMany(args);
// expect(normalizeWhitespace(query)).toBe(
// normalizeWhitespace(`
// query {
// TestTableCollection(
// first: 5,
// after: "someCursor",
// before: "anotherCursor",
// last: 3
// ) {
// name: column_name
// age: column_age
// ___complexField_subField1: column_subField1
// ___complexField_subField2: column_subField2
// }
// }
// `),
// );
// });
// test('findOne generates correct query with ID filter', () => {
// const args = { filter: { id: { eq: testUUID } } };
// const query = queryBuilder.findOne(args);
// expect(normalizeWhitespace(query)).toBe(
// normalizeWhitespace(`
// query {
// TestTableCollection(filter: { id: { eq: "${testUUID}" } }) {
// edges {
// node {
// name: column_name
// age: column_age
// ___complexField_subField1: column_subField1
// ___complexField_subField2: column_subField2
// }
// }
// }
// }
// `),
// );
// });
// test('createMany generates correct mutation with complex and nested fields', () => {
// const args = {
// data: [
// {
// name: 'Alice',
// age: 30,
// complexField: {
// subField1: 'data1',
// subField2: 'data2',
// },
// },
// ],
// };
// const query = queryBuilder.createMany(args);
// expect(normalizeWhitespace(query)).toBe(
// normalizeWhitespace(`
// mutation {
// insertIntoTestTableCollection(objects: [{
// id: "${testUUID}",
// column_name: "Alice",
// column_age: 30,
// column_subField1: "data1",
// column_subField2: "data2"
// }]) {
// affectedCount
// records {
// name: column_name
// age: column_age
// ___complexField_subField1: column_subField1
// ___complexField_subField2: column_subField2
// }
// }
// }
// `),
// );
// });
// test('updateOne generates correct mutation with complex and nested fields', () => {
// const args = {
// id: '1',
// data: {
// name: 'Bob',
// age: 40,
// complexField: {
// subField1: 'newData1',
// subField2: 'newData2',
// },
// },
// };
// const query = queryBuilder.updateOne(args);
// expect(normalizeWhitespace(query)).toBe(
// normalizeWhitespace(`
// mutation {
// updateTestTableCollection(
// set: {
// column_name: "Bob",
// column_age: 40,
// column_subField1: "newData1",
// column_subField2: "newData2"
// },
// filter: { id: { eq: "1" } }
// ) {
// affectedCount
// records {
// name: column_name
// age: column_age
// ___complexField_subField1: column_subField1
// ___complexField_subField2: column_subField2
// }
// }
// }
// `),
// );
// });
// });
it('should pass', () => {
expect(true).toBe(true);
});

View File

@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
@Injectable()
export class ArgsAliasFactory {
create(
args: Record<string, any>,
fieldMetadataCollection: FieldMetadataInterface[],
): Record<string, any> {
const fieldMetadataMap = new Map(
fieldMetadataCollection.map((fieldMetadata) => [
fieldMetadata.name,
fieldMetadata,
]),
);
return this.createArgsObjectRecursive(args, fieldMetadataMap);
}
private createArgsObjectRecursive(
args: Record<string, any>,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
) {
// If it's not an object, we don't need to do anything
if (typeof args !== 'object' || args === null) {
return args;
}
// If it's an array, we need to map all items
if (Array.isArray(args)) {
return args.map((arg) =>
this.createArgsObjectRecursive(arg, fieldMetadataMap),
);
}
const newArgs = {};
for (const [key, value] of Object.entries(args)) {
const fieldMetadata = fieldMetadataMap.get(key);
// If it's a special complex field, we need to map all columns
if (
fieldMetadata &&
typeof value === 'object' &&
value !== null &&
Object.values(fieldMetadata.targetColumnMap).length > 1
) {
for (const [subKey, subValue] of Object.entries(value)) {
const mappedKey = fieldMetadata.targetColumnMap[subKey];
if (mappedKey) {
newArgs[mappedKey] = subValue;
}
}
} else if (fieldMetadata) {
// Otherwise we just need to map the value
const mappedKey = fieldMetadata.targetColumnMap.value;
newArgs[mappedKey ?? key] = value;
} else {
// Recurse if value is a nested object, otherwise append field or alias
newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap);
}
}
return newArgs;
}
}

View File

@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
import { ArgsAliasFactory } from './args-alias.factory';
@Injectable()
export class ArgsStringFactory {
constructor(private readonly argsAliasFactory: ArgsAliasFactory) {}
create(
initialArgs: Record<string, any> | undefined,
fieldMetadataCollection: FieldMetadataInterface[],
): string | null {
if (!initialArgs) {
return null;
}
let argsString = '';
const computedArgs = this.argsAliasFactory.create(
initialArgs,
fieldMetadataCollection,
);
for (const key in computedArgs) {
// Check if the value is not undefined
if (computedArgs[key] === undefined) {
continue;
}
if (typeof computedArgs[key] === 'string') {
// If it's a string, add quotes
argsString += `${key}: "${computedArgs[key]}", `;
} else if (
typeof computedArgs[key] === 'object' &&
computedArgs[key] !== null
) {
// If it's an object (and not null), stringify it
argsString += `${key}: ${stringifyWithoutKeyQuote(
computedArgs[key],
)}, `;
} else {
// For other types (number, boolean), add as is
argsString += `${key}: ${computedArgs[key]}, `;
}
}
// Remove trailing comma and space, if present
if (argsString.endsWith(', ')) {
argsString = argsString.slice(0, -2);
}
return argsString;
}
}

View File

@ -0,0 +1,54 @@
import { Injectable, Logger } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { Record as IRecord } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
import { CreateManyResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
import { FieldsStringFactory } from './fields-string.factory';
import { ArgsAliasFactory } from './args-alias.factory';
@Injectable()
export class CreateManyQueryFactory {
private readonly logger = new Logger(CreateManyQueryFactory.name);
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsAliasFactory: ArgsAliasFactory,
) {}
async create<Record extends IRecord = IRecord>(
args: CreateManyResolverArgs<Record>,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
);
const computedArgs = this.argsAliasFactory.create(
args,
options.fieldMetadataCollection,
);
return `
mutation {
insertInto${
options.targetTableName
}Collection(objects: ${stringifyWithoutKeyQuote(
computedArgs.data.map((datum) => ({
id: uuidv4(),
...datum,
})),
)}) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { DeleteManyResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class DeleteManyQueryFactory {
constructor(private readonly fieldsStringFactory: FieldsStringFactory) {}
async create(
args: DeleteManyResolverArgs,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
);
return `
mutation {
deleteFrom${
options.targetTableName
}Collection(filter: ${stringifyWithoutKeyQuote(args.filter)}) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

@ -0,0 +1,34 @@
import { Injectable, Logger } from '@nestjs/common';
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { DeleteOneResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class DeleteOneQueryFactory {
private readonly logger = new Logger(DeleteOneQueryFactory.name);
constructor(private readonly fieldsStringFactory: FieldsStringFactory) {}
async create(
args: DeleteOneResolverArgs,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
);
return `
mutation {
deleteFrom${options.targetTableName}Collection(filter: { id: { eq: "${args.id}" } }) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

@ -0,0 +1,27 @@
import { ArgsAliasFactory } from './args-alias.factory';
import { ArgsStringFactory } from './args-string.factory';
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
import { CreateManyQueryFactory } from './create-many-query.factory';
import { DeleteOneQueryFactory } from './delete-one-query.factory';
import { FieldAliasFacotry } from './field-alias.factory';
import { FieldsStringFactory } from './fields-string.factory';
import { FindManyQueryFactory } from './find-many-query.factory';
import { FindOneQueryFactory } from './find-one-query.factory';
import { UpdateOneQueryFactory } from './update-one-query.factory';
import { UpdateManyQueryFactory } from './update-many-query.factory';
import { DeleteManyQueryFactory } from './delete-many-query.factory';
export const workspaceQueryBuilderFactories = [
ArgsAliasFactory,
ArgsStringFactory,
RelationFieldAliasFactory,
CreateManyQueryFactory,
DeleteOneQueryFactory,
FieldAliasFacotry,
FieldsStringFactory,
FindManyQueryFactory,
FindOneQueryFactory,
UpdateOneQueryFactory,
UpdateManyQueryFactory,
DeleteManyQueryFactory,
];

View File

@ -0,0 +1,30 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
@Injectable()
export class FieldAliasFacotry {
private readonly logger = new Logger(FieldAliasFacotry.name);
create(fieldKey: string, fieldMetadata: FieldMetadataInterface) {
const entries = Object.entries(fieldMetadata.targetColumnMap);
if (entries.length === 0) {
return null;
}
if (entries.length === 1) {
// If there is only one value, use it as the alias
const alias = entries[0][1];
return `${fieldKey}: ${alias}`;
}
// Otherwise it means it's a special type with multiple values, so we need map all columns
return `
${entries
.map(([key, value]) => `___${fieldMetadata.name}_${key}: ${value}`)
.join('\n')}
`;
}
}

View File

@ -0,0 +1,98 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields';
import isEmpty from 'lodash.isempty';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util';
import { FieldAliasFacotry } from './field-alias.factory';
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
@Injectable()
export class FieldsStringFactory {
private readonly logger = new Logger(FieldsStringFactory.name);
constructor(
private readonly fieldAliasFactory: FieldAliasFacotry,
private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
) {}
create(
info: GraphQLResolveInfo,
fieldMetadataCollection: FieldMetadataInterface[],
): Promise<string> {
// @ts-expect-error Todo: Fix typing error
const selectedFields: Record<string, any> = graphqlFields(info);
return this.createFieldsStringRecursive(
info,
selectedFields,
fieldMetadataCollection,
);
}
async createFieldsStringRecursive(
info: GraphQLResolveInfo,
selectedFields: Record<string, any>,
fieldMetadataCollection: FieldMetadataInterface[],
accumulator = '',
): Promise<string> {
const fieldMetadataMap = new Map(
fieldMetadataCollection.map((metadata) => [metadata.name, metadata]),
);
for (const [fieldKey, fieldValue] of Object.entries(selectedFields)) {
let fieldAlias: string | null;
if (fieldMetadataMap.has(fieldKey)) {
// We're sure that the field exists in the map after this if condition
// ES6 should tackle that more properly
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fieldMetadata = fieldMetadataMap.get(fieldKey)!;
// If the field is a relation field, we need to create a special alias
if (isRelationFieldMetadataType(fieldMetadata.type)) {
const alias = await this.relationFieldAliasFactory.create(
fieldKey,
fieldValue,
fieldMetadata,
info,
);
fieldAlias = alias;
} else {
// Otherwise we just need to create a simple alias
const alias = this.fieldAliasFactory.create(fieldKey, fieldMetadata);
fieldAlias = alias;
}
}
fieldAlias ??= fieldKey;
// Recurse if value is a nested object, otherwise append field or alias
if (
!fieldMetadataMap.has(fieldKey) &&
fieldValue &&
typeof fieldValue === 'object' &&
!isEmpty(fieldValue)
) {
accumulator += `${fieldKey} {\n`;
accumulator = await this.createFieldsStringRecursive(
info,
fieldValue,
fieldMetadataCollection,
accumulator,
);
accumulator += `}\n`;
} else {
accumulator += `${fieldAlias}\n`;
}
}
return accumulator;
}
}

View File

@ -0,0 +1,48 @@
import { Injectable, Logger } from '@nestjs/common';
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import {
RecordFilter,
RecordOrderBy,
} from 'src/workspace/workspace-query-builder/interfaces/record.interface';
import { FindManyResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ArgsStringFactory } from './args-string.factory';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class FindManyQueryFactory {
private readonly logger = new Logger(FindManyQueryFactory.name);
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsStringFactory: ArgsStringFactory,
) {}
async create<
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
);
const argsString = this.argsStringFactory.create(
args,
options.fieldMetadataCollection,
);
return `
query {
${options.targetTableName}Collection${
argsString ? `(${argsString})` : ''
} {
${fieldsString}
}
}
`;
}
}

View File

@ -0,0 +1,46 @@
import { Injectable, Logger } from '@nestjs/common';
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { RecordFilter } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
import { FindOneResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ArgsStringFactory } from './args-string.factory';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class FindOneQueryFactory {
private readonly logger = new Logger(FindOneQueryFactory.name);
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsStringFactory: ArgsStringFactory,
) {}
async create<Filter extends RecordFilter = RecordFilter>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
);
const argsString = this.argsStringFactory.create(
args,
options.fieldMetadataCollection,
);
return `
query {
${options.targetTableName}Collection${
argsString ? `(${argsString})` : ''
} {
edges {
node {
${fieldsString}
}
}
}
}
`;
}
}

View File

@ -0,0 +1,140 @@
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { GraphQLResolveInfo } from 'graphql';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import {
deduceRelationDirection,
RelationDirection,
} from 'src/workspace/utils/deduce-relation-direction.util';
import { getFieldArgumentsByKey } from 'src/workspace/workspace-query-builder/utils/get-field-arguments-by-key.util';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
import { FieldsStringFactory } from './fields-string.factory';
import { ArgsStringFactory } from './args-string.factory';
@Injectable()
export class RelationFieldAliasFactory {
private logger = new Logger(RelationFieldAliasFactory.name);
constructor(
@Inject(forwardRef(() => FieldsStringFactory))
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsStringFactory: ArgsStringFactory,
private readonly objectMetadataService: ObjectMetadataService,
) {}
create(
fieldKey: string,
fieldValue: any,
fieldMetadata: FieldMetadataInterface,
info: GraphQLResolveInfo,
): Promise<string> {
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
throw new Error(`Field ${fieldMetadata.name} is not a relation field`);
}
return this.createRelationAlias(fieldKey, fieldValue, fieldMetadata, info);
}
private async createRelationAlias(
fieldKey: string,
fieldValue: any,
fieldMetadata: FieldMetadataInterface,
info: GraphQLResolveInfo,
): Promise<string> {
const relationMetadata =
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
if (!relationMetadata) {
throw new Error(
`Relation metadata not found for field ${fieldMetadata.name}`,
);
}
if (!fieldMetadata.workspaceId) {
throw new Error(
`Workspace id not found for field ${fieldMetadata.name} in object metadata ${fieldMetadata.objectMetadataId}`,
);
}
const relationDirection = deduceRelationDirection(
fieldMetadata.objectMetadataId,
relationMetadata,
);
// Retrieve the referenced object metadata based on the relation direction
// Mandatory to handle n+n relations
const referencedObjectMetadata =
await this.objectMetadataService.findOneWithinWorkspace(
fieldMetadata.workspaceId,
{
where: {
id:
relationDirection == RelationDirection.TO
? relationMetadata.fromObjectMetadataId
: relationMetadata.toObjectMetadataId,
},
},
);
if (!referencedObjectMetadata) {
throw new Error(
`Referenced object metadata not found for relation ${relationMetadata.id}`,
);
}
// If it's a relation destination is of kind MANY, we need to add the collection suffix and extract the args
if (
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY &&
relationDirection === RelationDirection.FROM
) {
const args = getFieldArgumentsByKey(info, fieldKey);
const argsString = this.argsStringFactory.create(
args,
referencedObjectMetadata.fields ?? [],
);
const fieldsString =
await this.fieldsStringFactory.createFieldsStringRecursive(
info,
fieldValue,
referencedObjectMetadata.fields ?? [],
);
return `
${fieldKey}: ${referencedObjectMetadata.targetTableName}Collection${
argsString ? `(${argsString})` : ''
} {
${fieldsString}
}
`;
}
let relationAlias = fieldMetadata.isCustom
? `${fieldKey}: ${fieldMetadata.targetColumnMap.value}`
: fieldKey;
// For one to one relations, pg_graphql use the targetTableName on the side that is not storing the foreign key
// so we need to alias it to the field key
if (
relationMetadata.relationType === RelationMetadataType.ONE_TO_ONE &&
relationDirection === RelationDirection.FROM
) {
relationAlias = `${fieldKey}: ${referencedObjectMetadata.targetTableName}`;
}
const fieldsString =
await this.fieldsStringFactory.createFieldsStringRecursive(
info,
fieldValue,
referencedObjectMetadata.fields ?? [],
);
// Otherwise it means it's a relation destination is of kind ONE
return `
${relationAlias} {
${fieldsString}
}
`;
}
}

View File

@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import {
Record as IRecord,
RecordFilter,
} from 'src/workspace/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { UpdateManyResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
import { FieldsStringFactory } from 'src/workspace/workspace-query-builder/factories/fields-string.factory';
import { ArgsAliasFactory } from 'src/workspace/workspace-query-builder/factories/args-alias.factory';
@Injectable()
export class UpdateManyQueryFactory {
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsAliasFactory: ArgsAliasFactory,
) {}
async create<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: UpdateManyResolverArgs<Record, Filter>,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
);
const computedArgs = this.argsAliasFactory.create(
args,
options.fieldMetadataCollection,
);
return `
mutation {
update${options.targetTableName}Collection(
set: ${stringifyWithoutKeyQuote(computedArgs.data)},
filter: ${stringifyWithoutKeyQuote(args.filter)},
) {
affectedCount
records {
${fieldsString}
}
}
}`;
}
}

View File

@ -0,0 +1,49 @@
import { Injectable, Logger } from '@nestjs/common';
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { Record as IRecord } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
import { UpdateOneResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
import { FieldsStringFactory } from './fields-string.factory';
import { ArgsAliasFactory } from './args-alias.factory';
@Injectable()
export class UpdateOneQueryFactory {
private readonly logger = new Logger(UpdateOneQueryFactory.name);
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsAliasFactory: ArgsAliasFactory,
) {}
async create<Record extends IRecord = IRecord>(
args: UpdateOneResolverArgs<Record>,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
);
const computedArgs = this.argsAliasFactory.create(
args,
options.fieldMetadataCollection,
);
return `
mutation {
update${
options.targetTableName
}Collection(set: ${stringifyWithoutKeyQuote(
computedArgs.data,
)}, filter: { id: { eq: "${computedArgs.id}" } }) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

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

View File

@ -0,0 +1,9 @@
import { GraphQLResolveInfo } from 'graphql';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
export interface WorkspaceQueryBuilderOptions {
targetTableName: string;
info: GraphQLResolveInfo;
fieldMetadataCollection: FieldMetadataInterface[];
}

View File

@ -0,0 +1,58 @@
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
describe('stringifyWithoutKeyQuote', () => {
test('should stringify object correctly without quotes around keys', () => {
const obj = { name: 'John', age: 30, isAdmin: false };
const result = stringifyWithoutKeyQuote(obj);
expect(result).toBe('{name:"John",age:30,isAdmin:false}');
});
test('should handle nested objects', () => {
const obj = {
name: 'John',
age: 30,
address: { city: 'New York', zipCode: 10001 },
};
const result = stringifyWithoutKeyQuote(obj);
expect(result).toBe(
'{name:"John",age:30,address:{city:"New York",zipCode:10001}}',
);
});
test('should handle arrays', () => {
const obj = {
name: 'John',
age: 30,
hobbies: ['reading', 'movies', 'hiking'],
};
const result = stringifyWithoutKeyQuote(obj);
expect(result).toBe(
'{name:"John",age:30,hobbies:["reading","movies","hiking"]}',
);
});
test('should handle empty objects', () => {
const obj = {};
const result = stringifyWithoutKeyQuote(obj);
expect(result).toBe('{}');
});
test('should handle numbers, strings, and booleans', () => {
const num = 10;
const str = 'Hello';
const bool = false;
expect(stringifyWithoutKeyQuote(num)).toBe('10');
expect(stringifyWithoutKeyQuote(str)).toBe('"Hello"');
expect(stringifyWithoutKeyQuote(bool)).toBe('false');
});
test('should handle null and undefined', () => {
expect(stringifyWithoutKeyQuote(null)).toBe('null');
expect(stringifyWithoutKeyQuote(undefined)).toBe(undefined);
});
});

View File

@ -0,0 +1,96 @@
import {
GraphQLResolveInfo,
SelectionSetNode,
Kind,
SelectionNode,
FieldNode,
InlineFragmentNode,
ValueNode,
} from 'graphql';
const isFieldNode = (node: SelectionNode): node is FieldNode =>
node.kind === Kind.FIELD;
const isInlineFragmentNode = (
node: SelectionNode,
): node is InlineFragmentNode => node.kind === Kind.INLINE_FRAGMENT;
const findFieldNode = (
selectionSet: SelectionSetNode | undefined,
key: string,
): FieldNode | null => {
if (!selectionSet) return null;
let field: FieldNode | null = null;
for (const selection of selectionSet.selections) {
// We've found the field
if (isFieldNode(selection) && selection.name.value === key) {
return selection;
}
// Recursively search for the field in nested selections
if (
(isFieldNode(selection) || isInlineFragmentNode(selection)) &&
selection.selectionSet
) {
field = findFieldNode(selection.selectionSet, key);
// If we find the field in a nested selection, stop searching
if (field) break;
}
}
return field;
};
const parseValueNode = (
valueNode: ValueNode,
variables: GraphQLResolveInfo['variableValues'],
) => {
switch (valueNode.kind) {
case Kind.VARIABLE:
return variables[valueNode.name.value];
case Kind.INT:
case Kind.FLOAT:
return Number(valueNode.value);
case Kind.STRING:
case Kind.BOOLEAN:
case Kind.ENUM:
return valueNode.value;
case Kind.LIST:
return valueNode.values.map((value) => parseValueNode(value, variables));
case Kind.OBJECT:
return valueNode.fields.reduce((obj, field) => {
obj[field.name.value] = parseValueNode(field.value, variables);
return obj;
}, {});
default:
return null;
}
};
export const getFieldArgumentsByKey = (
info: GraphQLResolveInfo,
fieldKey: string,
): Record<string, any> => {
// Start from the first top-level field node and search recursively
const targetField = findFieldNode(info.fieldNodes[0].selectionSet, fieldKey);
// If the field is not found, throw an error
if (!targetField) {
throw new Error(`Field "${fieldKey}" not found.`);
}
// Extract the arguments from the field we've found
const args: Record<string, any> = {};
if (targetField.arguments && targetField.arguments.length) {
for (const arg of targetField.arguments) {
args[arg.name.value] = parseValueNode(arg.value, info.variableValues);
}
}
return args;
};

View File

@ -0,0 +1,6 @@
export const stringifyWithoutKeyQuote = (obj: any) => {
const jsonString = JSON.stringify(obj);
const jsonWithoutQuotes = jsonString?.replace(/"(\w+)"\s*:/g, '$1:');
return jsonWithoutQuotes;
};

View File

@ -0,0 +1,95 @@
import { Injectable, Logger } from '@nestjs/common';
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import {
Record as IRecord,
RecordFilter,
RecordOrderBy,
} from 'src/workspace/workspace-query-builder/interfaces/record.interface';
import {
FindManyResolverArgs,
FindOneResolverArgs,
CreateManyResolverArgs,
UpdateOneResolverArgs,
DeleteOneResolverArgs,
UpdateManyResolverArgs,
DeleteManyResolverArgs,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { FindManyQueryFactory } from './factories/find-many-query.factory';
import { FindOneQueryFactory } from './factories/find-one-query.factory';
import { CreateManyQueryFactory } from './factories/create-many-query.factory';
import { UpdateOneQueryFactory } from './factories/update-one-query.factory';
import { DeleteOneQueryFactory } from './factories/delete-one-query.factory';
import { UpdateManyQueryFactory } from './factories/update-many-query.factory';
import { DeleteManyQueryFactory } from './factories/delete-many-query.factory';
@Injectable()
export class WorkspaceQueryBuilderFactory {
private readonly logger = new Logger(WorkspaceQueryBuilderFactory.name);
constructor(
private readonly findManyQueryFactory: FindManyQueryFactory,
private readonly findOneQueryFactory: FindOneQueryFactory,
private readonly createManyQueryFactory: CreateManyQueryFactory,
private readonly updateOneQueryFactory: UpdateOneQueryFactory,
private readonly deleteOneQueryFactory: DeleteOneQueryFactory,
private readonly updateManyQueryFactory: UpdateManyQueryFactory,
private readonly deleteManyQueryFactory: DeleteManyQueryFactory,
) {}
findMany<
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.findManyQueryFactory.create<Filter, OrderBy>(args, options);
}
findOne<Filter extends RecordFilter = RecordFilter>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.findOneQueryFactory.create<Filter>(args, options);
}
createMany<Record extends IRecord = IRecord>(
args: CreateManyResolverArgs<Record>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.createManyQueryFactory.create<Record>(args, options);
}
updateOne<Record extends IRecord = IRecord>(
initialArgs: UpdateOneResolverArgs<Record>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.updateOneQueryFactory.create<Record>(initialArgs, options);
}
deleteOne(
args: DeleteOneResolverArgs,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.deleteOneQueryFactory.create(args, options);
}
updateMany<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: UpdateManyResolverArgs<Record, Filter>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.updateManyQueryFactory.create(args, options);
}
deleteMany<Filter extends RecordFilter = RecordFilter>(
args: DeleteManyResolverArgs<Filter>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.deleteManyQueryFactory.create(args, options);
}
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
import { WorkspaceQueryBuilderFactory } from './workspace-query-builder.factory';
import { workspaceQueryBuilderFactories } from './factories/factories';
@Module({
imports: [ObjectMetadataModule],
providers: [...workspaceQueryBuilderFactories, WorkspaceQueryBuilderFactory],
exports: [WorkspaceQueryBuilderFactory],
})
export class WorkspaceQueryBuilderModule {}

View File

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

View File

@ -0,0 +1,10 @@
import { GraphQLResolveInfo } from 'graphql';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
export interface WorkspaceQueryRunnerOptions {
targetTableName: string;
workspaceId: string;
info: GraphQLResolveInfo;
fieldMetadataCollection: FieldMetadataInterface[];
}

View File

@ -0,0 +1,105 @@
import {
isSpecialKey,
handleSpecialKey,
parseResult,
} from 'src/workspace/workspace-query-runner/utils/parse-result.util';
describe('isSpecialKey', () => {
test('should return true if the key starts with "___"', () => {
expect(isSpecialKey('___specialKey')).toBe(true);
});
test('should return false if the key does not start with "___"', () => {
expect(isSpecialKey('normalKey')).toBe(false);
});
});
describe('handleSpecialKey', () => {
let result;
beforeEach(() => {
result = {};
});
test('should correctly process a special key and add it to the result object', () => {
handleSpecialKey(result, '___complexField_link', 'value1');
expect(result).toEqual({
complexField: {
link: 'value1',
},
});
});
test('should add values under the same newKey if called multiple times', () => {
handleSpecialKey(result, '___complexField_link', 'value1');
handleSpecialKey(result, '___complexField_text', 'value2');
expect(result).toEqual({
complexField: {
link: 'value1',
text: 'value2',
},
});
});
test('should not create a new field if the special key is not correctly formed', () => {
handleSpecialKey(result, '___complexField', 'value1');
expect(result).toEqual({});
});
});
describe('parseResult', () => {
test('should recursively parse an object and handle special keys', () => {
const obj = {
normalField: 'value1',
___specialField_part1: 'value2',
nested: {
___specialFieldNested_part2: 'value3',
},
};
const expectedResult = {
normalField: 'value1',
specialField: {
part1: 'value2',
},
nested: {
specialFieldNested: {
part2: 'value3',
},
},
};
expect(parseResult(obj)).toEqual(expectedResult);
});
test('should handle arrays and parse each element', () => {
const objArray = [
{
___specialField_part1: 'value1',
},
{
___specialField_part2: 'value2',
},
];
const expectedResult = [
{
specialField: {
part1: 'value1',
},
},
{
specialField: {
part2: 'value2',
},
},
];
expect(parseResult(objArray)).toEqual(expectedResult);
});
test('should return the original value if it is not an object or array', () => {
expect(parseResult('stringValue')).toBe('stringValue');
expect(parseResult(12345)).toBe(12345);
});
});

View File

@ -0,0 +1,51 @@
export const isSpecialKey = (key: string): boolean => {
return key.startsWith('___');
};
export const handleSpecialKey = (
result: any,
key: string,
value: any,
): void => {
const parts = key.split('_').filter((part) => part);
// If parts don't contain enough information, return without altering result
if (parts.length < 2) {
return;
}
const newKey = parts.slice(0, -1).join('');
const subKey = parts[parts.length - 1];
if (!result[newKey]) {
result[newKey] = {};
}
result[newKey][subKey] = value;
};
export const parseResult = (obj: any): any => {
if (obj === null || typeof obj !== 'object' || typeof obj === 'function') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => parseResult(item));
}
const result: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
result[key] = parseResult(obj[key]);
} else if (isSpecialKey(key)) {
handleSpecialKey(result, key, obj[key]);
} else {
result[key] = obj[key];
}
}
}
return result;
};

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { WorkspaceQueryBuilderModule } from 'src/workspace/workspace-query-builder/workspace-query-builder.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
@Module({
imports: [WorkspaceQueryBuilderModule, WorkspaceDataSourceModule],
providers: [WorkspaceQueryRunnerService],
exports: [WorkspaceQueryRunnerService],
})
export class WorkspaceQueryRunnerModule {}

View File

@ -0,0 +1,227 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { IConnection } from 'src/utils/pagination/interfaces/connection.interface';
import {
Record as IRecord,
RecordFilter,
RecordOrderBy,
} from 'src/workspace/workspace-query-builder/interfaces/record.interface';
import {
CreateManyResolverArgs,
CreateOneResolverArgs,
DeleteManyResolverArgs,
DeleteOneResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
UpdateManyResolverArgs,
UpdateOneResolverArgs,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryBuilderFactory } from 'src/workspace/workspace-query-builder/workspace-query-builder.factory';
import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-optionts.interface';
import {
PGGraphQLMutation,
PGGraphQLResult,
} from './interfaces/pg-graphql.interface';
@Injectable()
export class WorkspaceQueryRunnerService {
private readonly logger = new Logger(WorkspaceQueryRunnerService.name);
constructor(
private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
async findMany<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<Record> | undefined> {
const { workspaceId, targetTableName } = options;
const query = await this.workspaceQueryBuilderFactory.findMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
return this.parseResult<IConnection<Record>>(result, targetTableName, '');
}
async findOne<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
if (!args.filter || Object.keys(args.filter).length === 0) {
throw new BadRequestException('Missing filter argument');
}
const { workspaceId, targetTableName } = options;
const query = await this.workspaceQueryBuilderFactory.findOne(
args,
options,
);
const result = await this.execute(query, workspaceId);
const parsedResult = this.parseResult<IConnection<Record>>(
result,
targetTableName,
'',
);
return parsedResult?.edges?.[0]?.node;
}
async createMany<Record extends IRecord = IRecord>(
args: CreateManyResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, targetTableName } = options;
const query = await this.workspaceQueryBuilderFactory.createMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(
result,
targetTableName,
'insertInto',
)?.records;
}
async createOne<Record extends IRecord = IRecord>(
args: CreateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const records = await this.createMany({ data: [args.data] }, options);
return records?.[0];
}
async updateOne<Record extends IRecord = IRecord>(
args: UpdateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const { workspaceId, targetTableName } = options;
const query = await this.workspaceQueryBuilderFactory.updateOne(
args,
options,
);
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(
result,
targetTableName,
'update',
)?.records?.[0];
}
async deleteOne<Record extends IRecord = IRecord>(
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const { workspaceId, targetTableName } = options;
const query = await this.workspaceQueryBuilderFactory.deleteOne(
args,
options,
);
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(
result,
targetTableName,
'deleteFrom',
)?.records?.[0];
}
async updateMany<Record extends IRecord = IRecord>(
args: UpdateManyResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, targetTableName } = options;
const query = await this.workspaceQueryBuilderFactory.updateMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(
result,
targetTableName,
'update',
)?.records;
}
async deleteMany<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: DeleteManyResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, targetTableName } = options;
const query = await this.workspaceQueryBuilderFactory.deleteMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(
result,
targetTableName,
'deleteFrom',
)?.records;
}
private async execute(
query: string,
workspaceId: string,
): Promise<PGGraphQLResult | undefined> {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
await workspaceDataSource?.query(`
SET search_path TO ${this.workspaceDataSourceService.getSchemaName(
workspaceId,
)};
`);
const results = await workspaceDataSource?.query<PGGraphQLResult>(`
SELECT graphql.resolve($$
${query}
$$);
`);
return results;
}
private parseResult<Result>(
graphqlResult: PGGraphQLResult | undefined,
targetTableName: string,
command: string,
): Result {
const entityKey = `${command}${targetTableName}Collection`;
const result = graphqlResult?.[0]?.resolve?.data?.[entityKey];
const errors = graphqlResult?.[0]?.resolve?.errors;
if (Array.isArray(errors) && errors.length > 0) {
console.error('GraphQL errors', errors);
}
if (!result) {
throw new BadRequestException('Malformed result from GraphQL query');
}
return parseResult(result);
}
}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import {
CreateManyResolverArgs,
Resolver,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class CreateManyResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'createMany' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<CreateManyResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.createMany(args, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
};
}
}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import {
CreateOneResolverArgs,
Resolver,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class CreateOneResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'createOne' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<CreateOneResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.createOne(args, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
};
}
}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import {
DeleteManyResolverArgs,
Resolver,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class DeleteManyResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'deleteMany' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<DeleteManyResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.deleteMany(args, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
};
}
}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import {
DeleteOneResolverArgs,
Resolver,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class DeleteOneResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'deleteOne' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<DeleteOneResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.deleteOne(args, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
};
}
}

View File

@ -0,0 +1,35 @@
import { UpdateManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/update-many-resolver.factory';
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';
import { DeleteManyResolverFactory } from './delete-many-resolver.factory';
export const workspaceResolverBuilderFactories = [
FindManyResolverFactory,
FindOneResolverFactory,
CreateManyResolverFactory,
CreateOneResolverFactory,
UpdateOneResolverFactory,
DeleteOneResolverFactory,
UpdateManyResolverFactory,
DeleteManyResolverFactory,
];
export const workspaceResolverBuilderMethodNames = {
queries: [
FindManyResolverFactory.methodName,
FindOneResolverFactory.methodName,
],
mutations: [
CreateManyResolverFactory.methodName,
CreateOneResolverFactory.methodName,
UpdateOneResolverFactory.methodName,
DeleteOneResolverFactory.methodName,
UpdateManyResolverFactory.methodName,
DeleteManyResolverFactory.methodName,
],
} as const;

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import {
FindManyResolverArgs,
Resolver,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class FindManyResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'findMany' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<FindManyResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.findMany(args, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
};
}
}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import {
FindOneResolverArgs,
Resolver,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class FindOneResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'findOne' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<FindOneResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.findOne(args, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
};
}
}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import {
Resolver,
UpdateManyResolverArgs,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class UpdateManyResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'updateMany' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<UpdateManyResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.updateMany(args, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
};
}
}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import {
Resolver,
UpdateOneResolverArgs,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class UpdateOneResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'updateOne' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<UpdateOneResolverArgs> {
const internalContext = context;
return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.updateOne(args, {
targetTableName: internalContext.targetTableName,
workspaceId: internalContext.workspaceId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
});
};
}
}

View File

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

View File

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

View File

@ -0,0 +1,71 @@
import { GraphQLFieldResolver } from 'graphql';
import {
Record,
RecordFilter,
RecordOrderBy,
} from 'src/workspace/workspace-query-builder/interfaces/record.interface';
import { workspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/factories/factories';
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 UpdateManyResolverArgs<
Data extends Record = Record,
Filter = any,
> {
filter: Filter;
data: Data;
}
export interface DeleteOneResolverArgs {
id: string;
}
export interface DeleteManyResolverArgs<Filter = any> {
filter: Filter;
}
export type WorkspaceResolverBuilderQueryMethodNames =
(typeof workspaceResolverBuilderMethodNames.queries)[number];
export type WorkspaceResolverBuilderMutationMethodNames =
(typeof workspaceResolverBuilderMethodNames.mutations)[number];
export type WorkspaceResolverBuilderMethodNames =
| WorkspaceResolverBuilderQueryMethodNames
| WorkspaceResolverBuilderMutationMethodNames;
export interface WorkspaceResolverBuilderMethods {
readonly queries: readonly WorkspaceResolverBuilderQueryMethodNames[];
readonly mutations: readonly WorkspaceResolverBuilderMutationMethodNames[];
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { WorkspaceQueryRunnerModule } from 'src/workspace/workspace-query-runner/workspace-query-runner.module';
import { WorkspaceResolverFactory } from './workspace-resolver.factory';
import { workspaceResolverBuilderFactories } from './factories/factories';
@Module({
imports: [WorkspaceQueryRunnerModule],
providers: [...workspaceResolverBuilderFactories, WorkspaceResolverFactory],
exports: [WorkspaceResolverFactory],
})
export class WorkspaceResolverBuilderModule {}

View File

@ -0,0 +1,109 @@
import { Injectable, Logger } from '@nestjs/common';
import { IResolvers } from '@graphql-tools/utils';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { getResolverName } from 'src/workspace/utils/get-resolver-name.util';
import { UpdateManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/update-many-resolver.factory';
import { DeleteManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/delete-many-resolver.factory';
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 {
WorkspaceResolverBuilderMethodNames,
WorkspaceResolverBuilderMethods,
} from './interfaces/workspace-resolvers-builder.interface';
import { WorkspaceResolverBuilderFactoryInterface } from './interfaces/workspace-resolver-builder-factory.interface';
@Injectable()
export class WorkspaceResolverFactory {
private readonly logger = new Logger(WorkspaceResolverFactory.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,
private readonly updateManyResolverFactory: UpdateManyResolverFactory,
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
) {}
async create(
workspaceId: string,
objectMetadataCollection: ObjectMetadataInterface[],
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
): Promise<IResolvers> {
const factories = new Map<
WorkspaceResolverBuilderMethodNames,
WorkspaceResolverBuilderFactoryInterface
>([
['findMany', this.findManyResolverFactory],
['findOne', this.findOneResolverFactory],
['createMany', this.createManyResolverFactory],
['createOne', this.createOneResolverFactory],
['updateOne', this.updateOneResolverFactory],
['deleteOne', this.deleteOneResolverFactory],
['updateMany', this.updateManyResolverFactory],
['deleteMany', this.deleteManyResolverFactory],
]);
const resolvers: IResolvers = {
Query: {},
Mutation: {},
};
for (const objectMetadata of objectMetadataCollection) {
// Generate query resolvers
for (const methodName of workspaceResolverBuilderMethods.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 workspaceResolverBuilderMethods.mutations) {
const resolverName = getResolverName(objectMetadata, methodName);
const resolverFactory = factories.get(methodName);
if (!resolverFactory) {
this.logger.error(`Unknown mutation resolver type: ${methodName}`, {
objectMetadata,
methodName,
resolverName,
});
throw new Error(`Unknown mutation resolver type: ${methodName}`);
}
resolvers.Mutation[resolverName] = resolverFactory.create({
workspaceId,
targetTableName: objectMetadata.targetTableName,
fieldMetadataCollection: objectMetadata.fields,
});
}
}
return resolvers;
}
}

View File

@ -0,0 +1,99 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigArgumentMap } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ArgsMetadata } from 'src/workspace/workspace-schema-builder/interfaces/param-metadata.interface';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { TypeMapperService } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
@Injectable()
export class ArgsFactory {
private readonly logger = new Logger(ArgsFactory.name);
constructor(
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
private readonly typeMapperService: TypeMapperService,
) {}
public create(
{ args, objectMetadataId }: ArgsMetadata,
options: WorkspaceBuildSchemaOptions,
): GraphQLFieldConfigArgumentMap {
const fieldConfigMap: GraphQLFieldConfigArgumentMap = {};
for (const key in args) {
if (!args.hasOwnProperty(key)) {
continue;
}
const arg = args[key];
// Argument is a scalar type
if (arg.type) {
const fieldType = this.typeMapperService.mapToScalarType(
arg.type,
options.dateScalarMode,
options.numberScalarMode,
);
if (!fieldType) {
this.logger.error(
`Could not find a GraphQL type for ${arg.type.toString()}`,
{
arg,
options,
},
);
throw new Error(
`Could not find a GraphQL type for ${arg.type.toString()}`,
);
}
const gqlType = this.typeMapperService.mapToGqlType(fieldType, {
defaultValue: arg.defaultValue,
nullable: arg.isNullable,
isArray: arg.isArray,
});
fieldConfigMap[key] = {
type: gqlType,
};
}
// Argument is an input type
if (arg.kind) {
const inputType = this.typeDefinitionsStorage.getInputTypeByKey(
objectMetadataId,
arg.kind,
);
if (!inputType) {
this.logger.error(
`Could not find a GraphQL input type for ${objectMetadataId}`,
{
objectMetadataId,
options,
},
);
throw new Error(
`Could not find a GraphQL input type for ${objectMetadataId}`,
);
}
const gqlType = this.typeMapperService.mapToGqlType(inputType, {
nullable: arg.isNullable,
isArray: arg.isArray,
});
fieldConfigMap[key] = {
type: gqlType,
};
}
}
return fieldConfigMap;
}
}

View File

@ -0,0 +1,76 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import {
ObjectTypeDefinition,
ObjectTypeDefinitionKind,
} from './object-type-definition.factory';
import { ConnectionTypeFactory } from './connection-type.factory';
export enum ConnectionTypeDefinitionKind {
Edge = 'Edge',
PageInfo = 'PageInfo',
}
@Injectable()
export class ConnectionTypeDefinitionFactory {
private readonly logger = new Logger(ConnectionTypeDefinitionFactory.name);
constructor(private readonly connectionTypeFactory: ConnectionTypeFactory) {}
public create(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): ObjectTypeDefinition {
const kind = ObjectTypeDefinitionKind.Connection;
return {
target: objectMetadata.id,
kind,
type: new GraphQLObjectType({
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
description: objectMetadata.description,
fields: () => this.generateFields(objectMetadata, options),
}),
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): GraphQLFieldConfigMap<any, any> {
const fields: GraphQLFieldConfigMap<any, any> = {};
fields.edges = {
type: this.connectionTypeFactory.create(
objectMetadata,
ConnectionTypeDefinitionKind.Edge,
options,
{
isArray: true,
arrayDepth: 1,
nullable: false,
},
),
};
fields.pageInfo = {
type: this.connectionTypeFactory.create(
objectMetadata,
ConnectionTypeDefinitionKind.PageInfo,
options,
{
nullable: false,
},
),
};
return fields;
}
}

View File

@ -0,0 +1,58 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { PageInfoType } from 'src/workspace/workspace-schema-builder/graphql-types/object';
import { ConnectionTypeDefinitionKind } from './connection-type-definition.factory';
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
@Injectable()
export class ConnectionTypeFactory {
private readonly logger = new Logger(ConnectionTypeFactory.name);
constructor(
private readonly typeMapperService: TypeMapperService,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
objectMetadata: ObjectMetadataInterface,
kind: ConnectionTypeDefinitionKind,
buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLOutputType {
if (kind === ConnectionTypeDefinitionKind.PageInfo) {
return this.typeMapperService.mapToGqlType(PageInfoType, typeOptions);
}
const edgeType = this.typeDefinitionsStorage.getObjectTypeByKey(
objectMetadata.id,
kind as unknown as ObjectTypeDefinitionKind,
);
if (!edgeType) {
this.logger.error(
`Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
{
objectMetadata,
buildOtions,
},
);
throw new Error(
`Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
);
}
return this.typeMapperService.mapToGqlType(edgeType, typeOptions);
}
}

View File

@ -0,0 +1,74 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import {
ObjectTypeDefinition,
ObjectTypeDefinitionKind,
} from './object-type-definition.factory';
import { EdgeTypeFactory } from './edge-type.factory';
export enum EdgeTypeDefinitionKind {
Node = 'Node',
Cursor = 'Cursor',
}
@Injectable()
export class EdgeTypeDefinitionFactory {
private readonly logger = new Logger(EdgeTypeDefinitionFactory.name);
constructor(private readonly edgeTypeFactory: EdgeTypeFactory) {}
public create(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): ObjectTypeDefinition {
const kind = ObjectTypeDefinitionKind.Edge;
return {
target: objectMetadata.id,
kind,
type: new GraphQLObjectType({
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
description: objectMetadata.description,
fields: () => this.generateFields(objectMetadata, options),
}),
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): GraphQLFieldConfigMap<any, any> {
const fields: GraphQLFieldConfigMap<any, any> = {};
fields.node = {
type: this.edgeTypeFactory.create(
objectMetadata,
EdgeTypeDefinitionKind.Node,
options,
{
nullable: false,
},
),
};
fields.cursor = {
type: this.edgeTypeFactory.create(
objectMetadata,
EdgeTypeDefinitionKind.Cursor,
options,
{
nullable: false,
},
),
};
return fields;
}
}

View File

@ -0,0 +1,58 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { CursorScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
import { EdgeTypeDefinitionKind } from './edge-type-definition.factory';
@Injectable()
export class EdgeTypeFactory {
private readonly logger = new Logger(EdgeTypeFactory.name);
constructor(
private readonly typeMapperService: TypeMapperService,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
objectMetadata: ObjectMetadataInterface,
kind: EdgeTypeDefinitionKind,
buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLOutputType {
if (kind === EdgeTypeDefinitionKind.Cursor) {
return this.typeMapperService.mapToGqlType(CursorScalarType, typeOptions);
}
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
objectMetadata.id,
ObjectTypeDefinitionKind.Plain,
);
if (!objectType) {
this.logger.error(
`Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
{
objectMetadata,
buildOtions,
},
);
throw new Error(
`Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
);
}
return this.typeMapperService.mapToGqlType(objectType, typeOptions);
}
}

View File

@ -0,0 +1,85 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLEnumType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import {
FieldMetadataComplexOptions,
FieldMetadataDefaultOptions,
} from 'src/metadata/field-metadata/dtos/options.input';
import { isEnumFieldMetadataType } from 'src/metadata/field-metadata/utils/is-enum-field-metadata-type.util';
export interface EnumTypeDefinition {
target: string;
type: GraphQLEnumType;
}
@Injectable()
export class EnumTypeDefinitionFactory {
private readonly logger = new Logger(EnumTypeDefinitionFactory.name);
public create(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): EnumTypeDefinition[] {
const enumTypeDefinitions: EnumTypeDefinition[] = [];
for (const fieldMetadata of objectMetadata.fields) {
if (!isEnumFieldMetadataType(fieldMetadata.type)) {
continue;
}
enumTypeDefinitions.push({
target: fieldMetadata.id,
type: this.generateEnum(
objectMetadata.nameSingular,
fieldMetadata,
options,
),
});
}
return enumTypeDefinitions;
}
private generateEnum(
objectName: string,
fieldMetadata: FieldMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): GraphQLEnumType {
// FixMe: It's a hack until Typescript get fixed on union types for reduce function
// https://github.com/microsoft/TypeScript/issues/36390
const enumOptions = fieldMetadata.options as Array<
FieldMetadataDefaultOptions | FieldMetadataComplexOptions
>;
if (!enumOptions) {
this.logger.error(
`Enum options are not defined for ${fieldMetadata.name}`,
{
fieldMetadata,
options,
},
);
throw new Error(`Enum options are not defined for ${fieldMetadata.name}`);
}
return new GraphQLEnumType({
name: `${pascalCase(objectName)}${pascalCase(fieldMetadata.name)}Enum`,
description: fieldMetadata.description,
values: enumOptions.reduce((acc, enumOption) => {
acc[enumOption.value] = {
value: enumOption.value,
description: enumOption.label,
};
return acc;
}, {} as { [key: string]: { value: string; description: string } }),
});
}
}

View File

@ -0,0 +1,176 @@
import { Injectable, Logger } from '@nestjs/common';
import {
GraphQLFieldConfigArgumentMap,
GraphQLFieldConfigMap,
GraphQLObjectType,
} from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { objectContainsRelationField } from 'src/workspace/workspace-schema-builder/utils/object-contains-relation-field';
import { getResolverArgs } from 'src/workspace/workspace-schema-builder/utils/get-resolver-args.util';
import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util';
import {
RelationDirection,
deduceRelationDirection,
} from 'src/workspace/utils/deduce-relation-direction.util';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { RelationTypeFactory } from './relation-type.factory';
import { ArgsFactory } from './args.factory';
export enum ObjectTypeDefinitionKind {
Connection = 'Connection',
Edge = 'Edge',
Plain = '',
}
export interface ObjectTypeDefinition {
target: string;
kind: ObjectTypeDefinitionKind;
type: GraphQLObjectType;
}
@Injectable()
export class ExtendObjectTypeDefinitionFactory {
private readonly logger = new Logger(ExtendObjectTypeDefinitionFactory.name);
constructor(
private readonly relationTypeFactory: RelationTypeFactory,
private readonly argsFactory: ArgsFactory,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): ObjectTypeDefinition {
const kind = ObjectTypeDefinitionKind.Plain;
const gqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
objectMetadata.id,
kind,
);
const containsRelationField = objectContainsRelationField(objectMetadata);
if (!gqlType) {
this.logger.error(
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
{
objectMetadata,
options,
},
);
throw new Error(
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
);
}
// Security check to avoid extending an object that does not need to be extended
if (!containsRelationField) {
this.logger.error(
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
{
objectMetadata,
options,
},
);
throw new Error(
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
);
}
// Extract current object config to extend it
const config = gqlType.toConfig();
// Recreate the same object type with the new fields
return {
target: objectMetadata.id,
kind,
type: new GraphQLObjectType({
...config,
fields: () => ({
...config.fields,
...this.generateFields(objectMetadata, options),
}),
}),
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): GraphQLFieldConfigMap<any, any> {
const fields: GraphQLFieldConfigMap<any, any> = {};
for (const fieldMetadata of objectMetadata.fields) {
// Ignore relation fields as they are already defined
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
continue;
}
switch (fieldMetadata.type) {
case FieldMetadataType.RELATION: {
const relationMetadata =
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata;
if (!relationMetadata) {
this.logger.error(
`Could not find a relation metadata for ${fieldMetadata.id}`,
{
fieldMetadata,
},
);
throw new Error(
`Could not find a relation metadata for ${fieldMetadata.id}`,
);
}
const relationDirection = deduceRelationDirection(
fieldMetadata.objectMetadataId,
relationMetadata,
);
const relationType = this.relationTypeFactory.create(
fieldMetadata,
relationMetadata,
relationDirection,
);
let argsType: GraphQLFieldConfigArgumentMap | undefined = undefined;
// Args are only needed when relation is of kind `oneToMany` and the relation direction is `from`
if (
relationMetadata.relationType ===
RelationMetadataType.ONE_TO_MANY &&
relationDirection === RelationDirection.FROM
) {
const args = getResolverArgs('findMany');
argsType = this.argsFactory.create(
{
args,
objectMetadataId: relationMetadata.toObjectMetadataId,
},
options,
);
}
fields[fieldMetadata.name] = {
type: relationType,
args: argsType,
description: fieldMetadata.description,
};
break;
}
}
}
return fields;
}
}

View File

@ -0,0 +1,44 @@
import { EnumTypeDefinitionFactory } from 'src/workspace/workspace-schema-builder/factories/enum-type-definition.factory';
import { ArgsFactory } from './args.factory';
import { InputTypeFactory } from './input-type.factory';
import { InputTypeDefinitionFactory } from './input-type-definition.factory';
import { ObjectTypeDefinitionFactory } from './object-type-definition.factory';
import { OutputTypeFactory } from './output-type.factory';
import { QueryTypeFactory } from './query-type.factory';
import { RootTypeFactory } from './root-type.factory';
import { FilterTypeFactory } from './filter-type.factory';
import { FilterTypeDefinitionFactory } from './filter-type-definition.factory';
import { ConnectionTypeFactory } from './connection-type.factory';
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
import { EdgeTypeFactory } from './edge-type.factory';
import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory';
import { MutationTypeFactory } from './mutation-type.factory';
import { OrderByTypeFactory } from './order-by-type.factory';
import { OrderByTypeDefinitionFactory } from './order-by-type-definition.factory';
import { RelationTypeFactory } from './relation-type.factory';
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
import { OrphanedTypesFactory } from './orphaned-types.factory';
export const workspaceSchemaBuilderFactories = [
ArgsFactory,
InputTypeFactory,
InputTypeDefinitionFactory,
OutputTypeFactory,
ObjectTypeDefinitionFactory,
EnumTypeDefinitionFactory,
RelationTypeFactory,
ExtendObjectTypeDefinitionFactory,
FilterTypeFactory,
FilterTypeDefinitionFactory,
OrderByTypeFactory,
OrderByTypeDefinitionFactory,
ConnectionTypeFactory,
ConnectionTypeDefinitionFactory,
EdgeTypeFactory,
EdgeTypeDefinitionFactory,
RootTypeFactory,
QueryTypeFactory,
MutationTypeFactory,
OrphanedTypesFactory,
];

View File

@ -0,0 +1,91 @@
import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import { TypeMapperService } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util';
import { FilterTypeFactory } from './filter-type.factory';
import {
InputTypeDefinition,
InputTypeDefinitionKind,
} from './input-type-definition.factory';
@Injectable()
export class FilterTypeDefinitionFactory {
constructor(
private readonly filterTypeFactory: FilterTypeFactory,
private readonly typeMapperService: TypeMapperService,
) {}
public create(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): InputTypeDefinition {
const kind = InputTypeDefinitionKind.Filter;
const filterInputType = new GraphQLInputObjectType({
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
description: objectMetadata.description,
fields: () => {
const andOrType = this.typeMapperService.mapToGqlType(filterInputType, {
isArray: true,
arrayDepth: 1,
nullable: true,
});
return {
...this.generateFields(objectMetadata, options),
and: {
type: andOrType,
},
or: {
type: andOrType,
},
not: {
type: this.typeMapperService.mapToGqlType(filterInputType, {
nullable: true,
}),
},
};
},
});
return {
target: objectMetadata.id,
kind,
type: filterInputType,
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
for (const fieldMetadata of objectMetadata.fields) {
// Relation types are generated during extension of object type definition
if (isRelationFieldMetadataType(fieldMetadata.type)) {
//continue;
}
const type = this.filterTypeFactory.create(fieldMetadata, options, {
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
});
fields[fieldMetadata.name] = {
type,
description: fieldMetadata.description,
// TODO: Add default value
defaultValue: undefined,
};
}
return fields;
}
}

View File

@ -0,0 +1,102 @@
import { Injectable, Logger } from '@nestjs/common';
import {
GraphQLInputObjectType,
GraphQLInputType,
GraphQLList,
GraphQLScalarType,
} from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util';
import { isEnumFieldMetadataType } from 'src/metadata/field-metadata/utils/is-enum-field-metadata-type.util';
import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type';
import { InputTypeDefinitionKind } from './input-type-definition.factory';
@Injectable()
export class FilterTypeFactory {
private readonly logger = new Logger(FilterTypeFactory.name);
constructor(
private readonly typeMapperService: TypeMapperService,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
fieldMetadata: FieldMetadataInterface,
buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLInputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let filterType: GraphQLInputObjectType | GraphQLScalarType | undefined =
undefined;
if (isEnumFieldMetadataType(fieldMetadata.type)) {
filterType = this.createEnumFilterType(fieldMetadata);
} else {
filterType = this.typeMapperService.mapToFilterType(
fieldMetadata.type,
buildOtions.dateScalarMode,
buildOtions.numberScalarMode,
);
filterType ??= this.typeDefinitionsStorage.getInputTypeByKey(
target,
InputTypeDefinitionKind.Filter,
);
}
if (!filterType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata,
buildOtions,
typeOptions,
});
throw new Error(`Could not find a GraphQL type for ${target}`);
}
return this.typeMapperService.mapToGqlType(filterType, typeOptions);
}
private createEnumFilterType(
fieldMetadata: FieldMetadataInterface,
): GraphQLInputObjectType {
const enumType = this.typeDefinitionsStorage.getEnumTypeByKey(
fieldMetadata.id,
);
if (!enumType) {
this.logger.error(
`Could not find a GraphQL enum type for ${fieldMetadata.id}`,
{
fieldMetadata,
},
);
throw new Error(
`Could not find a GraphQL enum type for ${fieldMetadata.id}`,
);
}
return new GraphQLInputObjectType({
name: `${enumType.name}Filter`,
fields: () => ({
eq: { type: enumType },
neq: { type: enumType },
in: { type: new GraphQLList(enumType) },
is: { type: FilterIs },
}),
});
}
}

View File

@ -0,0 +1,78 @@
import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util';
import { FieldMetadataType } 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: WorkspaceBuildSchemaOptions,
): InputTypeDefinition {
return {
target: objectMetadata.id,
kind,
type: new GraphQLInputObjectType({
name: `${pascalCase(
objectMetadata.nameSingular,
)}${kind.toString()}Input`,
description: objectMetadata.description,
fields: this.generateFields(objectMetadata, kind, options),
}),
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
kind: InputTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
for (const fieldMetadata of objectMetadata.fields) {
// Relation field types are generated during extension of object type definition
if (isRelationFieldMetadataType(fieldMetadata.type)) {
//continue;
}
const type = this.inputTypeFactory.create(fieldMetadata, kind, options, {
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
});
fields[fieldMetadata.name] = {
type,
description: fieldMetadata.description,
// TODO: Add default value
defaultValue: undefined,
};
}
return fields;
}
}

View File

@ -0,0 +1,59 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLInputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util';
import { InputTypeDefinitionKind } from './input-type-definition.factory';
@Injectable()
export class InputTypeFactory {
private readonly logger = new Logger(InputTypeFactory.name);
constructor(
private readonly typeMapperService: TypeMapperService,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
fieldMetadata: FieldMetadataInterface,
kind: InputTypeDefinitionKind,
buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLInputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let inputType: GraphQLInputType | undefined =
this.typeMapperService.mapToScalarType(
fieldMetadata.type,
buildOtions.dateScalarMode,
buildOtions.numberScalarMode,
);
inputType ??= this.typeDefinitionsStorage.getInputTypeByKey(target, kind);
inputType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
if (!inputType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata,
kind,
buildOtions,
typeOptions,
});
throw new Error(`Could not find a GraphQL type for ${target}`);
}
return this.typeMapperService.mapToGqlType(inputType, typeOptions);
}
}

View File

@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { WorkspaceResolverBuilderMutationMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
@Injectable()
export class MutationTypeFactory {
constructor(private readonly rootTypeFactory: RootTypeFactory) {}
create(
objectMetadataCollection: ObjectMetadataInterface[],
workspaceResolverMethodNames: WorkspaceResolverBuilderMutationMethodNames[],
options: WorkspaceBuildSchemaOptions,
): GraphQLObjectType {
return this.rootTypeFactory.create(
objectMetadataCollection,
workspaceResolverMethodNames,
ObjectTypeName.Mutation,
options,
);
}
}

View File

@ -0,0 +1,72 @@
import { Injectable } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { OutputTypeFactory } from './output-type.factory';
export enum ObjectTypeDefinitionKind {
Connection = 'Connection',
Edge = 'Edge',
Plain = '',
}
export interface ObjectTypeDefinition {
target: string;
kind: ObjectTypeDefinitionKind;
type: GraphQLObjectType;
}
@Injectable()
export class ObjectTypeDefinitionFactory {
constructor(private readonly outputTypeFactory: OutputTypeFactory) {}
public create(
objectMetadata: ObjectMetadataInterface,
kind: ObjectTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions,
): ObjectTypeDefinition {
return {
target: objectMetadata.id,
kind,
type: new GraphQLObjectType({
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
description: objectMetadata.description,
fields: this.generateFields(objectMetadata, kind, options),
}),
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
kind: ObjectTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions,
): GraphQLFieldConfigMap<any, any> {
const fields: GraphQLFieldConfigMap<any, any> = {};
for (const fieldMetadata of objectMetadata.fields) {
// Relation field types are generated during extension of object type definition
if (isRelationFieldMetadataType(fieldMetadata.type)) {
continue;
}
const type = this.outputTypeFactory.create(fieldMetadata, kind, options, {
nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
});
fields[fieldMetadata.name] = {
type,
description: fieldMetadata.description,
};
}
return fields;
}
}

View File

@ -0,0 +1,66 @@
import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { pascalCase } from 'src/utils/pascal-case';
import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util';
import {
InputTypeDefinition,
InputTypeDefinitionKind,
} from './input-type-definition.factory';
import { OrderByTypeFactory } from './order-by-type.factory';
@Injectable()
export class OrderByTypeDefinitionFactory {
constructor(private readonly orderByTypeFactory: OrderByTypeFactory) {}
public create(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): InputTypeDefinition {
const kind = InputTypeDefinitionKind.OrderBy;
return {
target: objectMetadata.id,
kind,
type: new GraphQLInputObjectType({
name: `${pascalCase(
objectMetadata.nameSingular,
)}${kind.toString()}Input`,
description: objectMetadata.description,
fields: this.generateFields(objectMetadata, options),
}),
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
for (const fieldMetadata of objectMetadata.fields) {
// Relation field types are generated during extension of object type definition
if (isRelationFieldMetadataType(fieldMetadata.type)) {
continue;
}
const type = this.orderByTypeFactory.create(fieldMetadata, options, {
nullable: fieldMetadata.isNullable,
});
fields[fieldMetadata.name] = {
type,
description: fieldMetadata.description,
// TODO: Add default value
defaultValue: undefined,
};
}
return fields;
}
}

View File

@ -0,0 +1,55 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLInputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util';
import { InputTypeDefinitionKind } from './input-type-definition.factory';
@Injectable()
export class OrderByTypeFactory {
private readonly logger = new Logger(OrderByTypeFactory.name);
constructor(
private readonly typeMapperService: TypeMapperService,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
fieldMetadata: FieldMetadataInterface,
buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLInputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let orderByType = this.typeMapperService.mapToOrderByType(
fieldMetadata.type,
);
orderByType ??= this.typeDefinitionsStorage.getInputTypeByKey(
target,
InputTypeDefinitionKind.OrderBy,
);
if (!orderByType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata,
buildOtions,
typeOptions,
});
throw new Error(`Could not find a GraphQL type for ${target}`);
}
return this.typeMapperService.mapToGqlType(orderByType, typeOptions);
}
}

View File

@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { GraphQLNamedType } from 'graphql';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
@Injectable()
export class OrphanedTypesFactory {
constructor(
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(): GraphQLNamedType[] {
const objectTypeDefs =
this.typeDefinitionsStorage.getAllObjectTypeDefinitions();
const inputTypeDefs =
this.typeDefinitionsStorage.getAllInputTypeDefinitions();
const classTypeDefs = [...objectTypeDefs, ...inputTypeDefs];
return [...classTypeDefs.map(({ type }) => type)];
}
}

View File

@ -0,0 +1,58 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import {
TypeMapperService,
TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
@Injectable()
export class OutputTypeFactory {
private readonly logger = new Logger(OutputTypeFactory.name);
constructor(
private readonly typeMapperService: TypeMapperService,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
fieldMetadata: FieldMetadataInterface,
kind: ObjectTypeDefinitionKind,
buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions,
): GraphQLOutputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let gqlType: GraphQLOutputType | undefined =
this.typeMapperService.mapToScalarType(
fieldMetadata.type,
buildOtions.dateScalarMode,
buildOtions.numberScalarMode,
);
gqlType ??= this.typeDefinitionsStorage.getObjectTypeByKey(target, kind);
gqlType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
if (!gqlType) {
this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata,
buildOtions,
typeOptions,
});
throw new Error(`Could not find a GraphQL type for ${target}`);
}
return this.typeMapperService.mapToGqlType(gqlType, typeOptions);
}
}

View File

@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { WorkspaceResolverBuilderQueryMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
@Injectable()
export class QueryTypeFactory {
constructor(private readonly rootTypeFactory: RootTypeFactory) {}
create(
objectMetadataCollection: ObjectMetadataInterface[],
workspaceResolverMethodNames: WorkspaceResolverBuilderQueryMethodNames[],
options: WorkspaceBuildSchemaOptions,
): GraphQLObjectType {
return this.rootTypeFactory.create(
objectMetadataCollection,
workspaceResolverMethodNames,
ObjectTypeName.Query,
options,
);
}
}

View File

@ -0,0 +1,62 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import { RelationMetadataInterface } from 'src/metadata/field-metadata/interfaces/relation-metadata.interface';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { RelationDirection } from 'src/workspace/utils/deduce-relation-direction.util';
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
@Injectable()
export class RelationTypeFactory {
private readonly logger = new Logger(RelationTypeFactory.name);
constructor(
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
fieldMetadata: FieldMetadataInterface,
relationMetadata: RelationMetadataInterface,
relationDirection: RelationDirection,
): GraphQLOutputType {
let relationQqlType: GraphQLOutputType | undefined = undefined;
if (
relationDirection === RelationDirection.FROM &&
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY
) {
relationQqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
relationMetadata.toObjectMetadataId,
ObjectTypeDefinitionKind.Connection,
);
} else {
const relationObjectId =
relationDirection === RelationDirection.FROM
? relationMetadata.toObjectMetadataId
: relationMetadata.fromObjectMetadataId;
relationQqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
relationObjectId,
ObjectTypeDefinitionKind.Plain,
);
}
if (!relationQqlType) {
this.logger.error(
`Could not find a relation type for ${fieldMetadata.id}`,
{
fieldMetadata,
},
);
throw new Error(`Could not find a relation type for ${fieldMetadata.id}`);
}
return relationQqlType;
}
}

View File

@ -0,0 +1,120 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { getResolverName } from 'src/workspace/utils/get-resolver-name.util';
import { getResolverArgs } from 'src/workspace/workspace-schema-builder/utils/get-resolver-args.util';
import { TypeMapperService } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
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 typeMapperService: TypeMapperService,
private readonly argsFactory: ArgsFactory,
) {}
create(
objectMetadataCollection: ObjectMetadataInterface[],
workspaceResolverMethodNames: WorkspaceResolverBuilderMethodNames[],
objectTypeName: ObjectTypeName,
options: WorkspaceBuildSchemaOptions,
): GraphQLObjectType {
if (workspaceResolverMethodNames.length === 0) {
this.logger.error(
`No resolver methods were found for ${objectTypeName.toString()}`,
{
workspaceResolverMethodNames,
objectTypeName,
options,
},
);
throw new Error(
`No resolvers were found for ${objectTypeName.toString()}`,
);
}
return new GraphQLObjectType({
name: objectTypeName.toString(),
fields: this.generateFields(
objectMetadataCollection,
workspaceResolverMethodNames,
options,
),
});
}
private generateFields<T = any, U = any>(
objectMetadataCollection: ObjectMetadataInterface[],
workspaceResolverMethodNames: WorkspaceResolverBuilderMethodNames[],
options: WorkspaceBuildSchemaOptions,
): GraphQLFieldConfigMap<T, U> {
const fieldConfigMap: GraphQLFieldConfigMap<T, U> = {};
for (const objectMetadata of objectMetadataCollection) {
for (const methodName of workspaceResolverMethodNames) {
const name = getResolverName(objectMetadata, methodName);
const args = getResolverArgs(methodName);
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
objectMetadata.id,
methodName === 'findMany'
? ObjectTypeDefinitionKind.Connection
: ObjectTypeDefinitionKind.Plain,
);
const argsType = this.argsFactory.create(
{
args,
objectMetadataId: objectMetadata.id,
},
options,
);
if (!objectType) {
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}`,
);
}
const outputType = this.typeMapperService.mapToGqlType(objectType, {
isArray: ['updateMany', 'deleteMany', 'createMany'].includes(
methodName,
),
});
fieldConfigMap[name] = {
type: outputType,
args: argsType,
resolve: undefined,
};
}
}
return fieldConfigMap;
}
}

View File

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

View File

@ -0,0 +1,24 @@
import { GraphQLEnumType } from 'graphql';
export const OrderByDirectionType = new GraphQLEnumType({
name: 'OrderByDirection',
description: 'This enum is used to specify the order of results',
values: {
AscNullsFirst: {
value: 'AscNullsFirst',
description: 'Ascending order, nulls first',
},
AscNullsLast: {
value: 'AscNullsLast',
description: 'Ascending order, nulls last',
},
DescNullsFirst: {
value: 'DescNullsFirst',
description: 'Descending order, nulls first',
},
DescNullsLast: {
value: 'DescNullsLast',
description: 'Descending order, nulls last',
},
},
});

View File

@ -0,0 +1,18 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type';
import { BigFloatScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
export const BigFloatFilterType = new GraphQLInputObjectType({
name: 'BigFloatFilter',
fields: {
eq: { type: BigFloatScalarType },
gt: { type: BigFloatScalarType },
gte: { type: BigFloatScalarType },
in: { type: new GraphQLList(new GraphQLNonNull(BigFloatScalarType)) },
lt: { type: BigFloatScalarType },
lte: { type: BigFloatScalarType },
neq: { type: BigFloatScalarType },
is: { type: FilterIs },
},
});

View File

@ -0,0 +1,22 @@
import {
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull,
GraphQLInt,
} from 'graphql';
import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const BigIntFilterType = new GraphQLInputObjectType({
name: 'BigIntFilter',
fields: {
eq: { type: GraphQLInt },
gt: { type: GraphQLInt },
gte: { type: GraphQLInt },
in: { type: new GraphQLList(new GraphQLNonNull(GraphQLInt)) },
lt: { type: GraphQLInt },
lte: { type: GraphQLInt },
neq: { type: GraphQLInt },
is: { type: FilterIs },
},
});

View File

@ -0,0 +1,11 @@
import { GraphQLBoolean, GraphQLInputObjectType } from 'graphql';
import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const BooleanFilterType = new GraphQLInputObjectType({
name: 'BooleanFilter',
fields: {
eq: { type: GraphQLBoolean },
is: { type: FilterIs },
},
});

View File

@ -0,0 +1,18 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type';
import { DateScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
export const DateFilterType = new GraphQLInputObjectType({
name: 'DateFilter',
fields: {
eq: { type: DateScalarType },
gt: { type: DateScalarType },
gte: { type: DateScalarType },
in: { type: new GraphQLList(new GraphQLNonNull(DateScalarType)) },
lt: { type: DateScalarType },
lte: { type: DateScalarType },
neq: { type: DateScalarType },
is: { type: FilterIs },
},
});

View File

@ -0,0 +1,18 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type';
import { DateTimeScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
export const DatetimeFilterType = new GraphQLInputObjectType({
name: 'DateTimeFilter',
fields: {
eq: { type: DateTimeScalarType },
gt: { type: DateTimeScalarType },
gte: { type: DateTimeScalarType },
in: { type: new GraphQLList(new GraphQLNonNull(DateTimeScalarType)) },
lt: { type: DateTimeScalarType },
lte: { type: DateTimeScalarType },
neq: { type: DateTimeScalarType },
is: { type: FilterIs },
},
});

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