Sync metadata generate migrations (#2864)
* Sync Metadata generates migrations * add execute migrations * fix relations + add isActive on creation * fix composite fields migration * remove dependency * use new metadata setup for seed-dev * fix rebase * remove unused code * fix viewField dev seeds * fix isSystem
This commit is contained in:
@ -0,0 +1,46 @@
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync.metadata.service';
|
||||
|
||||
// TODO: implement dry-run
|
||||
interface RunWorkspaceMigrationsOptions {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'workspace:sync-metadata',
|
||||
description: 'Sync metadata',
|
||||
})
|
||||
export class SyncWorkspaceMetadataCommand extends CommandRunner {
|
||||
constructor(
|
||||
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: RunWorkspaceMigrationsOptions,
|
||||
): Promise<void> {
|
||||
// TODO: run in a dedicated job + run queries in a transaction.
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
options.workspaceId,
|
||||
);
|
||||
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
|
||||
dataSourceMetadata.id,
|
||||
options.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id',
|
||||
required: true,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/workspace/workspace-sync-metadata/worksapce-sync-metadata.module';
|
||||
|
||||
import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceSyncMetadataModule, DataSourceModule],
|
||||
providers: [SyncWorkspaceMetadataCommand],
|
||||
})
|
||||
export class WorkspaceSyncMetadataCommandsModule {}
|
||||
@ -0,0 +1,183 @@
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/generate-target-column-map.util';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import { generateDefaultValue } from 'src/metadata/field-metadata/utils/generate-default-value';
|
||||
|
||||
export type FieldMetadataDecorator = {
|
||||
type: FieldMetadataType;
|
||||
label: string;
|
||||
description?: string | null;
|
||||
icon?: string | null;
|
||||
defaultValue?: FieldMetadataDefaultValue | null;
|
||||
joinColumn?: string;
|
||||
};
|
||||
|
||||
export type ObjectMetadataDecorator = {
|
||||
namePlural: string;
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
description?: string | null;
|
||||
icon?: string | null;
|
||||
};
|
||||
|
||||
export type RelationMetadataDecorator = {
|
||||
type: RelationMetadataType;
|
||||
objectName: string;
|
||||
inverseSideFieldName?: string;
|
||||
};
|
||||
|
||||
function convertClassNameToObjectMetadataName(name: string): string {
|
||||
const classSuffix = 'ObjectMetadata';
|
||||
let objectName = camelCase(name);
|
||||
|
||||
if (objectName.endsWith(classSuffix)) {
|
||||
objectName = objectName.slice(0, -classSuffix.length);
|
||||
}
|
||||
|
||||
return objectName;
|
||||
}
|
||||
|
||||
export function ObjectMetadata(
|
||||
metadata: ObjectMetadataDecorator,
|
||||
): ClassDecorator {
|
||||
return (target) => {
|
||||
const isSystem = Reflect.getMetadata('isSystem', target) || false;
|
||||
|
||||
const objectName = convertClassNameToObjectMetadataName(target.name);
|
||||
|
||||
Reflect.defineMetadata(
|
||||
'objectMetadata',
|
||||
{
|
||||
nameSingular: objectName,
|
||||
...metadata,
|
||||
targetTableName: objectName,
|
||||
isSystem,
|
||||
isCustom: false,
|
||||
description: metadata.description ?? null,
|
||||
icon: metadata.icon ?? null,
|
||||
},
|
||||
target,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function IsNullable() {
|
||||
return function (target: object, fieldKey: string) {
|
||||
Reflect.defineMetadata('isNullable', true, target, fieldKey);
|
||||
};
|
||||
}
|
||||
|
||||
export function IsSystem() {
|
||||
return function (target: object, fieldKey?: string) {
|
||||
if (fieldKey) {
|
||||
Reflect.defineMetadata('isSystem', true, target, fieldKey);
|
||||
} else {
|
||||
Reflect.defineMetadata('isSystem', true, target);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function FieldMetadata(
|
||||
metadata: FieldMetadataDecorator,
|
||||
): PropertyDecorator {
|
||||
return (target: object, fieldKey: string) => {
|
||||
const existingFieldMetadata =
|
||||
Reflect.getMetadata('fieldMetadata', target.constructor) || {};
|
||||
|
||||
const isNullable =
|
||||
Reflect.getMetadata('isNullable', target, fieldKey) || false;
|
||||
|
||||
const isSystem = Reflect.getMetadata('isSystem', target, fieldKey) || false;
|
||||
|
||||
const { joinColumn, ...fieldMetadata } = metadata;
|
||||
|
||||
Reflect.defineMetadata(
|
||||
'fieldMetadata',
|
||||
{
|
||||
...existingFieldMetadata,
|
||||
[fieldKey]: generateFieldMetadata(
|
||||
fieldMetadata,
|
||||
fieldKey,
|
||||
isNullable,
|
||||
isSystem,
|
||||
),
|
||||
...(joinColumn && fieldMetadata.type === FieldMetadataType.RELATION
|
||||
? {
|
||||
[joinColumn]: generateFieldMetadata(
|
||||
{
|
||||
...fieldMetadata,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: `${fieldMetadata.label} id (foreign key)`,
|
||||
description: `${fieldMetadata.description} id foreign key`,
|
||||
defaultValue: null,
|
||||
},
|
||||
joinColumn,
|
||||
isNullable,
|
||||
true,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
target.constructor,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function generateFieldMetadata(
|
||||
metadata: FieldMetadataDecorator,
|
||||
fieldKey: string,
|
||||
isNullable: boolean,
|
||||
isSystem: boolean,
|
||||
) {
|
||||
const targetColumnMap = JSON.stringify(
|
||||
generateTargetColumnMap(metadata.type, false, fieldKey),
|
||||
);
|
||||
const defaultValue =
|
||||
metadata.defaultValue ?? generateDefaultValue(metadata.type);
|
||||
|
||||
return {
|
||||
name: fieldKey,
|
||||
...metadata,
|
||||
targetColumnMap: targetColumnMap,
|
||||
isNullable,
|
||||
isSystem,
|
||||
isCustom: false,
|
||||
options: null, // TODO: handle options + stringify for the diff.
|
||||
description: metadata.description ?? null,
|
||||
icon: metadata.icon ?? null,
|
||||
defaultValue: defaultValue ? JSON.stringify(defaultValue) : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function RelationMetadata(
|
||||
metadata: RelationMetadataDecorator,
|
||||
): PropertyDecorator {
|
||||
return (target: object, fieldKey: string) => {
|
||||
const existingRelationMetadata =
|
||||
Reflect.getMetadata('relationMetadata', target.constructor) || [];
|
||||
|
||||
const objectName = convertClassNameToObjectMetadataName(
|
||||
target.constructor.name,
|
||||
);
|
||||
|
||||
Reflect.defineMetadata(
|
||||
'relationMetadata',
|
||||
[
|
||||
...existingRelationMetadata,
|
||||
{
|
||||
type: metadata.type,
|
||||
fromObjectNameSingular: objectName,
|
||||
toObjectNameSingular: metadata.objectName,
|
||||
fromFieldMetadataName: fieldKey,
|
||||
toFieldMetadataName: metadata.inverseSideFieldName ?? objectName,
|
||||
},
|
||||
],
|
||||
target.constructor,
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
FieldMetadata,
|
||||
IsSystem,
|
||||
IsNullable,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'activityTargets',
|
||||
labelSingular: 'Activity Target',
|
||||
labelPlural: 'Activity Targets',
|
||||
description: 'An activity target',
|
||||
icon: 'IconCheckbox',
|
||||
})
|
||||
@IsSystem()
|
||||
export class ActivityTargetObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Activity',
|
||||
description: 'ActivityTarget activity',
|
||||
icon: 'IconNotes',
|
||||
joinColumn: 'activityId',
|
||||
})
|
||||
activity: object;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Person',
|
||||
description: 'ActivityTarget person',
|
||||
icon: 'IconUser',
|
||||
joinColumn: 'personId',
|
||||
})
|
||||
@IsNullable()
|
||||
person: object;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Company',
|
||||
description: 'ActivityTarget company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
joinColumn: 'companyId',
|
||||
})
|
||||
@IsNullable()
|
||||
company: object;
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
IsNullable,
|
||||
FieldMetadata,
|
||||
RelationMetadata,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'activities',
|
||||
labelSingular: 'Activity',
|
||||
labelPlural: 'Activities',
|
||||
description: 'An activity',
|
||||
icon: 'IconCheckbox',
|
||||
})
|
||||
@IsSystem()
|
||||
export class ActivityObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Title',
|
||||
description: 'Activity title',
|
||||
icon: 'IconNotes',
|
||||
})
|
||||
@IsNullable()
|
||||
title: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Body',
|
||||
description: 'Activity body',
|
||||
icon: 'IconList',
|
||||
})
|
||||
@IsNullable()
|
||||
body: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Type',
|
||||
description: 'Activity type',
|
||||
icon: 'IconCheckbox',
|
||||
defaultValue: { value: 'Note' },
|
||||
})
|
||||
type: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Reminder Date',
|
||||
description: 'Activity reminder date',
|
||||
icon: 'IconCalendarEvent',
|
||||
})
|
||||
@IsNullable()
|
||||
reminderAt: Date;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Due Date',
|
||||
description: 'Activity due date',
|
||||
icon: 'IconCalendarEvent',
|
||||
})
|
||||
@IsNullable()
|
||||
dueAt: Date;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Completion Date',
|
||||
description: 'Activity completion date',
|
||||
icon: 'IconCheck',
|
||||
})
|
||||
@IsNullable()
|
||||
completedAt: Date;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Targets',
|
||||
description: 'Activity targets',
|
||||
icon: 'IconCheckbox',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'activityTarget',
|
||||
})
|
||||
activityTargets: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Attachments',
|
||||
description: 'Activity attachments',
|
||||
icon: 'IconFileImport',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'attachment',
|
||||
})
|
||||
attachments: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Comments',
|
||||
description: 'Activity comments',
|
||||
icon: 'IconComment',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'comment',
|
||||
})
|
||||
comments: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Author',
|
||||
description: 'Activity author',
|
||||
icon: 'IconUserCircle',
|
||||
joinColumn: 'authorId',
|
||||
})
|
||||
author: object;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Assignee',
|
||||
description: 'Acitivity assignee',
|
||||
icon: 'IconUserCircle',
|
||||
joinColumn: 'assigneeId',
|
||||
})
|
||||
assignee: object;
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
IsSystem,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'apiKeys',
|
||||
labelSingular: 'Api Key',
|
||||
labelPlural: 'Api Keys',
|
||||
description: 'An api key',
|
||||
icon: 'IconRobot',
|
||||
})
|
||||
@IsSystem()
|
||||
export class ApiKeyObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
description: 'ApiKey name',
|
||||
icon: 'IconLink',
|
||||
defaultValue: { value: '' },
|
||||
})
|
||||
name: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Expiration date',
|
||||
description: 'ApiKey expiration date',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
expiresAt: Date;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Revocation date',
|
||||
description: 'ApiKey revocation date',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
@IsNullable()
|
||||
revokedAt?: Date;
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'attachments',
|
||||
labelSingular: 'Attachment',
|
||||
labelPlural: 'Attachments',
|
||||
description: 'An attachment',
|
||||
icon: 'IconFileImport',
|
||||
})
|
||||
@IsSystem()
|
||||
export class AttachmentObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
description: 'Attachment name',
|
||||
icon: 'IconFileUpload',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Full path',
|
||||
description: 'Attachment full path',
|
||||
icon: 'IconLink',
|
||||
})
|
||||
fullPath: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Type',
|
||||
description: 'Attachment type',
|
||||
icon: 'IconList',
|
||||
})
|
||||
type: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Author',
|
||||
description: 'Attachment author',
|
||||
icon: 'IconCircleUser',
|
||||
joinColumn: 'authorId',
|
||||
})
|
||||
author: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Activity',
|
||||
description: 'Attachment activity',
|
||||
icon: 'IconNotes',
|
||||
joinColumn: 'activityId',
|
||||
})
|
||||
activity: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Person',
|
||||
description: 'Attachment person',
|
||||
icon: 'IconUser',
|
||||
joinColumn: 'personId',
|
||||
})
|
||||
@IsNullable()
|
||||
person: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Company',
|
||||
description: 'Attachment company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
joinColumn: 'companyId',
|
||||
})
|
||||
@IsNullable()
|
||||
company: string;
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
FieldMetadata,
|
||||
IsSystem,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
|
||||
export abstract class BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Id',
|
||||
icon: null,
|
||||
description: null,
|
||||
defaultValue: { type: 'uuid' },
|
||||
})
|
||||
@IsSystem()
|
||||
id: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Creation date',
|
||||
description: null,
|
||||
icon: 'IconCalendar',
|
||||
defaultValue: { type: 'now' },
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Update date',
|
||||
description: null,
|
||||
icon: 'IconCalendar',
|
||||
defaultValue: { type: 'now' },
|
||||
})
|
||||
@IsSystem()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
FieldMetadata,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'comments',
|
||||
labelSingular: 'Comment',
|
||||
labelPlural: 'Comments',
|
||||
description: 'A comment',
|
||||
icon: 'IconMessageCircle',
|
||||
})
|
||||
@IsSystem()
|
||||
export class CommentObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Body',
|
||||
description: 'Comment body',
|
||||
icon: 'IconLink',
|
||||
defaultValue: { value: '' },
|
||||
})
|
||||
body: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Author',
|
||||
description: 'Comment author',
|
||||
icon: 'IconCircleUser',
|
||||
joinColumn: 'authorId',
|
||||
})
|
||||
author: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Activity',
|
||||
description: 'Comment activity',
|
||||
icon: 'IconNotes',
|
||||
joinColumn: 'activityId',
|
||||
})
|
||||
activity: string;
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
RelationMetadata,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'companies',
|
||||
labelSingular: 'Company',
|
||||
labelPlural: 'Companies',
|
||||
description: 'A company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
})
|
||||
export class CompanyObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
description: 'The company name',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Domain Name',
|
||||
description:
|
||||
'The company website URL. We use this url to fetch the company icon',
|
||||
icon: 'IconLink',
|
||||
})
|
||||
@IsNullable()
|
||||
domainName?: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Address',
|
||||
description: 'The company address',
|
||||
icon: 'IconMap',
|
||||
})
|
||||
@IsNullable()
|
||||
address: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Employees',
|
||||
description: 'Number of employees in the company',
|
||||
icon: 'IconUsers',
|
||||
})
|
||||
@IsNullable()
|
||||
employees: number;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.LINK,
|
||||
label: 'Linkedin',
|
||||
description: 'The company Linkedin account',
|
||||
icon: 'IconBrandLinkedin',
|
||||
})
|
||||
@IsNullable()
|
||||
linkedinLink: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.LINK,
|
||||
label: 'X',
|
||||
description: 'The company Twitter/X account',
|
||||
icon: 'IconBrandX',
|
||||
})
|
||||
@IsNullable()
|
||||
xLink: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
label: 'ARR',
|
||||
description:
|
||||
'Annual Recurring Revenue: The actual or estimated annual revenue of the company',
|
||||
icon: 'IconMoneybag',
|
||||
})
|
||||
@IsNullable()
|
||||
annualRecurringRevenue: number;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'ICP',
|
||||
description:
|
||||
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
|
||||
icon: 'IconTarget',
|
||||
})
|
||||
@IsNullable()
|
||||
idealCustomerProfile: boolean;
|
||||
|
||||
// Relations
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'People',
|
||||
description: 'People linked to the company.',
|
||||
icon: 'IconUsers',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'person',
|
||||
})
|
||||
people: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Account Owner',
|
||||
description:
|
||||
'Your team member responsible for managing the company account',
|
||||
icon: 'IconUserCircle',
|
||||
joinColumn: 'accountOwnerId',
|
||||
})
|
||||
@IsNullable()
|
||||
accountOwner: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Activities',
|
||||
description: 'Activities tied to the company',
|
||||
icon: 'IconCheckbox',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'activityTarget',
|
||||
})
|
||||
activityTargets: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Opportunities',
|
||||
description: 'Opportunities linked to the company.',
|
||||
icon: 'IconTargetArrow',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'opportunity',
|
||||
})
|
||||
opportunities: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Favorites',
|
||||
description: 'Favorites linked to the company',
|
||||
icon: 'IconHeart',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'favorite',
|
||||
})
|
||||
favorites: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Attachments',
|
||||
description: 'Attachments linked to the company.',
|
||||
icon: 'IconFileImport',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'attachment',
|
||||
})
|
||||
attachments: object[];
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
FieldMetadata,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'favorites',
|
||||
labelSingular: 'Favorite',
|
||||
labelPlural: 'Favorites',
|
||||
description: 'A favorite',
|
||||
icon: 'IconHeart',
|
||||
})
|
||||
@IsSystem()
|
||||
export class FavoriteObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Position',
|
||||
description: 'Favorite position',
|
||||
icon: 'IconList',
|
||||
defaultValue: { value: 0 },
|
||||
})
|
||||
position: number;
|
||||
|
||||
// Relations
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Workspace Member',
|
||||
description: 'Favorite workspace member',
|
||||
icon: 'IconCircleUser',
|
||||
joinColumn: 'workspaceMemberId',
|
||||
})
|
||||
workspaceMember: object;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Person',
|
||||
description: 'Favorite person',
|
||||
icon: 'IconUser',
|
||||
joinColumn: 'personId',
|
||||
})
|
||||
person: object;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Company',
|
||||
description: 'Favorite company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
joinColumn: 'companyId',
|
||||
})
|
||||
company: object;
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { ActivityTargetObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata';
|
||||
import { ActivityObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity.object-metadata';
|
||||
import { ApiKeyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/api-key.object-metadata';
|
||||
import { AttachmentObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/attachment.object-metadata';
|
||||
import { CommentObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/comment.object-metadata';
|
||||
import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata';
|
||||
import { FavoriteObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata';
|
||||
import { OpportunityObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata';
|
||||
import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata';
|
||||
import { PipelineStepObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/pipeline-step.object-metadata';
|
||||
import { ViewFieldObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/view-field.object-metadata';
|
||||
import { ViewFilterObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/view-filter.object-metadata';
|
||||
import { ViewSortObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/view-sort.object-metadata';
|
||||
import { ViewObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/view.object-metadata';
|
||||
import { WebhookObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/webook.object-metadata';
|
||||
import { WorkspaceMemberObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata';
|
||||
|
||||
export const standardObjectMetadata = [
|
||||
ActivityTargetObjectMetadata,
|
||||
ActivityObjectMetadata,
|
||||
ApiKeyObjectMetadata,
|
||||
AttachmentObjectMetadata,
|
||||
CommentObjectMetadata,
|
||||
CompanyObjectMetadata,
|
||||
FavoriteObjectMetadata,
|
||||
OpportunityObjectMetadata,
|
||||
PersonObjectMetadata,
|
||||
PipelineStepObjectMetadata,
|
||||
ViewFieldObjectMetadata,
|
||||
ViewFilterObjectMetadata,
|
||||
ViewSortObjectMetadata,
|
||||
ViewObjectMetadata,
|
||||
WebhookObjectMetadata,
|
||||
WorkspaceMemberObjectMetadata,
|
||||
];
|
||||
@ -0,0 +1,85 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'opportunities',
|
||||
labelSingular: 'Opportunity',
|
||||
labelPlural: 'Opportunities',
|
||||
description: 'An opportunity',
|
||||
icon: 'IconTargetArrow',
|
||||
})
|
||||
export class OpportunityObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
label: 'Amount',
|
||||
description: 'Opportunity amount',
|
||||
icon: 'IconCurrencyDollar',
|
||||
})
|
||||
@IsNullable()
|
||||
amount: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Close date',
|
||||
description: 'Opportunity close date',
|
||||
icon: 'IconCalendarEvent',
|
||||
})
|
||||
@IsNullable()
|
||||
closeDate: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Probability',
|
||||
description: 'Opportunity probability',
|
||||
icon: 'IconProgressCheck',
|
||||
defaultValue: { value: '0' },
|
||||
})
|
||||
@IsNullable()
|
||||
probability: string;
|
||||
|
||||
// Relations
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Pipeline Step',
|
||||
description: 'Opportunity pipeline step',
|
||||
icon: 'IconKanban',
|
||||
joinColumn: 'pipelineStepId',
|
||||
})
|
||||
@IsNullable()
|
||||
pipelineStep: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Point of Contact',
|
||||
description: 'Opportunity point of contact',
|
||||
icon: 'IconUser',
|
||||
joinColumn: 'pointOfContactId',
|
||||
})
|
||||
@IsNullable()
|
||||
pointOfContact: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Person',
|
||||
description: 'Opportunity person',
|
||||
icon: 'IconUser',
|
||||
joinColumn: 'personId',
|
||||
})
|
||||
person: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Company',
|
||||
description: 'Opportunity company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
joinColumn: 'companyId',
|
||||
})
|
||||
@IsNullable()
|
||||
company: string;
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
RelationMetadata,
|
||||
IsSystem,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'people',
|
||||
labelSingular: 'Person',
|
||||
labelPlural: 'People',
|
||||
description: 'A person',
|
||||
icon: 'IconUser',
|
||||
})
|
||||
export class PersonObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
label: 'Name',
|
||||
description: 'Contact’s name',
|
||||
icon: 'IconUser',
|
||||
})
|
||||
@IsNullable()
|
||||
name: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.EMAIL,
|
||||
label: 'Email',
|
||||
description: 'Contact’s Email',
|
||||
icon: 'IconMail',
|
||||
})
|
||||
@IsNullable()
|
||||
email: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.LINK,
|
||||
label: 'Linkedin',
|
||||
description: 'Contact’s Linkedin account',
|
||||
icon: 'IconBrandLinkedin',
|
||||
})
|
||||
@IsNullable()
|
||||
linkedinLink: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.LINK,
|
||||
label: 'X',
|
||||
description: 'Contact’s X/Twitter account',
|
||||
icon: 'IconBrandX',
|
||||
})
|
||||
@IsNullable()
|
||||
xLink: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Job Title',
|
||||
description: 'Contact’s job title',
|
||||
icon: 'IconBriefcase',
|
||||
})
|
||||
@IsNullable()
|
||||
jobTitle: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Phone',
|
||||
description: 'Contact’s phone number',
|
||||
icon: 'IconPhone',
|
||||
})
|
||||
@IsNullable()
|
||||
phone: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'City',
|
||||
description: 'Contact’s city',
|
||||
icon: 'IconMap',
|
||||
})
|
||||
@IsNullable()
|
||||
city: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Avatar',
|
||||
description: 'Contact’s avatar',
|
||||
icon: 'IconFileUpload',
|
||||
})
|
||||
@IsSystem()
|
||||
@IsNullable()
|
||||
avatarUrl: string;
|
||||
|
||||
// Relations
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Company',
|
||||
description: 'Contact’s company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
joinColumn: 'companyId',
|
||||
})
|
||||
@IsNullable()
|
||||
company: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'POC for Opportunities',
|
||||
description: 'Point of Contact for Opportunities',
|
||||
icon: 'IconTargetArrow',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'opportunity',
|
||||
inverseSideFieldName: 'pointOfContact',
|
||||
})
|
||||
pointOfContactForOpportunities: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Activities',
|
||||
description: 'Activities tied to the contact',
|
||||
icon: 'IconCheckbox',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'activityTarget',
|
||||
})
|
||||
activityTargets: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Opportunities',
|
||||
description: 'Opportunities linked to the contact.',
|
||||
icon: 'IconTargetArrow',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'opportunity',
|
||||
})
|
||||
opportunities: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Favorites',
|
||||
description: 'Favorites linked to the contact',
|
||||
icon: 'IconHeart',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'favorite',
|
||||
})
|
||||
favorites: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Attachments',
|
||||
description: 'Attachments linked to the contact.',
|
||||
icon: 'IconFileImport',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'attachment',
|
||||
})
|
||||
attachments: object[];
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
IsSystem,
|
||||
RelationMetadata,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'pipelineSteps',
|
||||
labelSingular: 'Pipeline Step',
|
||||
labelPlural: 'Pipeline Steps',
|
||||
description: 'A pipeline step',
|
||||
icon: 'IconLayoutKanban',
|
||||
})
|
||||
@IsSystem()
|
||||
export class PipelineStepObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
description: 'Pipeline Step name',
|
||||
icon: 'IconCurrencyDollar',
|
||||
})
|
||||
@IsNullable()
|
||||
name: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Color',
|
||||
description: 'Pipeline Step color',
|
||||
icon: 'IconColorSwatch',
|
||||
})
|
||||
@IsNullable()
|
||||
color: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Position',
|
||||
description: 'Pipeline Step position',
|
||||
icon: 'IconHierarchy2',
|
||||
defaultValue: { value: 0 },
|
||||
})
|
||||
@IsNullable()
|
||||
position: number;
|
||||
|
||||
// Relations
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Opportunities',
|
||||
description: 'Opportunities linked to the step.',
|
||||
icon: 'IconTargetArrow',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'opportunity',
|
||||
})
|
||||
@IsNullable()
|
||||
opportunities: object[];
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'viewFields',
|
||||
labelSingular: 'View Field',
|
||||
labelPlural: 'View Fields',
|
||||
description: '(System) View Fields',
|
||||
icon: 'IconTag',
|
||||
})
|
||||
@IsSystem()
|
||||
export class ViewFieldObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Field Metadata Id',
|
||||
description: 'View Field target field',
|
||||
icon: 'IconTag',
|
||||
})
|
||||
fieldMetadataId: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Visible',
|
||||
description: 'View Field visibility',
|
||||
icon: 'IconEye',
|
||||
defaultValue: { value: true },
|
||||
})
|
||||
isVisible: boolean;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Size',
|
||||
description: 'View Field size',
|
||||
icon: 'IconEye',
|
||||
defaultValue: { value: 0 },
|
||||
})
|
||||
size: number;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Position',
|
||||
description: 'View Field position',
|
||||
icon: 'IconList',
|
||||
defaultValue: { value: 0 },
|
||||
})
|
||||
position: number;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'View',
|
||||
description: 'View Field related view',
|
||||
icon: 'IconLayoutCollage',
|
||||
joinColumn: 'viewId',
|
||||
})
|
||||
@IsNullable()
|
||||
view?: object;
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'viewFilters',
|
||||
labelSingular: 'View Filter',
|
||||
labelPlural: 'View Filters',
|
||||
description: '(System) View Filters',
|
||||
icon: 'IconFilterBolt',
|
||||
})
|
||||
@IsSystem()
|
||||
export class ViewFilterObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Field Metadata Id',
|
||||
description: 'View Filter target field',
|
||||
icon: null,
|
||||
})
|
||||
fieldMetadataId: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Operand',
|
||||
description: 'View Filter operand',
|
||||
icon: null,
|
||||
defaultValue: { value: 'Contains' },
|
||||
})
|
||||
operand: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Value',
|
||||
description: 'View Filter value',
|
||||
icon: null,
|
||||
defaultValue: { value: '' },
|
||||
})
|
||||
value: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Display Value',
|
||||
description: 'View Filter Display Value',
|
||||
icon: null,
|
||||
defaultValue: { value: '' },
|
||||
})
|
||||
displayValue: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'View',
|
||||
description: 'View Filter related view',
|
||||
icon: 'IconLayoutCollage',
|
||||
joinColumn: 'viewId',
|
||||
})
|
||||
@IsNullable()
|
||||
view: string;
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
IsSystem,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'viewSorts',
|
||||
labelSingular: 'View Sort',
|
||||
labelPlural: 'View Sorts',
|
||||
description: '(System) View Sorts',
|
||||
icon: 'IconArrowsSort',
|
||||
})
|
||||
@IsSystem()
|
||||
export class ViewSortObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Field Metadata Id',
|
||||
description: 'View Sort target field',
|
||||
icon: 'IconTag',
|
||||
})
|
||||
fieldMetadataId: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Direction',
|
||||
description: 'View Sort direction',
|
||||
icon: null,
|
||||
defaultValue: { value: 'asc' },
|
||||
})
|
||||
direction: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'View',
|
||||
description: 'View Sort related view',
|
||||
icon: 'IconLayoutCollage',
|
||||
joinColumn: 'viewId',
|
||||
})
|
||||
@IsNullable()
|
||||
view: string;
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
FieldMetadata,
|
||||
RelationMetadata,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'views',
|
||||
labelSingular: 'View',
|
||||
labelPlural: 'Views',
|
||||
description: '(System) Views',
|
||||
icon: 'IconLayoutCollage',
|
||||
})
|
||||
@IsSystem()
|
||||
export class ViewObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
description: 'View name',
|
||||
icon: null,
|
||||
defaultValue: { value: '' },
|
||||
})
|
||||
name: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Object Metadata Id',
|
||||
description: 'View target object',
|
||||
icon: null,
|
||||
})
|
||||
objectMetadataId: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Type',
|
||||
description: 'View type',
|
||||
icon: null,
|
||||
defaultValue: { value: 'table' },
|
||||
})
|
||||
type: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'View Fields',
|
||||
description: 'View Fields',
|
||||
icon: 'IconTag',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'viewField',
|
||||
})
|
||||
viewFields: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'View Filters',
|
||||
description: 'View Filters',
|
||||
icon: 'IconFilterBolt',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'viewFilter',
|
||||
})
|
||||
viewFilters: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'View Sorts',
|
||||
description: 'View Sorts',
|
||||
icon: 'IconArrowsSort',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'viewSort',
|
||||
})
|
||||
viewSorts: object[];
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
FieldMetadata,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'webhooks',
|
||||
labelSingular: 'Webhook',
|
||||
labelPlural: 'Webhooks',
|
||||
description: 'A webhook',
|
||||
icon: 'IconRobot',
|
||||
})
|
||||
@IsSystem()
|
||||
export class WebhookObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Target Url',
|
||||
description: 'Webhook target url',
|
||||
icon: 'IconLink',
|
||||
defaultValue: { value: '' },
|
||||
})
|
||||
targetUrl: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Operation',
|
||||
description: 'Webhook operation',
|
||||
icon: 'IconCheckbox',
|
||||
defaultValue: { value: '' },
|
||||
})
|
||||
operation: string;
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
ObjectMetadata,
|
||||
IsSystem,
|
||||
FieldMetadata,
|
||||
IsNullable,
|
||||
RelationMetadata,
|
||||
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
namePlural: 'workspaceMembers',
|
||||
labelSingular: 'Workspace Member',
|
||||
labelPlural: 'Workspace Members',
|
||||
description: 'A workspace member',
|
||||
icon: 'IconUserCircle',
|
||||
})
|
||||
@IsSystem()
|
||||
export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
label: 'Name',
|
||||
description: 'Workspace member name',
|
||||
icon: 'IconCircleUser',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Color Scheme',
|
||||
description: 'Preferred color scheme',
|
||||
icon: 'IconColorSwatch',
|
||||
defaultValue: { value: 'Light' },
|
||||
})
|
||||
colorScheme: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Language',
|
||||
description: 'Preferred language',
|
||||
icon: 'IconLanguage',
|
||||
defaultValue: { value: 'en' },
|
||||
})
|
||||
locale: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Avatar Url',
|
||||
description: 'Workspace member avatar',
|
||||
icon: 'IconFileUpload',
|
||||
defaultValue: { value: '' },
|
||||
})
|
||||
@IsNullable()
|
||||
avatarUrl: string;
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'User Id',
|
||||
description: 'Associated User Id',
|
||||
icon: 'IconCircleUsers',
|
||||
})
|
||||
userId: string;
|
||||
|
||||
// Relations
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Authored activities',
|
||||
description: 'Activities created by the workspace member',
|
||||
icon: 'IconCheckbox',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'activity',
|
||||
inverseSideFieldName: 'author',
|
||||
})
|
||||
authoredActivities: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Assigned activities',
|
||||
description: 'Activities assigned to the workspace member',
|
||||
icon: 'IconCheckbox',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'activity',
|
||||
inverseSideFieldName: 'assignee',
|
||||
})
|
||||
assignedActivities: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Favorites',
|
||||
description: 'Favorites linked to the workspace member',
|
||||
icon: 'IconHeart',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'favorite',
|
||||
})
|
||||
favorites: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Account Owner For Companies',
|
||||
description: 'Account owner for companies',
|
||||
icon: 'IconBriefcase',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'company',
|
||||
inverseSideFieldName: 'accountOwner',
|
||||
})
|
||||
accountOwnerForCompanies: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Authored attachments',
|
||||
description: 'Attachments created by the workspace member',
|
||||
icon: 'IconFileImport',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'attachment',
|
||||
inverseSideFieldName: 'author',
|
||||
})
|
||||
authoredAttachments: object[];
|
||||
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Authored comments',
|
||||
description: 'Authored comments',
|
||||
icon: 'IconComment',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
objectName: 'comment',
|
||||
inverseSideFieldName: 'author',
|
||||
})
|
||||
authoredComments: object[];
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
export class MetadataParser {
|
||||
static parseMetadata(
|
||||
metadata: typeof BaseObjectMetadata,
|
||||
workspaceId: string,
|
||||
dataSourceId: string,
|
||||
) {
|
||||
const objectMetadata = Reflect.getMetadata('objectMetadata', metadata);
|
||||
const fieldMetadata = Reflect.getMetadata('fieldMetadata', metadata);
|
||||
|
||||
if (objectMetadata) {
|
||||
const fields = Object.values(fieldMetadata);
|
||||
|
||||
return {
|
||||
...objectMetadata,
|
||||
workspaceId,
|
||||
dataSourceId,
|
||||
fields: fields.map((field: FieldMetadataEntity) => ({
|
||||
...field,
|
||||
workspaceId,
|
||||
isSystem: objectMetadata.isSystem || field.isSystem,
|
||||
defaultValue: field.defaultValue || null,
|
||||
options: field.options || null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static parseAllMetadata(
|
||||
metadata: (typeof BaseObjectMetadata)[],
|
||||
workspaceId: string,
|
||||
dataSourceId: string,
|
||||
) {
|
||||
return metadata.map((_metadata) =>
|
||||
MetadataParser.parseMetadata(_metadata, workspaceId, dataSourceId),
|
||||
);
|
||||
}
|
||||
|
||||
static parseRelationMetadata(
|
||||
metadata: typeof BaseObjectMetadata,
|
||||
workspaceId: string,
|
||||
objectMetadataFromDB: Record<string, ObjectMetadataEntity>,
|
||||
) {
|
||||
const objectMetadata = Reflect.getMetadata('objectMetadata', metadata);
|
||||
const relationMetadata = Reflect.getMetadata('relationMetadata', metadata);
|
||||
|
||||
if (!relationMetadata) return [];
|
||||
|
||||
return relationMetadata.map((relation) => {
|
||||
const fromObjectMetadata =
|
||||
objectMetadataFromDB[relation.fromObjectNameSingular];
|
||||
assert(
|
||||
fromObjectMetadata,
|
||||
`Object ${relation.fromObjectNameSingular} not found in DB
|
||||
for relation defined in class ${objectMetadata.nameSingular}`,
|
||||
);
|
||||
const toObjectMetadata =
|
||||
objectMetadataFromDB[relation.toObjectNameSingular];
|
||||
assert(
|
||||
toObjectMetadata,
|
||||
`Object ${relation.toObjectNameSingular} not found in DB
|
||||
for relation defined in class ${objectMetadata.nameSingular}`,
|
||||
);
|
||||
const fromFieldMetadata =
|
||||
fromObjectMetadata?.fields[relation.fromFieldMetadataName];
|
||||
assert(
|
||||
fromFieldMetadata,
|
||||
`Field ${relation.fromFieldMetadataName} not found in object ${relation.fromObjectNameSingular}
|
||||
for relation defined in class ${objectMetadata.nameSingular}`,
|
||||
);
|
||||
const toFieldMetadata =
|
||||
toObjectMetadata?.fields[relation.toFieldMetadataName];
|
||||
assert(
|
||||
toFieldMetadata,
|
||||
`Field ${relation.toFieldMetadataName} not found in object ${relation.toObjectNameSingular}
|
||||
for relation defined in class ${objectMetadata.nameSingular}`,
|
||||
);
|
||||
return {
|
||||
relationType: relation.type,
|
||||
fromObjectMetadataId: fromObjectMetadata?.id,
|
||||
toObjectMetadataId: toObjectMetadata?.id,
|
||||
fromFieldMetadataId: fromFieldMetadata?.id,
|
||||
toFieldMetadataId: toFieldMetadata?.id,
|
||||
workspaceId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static parseAllRelations(
|
||||
metadata: (typeof BaseObjectMetadata)[],
|
||||
workspaceId: string,
|
||||
objectMetadataFromDB: Record<string, ObjectMetadataEntity>,
|
||||
) {
|
||||
return metadata.flatMap((_metadata) =>
|
||||
MetadataParser.parseRelationMetadata(
|
||||
_metadata,
|
||||
workspaceId,
|
||||
objectMetadataFromDB,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
import {
|
||||
filterIgnoredProperties,
|
||||
mapObjectMetadataByUniqueIdentifier,
|
||||
} from './sync-metadata.util';
|
||||
|
||||
describe('filterIgnoredProperties', () => {
|
||||
it('should filter out properties based on the ignore list', () => {
|
||||
const obj = {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
email: 'john@example.com',
|
||||
address: '123 Main St',
|
||||
};
|
||||
const propertiesToIgnore = ['age', 'address'];
|
||||
|
||||
const filteredObj = filterIgnoredProperties(obj, propertiesToIgnore);
|
||||
|
||||
expect(filteredObj).toEqual({
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the original object if ignore list is empty', () => {
|
||||
const obj = {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
email: 'john@example.com',
|
||||
address: '123 Main St',
|
||||
};
|
||||
const propertiesToIgnore: string[] = [];
|
||||
|
||||
const filteredObj = filterIgnoredProperties(obj, propertiesToIgnore);
|
||||
|
||||
expect(filteredObj).toEqual(obj);
|
||||
});
|
||||
|
||||
it('should return an empty object if the original object is empty', () => {
|
||||
const obj = {};
|
||||
const propertiesToIgnore = ['age', 'address'];
|
||||
|
||||
const filteredObj = filterIgnoredProperties(obj, propertiesToIgnore);
|
||||
|
||||
expect(filteredObj).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapObjectMetadataByUniqueIdentifier', () => {
|
||||
it('should convert an array of ObjectMetadataEntity objects into a map', () => {
|
||||
const arr: DeepPartial<ObjectMetadataEntity>[] = [
|
||||
{
|
||||
nameSingular: 'user',
|
||||
fields: [
|
||||
{ name: 'id', type: FieldMetadataType.UUID },
|
||||
{ name: 'name', type: FieldMetadataType.TEXT },
|
||||
],
|
||||
},
|
||||
{
|
||||
nameSingular: 'product',
|
||||
fields: [
|
||||
{ name: 'id', type: FieldMetadataType.UUID },
|
||||
{ name: 'name', type: FieldMetadataType.TEXT },
|
||||
{ name: 'price', type: FieldMetadataType.UUID },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mappedObject = mapObjectMetadataByUniqueIdentifier(
|
||||
arr as ObjectMetadataEntity[],
|
||||
);
|
||||
|
||||
expect(mappedObject).toEqual({
|
||||
user: {
|
||||
nameSingular: 'user',
|
||||
fields: {
|
||||
id: { name: 'id', type: FieldMetadataType.UUID },
|
||||
name: { name: 'name', type: FieldMetadataType.TEXT },
|
||||
},
|
||||
},
|
||||
product: {
|
||||
nameSingular: 'product',
|
||||
fields: {
|
||||
id: { name: 'id', type: FieldMetadataType.UUID },
|
||||
name: { name: 'name', type: FieldMetadataType.TEXT },
|
||||
price: { name: 'price', type: FieldMetadataType.UUID },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an empty map if the input array is empty', () => {
|
||||
const arr: ObjectMetadataEntity[] = [];
|
||||
|
||||
const mappedObject = mapObjectMetadataByUniqueIdentifier(arr);
|
||||
|
||||
expect(mappedObject).toEqual({});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,65 @@
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
/**
|
||||
* This utility function filters out properties from an object based on a list of properties to ignore.
|
||||
* It returns a new object with only the properties that are not in the ignore list.
|
||||
*
|
||||
* @param obj - The object to filter.
|
||||
* @param propertiesToIgnore - An array of property names to ignore.
|
||||
* @returns A new object with filtered properties.
|
||||
*/
|
||||
export const filterIgnoredProperties = (
|
||||
obj: any,
|
||||
propertiesToIgnore: string[],
|
||||
mapFunction?: (value: any) => any,
|
||||
) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj)
|
||||
.filter(([key]) => !propertiesToIgnore.includes(key))
|
||||
.map(([key, value]) => [key, mapFunction ? mapFunction(value) : value]),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This utility function converts an array of ObjectMetadataEntity objects into a map,
|
||||
* where the keys are the nameSingular properties of the objects.
|
||||
* Each object in the map contains the original object metadata and its fields as a nested map.
|
||||
*
|
||||
* @param arr - The array of ObjectMetadataEntity objects to convert.
|
||||
* @returns A map of object metadata, with nameSingular as the key and the object as the value.
|
||||
*/
|
||||
export const mapObjectMetadataByUniqueIdentifier = (
|
||||
arr: ObjectMetadataEntity[],
|
||||
) => {
|
||||
return arr.reduce((acc, curr) => {
|
||||
acc[curr.nameSingular] = {
|
||||
...curr,
|
||||
fields: curr.fields.reduce((acc, curr) => {
|
||||
acc[curr.name] = curr;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const convertStringifiedFieldsToJSON = (fieldMetadata) => {
|
||||
if (fieldMetadata.targetColumnMap) {
|
||||
fieldMetadata.targetColumnMap = JSON.parse(
|
||||
fieldMetadata.targetColumnMap as unknown as string,
|
||||
);
|
||||
}
|
||||
if (fieldMetadata.defaultValue) {
|
||||
fieldMetadata.defaultValue = JSON.parse(
|
||||
fieldMetadata.defaultValue as unknown as string,
|
||||
);
|
||||
}
|
||||
if (fieldMetadata.options) {
|
||||
fieldMetadata.options = JSON.parse(
|
||||
fieldMetadata.options as unknown as string,
|
||||
);
|
||||
}
|
||||
return fieldMetadata;
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync.metadata.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
TypeOrmModule.forFeature(
|
||||
[
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
RelationMetadataEntity,
|
||||
WorkspaceMigrationEntity,
|
||||
],
|
||||
'metadata',
|
||||
),
|
||||
],
|
||||
exports: [WorkspaceSyncMetadataService],
|
||||
providers: [WorkspaceSyncMetadataService],
|
||||
})
|
||||
export class WorkspaceSyncMetadataModule {}
|
||||
@ -0,0 +1,435 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import diff from 'microdiff';
|
||||
import { Repository } from 'typeorm';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
RelationMetadataEntity,
|
||||
RelationMetadataType,
|
||||
} from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import { MetadataParser } from 'src/workspace/workspace-sync-metadata/utils/metadata.parser';
|
||||
import {
|
||||
mapObjectMetadataByUniqueIdentifier,
|
||||
filterIgnoredProperties,
|
||||
convertStringifiedFieldsToJSON,
|
||||
} from 'src/workspace/workspace-sync-metadata/utils/sync-metadata.util';
|
||||
import { standardObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationColumnRelation,
|
||||
WorkspaceMigrationEntity,
|
||||
WorkspaceMigrationTableAction,
|
||||
} from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFactory } from 'src/metadata/workspace-migration/workspace-migration.factory';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceSyncMetadataService {
|
||||
constructor(
|
||||
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(RelationMetadataEntity, 'metadata')
|
||||
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
|
||||
@InjectRepository(WorkspaceMigrationEntity, 'metadata')
|
||||
private readonly workspaceMigrationRepository: Repository<WorkspaceMigrationEntity>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* Sync all standard objects and fields metadata for a given workspace and data source
|
||||
* This will update the metadata if it has changed and generate migrations based on the diff.
|
||||
*
|
||||
* @param dataSourceId
|
||||
* @param workspaceId
|
||||
*/
|
||||
public async syncStandardObjectsAndFieldsMetadata(
|
||||
dataSourceId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const standardObjects = MetadataParser.parseAllMetadata(
|
||||
standardObjectMetadata,
|
||||
workspaceId,
|
||||
dataSourceId,
|
||||
);
|
||||
|
||||
try {
|
||||
const objectsInDB = await this.objectMetadataRepository.find({
|
||||
where: { workspaceId, dataSourceId, isCustom: false },
|
||||
relations: ['fields'],
|
||||
});
|
||||
|
||||
const objectsInDBByName =
|
||||
mapObjectMetadataByUniqueIdentifier(objectsInDB);
|
||||
const standardObjectsByName =
|
||||
mapObjectMetadataByUniqueIdentifier(standardObjects);
|
||||
|
||||
const objectsToCreate: ObjectMetadataEntity[] = [];
|
||||
const objectsToDelete = objectsInDB.filter(
|
||||
(objectInDB) => !standardObjectsByName[objectInDB.nameSingular],
|
||||
);
|
||||
const objectsToUpdate: Record<string, ObjectMetadataEntity> = {};
|
||||
|
||||
const fieldsToCreate: FieldMetadataEntity[] = [];
|
||||
const fieldsToDelete: FieldMetadataEntity[] = [];
|
||||
const fieldsToUpdate: Record<string, FieldMetadataEntity> = {};
|
||||
|
||||
for (const standardObjectName in standardObjectsByName) {
|
||||
const standardObject = standardObjectsByName[standardObjectName];
|
||||
const objectInDB = objectsInDBByName[standardObjectName];
|
||||
|
||||
if (!objectInDB) {
|
||||
objectsToCreate.push(standardObject);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Deconstruct fields and compare objects and fields independently
|
||||
const { fields: objectInDBFields, ...objectInDBWithoutFields } =
|
||||
objectInDB;
|
||||
const { fields: standardObjectFields, ...standardObjectWithoutFields } =
|
||||
standardObject;
|
||||
|
||||
const objectPropertiesToIgnore = [
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'labelIdentifierFieldMetadataId',
|
||||
'imageIdentifierFieldMetadataId',
|
||||
'isActive',
|
||||
];
|
||||
const objectDiffWithoutIgnoredProperties = filterIgnoredProperties(
|
||||
objectInDBWithoutFields,
|
||||
objectPropertiesToIgnore,
|
||||
);
|
||||
|
||||
const fieldPropertiesToIgnore = [
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'objectMetadataId',
|
||||
'isActive',
|
||||
];
|
||||
const objectInDBFieldsWithoutDefaultFields = Object.fromEntries(
|
||||
Object.entries(objectInDBFields).map(([key, value]) => {
|
||||
if (value === null || typeof value !== 'object') {
|
||||
return [key, value];
|
||||
}
|
||||
return [
|
||||
key,
|
||||
filterIgnoredProperties(
|
||||
value,
|
||||
fieldPropertiesToIgnore,
|
||||
(property) => {
|
||||
if (property !== null && typeof property === 'object') {
|
||||
return JSON.stringify(property);
|
||||
}
|
||||
return property;
|
||||
},
|
||||
),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
// Compare objects
|
||||
const objectDiff = diff(
|
||||
objectDiffWithoutIgnoredProperties,
|
||||
standardObjectWithoutFields,
|
||||
);
|
||||
|
||||
// Compare fields
|
||||
const fieldsDiff = diff(
|
||||
objectInDBFieldsWithoutDefaultFields,
|
||||
standardObjectFields,
|
||||
);
|
||||
|
||||
for (const diff of objectDiff) {
|
||||
// We only handle CHANGE here as REMOVE and CREATE are handled earlier.
|
||||
if (diff.type === 'CHANGE') {
|
||||
const property = diff.path[0];
|
||||
objectsToUpdate[objectInDB.id] = {
|
||||
...objectsToUpdate[objectInDB.id],
|
||||
[property]: diff.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const diff of fieldsDiff) {
|
||||
const fieldName = diff.path[0];
|
||||
if (diff.type === 'CREATE')
|
||||
fieldsToCreate.push({
|
||||
...standardObjectFields[fieldName],
|
||||
objectMetadataId: objectInDB.id,
|
||||
});
|
||||
if (diff.type === 'REMOVE' && diff.path.length === 1)
|
||||
fieldsToDelete.push(objectInDBFields[fieldName]);
|
||||
if (diff.type === 'CHANGE') {
|
||||
const property = diff.path[diff.path.length - 1];
|
||||
fieldsToUpdate[objectInDBFields[fieldName].id] = {
|
||||
...fieldsToUpdate[objectInDBFields[fieldName].id],
|
||||
[property]: diff.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CREATE OBJECTS
|
||||
await this.objectMetadataRepository.save(
|
||||
objectsToCreate.map((object) => ({
|
||||
...object,
|
||||
isActive: true,
|
||||
fields: Object.values(object.fields).map((field) => ({
|
||||
...convertStringifiedFieldsToJSON(field),
|
||||
isActive: true,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
// UPDATE OBJECTS, this is not optimal as we are running n queries here.
|
||||
for (const [key, value] of Object.entries(objectsToUpdate)) {
|
||||
await this.objectMetadataRepository.update(key, value);
|
||||
}
|
||||
// DELETE OBJECTS
|
||||
if (objectsToDelete.length > 0) {
|
||||
await this.objectMetadataRepository.delete(
|
||||
objectsToDelete.map((object) => object.id),
|
||||
);
|
||||
}
|
||||
|
||||
// CREATE FIELDS
|
||||
await this.fieldMetadataRepository.save(
|
||||
fieldsToCreate.map((field) => convertStringifiedFieldsToJSON(field)),
|
||||
);
|
||||
// UPDATE FIELDS
|
||||
for (const [key, value] of Object.entries(fieldsToUpdate)) {
|
||||
await this.fieldMetadataRepository.update(
|
||||
key,
|
||||
convertStringifiedFieldsToJSON(value),
|
||||
);
|
||||
}
|
||||
// DELETE FIELDS
|
||||
// TODO: handle relation fields deletion. We need to delete the relation metadata first due to the DB constraint.
|
||||
const fieldsToDeleteWithoutRelationType = fieldsToDelete.filter(
|
||||
(field) => field.type !== FieldMetadataType.RELATION,
|
||||
);
|
||||
if (fieldsToDeleteWithoutRelationType.length > 0) {
|
||||
await this.fieldMetadataRepository.delete(
|
||||
fieldsToDeleteWithoutRelationType.map((field) => field.id),
|
||||
);
|
||||
}
|
||||
|
||||
// Generate migrations
|
||||
await this.generateMigrationsFromSync(
|
||||
objectsToCreate,
|
||||
objectsToDelete,
|
||||
fieldsToCreate,
|
||||
fieldsToDelete,
|
||||
);
|
||||
|
||||
// We run syncRelationMetadata after everything to ensure that all objects and fields are
|
||||
// in the DB before creating relations.
|
||||
await this.syncRelationMetadata(workspaceId, dataSourceId);
|
||||
|
||||
// Execute migrations
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Sync of standard objects failed with:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async syncRelationMetadata(
|
||||
workspaceId: string,
|
||||
dataSourceId: string,
|
||||
) {
|
||||
const objectsInDB = await this.objectMetadataRepository.find({
|
||||
where: { workspaceId, dataSourceId, isCustom: false },
|
||||
relations: ['fields'],
|
||||
});
|
||||
const objectsInDBByName = mapObjectMetadataByUniqueIdentifier(objectsInDB);
|
||||
const standardRelations = MetadataParser.parseAllRelations(
|
||||
standardObjectMetadata,
|
||||
workspaceId,
|
||||
objectsInDBByName,
|
||||
).reduce((result, currentObject) => {
|
||||
const key = `${currentObject.fromObjectMetadataId}->${currentObject.fromFieldMetadataId}`;
|
||||
result[key] = currentObject;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
// TODO: filter out custom relations once isCustom has been added to relationMetadata table
|
||||
const relationsInDB = await this.relationMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
// We filter out 'id' later because we need it to remove the relation from DB
|
||||
const relationsInDBWithoutIgnoredProperties = relationsInDB
|
||||
.map((relation) =>
|
||||
filterIgnoredProperties(relation, ['createdAt', 'updatedAt']),
|
||||
)
|
||||
.reduce((result, currentObject) => {
|
||||
const key = `${currentObject.fromObjectMetadataId}->${currentObject.fromFieldMetadataId}`;
|
||||
result[key] = currentObject;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
// Compare relations
|
||||
const relationsDiff = diff(
|
||||
relationsInDBWithoutIgnoredProperties,
|
||||
standardRelations,
|
||||
);
|
||||
|
||||
const relationsToCreate: RelationMetadataEntity[] = [];
|
||||
const relationsToDelete: RelationMetadataEntity[] = [];
|
||||
|
||||
for (const diff of relationsDiff) {
|
||||
if (diff.type === 'CREATE') {
|
||||
relationsToCreate.push(diff.value);
|
||||
}
|
||||
if (diff.type === 'REMOVE' && diff.path[diff.path.length - 1] !== 'id') {
|
||||
relationsToDelete.push(diff.oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// CREATE RELATIONS
|
||||
await this.relationMetadataRepository.save(relationsToCreate);
|
||||
// DELETE RELATIONS
|
||||
if (relationsToDelete.length > 0) {
|
||||
await this.relationMetadataRepository.delete(
|
||||
relationsToDelete.map((relation) => relation.id),
|
||||
);
|
||||
}
|
||||
|
||||
await this.generateRelationMigrationsFromSync(
|
||||
relationsToCreate,
|
||||
relationsToDelete,
|
||||
objectsInDB,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Sync of standard relations failed with:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async generateMigrationsFromSync(
|
||||
objectsToCreate: ObjectMetadataEntity[],
|
||||
_objectsToDelete: ObjectMetadataEntity[],
|
||||
_fieldsToCreate: FieldMetadataEntity[],
|
||||
_fieldsToDelete: FieldMetadataEntity[],
|
||||
) {
|
||||
const migrationsToSave: Partial<WorkspaceMigrationEntity>[] = [];
|
||||
|
||||
if (objectsToCreate.length > 0) {
|
||||
objectsToCreate.map((object) => {
|
||||
const migrations = [
|
||||
{
|
||||
name: object.targetTableName,
|
||||
action: 'create',
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
...Object.values(object.fields)
|
||||
.filter((field) => field.type !== FieldMetadataType.RELATION)
|
||||
.map(
|
||||
(field) =>
|
||||
({
|
||||
name: object.targetTableName,
|
||||
action: 'alter',
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
field,
|
||||
),
|
||||
} satisfies WorkspaceMigrationTableAction),
|
||||
),
|
||||
];
|
||||
|
||||
migrationsToSave.push({
|
||||
workspaceId: object.workspaceId,
|
||||
isCustom: false,
|
||||
migrations,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await this.workspaceMigrationRepository.save(migrationsToSave);
|
||||
|
||||
// TODO: handle delete migrations
|
||||
}
|
||||
|
||||
private async generateRelationMigrationsFromSync(
|
||||
relationsToCreate: RelationMetadataEntity[],
|
||||
_relationsToDelete: RelationMetadataEntity[],
|
||||
objectsInDB: ObjectMetadataEntity[],
|
||||
) {
|
||||
const relationsMigrationsToSave: Partial<WorkspaceMigrationEntity>[] = [];
|
||||
|
||||
if (relationsToCreate.length > 0) {
|
||||
relationsToCreate.map((relation) => {
|
||||
const toObjectMetadata = objectsInDB.find(
|
||||
(object) => object.id === relation.toObjectMetadataId,
|
||||
);
|
||||
|
||||
const fromObjectMetadata = objectsInDB.find(
|
||||
(object) => object.id === relation.fromObjectMetadataId,
|
||||
);
|
||||
|
||||
if (!toObjectMetadata) {
|
||||
throw new Error(
|
||||
`ObjectMetadata with id ${relation.toObjectMetadataId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!fromObjectMetadata) {
|
||||
throw new Error(
|
||||
`ObjectMetadata with id ${relation.fromObjectMetadataId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const toFieldMetadata = toObjectMetadata.fields.find(
|
||||
(field) => field.id === relation.toFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!toFieldMetadata) {
|
||||
throw new Error(
|
||||
`FieldMetadata with id ${relation.toFieldMetadataId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const migrations = [
|
||||
{
|
||||
name: toObjectMetadata.targetTableName,
|
||||
action: 'alter',
|
||||
columns: [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.RELATION,
|
||||
columnName: `${camelCase(toFieldMetadata.name)}Id`,
|
||||
referencedTableName: fromObjectMetadata.targetTableName,
|
||||
referencedTableColumnName: 'id',
|
||||
isUnique:
|
||||
relation.relationType === RelationMetadataType.ONE_TO_ONE,
|
||||
} satisfies WorkspaceMigrationColumnRelation,
|
||||
],
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
];
|
||||
|
||||
relationsMigrationsToSave.push({
|
||||
workspaceId: relation.workspaceId,
|
||||
isCustom: false,
|
||||
migrations,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await this.workspaceMigrationRepository.save(relationsMigrationsToSave);
|
||||
|
||||
// TODO: handle delete migrations
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user