feat: add findAll and findUnique resolver for universal objects (#1576)

* wip: refacto and start creating custom resolver

* feat: findMany & findUnique of a custom entity

* feat: wip pagination

* feat: initial metadata migration

* feat: universal findAll with pagination

* fix: clean small stuff in pagination

* fix: test

* fix: miss file

* feat: rename custom into universal

* feat: create metadata schema in default database

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2023-09-21 02:24:13 +02:00
committed by GitHub
parent dafe08ef78
commit b1171e22a3
41 changed files with 1043 additions and 29 deletions
@@ -0,0 +1,11 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';
@ArgsType()
export class BaseUniversalArgs {
@Field(() => String)
@IsNotEmpty()
@IsString()
entity: string;
}
@@ -0,0 +1,10 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { BaseUniversalArgs } from './base-universal.args';
import { UniversalEntityInput } from './universal-entity.input';
@ArgsType()
export class DeleteOneUniversalArgs extends BaseUniversalArgs {
@Field(() => UniversalEntityInput, { nullable: true })
where?: UniversalEntityInput;
}
@@ -0,0 +1,35 @@
import { ArgsType, Field, Int } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { IsNotEmpty, IsString } from 'class-validator';
import { ConnectionArgs } from 'src/utils/pagination';
import { UniversalEntityInput } from './universal-entity.input';
import { UniversalEntityOrderByRelationInput } from './universal-entity-order-by-relation.input';
@ArgsType()
export class FindManyUniversalArgs extends ConnectionArgs {
@Field(() => String)
@IsNotEmpty()
@IsString()
entity: string;
@Field(() => UniversalEntityInput, { nullable: true })
where?: UniversalEntityInput;
@Field(() => UniversalEntityOrderByRelationInput, { nullable: true })
orderBy?: UniversalEntityOrderByRelationInput;
@Field(() => GraphQLJSON, { nullable: true })
cursor?: UniversalEntityInput;
@Field(() => Int, { nullable: true })
take?: number;
@Field(() => Int, { nullable: true })
skip?: number;
@Field(() => [String], { nullable: true })
distinct?: Array<string>;
}
@@ -0,0 +1,10 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { BaseUniversalArgs } from './base-universal.args';
import { UniversalEntityInput } from './universal-entity.input';
@ArgsType()
export class FindUniqueUniversalArgs extends BaseUniversalArgs {
@Field(() => UniversalEntityInput, { nullable: true })
where?: UniversalEntityInput;
}
@@ -0,0 +1,29 @@
import { Field, InputType } from '@nestjs/graphql';
import { registerEnumType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
export enum TypeORMSortOrder {
ASC = 'ASC',
DESC = 'DESC',
}
registerEnumType(TypeORMSortOrder, {
name: 'TypeORMSortOrder',
description: undefined,
});
@InputType()
export class UniversalEntityOrderByRelationInput {
@Field(() => TypeORMSortOrder, { nullable: true })
id?: keyof typeof TypeORMSortOrder;
@Field(() => GraphQLJSON, { nullable: true })
data?: Record<string, keyof typeof TypeORMSortOrder>;
@Field(() => TypeORMSortOrder, { nullable: true })
createdAt?: keyof typeof TypeORMSortOrder;
@Field(() => TypeORMSortOrder, { nullable: true })
updatedAt?: keyof typeof TypeORMSortOrder;
}
@@ -0,0 +1,18 @@
import { Field, ID, InputType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
@InputType()
export class UniversalEntityInput {
@Field(() => ID, { nullable: true })
id?: string;
@Field(() => GraphQLJSON, { nullable: true })
data?: Record<string, unknown>;
@Field(() => Date, { nullable: true })
createdAt?: Date;
@Field(() => Date, { nullable: true })
updatedAt?: Date;
}
@@ -0,0 +1,13 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { BaseUniversalArgs } from './base-universal.args';
import { UniversalEntityInput } from './universal-entity.input';
@ArgsType()
export class UpdateOneCustomArgs extends BaseUniversalArgs {
@Field(() => UniversalEntityInput, { nullable: false })
data!: UniversalEntityInput;
@Field(() => UniversalEntityInput, { nullable: true })
where?: UniversalEntityInput;
}
@@ -0,0 +1,25 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { Paginated } from 'src/utils/pagination';
@ObjectType()
export class UniversalEntity {
@Field(() => ID, { nullable: false })
id!: string;
@Field(() => GraphQLJSON, { nullable: false })
data!: Record<string, unknown>;
@Field(() => Date, { nullable: false })
createdAt!: Date;
@Field(() => Date, { nullable: false })
updatedAt!: Date;
}
@ObjectType()
export class PaginatedUniversalEntity extends Paginated<UniversalEntity>(
UniversalEntity,
) {}
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { DataSourceModule } from 'src/tenant/metadata/data-source/data-source.module';
import { UniversalService } from './universal.service';
import { UniversalResolver } from './universal.resolver';
@Module({
imports: [DataSourceModule],
providers: [UniversalService, UniversalResolver],
})
export class UniversalModule {}
@@ -0,0 +1,26 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UniversalResolver } from './universal.resolver';
import { UniversalService } from './universal.service';
describe('UniversalResolver', () => {
let resolver: UniversalResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UniversalResolver,
{
provide: UniversalService,
useValue: {},
},
],
}).compile();
resolver = module.get<UniversalResolver>(UniversalResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});
@@ -0,0 +1,56 @@
import { Args, Query, Resolver } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Workspace } from '@prisma/client';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { UniversalEntity, PaginatedUniversalEntity } from './universal.entity';
import { UniversalService } from './universal.service';
import { FindManyUniversalArgs } from './args/find-many-universal.args';
import { FindUniqueUniversalArgs } from './args/find-unique-universal.args';
import { UpdateOneCustomArgs } from './args/update-one-custom.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => UniversalEntity)
export class UniversalResolver {
constructor(private readonly customService: UniversalService) {}
@Query(() => PaginatedUniversalEntity)
findMany(
@Args() args: FindManyUniversalArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<PaginatedUniversalEntity> {
return this.customService.findManyUniversal(args, workspace);
}
@Query(() => UniversalEntity)
findUnique(
@Args() args: FindUniqueUniversalArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<UniversalEntity | undefined> {
return this.customService.findUniqueUniversal(args, workspace);
}
@Query(() => UniversalEntity)
updateOneCustom(@Args() args: UpdateOneCustomArgs): UniversalEntity {
return {
id: 'exampleId',
data: {},
createdAt: new Date(),
updatedAt: new Date(),
};
}
@Query(() => UniversalEntity)
deleteOneCustom(@Args() args: UpdateOneCustomArgs): UniversalEntity {
return {
id: 'exampleId',
data: {},
createdAt: new Date(),
updatedAt: new Date(),
};
}
}
@@ -0,0 +1,27 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DataSourceService } from 'src/tenant/metadata/data-source/data-source.service';
import { UniversalService } from './universal.service';
describe('UniversalService', () => {
let service: UniversalService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UniversalService,
{
provide: DataSourceService,
useValue: {},
},
],
}).compile();
service = module.get<UniversalService>(UniversalService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
@@ -0,0 +1,100 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { Workspace } from '@prisma/client';
import { DataSourceService } from 'src/tenant/metadata/data-source/data-source.service';
import { findManyCursorConnection } from 'src/utils/pagination';
import { UniversalEntity, PaginatedUniversalEntity } from './universal.entity';
import {
getRawTypeORMOrderByClause,
getRawTypeORMWhereClause,
} from './universal.util';
import { FindManyUniversalArgs } from './args/find-many-universal.args';
import { FindUniqueUniversalArgs } from './args/find-unique-universal.args';
@Injectable()
export class UniversalService {
constructor(private readonly dataSourceService: DataSourceService) {}
async findManyUniversal(
args: FindManyUniversalArgs,
workspace: Workspace,
): Promise<PaginatedUniversalEntity> {
await this.dataSourceService.createWorkspaceSchema(workspace.id);
const workspaceDataSource =
await this.dataSourceService.connectToWorkspaceDataSource(workspace.id);
let query = workspaceDataSource
?.createQueryBuilder()
.select()
.from(args.entity, args.entity);
if (!query) {
throw new InternalServerErrorException();
}
if (query && args.where) {
const { where, parameters } = getRawTypeORMWhereClause(
args.entity,
args.where,
);
query = query.where(where, parameters);
}
if (query && args.orderBy) {
const orderBy = getRawTypeORMOrderByClause(args.entity, args.orderBy);
query = query.orderBy(orderBy);
}
return findManyCursorConnection(query, args, {
recordToEdge({ id, createdAt, updatedAt, ...data }) {
return {
node: {
id,
data,
createdAt,
updatedAt,
},
};
},
});
}
async findUniqueUniversal(
args: FindUniqueUniversalArgs,
workspace: Workspace,
): Promise<UniversalEntity | undefined> {
await this.dataSourceService.createWorkspaceSchema(workspace.id);
const workspaceDataSource =
await this.dataSourceService.connectToWorkspaceDataSource(workspace.id);
let query = workspaceDataSource
?.createQueryBuilder()
.select()
.from(args.entity, args.entity);
if (query && args.where) {
const { where, parameters } = getRawTypeORMWhereClause(
args.entity,
args.where,
);
query = query.where(where, parameters);
}
const { id, createdAt, updatedAt, ...data } = await query?.getRawOne();
return {
id,
data,
createdAt,
updatedAt,
};
}
}
@@ -0,0 +1,59 @@
import { snakeCase } from 'src/utils/snake-case';
import { UniversalEntityInput } from './args/universal-entity.input';
import {
UniversalEntityOrderByRelationInput,
TypeORMSortOrder,
} from './args/universal-entity-order-by-relation.input';
export const getRawTypeORMWhereClause = (
entity: string,
where?: UniversalEntityInput | undefined,
) => {
if (!where) {
return {
where: '',
parameters: {},
};
}
const { id, data, createdAt, updatedAt } = where;
const flattenWhere: any = {
...(id ? { id } : {}),
...data,
...(createdAt ? { createdAt } : {}),
...(updatedAt ? { updatedAt } : {}),
};
return {
where: Object.keys(flattenWhere)
.map((key) => `${entity}.${snakeCase(key)} = :${key}`)
.join(' AND '),
parameters: flattenWhere,
};
};
export const getRawTypeORMOrderByClause = (
entity: string,
orderBy?: UniversalEntityOrderByRelationInput | undefined,
) => {
if (!orderBy) {
return {};
}
const { id, data, createdAt, updatedAt } = orderBy;
const flattenWhere: any = {
...(id ? { id } : {}),
...data,
...(createdAt ? { createdAt } : {}),
...(updatedAt ? { updatedAt } : {}),
};
const orderByClause: Record<string, TypeORMSortOrder> = {};
for (const key of Object.keys(flattenWhere)) {
orderByClause[`${entity}.${snakeCase(key)}`] = flattenWhere[key];
}
return orderByClause;
};