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:
@ -6,9 +6,16 @@
|
||||
* @returns
|
||||
*/
|
||||
export function 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 base36String;
|
||||
return `${devId ? 'twenty_' : ''}${base36String}`;
|
||||
}
|
||||
|
||||
@ -4,4 +4,12 @@ export const baseColumns = {
|
||||
type: 'uuid',
|
||||
generated: 'uuid',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true,
|
||||
},
|
||||
} as const;
|
||||
|
||||
@ -9,6 +9,7 @@ import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { DataSourceMetadataService } from './data-source-metadata/data-source-metadata.service';
|
||||
import { EntitySchemaGeneratorService } from './entity-schema-generator/entity-schema-generator.service';
|
||||
import { DataSourceService } from './data-source/data-source.service';
|
||||
import { uuidToBase36 } from './data-source/data-source.util';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('metadata')
|
||||
@ -39,6 +40,10 @@ export class MetadataController {
|
||||
entities.push(...dataSourceEntities);
|
||||
}
|
||||
|
||||
this.dataSourceService.createWorkspaceSchema(workspace.id);
|
||||
|
||||
console.log('entities', uuidToBase36(workspace.id), workspace.id);
|
||||
|
||||
this.dataSourceService.connectToWorkspaceDataSource(workspace.id);
|
||||
|
||||
return entities;
|
||||
|
||||
25
server/src/tenant/metadata/metadata.datasource.ts
Normal file
25
server/src/tenant/metadata/metadata.datasource.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
config();
|
||||
|
||||
const configService = new ConfigService();
|
||||
|
||||
export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = {
|
||||
url: configService.get<string>('PG_DATABASE_URL')!,
|
||||
type: 'postgres',
|
||||
logging: false,
|
||||
schema: 'metadata',
|
||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
synchronize: false,
|
||||
migrationsRun: true,
|
||||
migrationsTableName: '_typeorm_migrations',
|
||||
migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
|
||||
};
|
||||
|
||||
export const connectionSource = new DataSource(
|
||||
typeORMMetadataModuleOptions as DataSourceOptions,
|
||||
);
|
||||
@ -1,36 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||
|
||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||
|
||||
import { MetadataService } from './metadata.service';
|
||||
import { MetadataController } from './metadata.controller';
|
||||
import { typeORMMetadataModuleOptions } from './metadata.datasource';
|
||||
|
||||
import { DataSourceModule } from './data-source/data-source.module';
|
||||
import { DataSourceMetadataModule } from './data-source-metadata/data-source-metadata.module';
|
||||
import { FieldMetadataModule } from './field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataModule } from './object-metadata/object-metadata.module';
|
||||
import { EntitySchemaGeneratorModule } from './entity-schema-generator/entity-schema-generator.module';
|
||||
import { DataSourceMetadata } from './data-source-metadata/data-source-metadata.entity';
|
||||
import { FieldMetadata } from './field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadata } from './object-metadata/object-metadata.entity';
|
||||
|
||||
const typeORMFactory = async (
|
||||
environmentService: EnvironmentService,
|
||||
): Promise<TypeOrmModuleOptions> => ({
|
||||
url: environmentService.getPGDatabaseUrl(),
|
||||
type: 'postgres',
|
||||
logging: false,
|
||||
schema: 'metadata',
|
||||
entities: [DataSourceMetadata, FieldMetadata, ObjectMetadata],
|
||||
synchronize: true,
|
||||
const typeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
|
||||
...typeORMMetadataModuleOptions,
|
||||
});
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRootAsync({
|
||||
useFactory: typeORMFactory,
|
||||
inject: [EnvironmentService],
|
||||
name: 'metadata',
|
||||
}),
|
||||
DataSourceModule,
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class Migrations1695198840363 implements MigrationInterface {
|
||||
name = 'Migrations1695198840363';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "metadata"."data_source_metadata" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "url" character varying, "schema" character varying, "type" "metadata"."data_source_metadata_type_enum" NOT NULL DEFAULT 'postgres', "name" character varying, "is_remote" boolean NOT NULL DEFAULT false, "workspace_id" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_923752b7e62a300a4969bd0e038" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "metadata"."field_metadata" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "object_id" uuid NOT NULL, "type" character varying NOT NULL, "name" character varying NOT NULL, "is_custom" boolean NOT NULL DEFAULT false, "workspace_id" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_c75db587904cad6af109b5c65f1" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "metadata"."object_metadata" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "data_source_id" character varying NOT NULL, "name" character varying NOT NULL, "is_custom" boolean NOT NULL DEFAULT false, "workspace_id" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_c8c5f885767b356949c18c201c1" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."field_metadata" ADD CONSTRAINT "FK_38179b299795e48887fc99f937a" FOREIGN KEY ("object_id") REFERENCES "metadata"."object_metadata"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."field_metadata" DROP CONSTRAINT "FK_38179b299795e48887fc99f937a"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "metadata"."object_metadata"`);
|
||||
await queryRunner.query(`DROP TABLE "metadata"."field_metadata"`);
|
||||
await queryRunner.query(`DROP TABLE "metadata"."data_source_metadata"`);
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { MetadataModule } from './metadata/metadata.module';
|
||||
import { UniversalModule } from './universal/universal.module';
|
||||
|
||||
@Module({
|
||||
imports: [MetadataModule],
|
||||
imports: [MetadataModule, UniversalModule],
|
||||
})
|
||||
export class TenantModule {}
|
||||
|
||||
11
server/src/tenant/universal/args/base-universal.args.ts
Normal file
11
server/src/tenant/universal/args/base-universal.args.ts
Normal file
@ -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;
|
||||
}
|
||||
35
server/src/tenant/universal/args/find-many-universal.args.ts
Normal file
35
server/src/tenant/universal/args/find-many-universal.args.ts
Normal file
@ -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;
|
||||
}
|
||||
18
server/src/tenant/universal/args/universal-entity.input.ts
Normal file
18
server/src/tenant/universal/args/universal-entity.input.ts
Normal file
@ -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;
|
||||
}
|
||||
13
server/src/tenant/universal/args/update-one-custom.args.ts
Normal file
13
server/src/tenant/universal/args/update-one-custom.args.ts
Normal file
@ -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;
|
||||
}
|
||||
25
server/src/tenant/universal/universal.entity.ts
Normal file
25
server/src/tenant/universal/universal.entity.ts
Normal file
@ -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,
|
||||
) {}
|
||||
12
server/src/tenant/universal/universal.module.ts
Normal file
12
server/src/tenant/universal/universal.module.ts
Normal file
@ -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 {}
|
||||
26
server/src/tenant/universal/universal.resolver.spec.ts
Normal file
26
server/src/tenant/universal/universal.resolver.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
56
server/src/tenant/universal/universal.resolver.ts
Normal file
56
server/src/tenant/universal/universal.resolver.ts
Normal file
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
27
server/src/tenant/universal/universal.service.spec.ts
Normal file
27
server/src/tenant/universal/universal.service.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
100
server/src/tenant/universal/universal.service.ts
Normal file
100
server/src/tenant/universal/universal.service.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
59
server/src/tenant/universal/universal.util.ts
Normal file
59
server/src/tenant/universal/universal.util.ts
Normal file
@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user