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:
Weiko
2023-12-07 19:22:34 +01:00
committed by GitHub
parent 590912b30f
commit 5efc2f00b9
66 changed files with 2393 additions and 2943 deletions
@@ -1,103 +0,0 @@
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';
export type FieldMetadataDecorator = {
type: FieldMetadataType;
label: string;
description?: string | null;
icon?: string | null;
defaultValue?: FieldMetadataDefaultValue | null;
};
export type ObjectMetadataDecorator = {
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string | null;
icon?: string | null;
};
const classSuffix = 'ObjectMetadata';
export function ObjectMetadata(
metadata: ObjectMetadataDecorator,
): ClassDecorator {
return (target) => {
const isSystem = Reflect.getMetadata('isSystem', target) || false;
let objectName = camelCase(target.name);
if (objectName.endsWith(classSuffix)) {
objectName = objectName.slice(0, -classSuffix.length);
}
Reflect.defineMetadata(
'objectMetadata',
{
nameSingular: objectName,
...metadata,
targetTableName: objectName,
isSystem,
isCustom: false,
isActive: true,
},
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;
Reflect.defineMetadata(
'fieldMetadata',
{
...existingFieldMetadata,
[fieldKey]: {
name: fieldKey,
...metadata,
targetColumnMap: generateTargetColumnMap(
metadata.type,
false,
fieldKey,
),
isNullable,
isSystem,
isCustom: false,
isActive: true,
},
},
target.constructor,
);
};
}
@@ -1,90 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const activityTargetMetadata = {
nameSingular: 'activityTarget',
namePlural: 'activityTargets',
labelSingular: 'Activity Target',
labelPlural: 'Activity Targets',
targetTableName: 'activityTarget',
description: 'An activity target',
icon: 'IconCheckbox',
isActive: true,
isSystem: true,
fields: [
{
// Relations
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activity',
label: 'Activity',
targetColumnMap: {},
description: 'ActivityTarget activity',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'person',
label: 'Person',
targetColumnMap: {},
description: 'ActivityTarget person',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {},
description: 'ActivityTarget company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'activityId',
label: 'Activity id (foreign key)',
targetColumnMap: {},
description: 'ActivityTarget activity id foreign key',
icon: undefined,
isNullable: false,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'personId',
label: 'Person id (foreign key)',
targetColumnMap: {},
description: 'ActivityTarget person id foreign key',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'companyId',
label: 'Company id (foreign key)',
targetColumnMap: {},
description: 'ActivityTarget company id foreign key',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
],
};
export default activityTargetMetadata;
@@ -1,181 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const activityMetadata = {
nameSingular: 'activity',
namePlural: 'activities',
labelSingular: 'Activity',
labelPlural: 'Activities',
targetTableName: 'activity',
description: 'An activity',
icon: 'IconCheckbox',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'title',
label: 'Title',
targetColumnMap: {
value: 'title',
},
description: 'Activity title',
icon: 'IconNotes',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'body',
label: 'Body',
targetColumnMap: {
value: 'body',
},
description: 'Activity body',
icon: 'IconList',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'type',
label: 'Type',
targetColumnMap: {
value: 'type',
},
description: 'Activity type',
icon: 'IconCheckbox',
isNullable: false,
defaultValue: { value: 'Note' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'reminderAt',
label: 'Reminder Date',
targetColumnMap: {
value: 'reminderAt',
},
description: 'Activity reminder date',
icon: 'IconCalendarEvent',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'dueAt',
label: 'Due Date',
targetColumnMap: {
value: 'dueAt',
},
description: 'Activity due date',
icon: 'IconCalendarEvent',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'completedAt',
label: 'Completion Date',
targetColumnMap: {
value: 'completedAt',
},
description: 'Activity completion date',
icon: 'IconCheck',
isNullable: true,
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activityTargets',
label: 'Targets',
targetColumnMap: {},
description: 'Activity targets',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
targetColumnMap: {},
description: 'Activity attachments',
icon: 'IconFileImport',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'comments',
label: 'Comments',
targetColumnMap: {},
description: 'Activity comments',
icon: 'IconComment',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'author',
label: 'Author',
targetColumnMap: {},
description:
'Activity author. This is the person who created the activity',
icon: 'IconUserCircle',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'authorId',
label: 'Author id (foreign key)',
targetColumnMap: {},
description: 'Activity author id foreign key',
icon: undefined,
isNullable: false,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'assignee',
label: 'Assignee',
targetColumnMap: {},
description:
'Acitivity assignee. This is the workspace member assigned to the activity ',
icon: 'IconUserCircle',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'assigneeId',
label: 'Assignee id (foreign key)',
targetColumnMap: {},
description: 'Acitivity assignee id foreign key',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
],
};
export default activityMetadata;
@@ -1,57 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const apiKeyMetadata = {
nameSingular: 'apiKey',
namePlural: 'apiKeys',
labelSingular: 'Api Key',
labelPlural: 'Api Keys',
targetTableName: 'apiKey',
description: 'An api key',
icon: 'IconRobot',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'ApiKey name',
icon: 'IconLink',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'expiresAt',
label: 'Expiration date',
targetColumnMap: {
value: 'expiresAt',
},
description: 'ApiKey expiration date',
icon: 'IconCalendar',
isNullable: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'revokedAt',
label: 'Revocation date',
targetColumnMap: {
value: 'revokedAt',
},
description: 'ApiKey revocation date',
icon: 'IconCalendar',
isNullable: true,
},
],
};
export default apiKeyMetadata;
@@ -1,124 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const attachmentMetadata = {
nameSingular: 'attachment',
namePlural: 'attachments',
labelSingular: 'Attachment',
labelPlural: 'Attachments',
targetTableName: 'attachment',
description: 'An attachment',
icon: 'IconFileImport',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'Attachment name',
icon: 'IconFileUpload',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'fullPath',
label: 'Full path',
targetColumnMap: {
value: 'fullPath',
},
description: 'Attachment full path',
icon: 'IconLink',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'type',
label: 'Type',
targetColumnMap: {
value: 'type',
},
description: 'Attachment type',
icon: 'IconList',
isNullable: false,
defaultValue: { value: '' },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'author',
label: 'Author',
targetColumnMap: {
value: 'authorId',
},
description: 'Attachment author',
icon: 'IconCircleUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'authorId',
label: 'Author id (foreign key)',
targetColumnMap: {},
description: 'Activity author id foreign key',
icon: undefined,
isNullable: false,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activity',
label: 'Activity',
targetColumnMap: {
value: 'activityId',
},
description: 'Attachment activity',
icon: 'IconNotes',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'person',
label: 'Person',
targetColumnMap: {
value: 'personId',
},
description: 'Attachment person',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {
value: 'companyId',
},
description: 'Attachment company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
],
};
export default attachmentMetadata;
@@ -1,78 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const commentMetadata = {
nameSingular: 'comment',
namePlural: 'comments',
labelSingular: 'Comment',
labelPlural: 'Comments',
targetTableName: 'comment',
description: 'A comment',
icon: 'IconMessageCircle',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'body',
label: 'Body',
targetColumnMap: {
value: 'body',
},
description: 'Comment body',
icon: 'IconLink',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'authorId',
label: 'Author',
targetColumnMap: {},
description: 'Comment author',
icon: 'IconCircleUser',
isNullable: true,
isSystem: true,
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'author',
label: 'Author',
targetColumnMap: {},
description: 'Comment author',
icon: 'IconCircleUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activity',
label: 'Activity',
targetColumnMap: {},
description: 'Comment activity',
icon: 'IconNotes',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'activityId',
label: 'Activity',
targetColumnMap: {},
description: 'Comment activity',
icon: 'IconNotes',
isNullable: true,
isSystem: true,
},
],
};
export default commentMetadata;
@@ -1,212 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const companyMetadata = {
nameSingular: 'company',
namePlural: 'companies',
labelSingular: 'Company',
labelPlural: 'Companies',
targetTableName: 'company',
description: 'A company',
icon: 'IconBuildingSkyscraper',
isActive: true,
isSystem: false,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'The company name',
icon: 'IconBuildingSkyscraper',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'domainName',
label: 'Domain Name',
targetColumnMap: {
value: 'domainName',
},
description:
'The company website URL. We use this url to fetch the company icon',
icon: 'IconLink',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'address',
label: 'Address',
targetColumnMap: {
value: 'address',
},
description: 'The company address',
icon: 'IconMap',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'employees',
label: 'Employees',
targetColumnMap: {
value: 'employees',
},
description: 'Number of employees in the company',
icon: 'IconUsers',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.LINK,
name: 'linkedinLink',
label: 'Linkedin',
targetColumnMap: {
label: 'linkedinLinkLabel',
url: 'linkedinLinkUrl',
},
description: 'The company Linkedin account',
icon: 'IconBrandLinkedin',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.LINK,
name: 'xLink',
label: 'X',
targetColumnMap: {
label: 'xLinkLabel',
url: 'xLinkUrl',
},
description: 'The company Twitter/X account',
icon: 'IconBrandX',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.CURRENCY,
name: 'annualRecurringRevenue',
label: 'ARR',
targetColumnMap: {
amountMicros: 'annualRecurringRevenueAmountMicros',
currencyCode: 'annualRecurringRevenueCurrencyCode',
},
description:
'Annual Recurring Revenue: The actual or estimated annual revenue of the company',
icon: 'IconMoneybag',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.BOOLEAN,
name: 'idealCustomerProfile',
label: 'ICP',
targetColumnMap: {
value: 'idealCustomerProfile',
},
description:
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
icon: 'IconTarget',
isNullable: true,
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'people',
label: 'People',
targetColumnMap: {},
description: 'People linked to the company.',
icon: 'IconUsers',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'accountOwner',
label: 'Account Owner',
targetColumnMap: {
value: 'accountOwnerId',
},
description:
'Your team member responsible for managing the company account',
icon: 'IconUserCircle',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'accountOwnerId',
label: 'Account Owner ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for account owner',
icon: undefined,
isNullable: true,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activityTargets',
label: 'Activities',
targetColumnMap: {},
description: 'Activities tied to the company',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'opportunities',
label: 'Opportunities',
targetColumnMap: {},
description: 'Opportunities linked to the company.',
icon: 'IconTargetArrow',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
targetColumnMap: {},
description: 'Favorites linked to the company',
icon: 'IconHeart',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
targetColumnMap: {},
description: 'Attachments linked to the company.',
icon: 'IconFileImport',
isNullable: true,
},
],
};
export default companyMetadata;
@@ -1,104 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const favoriteMetadata = {
nameSingular: 'favorite',
namePlural: 'favorites',
labelSingular: 'Favorite',
labelPlural: 'Favorites',
targetTableName: 'favorite',
description: 'A favorite',
icon: 'IconHeart',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'position',
label: 'Position',
targetColumnMap: {
value: 'position',
},
description: 'Favorite position',
icon: 'IconList',
isNullable: false,
defaultValue: { value: 0 },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'workspaceMember',
label: 'Workspace Member',
targetColumnMap: {},
description: 'Favorite workspace member',
icon: 'IconCircleUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'person',
label: 'Person',
targetColumnMap: {},
description: 'Favorite person',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {},
description: 'Favorite company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'workspaceMemberId',
label: 'Workspace Member ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for workspace member',
icon: undefined,
isNullable: false,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'personId',
label: 'Person ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for person',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'companyId',
label: 'Company ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for company',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
],
};
export default favoriteMetadata;
@@ -1,165 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const opportunityMetadata = {
nameSingular: 'opportunity',
namePlural: 'opportunities',
labelSingular: 'Opportunity',
labelPlural: 'Opportunities',
targetTableName: 'opportunity',
description: 'An opportunity',
icon: 'IconTargetArrow',
isActive: true,
isSystem: false,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.CURRENCY,
name: 'amount',
label: 'Amount',
targetColumnMap: {
amountMicros: 'amountAmountMicros',
currencyCode: 'amountCurrencyCode',
},
description: 'Opportunity amount',
icon: 'IconCurrencyDollar',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'closeDate',
label: 'Close date',
targetColumnMap: {
value: 'closeDate',
},
description: 'Opportunity close date',
icon: 'IconCalendarEvent',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'probability',
label: 'Probability',
targetColumnMap: {
value: 'probability',
},
description: 'Opportunity probability',
icon: 'IconProgressCheck',
isNullable: true,
defaultValue: { value: '0' },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'pipelineStep',
label: 'Pipeline Step',
targetColumnMap: {
value: 'pipelineStepId',
},
description: 'Opportunity pipeline step',
icon: 'IconKanban',
isSystem: true,
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'pointOfContact',
label: 'Point of Contact',
targetColumnMap: {
value: 'pointOfContactId',
},
description: 'Opportunity point of contact',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'person',
label: 'Person',
targetColumnMap: {
value: 'personId',
},
description: 'Opportunity person',
icon: 'IconUser',
isNullable: true,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {
value: 'companyId',
},
description: 'Opportunity company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'companyId',
label: 'Company ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for company',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'personId',
label: 'Person ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for person',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'pointOfContactId',
label: 'Point of Contact ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for point of contact',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'pipelineStepId',
label: 'Pipeline Step ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for pipeline step',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
],
};
export default opportunityMetadata;
@@ -1,209 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const personMetadata = {
nameSingular: 'person',
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
targetTableName: 'person',
description: 'A person',
icon: 'IconUser',
isActive: true,
isSystem: false,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.FULL_NAME,
name: 'name',
label: 'Name',
targetColumnMap: {
firstName: 'nameFirstName',
lastName: 'nameLastName',
},
description: 'Contacts name',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.EMAIL,
name: 'email',
label: 'Email',
targetColumnMap: {
value: 'email',
},
description: 'Contacts Email',
icon: 'IconMail',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.LINK,
name: 'linkedinLink',
label: 'Linkedin',
targetColumnMap: {
label: 'linkedinLinkLabel',
url: 'linkedinLinkUrl',
},
description: 'Contacts Linkedin account',
icon: 'IconBrandLinkedin',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.LINK,
name: 'xLink',
label: 'X',
targetColumnMap: {
label: 'xLinkLabel',
url: 'xLinkUrl',
},
description: 'Contacts X/Twitter account',
icon: 'IconBrandX',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'jobTitle',
label: 'Job Title',
targetColumnMap: {
value: 'jobTitle',
},
description: 'Contacts job title',
icon: 'IconBriefcase',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'phone',
label: 'Phone',
targetColumnMap: {
value: 'phone',
},
description: 'Contacts phone number',
icon: 'IconPhone',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'city',
label: 'City',
targetColumnMap: {
value: 'city',
},
description: 'Contacts city',
icon: 'IconMap',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'avatarUrl',
label: 'Avatar',
targetColumnMap: {
value: 'avatarUrl',
},
description: 'Contacts avatar',
icon: 'IconFileUpload',
isNullable: true,
isSystem: true,
defaultValue: { value: '' },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {},
description: 'Contacts company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
isSystem: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'companyId',
label: 'Company ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for company',
icon: undefined,
isNullable: true,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'pointOfContactForOpportunities',
label: 'POC for Opportunities',
targetColumnMap: {},
description: 'Point of Contact for Opportunities',
icon: 'IconTargetArrow',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activityTargets',
label: 'Activities',
targetColumnMap: {},
description: 'Activities tied to the contact',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'opportunities',
label: 'Opportunities',
targetColumnMap: {},
description: 'Opportunities linked to the contact.',
icon: 'IconTargetArrow',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
targetColumnMap: {},
description: 'Favorites linked to the contact',
icon: 'IconHeart',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
targetColumnMap: {},
description: 'Attachments linked to the contact.',
icon: 'IconFileImport',
isNullable: true,
},
],
};
export default personMetadata;
@@ -1,71 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const pipelineStepMetadata = {
nameSingular: 'pipelineStep',
namePlural: 'pipelineSteps',
labelSingular: 'Pipeline Step',
labelPlural: 'Pipeline Steps',
targetTableName: 'pipelineStep',
description: 'A pipeline step',
icon: 'IconLayoutKanban',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'Pipeline Step name',
icon: 'IconCurrencyDollar',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'color',
label: 'Color',
targetColumnMap: {
value: 'color',
},
description: 'Pipeline Step color',
icon: 'IconColorSwatch',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'position',
label: 'Position',
targetColumnMap: {
value: 'position',
},
description: 'Pipeline Step position',
icon: 'IconHierarchy2',
isNullable: false,
defaultValue: { value: 0 },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'opportunities',
label: 'Opportunities',
targetColumnMap: {},
description: 'Opportunities linked to the step.',
icon: 'IconTargetArrow',
isNullable: true,
},
],
};
export default pipelineStepMetadata;
@@ -1,27 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const activityRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'activity',
toObjectNameSingular: 'activityTarget',
fromFieldMetadataName: 'activityTargets',
toFieldMetadataName: 'activity',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'activity',
toObjectNameSingular: 'attachment',
fromFieldMetadataName: 'attachments',
toFieldMetadataName: 'activity',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'activity',
toObjectNameSingular: 'comment',
fromFieldMetadataName: 'comments',
toFieldMetadataName: 'activity',
},
];
export default activityRelationMetadata;
@@ -1,41 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const companyRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'person',
fromFieldMetadataName: 'people',
toFieldMetadataName: 'company',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'favorite',
fromFieldMetadataName: 'favorites',
toFieldMetadataName: 'company',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'attachment',
fromFieldMetadataName: 'attachments',
toFieldMetadataName: 'company',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'opportunity',
fromFieldMetadataName: 'opportunities',
toFieldMetadataName: 'company',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'activityTarget',
fromFieldMetadataName: 'activityTargets',
toFieldMetadataName: 'company',
},
];
export default companyRelationMetadata;
@@ -1,41 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const personRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'favorite',
fromFieldMetadataName: 'favorites',
toFieldMetadataName: 'person',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'attachment',
fromFieldMetadataName: 'attachments',
toFieldMetadataName: 'person',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'opportunity',
fromFieldMetadataName: 'opportunities',
toFieldMetadataName: 'person',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'opportunity',
fromFieldMetadataName: 'pointOfContactForOpportunities',
toFieldMetadataName: 'pointOfContact',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'activityTarget',
fromFieldMetadataName: 'activityTargets',
toFieldMetadataName: 'person',
},
];
export default personRelationMetadata;
@@ -1,13 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const pipelineStepRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'pipelineStep',
toObjectNameSingular: 'opportunity',
fromFieldMetadataName: 'opportunities',
toFieldMetadataName: 'pipelineStep',
},
];
export default pipelineStepRelationMetadata;
@@ -1,27 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const viewRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'view',
toObjectNameSingular: 'viewField',
fromFieldMetadataName: 'viewFields',
toFieldMetadataName: 'view',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'view',
toObjectNameSingular: 'viewFilter',
fromFieldMetadataName: 'viewFilters',
toFieldMetadataName: 'view',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'view',
toObjectNameSingular: 'viewSort',
fromFieldMetadataName: 'viewSorts',
toFieldMetadataName: 'view',
},
];
export default viewRelationMetadata;
@@ -1,48 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const workspaceMemberRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'company',
fromFieldMetadataName: 'accountOwnerForCompanies',
toFieldMetadataName: 'accountOwner',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'favorite',
fromFieldMetadataName: 'favorites',
toFieldMetadataName: 'workspaceMember',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'activity',
fromFieldMetadataName: 'authoredActivities',
toFieldMetadataName: 'author',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'activity',
fromFieldMetadataName: 'assignedActivities',
toFieldMetadataName: 'assignee',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'comment',
fromFieldMetadataName: 'authoredComments',
toFieldMetadataName: 'author',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'attachment',
fromFieldMetadataName: 'authoredAttachments',
toFieldMetadataName: 'author',
},
];
export default workspaceMemberRelationMetadata;
@@ -1,82 +0,0 @@
import apiKeyMetadata from 'src/workspace/workspace-manager/standard-objects/api-key';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';
import activityMetadata from 'src/workspace/workspace-manager/standard-objects/activity';
import activityTargetMetadata from 'src/workspace/workspace-manager/standard-objects/activity-target';
import attachmentMetadata from 'src/workspace/workspace-manager/standard-objects/attachment';
import commentMetadata from 'src/workspace/workspace-manager/standard-objects/comment';
import companyMetadata from 'src/workspace/workspace-manager/standard-objects/company';
import favoriteMetadata from 'src/workspace/workspace-manager/standard-objects/favorite';
import opportunityMetadata from 'src/workspace/workspace-manager/standard-objects/opportunity';
import personMetadata from 'src/workspace/workspace-manager/standard-objects/person';
import pipelineStepMetadata from 'src/workspace/workspace-manager/standard-objects/pipeline-step';
import viewMetadata from 'src/workspace/workspace-manager/standard-objects/view';
import viewFieldMetadata from 'src/workspace/workspace-manager/standard-objects/view-field';
import viewFilterMetadata from 'src/workspace/workspace-manager/standard-objects/view-filter';
import viewSortMetadata from 'src/workspace/workspace-manager/standard-objects/view-sort';
import workspaceMemberMetadata from 'src/workspace/workspace-manager/standard-objects/workspace-member';
import connectedAccountMetadata from 'src/workspace/workspace-manager/standard-objects/connected-account';
export const standardObjectsMetadata = {
activityTarget: activityTargetMetadata,
activity: activityMetadata,
apiKey: apiKeyMetadata,
attachment: attachmentMetadata,
comment: commentMetadata,
company: companyMetadata,
connectedAccount: connectedAccountMetadata,
favorite: favoriteMetadata,
opportunity: opportunityMetadata,
person: personMetadata,
pipelineStep: pipelineStepMetadata,
viewField: viewFieldMetadata,
viewFilter: viewFilterMetadata,
viewSort: viewSortMetadata,
view: viewMetadata,
workspaceMember: workspaceMemberMetadata,
};
export const basicFieldsMetadata: Partial<FieldMetadataEntity>[] = [
{
name: 'id',
label: 'Id',
type: FieldMetadataType.UUID,
targetColumnMap: {
value: 'id',
},
isNullable: true,
isSystem: true,
isCustom: false,
isActive: true,
defaultValue: { type: 'uuid' },
},
{
name: 'createdAt',
label: 'Creation date',
type: FieldMetadataType.DATE_TIME,
targetColumnMap: {
value: 'createdAt',
},
icon: 'IconCalendar',
isNullable: true,
isCustom: false,
isActive: true,
defaultValue: { type: 'now' },
},
{
name: 'updatedAt',
label: 'Update date',
type: FieldMetadataType.DATE_TIME,
targetColumnMap: {
value: 'updatedAt',
},
icon: 'IconCalendar',
isNullable: true,
isCustom: false,
isSystem: true,
isActive: true,
defaultValue: { type: 'now' },
},
];
@@ -1,15 +0,0 @@
import activityRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/activity';
import companyRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/company';
import personRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/person';
import pipelineStepRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/pipeline-step';
import viewRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/view';
import workspaceMemberRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/workspace-member';
export const standardObjectRelationMetadata = [
...activityRelationMetadata,
...companyRelationMetadata,
...personRelationMetadata,
...pipelineStepRelationMetadata,
...viewRelationMetadata,
...workspaceMemberRelationMetadata,
];
@@ -1,96 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const viewFieldMetadata = {
nameSingular: 'viewField',
namePlural: 'viewFields',
labelSingular: 'View Field',
labelPlural: 'View Fields',
targetTableName: 'viewField',
description: '(System) View Fields',
icon: 'IconTag',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'fieldMetadataId',
label: 'Field Metadata Id',
targetColumnMap: {
value: 'fieldMetadataId',
},
description: 'View Field target field',
icon: 'IconTag',
isNullable: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.BOOLEAN,
name: 'isVisible',
label: 'Visible',
targetColumnMap: {
value: 'isVisible',
},
description: 'View Field visibility',
icon: 'IconEye',
isNullable: false,
defaultValue: { value: true },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'size',
label: 'Size',
targetColumnMap: {
value: 'size',
},
description: 'View Field size',
icon: 'IconEye',
isNullable: false,
defaultValue: { value: 0 },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'position',
label: 'Position',
targetColumnMap: {
value: 'position',
},
description: 'View Field position',
icon: 'IconList',
isNullable: false,
defaultValue: { value: 0 },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'view',
label: 'View',
targetColumnMap: {},
description: 'View Field related view',
icon: 'IconLayoutCollage',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'viewId',
label: 'View Id',
targetColumnMap: {
value: 'viewId',
},
description: 'View field related view',
icon: 'IconLayoutCollage',
isNullable: false,
},
],
};
export default viewFieldMetadata;
@@ -1,96 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const viewFilterMetadata = {
nameSingular: 'viewFilter',
namePlural: 'viewFilters',
labelSingular: 'View Filter',
labelPlural: 'View Filters',
targetTableName: 'viewFilter',
description: '(System) View Filters',
icon: 'IconFilterBolt',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'fieldMetadataId',
label: 'Field Metadata Id',
targetColumnMap: {
value: 'fieldMetadataId',
},
description: 'View Filter target field',
icon: null,
isNullable: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'operand',
label: 'Operand',
targetColumnMap: {
value: 'operand',
},
description: 'View Filter operand',
icon: null,
isNullable: false,
defaultValue: { value: 'Contains' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'value',
label: 'Value',
targetColumnMap: {
value: 'value',
},
description: 'View Filter value',
icon: null,
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'displayValue',
label: 'Display Value',
targetColumnMap: {
value: 'displayValue',
},
description: 'View Filter Display Value',
icon: null,
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'view',
label: 'View',
targetColumnMap: {},
description: 'View Filter related view',
icon: 'IconLayoutCollage',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'viewId',
label: 'View Id',
targetColumnMap: {
value: 'viewId',
},
description: 'View Filter related view',
icon: 'IconLayoutCollage',
isNullable: false,
},
],
};
export default viewFilterMetadata;
@@ -1,68 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const viewSortMetadata = {
nameSingular: 'viewSort',
namePlural: 'viewSorts',
labelSingular: 'View Sort',
labelPlural: 'View Sorts',
targetTableName: 'viewSort',
description: '(System) View Sorts',
icon: 'IconArrowsSort',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'fieldMetadataId',
label: 'Field Metadata Id',
targetColumnMap: {
value: 'fieldMetadataId',
},
description: 'View Sort target field',
icon: null,
isNullable: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'direction',
label: 'Direction',
targetColumnMap: {
value: 'direction',
},
description: 'View Sort direction',
icon: null,
isNullable: false,
defaultValue: { value: 'asc' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'view',
label: 'View',
targetColumnMap: {},
description: 'View Sort related view',
icon: 'IconLayoutCollage',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'viewId',
label: 'View Id',
targetColumnMap: {
value: 'viewId',
},
description: 'View Sort related view',
icon: 'IconLayoutCollage',
isNullable: false,
},
],
};
export default viewSortMetadata;
@@ -1,85 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const viewMetadata = {
nameSingular: 'view',
namePlural: 'views',
labelSingular: 'View',
labelPlural: 'Views',
targetTableName: 'view',
description: '(System) Views',
icon: 'IconLayoutCollage',
isActive: true,
isSystem: true,
fields: [
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'View name',
icon: null,
isNullable: false,
defaultValue: { value: '' },
},
{
type: FieldMetadataType.UUID,
name: 'objectMetadataId',
label: 'Object Metadata Id',
targetColumnMap: {
value: 'objectMetadataId',
},
description: 'View target object',
icon: null,
isNullable: false,
},
{
type: FieldMetadataType.TEXT,
name: 'type',
label: 'Type',
targetColumnMap: {
value: 'type',
},
description: 'View type',
icon: null,
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'viewFields',
label: 'View Fields',
targetColumnMap: {},
description: 'View Fields',
icon: 'IconTag',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'viewSorts',
label: 'View Sorts',
targetColumnMap: {},
description: 'View Sorts',
icon: 'IconArrowsSort',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'viewFilters',
label: 'View Filters',
targetColumnMap: {},
description: 'View Filters',
icon: 'IconFilterBolt',
isNullable: true,
},
],
};
export default viewMetadata;
@@ -1,45 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const webhookMetadata = {
nameSingular: 'webhook',
namePlural: 'webhooks',
labelSingular: 'Webhook',
labelPlural: 'Webhooks',
targetTableName: 'webhook',
description: 'A webhook',
icon: 'IconRobot',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'targetUrl',
label: 'Target Url',
targetColumnMap: {
value: 'targetUrl',
},
description: 'Webhook target url',
icon: 'IconLink',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'operation',
label: 'Operation',
targetColumnMap: {
value: 'operation',
},
description: 'Webhook operation',
icon: 'IconCheckbox',
isNullable: false,
defaultValue: { value: '' },
},
],
};
export default webhookMetadata;
@@ -1,156 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const workspaceMemberMetadata = {
nameSingular: 'workspaceMember',
namePlural: 'workspaceMembers',
labelSingular: 'Workspace Member',
labelPlural: 'Workspace Members',
targetTableName: 'workspaceMember',
description: 'A workspace member',
icon: 'IconUserCircle',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.FULL_NAME,
name: 'name',
label: 'Name',
targetColumnMap: {
firstName: 'nameFirstName',
lastName: 'nameLastName',
},
description: 'Workspace member name',
icon: 'IconCircleUser',
isNullable: false,
defaultValue: { firstName: '', lastName: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'colorScheme',
label: 'Color Scheme',
targetColumnMap: {
value: 'colorScheme',
},
description: 'Preferred color scheme',
icon: 'IconColorSwatch',
isNullable: false,
defaultValue: { value: 'Light' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'locale',
label: 'Language',
targetColumnMap: {
value: 'locale',
},
description: 'Preferred language',
icon: 'IconLanguage',
isNullable: false,
defaultValue: { value: 'en' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'avatarUrl',
label: 'Avatar Url',
targetColumnMap: {
value: 'avatarUrl',
},
description: 'Workspace member avatar',
icon: 'IconFileUpload',
isNullable: true,
isSystem: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'userId',
label: 'User Id',
targetColumnMap: {
value: 'userId',
},
description: 'Associated User Id',
icon: 'IconCircleUsers',
isNullable: false,
isSystem: false,
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'authoredActivities',
label: 'Authored activities',
targetColumnMap: {},
description: 'Activities created by the workspace member',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'assignedActivities',
label: 'Assigned activities',
targetColumnMap: {},
description: 'Activities assigned to the workspace member',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
targetColumnMap: {},
description: 'Favorites linked to the workspace member',
icon: 'IconHeart',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'accountOwnerForCompanies',
label: 'Account Owner For Companies',
targetColumnMap: {},
description: 'Account owner for companies',
icon: 'IconBriefcase',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'authoredAttachments',
label: 'Authored attachments',
targetColumnMap: {},
description: 'Attachments created by the workspace member',
icon: 'IconFileImport',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'authoredComments',
label: 'Authored comments',
targetColumnMap: {},
description: 'Authored comments',
icon: 'IconComment',
isNullable: true,
},
],
};
export default workspaceMemberMetadata;
@@ -1,42 +0,0 @@
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { BaseObjectMetadata } from 'src/workspace/workspace-manager/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, // TODO: use default default value based on field type
options: field.options || null,
})),
};
}
return undefined;
}
static parseAllMetadata(
metadata: (typeof BaseObjectMetadata)[],
workspaceId: string,
dataSourceId: string,
) {
return metadata.map((_metadata) =>
MetadataParser.parseMetadata(_metadata, workspaceId, dataSourceId),
);
}
}
@@ -1,14 +1,10 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { RelationMetadataModule } from 'src/metadata/relation-metadata/relation-metadata.module';
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { WorkspaceSyncMetadataModule } from 'src/workspace/workspace-sync-metadata/worksapce-sync-metadata.module';
import { WorkspaceManagerService } from './workspace-manager.service';
@@ -16,14 +12,9 @@ import { WorkspaceManagerService } from './workspace-manager.service';
imports: [
WorkspaceDataSourceModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
ObjectMetadataModule,
DataSourceModule,
RelationMetadataModule,
TypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
'metadata',
),
WorkspaceSyncMetadataModule,
],
exports: [WorkspaceManagerService],
providers: [WorkspaceManagerService],
@@ -1,8 +1,4 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import diff from 'microdiff';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
@@ -12,41 +8,16 @@ import { standardObjectsPrefillData } from 'src/workspace/workspace-manager/stan
import { demoObjectsPrefillData } from 'src/workspace/workspace-manager/demo-objects-prefill-data/demo-objects-prefill-data';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
import { RelationMetadataService } from 'src/metadata/relation-metadata/relation-metadata.service';
import { standardObjectRelationMetadata } from 'src/workspace/workspace-manager/standard-objects/standard-object-relation-metadata';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';
import { MetadataParser } from 'src/workspace/workspace-manager/utils/metadata.parser';
import { WebhookObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/webook.object-metadata';
import { ApiKeyObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/api-key.object-metadata';
import { ViewSortObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/view-sort.object-metadata';
import {
filterIgnoredProperties,
mapObjectMetadataByUniqueIdentifier,
} from 'src/workspace/workspace-manager/utils/sync-metadata.util';
import {
basicFieldsMetadata,
standardObjectsMetadata,
} from './standard-objects/standard-object-metadata';
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync.metadata.service';
@Injectable()
export class WorkspaceManagerService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly dataSourceService: DataSourceService,
private readonly relationMetadataService: RelationMetadataService,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
) {}
/**
@@ -68,22 +39,14 @@ export class WorkspaceManagerService {
await this.setWorkspaceMaxRow(workspaceId, schemaName);
await this.workspaceMigrationService.insertStandardMigrations(workspaceId);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
const createdObjectMetadata =
await this.createStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
await this.prefillWorkspaceWithStandardObjects(
dataSourceMetadata,
workspaceId,
createdObjectMetadata,
);
}
@@ -106,321 +69,12 @@ export class WorkspaceManagerService {
await this.setWorkspaceMaxRow(workspaceId, schemaName);
await this.workspaceMigrationService.insertStandardMigrations(workspaceId);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
const createdObjectMetadata =
await this.createStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
await this.prefillWorkspaceWithDemoObjects(
dataSourceMetadata,
workspaceId,
createdObjectMetadata,
);
}
/**
*
* Create all standard objects and fields metadata for a given workspace
*
* @param dataSourceId
* @param workspaceId
*/
public async createStandardObjectsAndFieldsMetadata(
dataSourceId: string,
workspaceId: string,
): Promise<ObjectMetadataEntity[]> {
const createdObjectMetadata = await this.objectMetadataService.createMany(
Object.values(standardObjectsMetadata).map((objectMetadata: any) => ({
...objectMetadata,
dataSourceId,
workspaceId,
isCustom: false,
isActive: true,
fields: [...basicFieldsMetadata, ...objectMetadata.fields].map(
(field) => ({
...field,
workspaceId,
isCustom: false,
isActive: true,
}),
),
})),
);
await this.relationMetadataService.createMany(
Object.values(standardObjectRelationMetadata).map((relationMetadata) =>
this.createStandardObjectRelations(
workspaceId,
createdObjectMetadata,
relationMetadata,
),
),
);
return createdObjectMetadata;
}
/**
*
* @param workspaceId
* @param createdObjectMetadata
* @param relationMetadata
* @returns Partial<RelationMetadataEntity>
*/
private createStandardObjectRelations(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity[],
relationMetadata: any,
) {
const createdObjectMetadataByNameSingular = createdObjectMetadata.reduce(
(acc, curr) => {
acc[curr.nameSingular] = curr;
return acc;
},
{},
);
const fromObjectMetadata =
createdObjectMetadataByNameSingular[
relationMetadata.fromObjectNameSingular
];
const toObjectMetadata =
createdObjectMetadataByNameSingular[
relationMetadata.toObjectNameSingular
];
if (!fromObjectMetadata) {
throw new Error(
`Could not find created object metadata with
fromObjectNameSingular: ${relationMetadata.fromObjectNameSingular}`,
);
}
if (!toObjectMetadata) {
throw new Error(
`Could not find created object metadata with
toObjectNameSingular: ${relationMetadata.toObjectNameSingular}`,
);
}
const fromFieldMetadata = createdObjectMetadataByNameSingular[
relationMetadata.fromObjectNameSingular
]?.fields.find(
(field: FieldMetadataEntity) =>
field.type === FieldMetadataType.RELATION &&
field.name === relationMetadata.fromFieldMetadataName,
);
const toFieldMetadata = createdObjectMetadataByNameSingular[
relationMetadata.toObjectNameSingular
]?.fields.find(
(field: FieldMetadataEntity) =>
field.type === FieldMetadataType.RELATION &&
field.name === relationMetadata.toFieldMetadataName,
);
if (!fromFieldMetadata) {
throw new Error(
`Could not find created field metadata with
fromFieldMetadataName: ${relationMetadata.fromFieldMetadataName}
for object: ${relationMetadata.fromObjectNameSingular}`,
);
}
if (!toFieldMetadata) {
throw new Error(
`Could not find created field metadata with
toFieldMetadataName: ${relationMetadata.toFieldMetadataName}
for object: ${relationMetadata.toObjectNameSingular}`,
);
}
return {
fromObjectMetadataId: fromObjectMetadata.id,
toObjectMetadataId: toObjectMetadata.id,
workspaceId,
relationType: relationMetadata.type,
fromFieldMetadataId: fromFieldMetadata.id,
toFieldMetadataId: toFieldMetadata.id,
};
}
/**
*
* 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(
[WebhookObjectMetadata, ApiKeyObjectMetadata, ViewSortObjectMetadata],
workspaceId,
dataSourceId,
);
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',
];
const objectDiffWithoutIgnoredProperties = filterIgnoredProperties(
objectInDBWithoutFields,
objectPropertiesToIgnore,
);
const fieldPropertiesToIgnore = [
'id',
'createdAt',
'updatedAt',
'objectMetadataId',
];
const objectInDBFieldsWithoutDefaultFields = Object.fromEntries(
Object.entries(objectInDBFields).map(([key, value]) => {
if (value === null || typeof value !== 'object') {
return [key, value];
}
return [key, filterIgnoredProperties(value, fieldPropertiesToIgnore)];
}),
);
// 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) {
if (diff.type === 'CREATE') {
const fieldName = diff.path[0];
const fieldMetadata = standardObjectFields[fieldName];
fieldsToCreate.push(fieldMetadata);
}
if (diff.type === 'CHANGE') {
const fieldName = diff.path[0];
const property = diff.path[diff.path.length - 1];
const fieldMetadata = objectInDBFields[fieldName];
fieldsToUpdate[fieldMetadata.id] = {
...fieldsToUpdate[fieldMetadata.id],
[property]: diff.value,
};
}
if (diff.type === 'REMOVE') {
const fieldName = diff.path[0];
const fieldMetadata = objectInDBFields[fieldName];
fieldsToDelete.push(fieldMetadata);
}
}
// console.log(standardObjectName + ':objectDiff', objectDiff);
// console.log(standardObjectName + ':fieldsDiff', fieldsDiff);
}
// TODO: Sync relationMetadata
// NOTE: Relations are handled like any field during the diff, so we ignore the relationMetadata table
// during the diff as it depends on the 2 fieldMetadata that we will compare here.
// However we need to make sure the relationMetadata table is in sync with the fieldMetadata table.
// TODO: Use transactions
// CREATE OBJECTS
try {
await this.objectMetadataRepository.save(objectsToCreate);
// 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);
// UPDATE FIELDS
for (const [key, value] of Object.entries(fieldsToUpdate)) {
await this.fieldMetadataRepository.update(key, value);
}
// DELETE FIELDS
if (fieldsToDelete.length > 0) {
await this.fieldMetadataRepository.delete(
fieldsToDelete.map((field) => field.id),
);
}
} catch (e) {
console.error('Sync of standard objects failed with:', e);
}
// TODO: Create migrations based on diff from above.
await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId);
}
/**
@@ -451,7 +105,6 @@ export class WorkspaceManagerService {
private async prefillWorkspaceWithStandardObjects(
dataSourceMetadata: DataSourceEntity,
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity[],
) {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
@@ -461,6 +114,10 @@ export class WorkspaceManagerService {
if (!workspaceDataSource) {
throw new Error('Could not connect to workspace data source');
}
const createdObjectMetadata =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
await standardObjectsPrefillData(
workspaceDataSource,
dataSourceMetadata.schema,
@@ -478,7 +135,6 @@ export class WorkspaceManagerService {
private async prefillWorkspaceWithDemoObjects(
dataSourceMetadata: DataSourceEntity,
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity[],
) {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
@@ -489,6 +145,9 @@ export class WorkspaceManagerService {
throw new Error('Could not connect to workspace data source');
}
const createdObjectMetadata =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
await demoObjectsPrefillData(
workspaceDataSource,
dataSourceMetadata.schema,
@@ -1,7 +1,7 @@
import { Command, CommandRunner, Option } from 'nest-commander';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync.metadata.service';
// TODO: implement dry-run
interface RunWorkspaceMigrationsOptions {
@@ -14,7 +14,7 @@ interface RunWorkspaceMigrationsOptions {
})
export class SyncWorkspaceMetadataCommand extends CommandRunner {
constructor(
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly dataSourceService: DataSourceService,
) {
super();
@@ -29,9 +29,7 @@ export class SyncWorkspaceMetadataCommand extends CommandRunner {
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
options.workspaceId,
);
// TODO: This solution could be improved, using a diff for example, we should not have to delete all metadata and recreate them.
await this.workspaceManagerService.syncStandardObjectsAndFieldsMetadata(
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
options.workspaceId,
);
@@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspace-manager.module';
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: [WorkspaceManagerModule, DataSourceModule],
imports: [WorkspaceSyncMetadataModule, DataSourceModule],
providers: [SyncWorkspaceMetadataCommand],
})
export class WorkspaceManagerCommandsModule {}
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;
}
@@ -1,17 +1,17 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
ObjectMetadata,
FieldMetadata,
IsNullable,
IsSystem,
ObjectMetadata,
} from 'src/workspace/workspace-manager/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/base.object-metadata';
} 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: 'A api key',
description: 'An api key',
icon: 'IconRobot',
})
@IsSystem()
@@ -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;
}
@@ -2,7 +2,7 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.en
import {
FieldMetadata,
IsSystem,
} from 'src/workspace/workspace-manager/decorators/metadata.decorator';
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
export abstract class BaseObjectMetadata {
@FieldMetadata({
@@ -22,7 +22,6 @@ export abstract class BaseObjectMetadata {
icon: 'IconCalendar',
defaultValue: { type: 'now' },
})
@IsSystem()
createdAt: Date;
@FieldMetadata({
@@ -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: 'Contacts name',
icon: 'IconUser',
})
@IsNullable()
name: string;
@FieldMetadata({
type: FieldMetadataType.EMAIL,
label: 'Email',
description: 'Contacts Email',
icon: 'IconMail',
})
@IsNullable()
email: string;
@FieldMetadata({
type: FieldMetadataType.LINK,
label: 'Linkedin',
description: 'Contacts Linkedin account',
icon: 'IconBrandLinkedin',
})
@IsNullable()
linkedinLink: string;
@FieldMetadata({
type: FieldMetadataType.LINK,
label: 'X',
description: 'Contacts X/Twitter account',
icon: 'IconBrandX',
})
@IsNullable()
xLink: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Job Title',
description: 'Contacts job title',
icon: 'IconBriefcase',
})
@IsNullable()
jobTitle: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Phone',
description: 'Contacts phone number',
icon: 'IconPhone',
})
@IsNullable()
phone: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'City',
description: 'Contacts city',
icon: 'IconMap',
})
@IsNullable()
city: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Avatar',
description: 'Contacts avatar',
icon: 'IconFileUpload',
})
@IsSystem()
@IsNullable()
avatarUrl: string;
// Relations
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Company',
description: 'Contacts 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;
}
@@ -4,8 +4,8 @@ import {
FieldMetadata,
IsNullable,
IsSystem,
} from 'src/workspace/workspace-manager/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/base.object-metadata';
} 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',
@@ -20,17 +20,26 @@ export class ViewSortObjectMetadata extends BaseObjectMetadata {
type: FieldMetadataType.UUID,
label: 'Field Metadata Id',
description: 'View Sort target field',
icon: null,
icon: 'IconTag',
})
fieldMetadataId: string;
// TODO: We could create a relation decorator but let's keep it simple for now.
@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?: object;
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[];
}
@@ -1,10 +1,10 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
FieldMetadata,
IsSystem,
ObjectMetadata,
} from 'src/workspace/workspace-manager/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/base.object-metadata';
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',
@@ -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,
),
);
}
}
@@ -11,9 +11,12 @@ import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metada
export const filterIgnoredProperties = (
obj: any,
propertiesToIgnore: string[],
mapFunction?: (value: any) => any,
) => {
return Object.fromEntries(
Object.entries(obj).filter(([key]) => !propertiesToIgnore.includes(key)),
Object.entries(obj)
.filter(([key]) => !propertiesToIgnore.includes(key))
.map(([key, value]) => [key, mapFunction ? mapFunction(value) : value]),
);
};
@@ -41,3 +44,22 @@ export const mapObjectMetadataByUniqueIdentifier = (
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
}
}