feat: add object/field create/update resolvers (#1963)
* feat: add object/field create/update resolvers * fix tests
This commit is contained in:
@ -1,21 +0,0 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class CreateCustomFieldInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
displayName: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
type: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
objectId: string;
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class CreateFieldInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
displayName: string;
|
||||
|
||||
// Todo: use a type enum and share with typeorm entity
|
||||
@IsEnum([
|
||||
'text',
|
||||
'phone',
|
||||
'email',
|
||||
'number',
|
||||
'boolean',
|
||||
'date',
|
||||
'url',
|
||||
'money',
|
||||
])
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
type: string;
|
||||
|
||||
@IsUUID()
|
||||
@Field()
|
||||
objectId: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
placeholder?: string;
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class UpdateFieldInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayName: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
placeholder?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
isActive?: boolean;
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import { SortDirection } from '@ptc-org/nestjs-query-core';
|
||||
import {
|
||||
AutoResolverOpts,
|
||||
PagingStrategies,
|
||||
ReadResolverOpts,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
|
||||
import { FieldMetadata } from './field-metadata.entity';
|
||||
|
||||
import { FieldMetadataService } from './services/field-metadata.service';
|
||||
import { CreateFieldInput } from './dtos/create-field.input';
|
||||
import { UpdateFieldInput } from './dtos/update-field.input';
|
||||
|
||||
export const fieldMetadataAutoResolverOpts: AutoResolverOpts<
|
||||
any,
|
||||
any,
|
||||
unknown,
|
||||
unknown,
|
||||
ReadResolverOpts<any>,
|
||||
PagingStrategies
|
||||
>[] = [
|
||||
{
|
||||
EntityClass: FieldMetadata,
|
||||
DTOClass: FieldMetadata,
|
||||
CreateDTOClass: CreateFieldInput,
|
||||
UpdateDTOClass: UpdateFieldInput,
|
||||
ServiceClass: FieldMetadataService,
|
||||
enableTotalCount: true,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
read: {
|
||||
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
|
||||
},
|
||||
create: {
|
||||
many: { disabled: true },
|
||||
},
|
||||
update: {
|
||||
many: { disabled: true },
|
||||
},
|
||||
delete: { disabled: true },
|
||||
guards: [JwtAuthGuard],
|
||||
},
|
||||
];
|
||||
@ -11,29 +11,32 @@ import {
|
||||
} from 'typeorm';
|
||||
import {
|
||||
Authorize,
|
||||
BeforeCreateOne,
|
||||
IDField,
|
||||
QueryOptions,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
import { BeforeCreateOneField } from './hooks/before-create-one-field.hook';
|
||||
|
||||
export type FieldMetadataTargetColumnMap = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
@Entity('field_metadata')
|
||||
@ObjectType('field')
|
||||
@BeforeCreateOne(BeforeCreateOneField)
|
||||
@Authorize({
|
||||
authorize: (context: any) => ({
|
||||
workspaceId: { eq: context?.req?.user?.workspace?.id },
|
||||
}),
|
||||
})
|
||||
@QueryOptions({
|
||||
defaultResultSize: 10,
|
||||
maxResultsSize: 100,
|
||||
disableFilter: true,
|
||||
disableSort: true,
|
||||
})
|
||||
@Authorize({
|
||||
authorize: (context: any) => ({
|
||||
workspaceId: { eq: context?.req?.user?.workspace?.id },
|
||||
}),
|
||||
})
|
||||
@Entity('field_metadata')
|
||||
export class FieldMetadata {
|
||||
@IDField(() => ID)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -90,9 +93,11 @@ export class FieldMetadata {
|
||||
@JoinColumn({ name: 'object_id' })
|
||||
object: ObjectMetadata;
|
||||
|
||||
@Field()
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@Field()
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@ -1,34 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
NestjsQueryGraphQLModule,
|
||||
PagingStrategies,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { MigrationRunnerModule } from 'src/metadata/migration-runner/migration-runner.module';
|
||||
import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module';
|
||||
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
|
||||
|
||||
import { FieldMetadataService } from './field-metadata.service';
|
||||
import { FieldMetadata } from './field-metadata.entity';
|
||||
import { fieldMetadataAutoResolverOpts } from './field-metadata.auto-resolver-opts';
|
||||
|
||||
import { FieldMetadataService } from './services/field-metadata.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature([FieldMetadata], 'metadata'),
|
||||
TenantMigrationModule,
|
||||
MigrationRunnerModule,
|
||||
ObjectMetadataModule,
|
||||
],
|
||||
resolvers: [
|
||||
{
|
||||
EntityClass: FieldMetadata,
|
||||
DTOClass: FieldMetadata,
|
||||
enableTotalCount: true,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
create: { disabled: true },
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
guards: [JwtAuthGuard],
|
||||
},
|
||||
],
|
||||
services: [FieldMetadataService],
|
||||
resolvers: fieldMetadataAutoResolverOpts,
|
||||
}),
|
||||
],
|
||||
providers: [FieldMetadataService],
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { FieldMetadata } from './field-metadata.entity';
|
||||
import {
|
||||
generateColumnName,
|
||||
generateTargetColumnMap,
|
||||
} from './field-metadata.util';
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(FieldMetadata, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadata>,
|
||||
) {}
|
||||
|
||||
public async createFieldMetadata(
|
||||
displayName: string,
|
||||
type: string,
|
||||
objectId: string,
|
||||
workspaceId: string,
|
||||
): Promise<FieldMetadata> {
|
||||
return await this.fieldMetadataRepository.save({
|
||||
displayName: displayName,
|
||||
type,
|
||||
objectId,
|
||||
isCustom: true,
|
||||
targetColumnName: generateColumnName(displayName), // deprecated
|
||||
workspaceId,
|
||||
targetColumnMap: generateTargetColumnMap(type),
|
||||
});
|
||||
}
|
||||
|
||||
public async getFieldMetadataByDisplayNameAndObjectId(
|
||||
name: string,
|
||||
objectId: string,
|
||||
): Promise<FieldMetadata | null> {
|
||||
return await this.fieldMetadataRepository.findOne({
|
||||
where: { displayName: name, objectId },
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { uuidToBase36 } from 'src/metadata/data-source/data-source.util';
|
||||
|
||||
import { FieldMetadataTargetColumnMap } from './field-metadata.entity';
|
||||
|
||||
/**
|
||||
* Generate a column name from a field name removing unsupported characters.
|
||||
*
|
||||
* @param name string
|
||||
* @returns string
|
||||
*/
|
||||
export function generateColumnName(name: string): string {
|
||||
return name.toLowerCase().replace(/ /g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a target column map for a given type, this is used to map the field to the correct column(s) in the database.
|
||||
* This is used to support fields that map to multiple columns in the database.
|
||||
*
|
||||
* @param type string
|
||||
* @returns FieldMetadataTargetColumnMap
|
||||
*/
|
||||
export function generateTargetColumnMap(
|
||||
type: string,
|
||||
): FieldMetadataTargetColumnMap {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'phone':
|
||||
case 'email':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'date':
|
||||
return {
|
||||
value: uuidToBase36(v4()),
|
||||
};
|
||||
case 'url':
|
||||
return {
|
||||
text: uuidToBase36(v4()),
|
||||
link: uuidToBase36(v4()),
|
||||
};
|
||||
case 'money':
|
||||
return {
|
||||
amount: uuidToBase36(v4()),
|
||||
currency: uuidToBase36(v4()),
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown type ${type}`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
BeforeCreateOneHook,
|
||||
CreateOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BeforeCreateOneField<T extends FieldMetadata>
|
||||
implements BeforeCreateOneHook<T, any>
|
||||
{
|
||||
async run(
|
||||
instance: CreateOneInputType<T>,
|
||||
context: any,
|
||||
): Promise<CreateOneInputType<T>> {
|
||||
const workspaceId = context?.req?.user?.workspace?.id;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
instance.input.workspaceId = workspaceId;
|
||||
instance.input.isActive = false;
|
||||
instance.input.isCustom = true;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,12 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service';
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service';
|
||||
|
||||
import { FieldMetadataService } from './field-metadata.service';
|
||||
import { FieldMetadata } from './field-metadata.entity';
|
||||
|
||||
describe('FieldMetadataService', () => {
|
||||
let service: FieldMetadataService;
|
||||
@ -15,6 +19,18 @@ describe('FieldMetadataService', () => {
|
||||
provide: getRepositoryToken(FieldMetadata, 'metadata'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TenantMigrationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: MigrationRunnerService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
convertFieldMetadataToColumnChanges,
|
||||
convertMetadataTypeToColumnType,
|
||||
generateColumnName,
|
||||
generateTargetColumnMap,
|
||||
} from 'src/metadata/field-metadata/utils/field-metadata.util';
|
||||
import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service';
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service';
|
||||
import {
|
||||
TenantMigrationColumnChange,
|
||||
TenantMigrationTableChange,
|
||||
} from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataService extends TypeOrmQueryService<FieldMetadata> {
|
||||
constructor(
|
||||
@InjectRepository(FieldMetadata, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadata>,
|
||||
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly tenantMigrationService: TenantMigrationService,
|
||||
private readonly migrationRunnerService: MigrationRunnerService,
|
||||
) {
|
||||
super(fieldMetadataRepository);
|
||||
}
|
||||
|
||||
override async createOne(record: FieldMetadata): Promise<FieldMetadata> {
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.findOneWithinWorkspace(
|
||||
record.objectId,
|
||||
record.workspaceId,
|
||||
);
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new NotFoundException('Object does not exist');
|
||||
}
|
||||
|
||||
const fieldAlreadyExists = await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
displayName: record.displayName,
|
||||
objectId: record.objectId,
|
||||
workspaceId: record.workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fieldAlreadyExists) {
|
||||
throw new ConflictException('Field already exists');
|
||||
}
|
||||
|
||||
const createdFieldMetadata = await super.createOne({
|
||||
...record,
|
||||
targetColumnName: generateColumnName(record.displayName), // deprecated
|
||||
targetColumnMap: generateTargetColumnMap(record.type),
|
||||
});
|
||||
|
||||
await this.tenantMigrationService.createMigration(record.workspaceId, [
|
||||
{
|
||||
name: objectMetadata.targetTableName,
|
||||
change: 'alter',
|
||||
columns: [
|
||||
...convertFieldMetadataToColumnChanges(createdFieldMetadata),
|
||||
// Deprecated
|
||||
{
|
||||
name: createdFieldMetadata.targetColumnName,
|
||||
type: convertMetadataTypeToColumnType(record.type),
|
||||
change: 'create',
|
||||
} satisfies TenantMigrationColumnChange,
|
||||
],
|
||||
} satisfies TenantMigrationTableChange,
|
||||
]);
|
||||
|
||||
await this.migrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
record.workspaceId,
|
||||
);
|
||||
|
||||
return createdFieldMetadata;
|
||||
}
|
||||
|
||||
public async getFieldMetadataByDisplayNameAndObjectId(
|
||||
name: string,
|
||||
objectId: string,
|
||||
): Promise<FieldMetadata | null> {
|
||||
return await this.fieldMetadataRepository.findOne({
|
||||
where: { displayName: name, objectId },
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,56 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { uuidToBase36 } from 'src/metadata/data-source/data-source.util';
|
||||
import {
|
||||
FieldMetadata,
|
||||
FieldMetadataTargetColumnMap,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { TenantMigrationColumnChange } from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
|
||||
import { FieldMetadata } from './field-metadata/field-metadata.entity';
|
||||
/**
|
||||
* Generate a column name from a field name removing unsupported characters.
|
||||
*
|
||||
* @param name string
|
||||
* @returns string
|
||||
*/
|
||||
export function generateColumnName(name: string): string {
|
||||
return name.toLowerCase().replace(/ /g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a target column map for a given type, this is used to map the field to the correct column(s) in the database.
|
||||
* This is used to support fields that map to multiple columns in the database.
|
||||
*
|
||||
* @param type string
|
||||
* @returns FieldMetadataTargetColumnMap
|
||||
*/
|
||||
export function generateTargetColumnMap(
|
||||
type: string,
|
||||
): FieldMetadataTargetColumnMap {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'phone':
|
||||
case 'email':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'date':
|
||||
return {
|
||||
value: uuidToBase36(v4()),
|
||||
};
|
||||
case 'url':
|
||||
return {
|
||||
text: uuidToBase36(v4()),
|
||||
link: uuidToBase36(v4()),
|
||||
};
|
||||
case 'money':
|
||||
return {
|
||||
amount: uuidToBase36(v4()),
|
||||
currency: uuidToBase36(v4()),
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown type ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function convertFieldMetadataToColumnChanges(
|
||||
fieldMetadata: FieldMetadata,
|
||||
@ -77,3 +127,24 @@ export function convertFieldMetadataToColumnChanges(
|
||||
throw new Error(`Unknown type ${fieldMetadata.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated with target_column_name deprecation
|
||||
export function convertMetadataTypeToColumnType(type: string) {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'url':
|
||||
case 'phone':
|
||||
case 'email':
|
||||
return 'text';
|
||||
case 'number':
|
||||
return 'int';
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
case 'date':
|
||||
return 'timestamp';
|
||||
case 'money':
|
||||
return 'integer';
|
||||
default:
|
||||
throw new Error('Invalid type');
|
||||
}
|
||||
}
|
||||
@ -5,12 +5,10 @@ import { GraphQLModule } from '@nestjs/graphql';
|
||||
import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
|
||||
import { MigrationGeneratorModule } from 'src/metadata/migration-generator/migration-generator.module';
|
||||
import { MigrationRunnerModule } from 'src/metadata/migration-runner/migration-runner.module';
|
||||
import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module';
|
||||
|
||||
import { MetadataService } from './metadata.service';
|
||||
import { typeORMMetadataModuleOptions } from './metadata.datasource';
|
||||
import { MetadataResolver } from './metadata.resolver';
|
||||
|
||||
import { DataSourceModule } from './data-source/data-source.module';
|
||||
import { DataSourceMetadataModule } from './data-source-metadata/data-source-metadata.module';
|
||||
@ -41,10 +39,8 @@ const typeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
|
||||
DataSourceMetadataModule,
|
||||
FieldMetadataModule,
|
||||
ObjectMetadataModule,
|
||||
MigrationGeneratorModule,
|
||||
MigrationRunnerModule,
|
||||
TenantMigrationModule,
|
||||
],
|
||||
providers: [MetadataService, MetadataResolver],
|
||||
exports: [MetadataService],
|
||||
})
|
||||
export class MetadataModule {}
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { MetadataResolver } from './metadata.resolver';
|
||||
import { MetadataService } from './metadata.service';
|
||||
|
||||
describe('MetadataResolver', () => {
|
||||
let resolver: MetadataResolver;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
MetadataResolver,
|
||||
{
|
||||
provide: MetadataService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
resolver = module.get<MetadataResolver>(MetadataResolver);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(resolver).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -1,30 +0,0 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { Workspace } from '@prisma/client';
|
||||
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
||||
|
||||
import { MetadataService } from './metadata.service';
|
||||
|
||||
import { CreateCustomFieldInput } from './args/create-custom-field.input';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Resolver()
|
||||
export class MetadataResolver {
|
||||
constructor(private readonly metadataService: MetadataService) {}
|
||||
|
||||
@Mutation(() => String)
|
||||
async createCustomField(
|
||||
@Args() createCustomFieldInput: CreateCustomFieldInput,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<string> {
|
||||
return this.metadataService.createCustomField(
|
||||
createCustomFieldInput.displayName,
|
||||
createCustomFieldInput.objectId,
|
||||
createCustomFieldInput.type,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { MigrationGeneratorService } from 'src/metadata/migration-generator/migration-generator.service';
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
|
||||
import { MetadataService } from './metadata.service';
|
||||
|
||||
import { DataSourceService } from './data-source/data-source.service';
|
||||
import { ObjectMetadataService } from './object-metadata/object-metadata.service';
|
||||
import { FieldMetadataService } from './field-metadata/field-metadata.service';
|
||||
|
||||
describe('MetadataService', () => {
|
||||
let service: MetadataService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
MetadataService,
|
||||
{
|
||||
provide: DataSourceService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: MigrationGeneratorService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TenantMigrationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: FieldMetadataService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MetadataService>(MetadataService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -1,107 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { MigrationGeneratorService } from 'src/metadata/migration-generator/migration-generator.service';
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
import {
|
||||
TenantMigrationColumnChange,
|
||||
TenantMigrationTableChange,
|
||||
} from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
|
||||
import { convertFieldMetadataToColumnChanges } from './metadata.util';
|
||||
|
||||
import { DataSourceService } from './data-source/data-source.service';
|
||||
import { FieldMetadataService } from './field-metadata/field-metadata.service';
|
||||
import { ObjectMetadataService } from './object-metadata/object-metadata.service';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataService {
|
||||
constructor(
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly migrationGenerator: MigrationGeneratorService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly tenantMigrationService: TenantMigrationService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
) {}
|
||||
|
||||
public async createCustomField(
|
||||
displayName: string,
|
||||
objectId: string,
|
||||
type: string,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const workspaceDataSource =
|
||||
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
throw new Error('Workspace data source not found');
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.getObjectMetadataFromId(objectId);
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new Error('Object not found');
|
||||
}
|
||||
|
||||
const fieldMetadataAlreadyExists =
|
||||
await this.fieldMetadataService.getFieldMetadataByDisplayNameAndObjectId(
|
||||
displayName,
|
||||
objectId,
|
||||
);
|
||||
|
||||
if (fieldMetadataAlreadyExists) {
|
||||
throw new Error('Field already exists');
|
||||
}
|
||||
|
||||
const createdFieldMetadata =
|
||||
await this.fieldMetadataService.createFieldMetadata(
|
||||
displayName,
|
||||
type,
|
||||
objectMetadata.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.tenantMigrationService.createMigration(workspaceId, [
|
||||
{
|
||||
name: objectMetadata.targetTableName,
|
||||
change: 'alter',
|
||||
columns: [
|
||||
...convertFieldMetadataToColumnChanges(createdFieldMetadata),
|
||||
// Deprecated
|
||||
{
|
||||
name: createdFieldMetadata.targetColumnName,
|
||||
type: this.convertMetadataTypeToColumnType(type),
|
||||
change: 'create',
|
||||
} satisfies TenantMigrationColumnChange,
|
||||
],
|
||||
} satisfies TenantMigrationTableChange,
|
||||
]);
|
||||
|
||||
await this.migrationGenerator.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return createdFieldMetadata.id;
|
||||
}
|
||||
|
||||
// Deprecated with target_column_name
|
||||
private convertMetadataTypeToColumnType(type: string) {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'url':
|
||||
case 'phone':
|
||||
case 'email':
|
||||
return 'text';
|
||||
case 'number':
|
||||
return 'int';
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
case 'date':
|
||||
return 'timestamp';
|
||||
case 'money':
|
||||
return 'integer';
|
||||
default:
|
||||
throw new Error('Invalid type');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,11 +3,11 @@ import { Module } from '@nestjs/common';
|
||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||
import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module';
|
||||
|
||||
import { MigrationGeneratorService } from './migration-generator.service';
|
||||
import { MigrationRunnerService } from './migration-runner.service';
|
||||
|
||||
@Module({
|
||||
imports: [DataSourceModule, TenantMigrationModule],
|
||||
exports: [MigrationGeneratorService],
|
||||
providers: [MigrationGeneratorService],
|
||||
exports: [MigrationRunnerService],
|
||||
providers: [MigrationRunnerService],
|
||||
})
|
||||
export class MigrationGeneratorModule {}
|
||||
export class MigrationRunnerModule {}
|
||||
@ -3,15 +3,15 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
|
||||
import { MigrationGeneratorService } from './migration-generator.service';
|
||||
import { MigrationRunnerService } from './migration-runner.service';
|
||||
|
||||
describe('MigrationGeneratorService', () => {
|
||||
let service: MigrationGeneratorService;
|
||||
describe('MigrationRunnerService', () => {
|
||||
let service: MigrationRunnerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
MigrationGeneratorService,
|
||||
MigrationRunnerService,
|
||||
{
|
||||
provide: DataSourceService,
|
||||
useValue: {},
|
||||
@ -23,7 +23,7 @@ describe('MigrationGeneratorService', () => {
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MigrationGeneratorService>(MigrationGeneratorService);
|
||||
service = module.get<MigrationRunnerService>(MigrationRunnerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@ -10,7 +10,7 @@ import {
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
|
||||
@Injectable()
|
||||
export class MigrationGeneratorService {
|
||||
export class MigrationRunnerService {
|
||||
constructor(
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly tenantMigrationService: TenantMigrationService,
|
||||
@ -113,13 +113,23 @@ export class MigrationGeneratorService {
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isPrimary: true,
|
||||
default: 'uuid_generate_v4()',
|
||||
default: 'public.uuid_generate_v4()',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'deleted_at',
|
||||
type: 'timestamp',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
true,
|
||||
@ -0,0 +1,32 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class CreateObjectInput {
|
||||
// Deprecated
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
displayName: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayNameSingular?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayNamePlural?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class UpdateObjectInput {
|
||||
// Deprecated
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayName: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayNameSingular?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayNamePlural?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
isActive?: boolean;
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
BeforeCreateOneHook,
|
||||
CreateOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
|
||||
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BeforeCreateOneObject<T extends ObjectMetadata>
|
||||
implements BeforeCreateOneHook<T, any>
|
||||
{
|
||||
constructor(readonly dataSourceMetadataService: DataSourceMetadataService) {}
|
||||
|
||||
async run(
|
||||
instance: CreateOneInputType<T>,
|
||||
context: any,
|
||||
): Promise<CreateOneInputType<T>> {
|
||||
const workspaceId = context?.req?.user?.workspace?.id;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const lastDataSourceMetadata =
|
||||
await this.dataSourceMetadataService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
instance.input.dataSourceId = lastDataSourceMetadata.id;
|
||||
instance.input.targetTableName = instance.input.displayName;
|
||||
instance.input.workspaceId = workspaceId;
|
||||
instance.input.isActive = false;
|
||||
instance.input.isCustom = true;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import { SortDirection } from '@ptc-org/nestjs-query-core';
|
||||
import {
|
||||
AutoResolverOpts,
|
||||
PagingStrategies,
|
||||
ReadResolverOpts,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
|
||||
import { ObjectMetadata } from './object-metadata.entity';
|
||||
|
||||
import { ObjectMetadataService } from './services/object-metadata.service';
|
||||
import { CreateObjectInput } from './dtos/create-object.input';
|
||||
import { UpdateObjectInput } from './dtos/update-object.input';
|
||||
|
||||
export const objectMetadataAutoResolverOpts: AutoResolverOpts<
|
||||
any,
|
||||
any,
|
||||
unknown,
|
||||
unknown,
|
||||
ReadResolverOpts<any>,
|
||||
PagingStrategies
|
||||
>[] = [
|
||||
{
|
||||
EntityClass: ObjectMetadata,
|
||||
DTOClass: ObjectMetadata,
|
||||
CreateDTOClass: CreateObjectInput,
|
||||
UpdateDTOClass: UpdateObjectInput,
|
||||
ServiceClass: ObjectMetadataService,
|
||||
enableTotalCount: true,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
read: {
|
||||
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
|
||||
},
|
||||
create: {
|
||||
many: { disabled: true },
|
||||
},
|
||||
update: {
|
||||
many: { disabled: true },
|
||||
},
|
||||
delete: { disabled: true },
|
||||
guards: [JwtAuthGuard],
|
||||
},
|
||||
];
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import {
|
||||
Authorize,
|
||||
BeforeCreateOne,
|
||||
CursorConnection,
|
||||
IDField,
|
||||
QueryOptions,
|
||||
@ -17,20 +18,23 @@ import {
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook';
|
||||
|
||||
@Entity('object_metadata')
|
||||
@ObjectType('object')
|
||||
@BeforeCreateOne(BeforeCreateOneObject)
|
||||
@Authorize({
|
||||
authorize: (context: any) => ({
|
||||
workspaceId: { eq: context?.req?.user?.workspace?.id },
|
||||
}),
|
||||
})
|
||||
@QueryOptions({
|
||||
defaultResultSize: 10,
|
||||
maxResultsSize: 100,
|
||||
disableFilter: true,
|
||||
disableSort: true,
|
||||
})
|
||||
@Authorize({
|
||||
authorize: (context: any) => ({
|
||||
workspaceId: { eq: context?.req?.user?.workspace?.id },
|
||||
}),
|
||||
})
|
||||
@CursorConnection('fields', () => FieldMetadata)
|
||||
@Entity('object_metadata')
|
||||
export class ObjectMetadata {
|
||||
@IDField(() => ID)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -44,19 +48,19 @@ export class ObjectMetadata {
|
||||
@Column({ nullable: false, name: 'display_name' })
|
||||
displayName: string;
|
||||
|
||||
@Field()
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, name: 'display_name_singular' })
|
||||
displayNameSingular: string;
|
||||
|
||||
@Field()
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, name: 'display_name_plural' })
|
||||
displayNamePlural: string;
|
||||
|
||||
@Field()
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, name: 'description', type: 'text' })
|
||||
description: string;
|
||||
|
||||
@Field()
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, name: 'icon' })
|
||||
icon: string;
|
||||
|
||||
|
||||
@ -1,34 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
NestjsQueryGraphQLModule,
|
||||
PagingStrategies,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data-source-metadata.module';
|
||||
import { MigrationRunnerModule } from 'src/metadata/migration-runner/migration-runner.module';
|
||||
import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module';
|
||||
|
||||
import { ObjectMetadataService } from './object-metadata.service';
|
||||
import { ObjectMetadata } from './object-metadata.entity';
|
||||
import { objectMetadataAutoResolverOpts } from './object-metadata.auto-resolver-opts';
|
||||
|
||||
import { ObjectMetadataService } from './services/object-metadata.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature([ObjectMetadata], 'metadata'),
|
||||
DataSourceMetadataModule,
|
||||
TenantMigrationModule,
|
||||
MigrationRunnerModule,
|
||||
],
|
||||
resolvers: [
|
||||
{
|
||||
EntityClass: ObjectMetadata,
|
||||
DTOClass: ObjectMetadata,
|
||||
enableTotalCount: true,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
create: { disabled: true },
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
guards: [JwtAuthGuard],
|
||||
},
|
||||
],
|
||||
services: [ObjectMetadataService],
|
||||
resolvers: objectMetadataAutoResolverOpts,
|
||||
}),
|
||||
],
|
||||
providers: [ObjectMetadataService],
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ObjectMetadata } from './object-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadata, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<ObjectMetadata>,
|
||||
) {}
|
||||
|
||||
public async getObjectMetadataFromDataSourceId(dataSourceId: string) {
|
||||
return this.fieldMetadataRepository.find({
|
||||
where: { dataSourceId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
}
|
||||
|
||||
public async getObjectMetadataFromId(objectMetadataId: string) {
|
||||
return this.fieldMetadataRepository.findOne({
|
||||
where: { id: objectMetadataId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service';
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
|
||||
import { ObjectMetadataService } from './object-metadata.service';
|
||||
import { ObjectMetadata } from './object-metadata.entity';
|
||||
|
||||
describe('ObjectMetadataService', () => {
|
||||
let service: ObjectMetadataService;
|
||||
@ -15,6 +18,14 @@ describe('ObjectMetadataService', () => {
|
||||
provide: getRepositoryToken(ObjectMetadata, 'metadata'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TenantMigrationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: MigrationRunnerService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
import { ConflictException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
import { TenantMigrationTableChange } from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service';
|
||||
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadata> {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadata, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadata>,
|
||||
|
||||
private readonly tenantMigrationService: TenantMigrationService,
|
||||
private readonly migrationRunnerService: MigrationRunnerService,
|
||||
) {
|
||||
super(objectMetadataRepository);
|
||||
}
|
||||
|
||||
override async createOne(record: ObjectMetadata): Promise<ObjectMetadata> {
|
||||
const objectAlreadyExists = await this.objectMetadataRepository.findOne({
|
||||
where: {
|
||||
displayName: record.displayName, // deprecated, use singular and plural
|
||||
workspaceId: record.workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (objectAlreadyExists) {
|
||||
throw new ConflictException('Object already exists');
|
||||
}
|
||||
|
||||
const createdObjectMetadata = await super.createOne(record);
|
||||
|
||||
await this.tenantMigrationService.createMigration(
|
||||
createdObjectMetadata.workspaceId,
|
||||
[
|
||||
{
|
||||
name: createdObjectMetadata.targetTableName,
|
||||
change: 'create',
|
||||
} satisfies TenantMigrationTableChange,
|
||||
],
|
||||
);
|
||||
|
||||
await this.migrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
createdObjectMetadata.workspaceId,
|
||||
);
|
||||
|
||||
return createdObjectMetadata;
|
||||
}
|
||||
|
||||
public async getObjectMetadataFromDataSourceId(dataSourceId: string) {
|
||||
return this.objectMetadataRepository.find({
|
||||
where: { dataSourceId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
}
|
||||
|
||||
public async findOneWithinWorkspace(
|
||||
objectMetadataId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
return this.objectMetadataRepository.findOne({
|
||||
where: { id: objectMetadataId, workspaceId },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user