Add targetColumnMap to FieldMetadata (#1863)
* Add targetColumnMap to FieldMetadata * fix * remove console.log * fix test
This commit is contained in:
@ -7,7 +7,7 @@ export class CreateCustomFieldInput {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
displayName: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data-source-metadata.module';
|
import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data-source-metadata.module';
|
||||||
import { EntitySchemaGeneratorModule } from 'src/metadata/entity-schema-generator/entity-schema-generator.module';
|
|
||||||
|
|
||||||
import { DataSourceService } from './data-source.service';
|
import { DataSourceService } from './data-source.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DataSourceMetadataModule, EntitySchemaGeneratorModule],
|
imports: [DataSourceMetadataModule],
|
||||||
providers: [DataSourceService],
|
providers: [DataSourceService],
|
||||||
exports: [DataSourceService],
|
exports: [DataSourceService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
|
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
|
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
|
||||||
import { EntitySchemaGeneratorService } from 'src/metadata/entity-schema-generator/entity-schema-generator.service';
|
|
||||||
|
|
||||||
import { DataSourceService } from './data-source.service';
|
import { DataSourceService } from './data-source.service';
|
||||||
|
|
||||||
@ -23,10 +22,6 @@ describe('DataSourceService', () => {
|
|||||||
provide: DataSourceMetadataService,
|
provide: DataSourceMetadataService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: EntitySchemaGeneratorService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { DataSource, QueryRunner, Table } from 'typeorm';
|
|||||||
|
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
|
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
|
||||||
import { EntitySchemaGeneratorService } from 'src/metadata/entity-schema-generator/entity-schema-generator.service';
|
|
||||||
import { TenantMigration } from 'src/metadata/tenant-migration/tenant-migration.entity';
|
import { TenantMigration } from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||||
|
|
||||||
import { uuidToBase36 } from './data-source.util';
|
import { uuidToBase36 } from './data-source.util';
|
||||||
@ -17,7 +16,6 @@ export class DataSourceService implements OnModuleInit, OnModuleDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly dataSourceMetadataService: DataSourceMetadataService,
|
private readonly dataSourceMetadataService: DataSourceMetadataService,
|
||||||
private readonly entitySchemaGeneratorService: EntitySchemaGeneratorService,
|
|
||||||
) {
|
) {
|
||||||
this.mainDataSource = new DataSource({
|
this.mainDataSource = new DataSource({
|
||||||
url: environmentService.getPGDatabaseUrl(),
|
url: environmentService.getPGDatabaseUrl(),
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
export const baseColumns = {
|
|
||||||
id: {
|
|
||||||
primary: true,
|
|
||||||
type: 'uuid',
|
|
||||||
generated: 'uuid',
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: 'timestamp',
|
|
||||||
createDate: true,
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: 'timestamp',
|
|
||||||
updateDate: true,
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
|
|
||||||
|
|
||||||
import { EntitySchemaGeneratorService } from './entity-schema-generator.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [ObjectMetadataModule],
|
|
||||||
providers: [EntitySchemaGeneratorService],
|
|
||||||
exports: [EntitySchemaGeneratorService],
|
|
||||||
})
|
|
||||||
export class EntitySchemaGeneratorModule {}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
|
|
||||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
|
|
||||||
|
|
||||||
import { EntitySchemaGeneratorService } from './entity-schema-generator.service';
|
|
||||||
|
|
||||||
describe('EntitySchemaGeneratorService', () => {
|
|
||||||
let service: EntitySchemaGeneratorService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
EntitySchemaGeneratorService,
|
|
||||||
{
|
|
||||||
provide: ObjectMetadataService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<EntitySchemaGeneratorService>(
|
|
||||||
EntitySchemaGeneratorService,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { EntitySchema } from 'typeorm';
|
|
||||||
|
|
||||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
|
|
||||||
|
|
||||||
import { baseColumns } from './base.entity';
|
|
||||||
import {
|
|
||||||
convertFieldTypeToPostgresType,
|
|
||||||
sanitizeColumnName,
|
|
||||||
} from './entity-schema-generator.util';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class EntitySchemaGeneratorService {
|
|
||||||
constructor(private readonly objectMetadataService: ObjectMetadataService) {}
|
|
||||||
|
|
||||||
async getTypeORMEntitiesByDataSourceId(dataSourceId: string) {
|
|
||||||
const objectMetadata =
|
|
||||||
await this.objectMetadataService.getObjectMetadataFromDataSourceId(
|
|
||||||
dataSourceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const entities = objectMetadata.map((object) => {
|
|
||||||
return new EntitySchema({
|
|
||||||
name: object.targetTableName,
|
|
||||||
columns: {
|
|
||||||
...baseColumns,
|
|
||||||
...object.fields.reduce((columns, field) => {
|
|
||||||
return {
|
|
||||||
...columns,
|
|
||||||
[sanitizeColumnName(field.targetColumnName)]: {
|
|
||||||
type: convertFieldTypeToPostgresType(field.type),
|
|
||||||
nullable: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return entities;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* Converts a UUID to a base 36 string.
|
|
||||||
* This is used to generate the schema name since hyphens from workspace uuid are not allowed in postgres schema names.
|
|
||||||
*
|
|
||||||
* @param uuid
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const uuidToBase36 = (uuid: string): string => {
|
|
||||||
const hexString = uuid.replace(/-/g, '');
|
|
||||||
const base10Number = BigInt('0x' + hexString);
|
|
||||||
const base36String = base10Number.toString(36);
|
|
||||||
return base36String;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes a column name by replacing all non-alphanumeric characters with an underscore.
|
|
||||||
* Note: Probablay not the best way to do this, leaving it here as a placeholder for now.
|
|
||||||
*
|
|
||||||
* @param columnName
|
|
||||||
* @returns string
|
|
||||||
*/
|
|
||||||
export const sanitizeColumnName = (columnName: string): string =>
|
|
||||||
columnName.replace(/[^a-zA-Z0-9]/g, '_');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a field type to a postgres type. Field types are defined in the UI.
|
|
||||||
*
|
|
||||||
* @param fieldType
|
|
||||||
* @returns string
|
|
||||||
*/
|
|
||||||
export const convertFieldTypeToPostgresType = (fieldType: string): string => {
|
|
||||||
switch (fieldType) {
|
|
||||||
case 'text':
|
|
||||||
case 'url':
|
|
||||||
return 'text';
|
|
||||||
case 'number':
|
|
||||||
return 'numeric';
|
|
||||||
case 'boolean':
|
|
||||||
return 'boolean';
|
|
||||||
case 'date':
|
|
||||||
return 'timestamp';
|
|
||||||
default:
|
|
||||||
return 'text';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -10,6 +10,10 @@ import {
|
|||||||
|
|
||||||
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||||
|
|
||||||
|
export type FieldMetadataTargetColumnMap = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
@Entity('field_metadata')
|
@Entity('field_metadata')
|
||||||
export class FieldMetadata {
|
export class FieldMetadata {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@ -27,12 +31,27 @@ export class FieldMetadata {
|
|||||||
@Column({ nullable: false, name: 'target_column_name' })
|
@Column({ nullable: false, name: 'target_column_name' })
|
||||||
targetColumnName: string;
|
targetColumnName: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'description', type: 'text' })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'icon' })
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'placeholder' })
|
||||||
|
placeholder: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'target_column_map', type: 'jsonb' })
|
||||||
|
targetColumnMap: FieldMetadataTargetColumnMap;
|
||||||
|
|
||||||
@Column('text', { nullable: true, array: true })
|
@Column('text', { nullable: true, array: true })
|
||||||
enums: string[];
|
enums: string[];
|
||||||
|
|
||||||
@Column({ default: false, name: 'is_custom' })
|
@Column({ default: false, name: 'is_custom' })
|
||||||
isCustom: boolean;
|
isCustom: boolean;
|
||||||
|
|
||||||
|
@Column({ default: false, name: 'is_active' })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
@Column({ nullable: true, default: true, name: 'is_nullable' })
|
@Column({ nullable: true, default: true, name: 'is_nullable' })
|
||||||
isNullable: boolean;
|
isNullable: boolean;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,10 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { FieldMetadata } from './field-metadata.entity';
|
import { FieldMetadata } from './field-metadata.entity';
|
||||||
import { generateColumnName } from './field-metadata.util';
|
import {
|
||||||
|
generateColumnName,
|
||||||
|
generateTargetColumnMap,
|
||||||
|
} from './field-metadata.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FieldMetadataService {
|
export class FieldMetadataService {
|
||||||
@ -14,22 +17,23 @@ export class FieldMetadataService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async createFieldMetadata(
|
public async createFieldMetadata(
|
||||||
name: string,
|
displayName: string,
|
||||||
type: string,
|
type: string,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<FieldMetadata> {
|
): Promise<FieldMetadata> {
|
||||||
return await this.fieldMetadataRepository.save({
|
return await this.fieldMetadataRepository.save({
|
||||||
displayName: name,
|
displayName: displayName,
|
||||||
type,
|
type,
|
||||||
objectId,
|
objectId,
|
||||||
isCustom: true,
|
isCustom: true,
|
||||||
targetColumnName: generateColumnName(name),
|
targetColumnName: generateColumnName(displayName), // deprecated
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
targetColumnMap: generateTargetColumnMap(type),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFieldMetadataByNameAndObjectId(
|
public async getFieldMetadataByDisplayNameAndObjectId(
|
||||||
name: string,
|
name: string,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
): Promise<FieldMetadata | null> {
|
): Promise<FieldMetadata | null> {
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
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.
|
* Generate a column name from a field name removing unsupported characters.
|
||||||
*
|
*
|
||||||
@ -7,3 +13,38 @@
|
|||||||
export function generateColumnName(name: string): string {
|
export function generateColumnName(name: string): string {
|
||||||
return name.toLowerCase().replace(/ /g, '_');
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
|
|
||||||
import { MetadataController } from './metadata.controller';
|
|
||||||
|
|
||||||
import { DataSourceService } from './data-source/data-source.service';
|
|
||||||
import { DataSourceMetadataService } from './data-source-metadata/data-source-metadata.service';
|
|
||||||
import { EntitySchemaGeneratorService } from './entity-schema-generator/entity-schema-generator.service';
|
|
||||||
import { MigrationGeneratorService } from './migration-generator/migration-generator.service';
|
|
||||||
|
|
||||||
describe('MetadataController', () => {
|
|
||||||
let controller: MetadataController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [MetadataController],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: DataSourceService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: DataSourceMetadataService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: EntitySchemaGeneratorService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: MigrationGeneratorService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
controller = module.get<MetadataController>(MetadataController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(controller).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { Workspace } from '@prisma/client';
|
|
||||||
import { EntitySchema } from 'typeorm';
|
|
||||||
|
|
||||||
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
|
||||||
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 { MigrationGeneratorService } from './migration-generator/migration-generator.service';
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Controller('metadata_legacy')
|
|
||||||
export class MetadataController {
|
|
||||||
constructor(
|
|
||||||
private readonly entitySchemaGeneratorService: EntitySchemaGeneratorService,
|
|
||||||
private readonly dataSourceMetadataService: DataSourceMetadataService,
|
|
||||||
private readonly dataSourceService: DataSourceService,
|
|
||||||
private readonly migrationGenerator: MigrationGeneratorService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
async getMetadata(@AuthWorkspace() workspace: Workspace) {
|
|
||||||
const dataSourcesMetadata =
|
|
||||||
await this.dataSourceMetadataService.getDataSourcesMetadataFromWorkspaceId(
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const entities: EntitySchema<{
|
|
||||||
id: unknown;
|
|
||||||
}>[] = [];
|
|
||||||
|
|
||||||
for (const dataSource of dataSourcesMetadata) {
|
|
||||||
const dataSourceEntities =
|
|
||||||
await this.entitySchemaGeneratorService.getTypeORMEntitiesByDataSourceId(
|
|
||||||
dataSource.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
entities.push(...dataSourceEntities);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dataSourceService.createWorkspaceSchema(workspace.id);
|
|
||||||
|
|
||||||
await this.migrationGenerator.executeMigrationFromPendingMigrations(
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.dataSourceService.connectToWorkspaceDataSource(workspace.id);
|
|
||||||
|
|
||||||
return entities;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,8 +5,10 @@ import { GraphQLModule } from '@nestjs/graphql';
|
|||||||
import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs';
|
import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs';
|
||||||
import GraphQLJSON from 'graphql-type-json';
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
|
||||||
|
import { MigrationGeneratorModule } from 'src/metadata/migration-generator/migration-generator.module';
|
||||||
|
import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module';
|
||||||
|
|
||||||
import { MetadataService } from './metadata.service';
|
import { MetadataService } from './metadata.service';
|
||||||
import { MetadataController } from './metadata.controller';
|
|
||||||
import { typeORMMetadataModuleOptions } from './metadata.datasource';
|
import { typeORMMetadataModuleOptions } from './metadata.datasource';
|
||||||
import { MetadataResolver } from './metadata.resolver';
|
import { MetadataResolver } from './metadata.resolver';
|
||||||
|
|
||||||
@ -14,9 +16,6 @@ import { DataSourceModule } from './data-source/data-source.module';
|
|||||||
import { DataSourceMetadataModule } from './data-source-metadata/data-source-metadata.module';
|
import { DataSourceMetadataModule } from './data-source-metadata/data-source-metadata.module';
|
||||||
import { FieldMetadataModule } from './field-metadata/field-metadata.module';
|
import { FieldMetadataModule } from './field-metadata/field-metadata.module';
|
||||||
import { ObjectMetadataModule } from './object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from './object-metadata/object-metadata.module';
|
||||||
import { EntitySchemaGeneratorModule } from './entity-schema-generator/entity-schema-generator.module';
|
|
||||||
import { MigrationGeneratorModule } from './migration-generator/migration-generator.module';
|
|
||||||
import { TenantMigrationModule } from './tenant-migration/tenant-migration.module';
|
|
||||||
|
|
||||||
const typeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
|
const typeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
|
||||||
...typeORMMetadataModuleOptions,
|
...typeORMMetadataModuleOptions,
|
||||||
@ -42,12 +41,10 @@ const typeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
|
|||||||
DataSourceMetadataModule,
|
DataSourceMetadataModule,
|
||||||
FieldMetadataModule,
|
FieldMetadataModule,
|
||||||
ObjectMetadataModule,
|
ObjectMetadataModule,
|
||||||
EntitySchemaGeneratorModule,
|
|
||||||
MigrationGeneratorModule,
|
MigrationGeneratorModule,
|
||||||
TenantMigrationModule,
|
TenantMigrationModule,
|
||||||
],
|
],
|
||||||
providers: [MetadataService, MetadataResolver],
|
providers: [MetadataService, MetadataResolver],
|
||||||
exports: [MetadataService],
|
exports: [MetadataService],
|
||||||
controllers: [MetadataController],
|
|
||||||
})
|
})
|
||||||
export class MetadataModule {}
|
export class MetadataModule {}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export class MetadataResolver {
|
|||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return this.metadataService.createCustomField(
|
return this.metadataService.createCustomField(
|
||||||
createCustomFieldInput.name,
|
createCustomFieldInput.displayName,
|
||||||
createCustomFieldInput.objectId,
|
createCustomFieldInput.objectId,
|
||||||
createCustomFieldInput.type,
|
createCustomFieldInput.type,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
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 { MetadataService } from './metadata.service';
|
||||||
|
|
||||||
import { MigrationGeneratorService } from './migration-generator/migration-generator.service';
|
|
||||||
import { DataSourceService } from './data-source/data-source.service';
|
import { DataSourceService } from './data-source/data-source.service';
|
||||||
import { ObjectMetadataService } from './object-metadata/object-metadata.service';
|
import { ObjectMetadataService } from './object-metadata/object-metadata.service';
|
||||||
import { TenantMigrationService } from './tenant-migration/tenant-migration.service';
|
|
||||||
import { FieldMetadataService } from './field-metadata/field-metadata.service';
|
import { FieldMetadataService } from './field-metadata/field-metadata.service';
|
||||||
|
|
||||||
describe('MetadataService', () => {
|
describe('MetadataService', () => {
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { DataSourceService } from './data-source/data-source.service';
|
import { MigrationGeneratorService } from 'src/metadata/migration-generator/migration-generator.service';
|
||||||
import { FieldMetadataService } from './field-metadata/field-metadata.service';
|
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||||
import { MigrationGeneratorService } from './migration-generator/migration-generator.service';
|
|
||||||
import { ObjectMetadataService } from './object-metadata/object-metadata.service';
|
|
||||||
import { TenantMigrationService } from './tenant-migration/tenant-migration.service';
|
|
||||||
import {
|
import {
|
||||||
TenantMigrationColumnChange,
|
TenantMigrationColumnChange,
|
||||||
TenantMigrationTableChange,
|
TenantMigrationTableChange,
|
||||||
} from './tenant-migration/tenant-migration.entity';
|
} 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()
|
@Injectable()
|
||||||
export class MetadataService {
|
export class MetadataService {
|
||||||
@ -21,7 +24,7 @@ export class MetadataService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async createCustomField(
|
public async createCustomField(
|
||||||
name: string,
|
displayName: string,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
type: string,
|
type: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
@ -41,8 +44,8 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fieldMetadataAlreadyExists =
|
const fieldMetadataAlreadyExists =
|
||||||
await this.fieldMetadataService.getFieldMetadataByNameAndObjectId(
|
await this.fieldMetadataService.getFieldMetadataByDisplayNameAndObjectId(
|
||||||
name,
|
displayName,
|
||||||
objectId,
|
objectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -52,7 +55,7 @@ export class MetadataService {
|
|||||||
|
|
||||||
const createdFieldMetadata =
|
const createdFieldMetadata =
|
||||||
await this.fieldMetadataService.createFieldMetadata(
|
await this.fieldMetadataService.createFieldMetadata(
|
||||||
name,
|
displayName,
|
||||||
type,
|
type,
|
||||||
objectMetadata.id,
|
objectMetadata.id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -63,6 +66,8 @@ export class MetadataService {
|
|||||||
name: objectMetadata.targetTableName,
|
name: objectMetadata.targetTableName,
|
||||||
change: 'alter',
|
change: 'alter',
|
||||||
columns: [
|
columns: [
|
||||||
|
...convertFieldMetadataToColumnChanges(createdFieldMetadata),
|
||||||
|
// Deprecated
|
||||||
{
|
{
|
||||||
name: createdFieldMetadata.targetColumnName,
|
name: createdFieldMetadata.targetColumnName,
|
||||||
type: this.convertMetadataTypeToColumnType(type),
|
type: this.convertMetadataTypeToColumnType(type),
|
||||||
@ -79,6 +84,7 @@ export class MetadataService {
|
|||||||
return createdFieldMetadata.id;
|
return createdFieldMetadata.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated with target_column_name
|
||||||
private convertMetadataTypeToColumnType(type: string) {
|
private convertMetadataTypeToColumnType(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
@ -92,6 +98,8 @@ export class MetadataService {
|
|||||||
return 'boolean';
|
return 'boolean';
|
||||||
case 'date':
|
case 'date':
|
||||||
return 'timestamp';
|
return 'timestamp';
|
||||||
|
case 'money':
|
||||||
|
return 'integer';
|
||||||
default:
|
default:
|
||||||
throw new Error('Invalid type');
|
throw new Error('Invalid type');
|
||||||
}
|
}
|
||||||
|
|||||||
79
server/src/metadata/metadata.util.ts
Normal file
79
server/src/metadata/metadata.util.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { TenantMigrationColumnChange } from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||||
|
|
||||||
|
import { FieldMetadata } from './field-metadata/field-metadata.entity';
|
||||||
|
|
||||||
|
export function convertFieldMetadataToColumnChanges(
|
||||||
|
fieldMetadata: FieldMetadata,
|
||||||
|
): TenantMigrationColumnChange[] {
|
||||||
|
switch (fieldMetadata.type) {
|
||||||
|
case 'text':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: fieldMetadata.targetColumnMap.value,
|
||||||
|
change: 'create',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'phone':
|
||||||
|
case 'email':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: fieldMetadata.targetColumnMap.value,
|
||||||
|
change: 'create',
|
||||||
|
type: 'varchar',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'number':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: fieldMetadata.targetColumnMap.value,
|
||||||
|
change: 'create',
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'boolean':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: fieldMetadata.targetColumnMap.value,
|
||||||
|
change: 'create',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'date':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: fieldMetadata.targetColumnMap.value,
|
||||||
|
change: 'create',
|
||||||
|
type: 'timestamp',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'url':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: fieldMetadata.targetColumnMap.text,
|
||||||
|
change: 'create',
|
||||||
|
type: 'varchar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fieldMetadata.targetColumnMap.link,
|
||||||
|
change: 'create',
|
||||||
|
type: 'varchar',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'money':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: fieldMetadata.targetColumnMap.amount,
|
||||||
|
change: 'create',
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fieldMetadata.targetColumnMap.currency,
|
||||||
|
change: 'create',
|
||||||
|
type: 'varchar',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown type ${fieldMetadata.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddTargetColumnMap1696409050890 implements MigrationInterface {
|
||||||
|
name = 'AddTargetColumnMap1696409050890';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" ADD "description" text`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" ADD "icon" character varying`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" ADD "placeholder" character varying`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" ADD "target_column_map" jsonb`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" ADD "is_active" boolean NOT NULL DEFAULT false`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" ADD "display_name_singular" character varying`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" ADD "display_name_plural" character varying`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" ADD "description" text`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" ADD "icon" character varying`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" ADD "is_active" boolean NOT NULL DEFAULT false`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" DROP COLUMN "is_active"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" DROP COLUMN "icon"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" DROP COLUMN "description"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" DROP COLUMN "display_name_plural"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."object_metadata" DROP COLUMN "display_name_singular"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" DROP COLUMN "is_active"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" DROP COLUMN "target_column_map"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" DROP COLUMN "placeholder"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" DROP COLUMN "icon"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."field_metadata" DROP COLUMN "description"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,15 +17,31 @@ export class ObjectMetadata {
|
|||||||
@Column({ nullable: false, name: 'data_source_id' })
|
@Column({ nullable: false, name: 'data_source_id' })
|
||||||
dataSourceId: string;
|
dataSourceId: string;
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
@Column({ nullable: false, name: 'display_name' })
|
@Column({ nullable: false, name: 'display_name' })
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'display_name_singular' })
|
||||||
|
displayNameSingular: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'display_name_plural' })
|
||||||
|
displayNamePlural: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'description', type: 'text' })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'icon' })
|
||||||
|
icon: string;
|
||||||
|
|
||||||
@Column({ nullable: false, name: 'target_table_name' })
|
@Column({ nullable: false, name: 'target_table_name' })
|
||||||
targetTableName: string;
|
targetTableName: string;
|
||||||
|
|
||||||
@Column({ default: false, name: 'is_custom' })
|
@Column({ default: false, name: 'is_custom' })
|
||||||
isCustom: boolean;
|
isCustom: boolean;
|
||||||
|
|
||||||
|
@Column({ default: false, name: 'is_active' })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
@Column({ nullable: false, name: 'workspace_id' })
|
@Column({ nullable: false, name: 'workspace_id' })
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { Module } from '@nestjs/common';
|
|||||||
import { EntityResolverModule } from 'src/tenant/entity-resolver/entity-resolver.module';
|
import { EntityResolverModule } from 'src/tenant/entity-resolver/entity-resolver.module';
|
||||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||||
import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data-source-metadata.module';
|
import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data-source-metadata.module';
|
||||||
import { EntitySchemaGeneratorModule } from 'src/metadata/entity-schema-generator/entity-schema-generator.module';
|
|
||||||
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
|
||||||
|
|
||||||
import { SchemaGenerationService } from './schema-generation.service';
|
import { SchemaGenerationService } from './schema-generation.service';
|
||||||
@ -12,7 +11,6 @@ import { SchemaGenerationService } from './schema-generation.service';
|
|||||||
imports: [
|
imports: [
|
||||||
EntityResolverModule,
|
EntityResolverModule,
|
||||||
DataSourceMetadataModule,
|
DataSourceMetadataModule,
|
||||||
EntitySchemaGeneratorModule,
|
|
||||||
ObjectMetadataModule,
|
ObjectMetadataModule,
|
||||||
],
|
],
|
||||||
providers: [SchemaGenerationService, JwtAuthGuard],
|
providers: [SchemaGenerationService, JwtAuthGuard],
|
||||||
|
|||||||
Reference in New Issue
Block a user