feat: refactor schema builder and resolver builder (#2215)
* feat: wip refactor schema builder * feat: wip store types and first queries generation * feat: refactor schema-builder and resolver-builder * fix: clean & small type fix * fix: avoid breaking change * fix: remove util from pg-graphql classes * fix: required default fields * Refactor frontend accordingly --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -8,6 +8,8 @@ import {
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
@InputType()
|
||||
export class CreateFieldInput {
|
||||
@IsString()
|
||||
@ -20,20 +22,10 @@ export class CreateFieldInput {
|
||||
@Field()
|
||||
label: string;
|
||||
|
||||
// Todo: use a type enum and share with typeorm entity
|
||||
@IsEnum([
|
||||
'text',
|
||||
'phone',
|
||||
'email',
|
||||
'number',
|
||||
'boolean',
|
||||
'date',
|
||||
'url',
|
||||
'money',
|
||||
])
|
||||
@IsEnum(FieldMetadataType)
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
type: string;
|
||||
@Field(() => FieldMetadataType)
|
||||
type: FieldMetadataType;
|
||||
|
||||
@IsUUID()
|
||||
@Field()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
@ -17,13 +17,31 @@ import {
|
||||
QueryOptions,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
import { BeforeCreateOneField } from './hooks/before-create-one-field.hook';
|
||||
import { FieldMetadataTargetColumnMap } from './interfaces/field-metadata-target-column-map.interface';
|
||||
|
||||
export enum FieldMetadataType {
|
||||
UUID = 'uuid',
|
||||
TEXT = 'TEXT',
|
||||
PHONE = 'PHONE',
|
||||
EMAIL = 'EMAIL',
|
||||
DATE = 'DATE',
|
||||
BOOLEAN = 'BOOLEAN',
|
||||
NUMBER = 'NUMBER',
|
||||
ENUM = 'ENUM',
|
||||
URL = 'URL',
|
||||
MONEY = 'MONEY',
|
||||
}
|
||||
|
||||
registerEnumType(FieldMetadataType, {
|
||||
name: 'FieldMetadataType',
|
||||
description: 'Type of the field',
|
||||
});
|
||||
|
||||
export type FieldMetadataTargetColumnMap = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@Entity('field_metadata')
|
||||
@ObjectType('field')
|
||||
@BeforeCreateOne(BeforeCreateOneField)
|
||||
@ -43,7 +61,7 @@ export type FieldMetadataTargetColumnMap = {
|
||||
'objectId',
|
||||
'workspaceId',
|
||||
])
|
||||
export class FieldMetadata {
|
||||
export class FieldMetadata implements FieldMetadataInterface {
|
||||
@IDField(() => ID)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
@ -51,9 +69,9 @@ export class FieldMetadata {
|
||||
@Column({ nullable: false, name: 'object_id' })
|
||||
objectId: string;
|
||||
|
||||
@Field()
|
||||
@Field(() => FieldMetadataType)
|
||||
@Column({ nullable: false })
|
||||
type: string;
|
||||
type: FieldMetadataType;
|
||||
|
||||
@Field()
|
||||
@Column({ nullable: false })
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export interface FieldMetadataTargetColumnMapValue {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface FieldMetadataTargetColumnMapUrl {
|
||||
text: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export interface FieldMetadataTargetColumnMapMoney {
|
||||
value: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
type AllFieldMetadataTypes = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type FieldMetadataTypeMapping = {
|
||||
[FieldMetadataType.URL]: FieldMetadataTargetColumnMapUrl;
|
||||
[FieldMetadataType.MONEY]: FieldMetadataTargetColumnMapMoney;
|
||||
};
|
||||
|
||||
type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> =
|
||||
T extends keyof FieldMetadataTypeMapping
|
||||
? FieldMetadataTypeMapping[T]
|
||||
: T extends 'default'
|
||||
? AllFieldMetadataTypes
|
||||
: FieldMetadataTargetColumnMapValue;
|
||||
|
||||
export type FieldMetadataTargetColumnMap<
|
||||
T extends FieldMetadataType | 'default' = 'default',
|
||||
> = TypeByFieldMetadata<T>;
|
||||
@ -1,9 +1,11 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
|
||||
|
||||
import { uuidToBase36 } from 'src/metadata/data-source/data-source.util';
|
||||
import {
|
||||
FieldMetadata,
|
||||
FieldMetadataTargetColumnMap,
|
||||
FieldMetadataType,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { TenantMigrationColumnAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
|
||||
@ -25,24 +27,24 @@ export function generateColumnName(name: string): string {
|
||||
* @returns FieldMetadataTargetColumnMap
|
||||
*/
|
||||
export function generateTargetColumnMap(
|
||||
type: string,
|
||||
type: FieldMetadataType,
|
||||
): FieldMetadataTargetColumnMap {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'phone':
|
||||
case 'email':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'date':
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.PHONE:
|
||||
case FieldMetadataType.EMAIL:
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
case FieldMetadataType.DATE:
|
||||
return {
|
||||
value: `column_${uuidToBase36(v4())}`,
|
||||
};
|
||||
case 'url':
|
||||
case FieldMetadataType.URL:
|
||||
return {
|
||||
text: `column_${uuidToBase36(v4())}`,
|
||||
link: `column_${uuidToBase36(v4())}`,
|
||||
};
|
||||
case 'money':
|
||||
case FieldMetadataType.MONEY:
|
||||
return {
|
||||
amount: `column_${uuidToBase36(v4())}`,
|
||||
currency: `column_${uuidToBase36(v4())}`,
|
||||
@ -56,7 +58,7 @@ export function convertFieldMetadataToColumnActions(
|
||||
fieldMetadata: FieldMetadata,
|
||||
): TenantMigrationColumnAction[] {
|
||||
switch (fieldMetadata.type) {
|
||||
case 'text':
|
||||
case FieldMetadataType.TEXT:
|
||||
return [
|
||||
{
|
||||
name: fieldMetadata.targetColumnMap.value,
|
||||
@ -64,8 +66,8 @@ export function convertFieldMetadataToColumnActions(
|
||||
type: 'text',
|
||||
},
|
||||
];
|
||||
case 'phone':
|
||||
case 'email':
|
||||
case FieldMetadataType.PHONE:
|
||||
case FieldMetadataType.EMAIL:
|
||||
return [
|
||||
{
|
||||
name: fieldMetadata.targetColumnMap.value,
|
||||
@ -73,7 +75,7 @@ export function convertFieldMetadataToColumnActions(
|
||||
type: 'varchar',
|
||||
},
|
||||
];
|
||||
case 'number':
|
||||
case FieldMetadataType.NUMBER:
|
||||
return [
|
||||
{
|
||||
name: fieldMetadata.targetColumnMap.value,
|
||||
@ -81,7 +83,7 @@ export function convertFieldMetadataToColumnActions(
|
||||
type: 'integer',
|
||||
},
|
||||
];
|
||||
case 'boolean':
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
return [
|
||||
{
|
||||
name: fieldMetadata.targetColumnMap.value,
|
||||
@ -89,7 +91,7 @@ export function convertFieldMetadataToColumnActions(
|
||||
type: 'boolean',
|
||||
},
|
||||
];
|
||||
case 'date':
|
||||
case FieldMetadataType.DATE:
|
||||
return [
|
||||
{
|
||||
name: fieldMetadata.targetColumnMap.value,
|
||||
@ -97,7 +99,7 @@ export function convertFieldMetadataToColumnActions(
|
||||
type: 'timestamp',
|
||||
},
|
||||
];
|
||||
case 'url':
|
||||
case FieldMetadataType.URL:
|
||||
return [
|
||||
{
|
||||
name: fieldMetadata.targetColumnMap.text,
|
||||
@ -110,7 +112,7 @@ export function convertFieldMetadataToColumnActions(
|
||||
type: 'varchar',
|
||||
},
|
||||
];
|
||||
case 'money':
|
||||
case FieldMetadataType.MONEY:
|
||||
return [
|
||||
{
|
||||
name: fieldMetadata.targetColumnMap.amount,
|
||||
@ -127,24 +129,3 @@ export function convertFieldMetadataToColumnActions(
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import { TableColumnOptions } from 'typeorm';
|
||||
|
||||
export const customTableDefaultColumns: TableColumnOptions[] = [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isPrimary: true,
|
||||
default: 'public.uuid_generate_v4()',
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updatedAt',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'deletedAt',
|
||||
type: 'timestamp',
|
||||
isNullable: true,
|
||||
},
|
||||
];
|
||||
@ -9,6 +9,8 @@ import {
|
||||
} from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
|
||||
import { customTableDefaultColumns } from './custom-table-default-column.util';
|
||||
|
||||
@Injectable()
|
||||
export class MigrationRunnerService {
|
||||
constructor(
|
||||
@ -114,29 +116,7 @@ export class MigrationRunnerService {
|
||||
new Table({
|
||||
name: tableName,
|
||||
schema: schemaName,
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isPrimary: true,
|
||||
default: 'public.uuid_generate_v4()',
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updatedAt',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'deletedAt',
|
||||
type: 'timestamp',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
columns: customTableDefaultColumns,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
@ -17,6 +17,8 @@ import {
|
||||
QueryOptions,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook';
|
||||
@ -41,7 +43,7 @@ import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook';
|
||||
'workspaceId',
|
||||
])
|
||||
@Unique('IndexOnNamePluralAndWorkspaceIdUnique', ['namePlural', 'workspaceId'])
|
||||
export class ObjectMetadata {
|
||||
export class ObjectMetadata implements ObjectMetadataInterface {
|
||||
@IDField(() => ID)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ -70,6 +70,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadata> {
|
||||
return createdObjectMetadata;
|
||||
}
|
||||
|
||||
public async getObjectMetadataFromWorkspaceId(workspaceId: string) {
|
||||
return this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
}
|
||||
|
||||
public async getObjectMetadataFromDataSourceId(dataSourceId: string) {
|
||||
return this.objectMetadataRepository.find({
|
||||
where: { dataSourceId },
|
||||
|
||||
@ -8,7 +8,7 @@ const companiesMetadata = {
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
targetColumnMap: {
|
||||
@ -19,7 +19,7 @@ const companiesMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'domainName',
|
||||
label: 'Domain Name',
|
||||
targetColumnMap: {
|
||||
@ -30,7 +30,7 @@ const companiesMetadata = {
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'address',
|
||||
label: 'Address',
|
||||
targetColumnMap: {
|
||||
@ -41,7 +41,7 @@ const companiesMetadata = {
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
name: 'employees',
|
||||
label: 'Employees',
|
||||
targetColumnMap: {
|
||||
|
||||
@ -8,7 +8,7 @@ const viewFieldsMetadata = {
|
||||
icon: 'IconColumns3',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'fieldId',
|
||||
label: 'Field Id',
|
||||
targetColumnMap: {
|
||||
@ -19,7 +19,7 @@ const viewFieldsMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'viewId',
|
||||
label: 'View Id',
|
||||
targetColumnMap: {
|
||||
@ -41,7 +41,7 @@ const viewFieldsMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
targetColumnMap: {
|
||||
@ -52,7 +52,7 @@ const viewFieldsMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
targetColumnMap: {
|
||||
|
||||
@ -8,7 +8,7 @@ const viewFiltersMetadata = {
|
||||
icon: 'IconFilterBolt',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'fieldId',
|
||||
label: 'Field Id',
|
||||
targetColumnMap: {
|
||||
@ -19,7 +19,7 @@ const viewFiltersMetadata = {
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'viewId',
|
||||
label: 'View Id',
|
||||
targetColumnMap: {
|
||||
@ -30,7 +30,7 @@ const viewFiltersMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'operand',
|
||||
label: 'Operand',
|
||||
targetColumnMap: {
|
||||
@ -41,7 +41,7 @@ const viewFiltersMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'value',
|
||||
label: 'Value',
|
||||
targetColumnMap: {
|
||||
@ -52,7 +52,7 @@ const viewFiltersMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'displayValue',
|
||||
label: 'Display Value',
|
||||
targetColumnMap: {
|
||||
|
||||
@ -8,7 +8,7 @@ const viewSortsMetadata = {
|
||||
icon: 'IconArrowsSort',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'fieldId',
|
||||
label: 'Field Id',
|
||||
targetColumnMap: {
|
||||
@ -19,7 +19,7 @@ const viewSortsMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'viewId',
|
||||
label: 'View Id',
|
||||
targetColumnMap: {
|
||||
@ -30,7 +30,7 @@ const viewSortsMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'direction',
|
||||
label: 'Direction',
|
||||
targetColumnMap: {
|
||||
|
||||
@ -8,7 +8,7 @@ const viewsMetadata = {
|
||||
icon: 'IconLayoutCollage',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
targetColumnMap: {
|
||||
@ -19,7 +19,7 @@ const viewsMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'objectId',
|
||||
label: 'Object Id',
|
||||
targetColumnMap: {
|
||||
@ -30,7 +30,7 @@ const viewsMetadata = {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
type: 'TEXT',
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
targetColumnMap: {
|
||||
|
||||
Reference in New Issue
Block a user