Feat/twenty orm (#5153)
## Introduction
This PR introduces "TwentyORM," a custom ORM module designed to
streamline database interactions within our workspace schema, reducing
the need for raw SQL queries. The API mirrors TypeORM's to provide a
familiar interface while integrating enhancements specific to our
project's needs.
To facilitate this integration, new decorators prefixed with `Workspace`
have been implemented. These decorators are used to define entity
metadata more explicitly and are critical in constructing our schema
dynamically.
## New Features
- **Custom ORM System**: Named "TwentyORM," which aligns closely with
TypeORM for ease of use but is tailored to our application's specific
requirements.
- **Decorator-Driven Configuration**: Entities are now configured with
`Workspace`-prefixed decorators that clearly define schema mappings and
relationships directly within the entity classes.
- **Injectable Repositories**: Repositories can be injected similarly to
TypeORM, allowing for flexible and straightforward data management.
## Example Implementations
### Decorated Entity Definitions
Entities are defined with new decorators that outline table and field
metadata, relationships, and constraints. Here are examples of these
implementations:
#### Company Metadata Object
```typescript
@WorkspaceObject({
standardId: STANDARD_OBJECT_IDS.company,
namePlural: 'companies',
labelSingular: 'Company',
labelPlural: 'Companies',
description: 'A company',
icon: 'IconBuildingSkyscraper',
})
export class CompanyObjectMetadata extends BaseObjectMetadata {
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.name,
type: FieldMetadataType.TEXT,
label: 'Name',
description: 'The company name',
icon: 'IconBuildingSkyscraper',
})
name: string;
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.xLink,
type: FieldMetadataType.LINK,
label: 'X',
description: 'The company Twitter/X account',
icon: 'IconBrandX',
})
@WorkspaceIsNullable()
xLink: LinkMetadata;
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.position,
type: FieldMetadataType.POSITION,
label: 'Position',
description: 'Company record position',
icon: 'IconHierarchy2',
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number;
@WorkspaceRelation({
standardId: COMPANY_STANDARD_FIELD_IDS.accountOwner,
label: 'Account Owner',
description: 'Your team member responsible for managing the company account',
type: RelationMetadataType.MANY_TO_ONE,
inverseSideTarget: () => WorkspaceMemberObjectMetadata,
inverseSideFieldKey: 'accountOwnerForCompanies',
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
accountOwner: WorkspaceMemberObjectMetadata;
}
```
#### Workspace Member Metadata Object
```typescript
@WorkspaceObject({
standardId: STANDARD_OBJECT_IDS.workspaceMember,
namePlural: 'workspaceMembers',
labelSingular: 'Workspace Member',
labelPlural: 'Workspace Members',
description: 'A workspace member',
icon: 'IconUserCircle',
})
@WorkspaceIsSystem()
@WorkspaceIsNotAuditLogged()
export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
@WorkspaceField({
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.name,
type: FieldMetadataType.FULL_NAME,
label: 'Name',
description: 'Workspace member name',
icon: 'IconCircleUser',
})
name: FullNameMetadata;
@WorkspaceRelation({
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.accountOwnerForCompanies,
label: 'Account Owner For Companies',
description: 'Account owner for companies',
icon: 'IconBriefcase',
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => CompanyObjectMetadata,
inverseSideFieldKey: 'accountOwner',
onDelete: RelationOnDeleteAction.SET_NULL,
})
accountOwnerForCompanies: Relation
<CompanyObjectMetadata[]>;
}
```
### Injectable Repository Usage
Repositories can be directly injected into services, allowing for
streamlined query operations:
```typescript
export class CompanyService {
constructor(
@InjectWorkspaceRepository(CompanyObjectMetadata)
private readonly companyObjectMetadataRepository: WorkspaceRepository<CompanyObjectMetadata>,
) {}
async companies(): Promise<CompanyObjectMetadata[]> {
// Example queries demonstrating simple and relation-loaded operations
const simpleCompanies = await this.companyObjectMetadataRepository.find({});
const companiesWithOwners = await this.companyObjectMetadataRepository.find({
relations: ['accountOwner'],
});
const companiesFilteredByLinkLabel = await this.companyObjectMetadataRepository.find({
where: { xLinkLabel: 'MyLabel' },
});
return companiesFilteredByLinkLabel;
}
}
```
## Conclusions
This PR sets the foundation for a decorator-driven ORM layer that
simplifies data interactions and supports complex entity relationships
while maintaining clean and manageable code architecture. This is not
finished yet, and should be extended.
All the standard objects needs to be migrated and all the module using
the old decorators too.
---------
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
@ -11,9 +11,9 @@ import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/ti
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { HealthModule } from 'src/engine/core-modules/health/health.module';
|
||||
|
||||
import { AnalyticsModule } from './analytics/analytics.module';
|
||||
import { FileModule } from './file/file.module';
|
||||
import { ClientConfigModule } from './client-config/client-config.module';
|
||||
import { FileModule } from './file/file.module';
|
||||
import { AnalyticsModule } from './analytics/analytics.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
|
||||
import { currencyCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||
import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
import { linkCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
||||
import {
|
||||
CurrencyMetadata,
|
||||
currencyCompositeType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||
import {
|
||||
FullNameMetadata,
|
||||
fullNameCompositeType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
import {
|
||||
LinkMetadata,
|
||||
linkCompositeType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { addressCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||
import {
|
||||
AddressMetadata,
|
||||
addressCompositeType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||
|
||||
export type CompositeFieldsDefinitionFunction = (
|
||||
fieldMetadata?: FieldMetadataInterface,
|
||||
@ -20,3 +32,9 @@ export const compositeTypeDefintions = new Map<
|
||||
[FieldMetadataType.FULL_NAME, fullNameCompositeType],
|
||||
[FieldMetadataType.ADDRESS, addressCompositeType],
|
||||
]);
|
||||
|
||||
export type CompositeMetadataTypes =
|
||||
| AddressMetadata
|
||||
| CurrencyMetadata
|
||||
| FullNameMetadata
|
||||
| LinkMetadata;
|
||||
|
||||
@ -503,7 +503,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
relationMetadata.fromFieldMetadata.id === fieldMetadataDTO.id;
|
||||
|
||||
// TODO: implement MANY_TO_MANY
|
||||
if (relationMetadata.relationType === RelationMetadataType.MANY_TO_MANY) {
|
||||
if (
|
||||
relationMetadata.relationType === RelationMetadataType.MANY_TO_MANY ||
|
||||
relationMetadata.relationType === RelationMetadataType.MANY_TO_ONE
|
||||
) {
|
||||
throw new Error(`
|
||||
Relation type ${relationMetadata.relationType} not supported
|
||||
`);
|
||||
|
||||
@ -36,18 +36,37 @@ export function computeColumnName<T extends FieldMetadataType | 'default'>(
|
||||
|
||||
return generateName(fieldMetadataOrFieldName.name);
|
||||
}
|
||||
|
||||
export const computeCompositeColumnName = <
|
||||
export function computeCompositeColumnName(
|
||||
fieldName: string,
|
||||
compositeProperty: CompositeProperty,
|
||||
): string;
|
||||
export function computeCompositeColumnName<
|
||||
T extends FieldMetadataType | 'default',
|
||||
>(
|
||||
fieldMetadata: FieldMetadataInterface<T>,
|
||||
compositeProperty: CompositeProperty,
|
||||
): string => {
|
||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
): string;
|
||||
export function computeCompositeColumnName<
|
||||
T extends FieldMetadataType | 'default',
|
||||
>(
|
||||
fieldMetadataOrFieldName: FieldMetadataInterface<T> | string,
|
||||
compositeProperty: CompositeProperty,
|
||||
): string {
|
||||
const generateName = (name: string) => {
|
||||
return `${name}${pascalCase(compositeProperty.name)}`;
|
||||
};
|
||||
|
||||
if (typeof fieldMetadataOrFieldName === 'string') {
|
||||
return generateName(fieldMetadataOrFieldName);
|
||||
}
|
||||
|
||||
if (!isCompositeFieldMetadataType(fieldMetadataOrFieldName.type)) {
|
||||
throw new Error(
|
||||
`Cannot compute composite column name for non-composite field metadata type: ${fieldMetadata.type}`,
|
||||
`Cannot compute composite column name for non-composite field metadata type: ${fieldMetadataOrFieldName.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
return `${fieldMetadata.name}${pascalCase(compositeProperty.name)}`;
|
||||
};
|
||||
return `${fieldMetadataOrFieldName.name}${pascalCase(
|
||||
compositeProperty.name,
|
||||
)}`;
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
export enum RelationMetadataType {
|
||||
ONE_TO_ONE = 'ONE_TO_ONE',
|
||||
ONE_TO_MANY = 'ONE_TO_MANY',
|
||||
MANY_TO_ONE = 'MANY_TO_ONE',
|
||||
MANY_TO_MANY = 'MANY_TO_MANY',
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants';
|
||||
|
||||
export const InjectWorkspaceDatasource = () =>
|
||||
Inject(TWENTY_ORM_WORKSPACE_DATASOURCE);
|
||||
@ -0,0 +1,8 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
|
||||
|
||||
import { getWorkspaceRepositoryToken } from 'src/engine/twenty-orm/utils/get-workspace-repository-token.util';
|
||||
|
||||
export const InjectWorkspaceRepository = (
|
||||
entity: EntityClassOrSchema,
|
||||
): ReturnType<typeof Inject> => Inject(getWorkspaceRepositoryToken(entity));
|
||||
@ -0,0 +1,63 @@
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export interface WorkspaceFieldOptions<
|
||||
T extends FieldMetadataType | 'default',
|
||||
> {
|
||||
standardId: string;
|
||||
type: T;
|
||||
label: string | ((objectMetadata: ObjectMetadataEntity) => string);
|
||||
description?: string | ((objectMetadata: ObjectMetadataEntity) => string);
|
||||
icon?: string;
|
||||
defaultValue?: FieldMetadataDefaultValue<T>;
|
||||
joinColumn?: string;
|
||||
options?: FieldMetadataOptions<T>;
|
||||
}
|
||||
|
||||
export function WorkspaceField<T extends FieldMetadataType>(
|
||||
options: WorkspaceFieldOptions<T>,
|
||||
): PropertyDecorator {
|
||||
return (object, propertyKey) => {
|
||||
const isPrimary = TypedReflect.getMetadata(
|
||||
'workspace:is-primary-field-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
const isNullable = TypedReflect.getMetadata(
|
||||
'workspace:is-nullable-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
const isSystem = TypedReflect.getMetadata(
|
||||
'workspace:is-system-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
const gate = TypedReflect.getMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
|
||||
metadataArgsStorage.fields.push({
|
||||
target: object.constructor,
|
||||
standardId: options.standardId,
|
||||
name: propertyKey.toString(),
|
||||
label: options.label,
|
||||
type: options.type,
|
||||
description: options.description,
|
||||
icon: options.icon,
|
||||
defaultValue: options.defaultValue,
|
||||
options: options.options,
|
||||
isPrimary,
|
||||
isNullable,
|
||||
isSystem,
|
||||
gate,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export interface WorkspaceGateOptions {
|
||||
featureFlag: string;
|
||||
}
|
||||
|
||||
export function WorkspaceGate(options: WorkspaceGateOptions) {
|
||||
return (target: any, propertyKey?: string | symbol) => {
|
||||
if (propertyKey !== undefined) {
|
||||
TypedReflect.defineMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
options,
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
} else {
|
||||
TypedReflect.defineMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
options,
|
||||
target,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export function WorkspaceIsNotAuditLogged(): ClassDecorator {
|
||||
return (object) => {
|
||||
TypedReflect.defineMetadata(
|
||||
'workspace:is-audit-logged-metadata-args',
|
||||
false,
|
||||
object,
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export function WorkspaceIsNullable(): PropertyDecorator {
|
||||
return (object, propertyKey) => {
|
||||
TypedReflect.defineMetadata(
|
||||
'workspace:is-nullable-metadata-args',
|
||||
true,
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export function WorkspaceIsPimaryField(): PropertyDecorator {
|
||||
return (object, propertyKey) => {
|
||||
TypedReflect.defineMetadata(
|
||||
'workspace:is-primary-field-metadata-args',
|
||||
true,
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export function WorkspaceIsSystem() {
|
||||
return function (target: any, propertyKey?: string | symbol): void {
|
||||
if (propertyKey !== undefined) {
|
||||
TypedReflect.defineMetadata(
|
||||
'workspace:is-system-metadata-args',
|
||||
true,
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
} else {
|
||||
TypedReflect.defineMetadata(
|
||||
'workspace:is-system-metadata-args',
|
||||
true,
|
||||
target,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
interface WorkspaceObjectOptions {
|
||||
standardId: string;
|
||||
namePlural: string;
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export function WorkspaceObject(
|
||||
options: WorkspaceObjectOptions,
|
||||
): ClassDecorator {
|
||||
return (target) => {
|
||||
const isAuditLogged =
|
||||
TypedReflect.getMetadata(
|
||||
'workspace:is-audit-logged-metadata-args',
|
||||
target,
|
||||
) ?? true;
|
||||
const isSystem = TypedReflect.getMetadata(
|
||||
'workspace:is-system-metadata-args',
|
||||
target,
|
||||
);
|
||||
const gate = TypedReflect.getMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
target,
|
||||
);
|
||||
const objectName = convertClassNameToObjectMetadataName(target.name);
|
||||
|
||||
metadataArgsStorage.objects.push({
|
||||
target,
|
||||
standardId: options.standardId,
|
||||
nameSingular: objectName,
|
||||
namePlural: options.namePlural,
|
||||
labelSingular: options.labelSingular,
|
||||
labelPlural: options.labelPlural,
|
||||
description: options.description,
|
||||
icon: options.icon,
|
||||
isAuditLogged,
|
||||
isSystem,
|
||||
gate,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import { ObjectType } from 'typeorm';
|
||||
|
||||
import {
|
||||
RelationMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
interface WorkspaceBaseRelationOptions<TType, TClass> {
|
||||
standardId: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
type: TType;
|
||||
inverseSideTarget: () => ObjectType<TClass>;
|
||||
inverseSideFieldKey?: keyof TClass;
|
||||
onDelete?: RelationOnDeleteAction;
|
||||
}
|
||||
|
||||
export interface WorkspaceManyToOneRelationOptions<TClass>
|
||||
extends WorkspaceBaseRelationOptions<
|
||||
RelationMetadataType.MANY_TO_ONE | RelationMetadataType.ONE_TO_ONE,
|
||||
TClass
|
||||
> {
|
||||
joinColumn?: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceOtherRelationOptions<TClass>
|
||||
extends WorkspaceBaseRelationOptions<
|
||||
RelationMetadataType.ONE_TO_MANY | RelationMetadataType.MANY_TO_MANY,
|
||||
TClass
|
||||
> {}
|
||||
|
||||
export function WorkspaceRelation<TClass extends object>(
|
||||
options:
|
||||
| WorkspaceManyToOneRelationOptions<TClass>
|
||||
| WorkspaceOtherRelationOptions<TClass>,
|
||||
): PropertyDecorator {
|
||||
return (object, propertyKey) => {
|
||||
const isPrimary = TypedReflect.getMetadata(
|
||||
'workspace:is-primary-field-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
const isNullable = TypedReflect.getMetadata(
|
||||
'workspace:is-nullable-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
const isSystem = TypedReflect.getMetadata(
|
||||
'workspace:is-system-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
const gate = TypedReflect.getMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
|
||||
let joinColumn: string | undefined;
|
||||
|
||||
if ('joinColumn' in options) {
|
||||
joinColumn = options.joinColumn
|
||||
? options.joinColumn
|
||||
: `${propertyKey.toString()}Id`;
|
||||
}
|
||||
|
||||
metadataArgsStorage.relations.push({
|
||||
target: object.constructor,
|
||||
standardId: options.standardId,
|
||||
name: propertyKey.toString(),
|
||||
label: options.label,
|
||||
type: options.type,
|
||||
description: options.description,
|
||||
icon: options.icon,
|
||||
inverseSideTarget: options.inverseSideTarget,
|
||||
inverseSideFieldKey: options.inverseSideFieldKey as string | undefined,
|
||||
onDelete: options.onDelete,
|
||||
joinColumn,
|
||||
isPrimary,
|
||||
isNullable,
|
||||
isSystem,
|
||||
gate,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ColumnType, EntitySchemaColumnOptions } from 'typeorm';
|
||||
|
||||
import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
|
||||
|
||||
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
type EntitySchemaColumnMap = {
|
||||
[key: string]: EntitySchemaColumnOptions;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class EntitySchemaColumnFactory {
|
||||
create(
|
||||
fieldMetadataArgsCollection: WorkspaceFieldMetadataArgs[],
|
||||
): EntitySchemaColumnMap {
|
||||
let entitySchemaColumnMap: EntitySchemaColumnMap = {};
|
||||
|
||||
for (const fieldMetadataArgs of fieldMetadataArgsCollection) {
|
||||
const key = fieldMetadataArgs.name;
|
||||
|
||||
if (isCompositeFieldMetadataType(fieldMetadataArgs.type)) {
|
||||
const compositeColumns = this.createCompositeColumns(fieldMetadataArgs);
|
||||
|
||||
entitySchemaColumnMap = {
|
||||
...entitySchemaColumnMap,
|
||||
...compositeColumns,
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const columnType = fieldMetadataTypeToColumnType(fieldMetadataArgs.type);
|
||||
const defaultValue = serializeDefaultValue(
|
||||
fieldMetadataArgs.defaultValue,
|
||||
);
|
||||
|
||||
entitySchemaColumnMap[key] = {
|
||||
name: key,
|
||||
type: columnType as ColumnType,
|
||||
primary: fieldMetadataArgs.isPrimary,
|
||||
nullable: fieldMetadataArgs.isNullable,
|
||||
createDate: key === 'createdAt',
|
||||
updateDate: key === 'updatedAt',
|
||||
array: fieldMetadataArgs.type === FieldMetadataType.MULTI_SELECT,
|
||||
default: defaultValue,
|
||||
};
|
||||
|
||||
if (isEnumFieldMetadataType(fieldMetadataArgs.type)) {
|
||||
const values = fieldMetadataArgs.options?.map((option) => option.value);
|
||||
|
||||
if (values && values.length > 0) {
|
||||
entitySchemaColumnMap[key].enum = values;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entitySchemaColumnMap;
|
||||
}
|
||||
|
||||
private createCompositeColumns(
|
||||
fieldMetadataArgs: WorkspaceFieldMetadataArgs,
|
||||
): EntitySchemaColumnMap {
|
||||
const entitySchemaColumnMap: EntitySchemaColumnMap = {};
|
||||
const compositeType = compositeTypeDefintions.get(fieldMetadataArgs.type);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new Error(
|
||||
`Composite type ${fieldMetadataArgs.type} is not defined in compositeTypeDefintions`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const compositeProperty of compositeType.properties) {
|
||||
const columnName = computeCompositeColumnName(
|
||||
fieldMetadataArgs.name,
|
||||
compositeProperty,
|
||||
);
|
||||
const columnType = fieldMetadataTypeToColumnType(compositeProperty.type);
|
||||
const defaultValue = serializeDefaultValue(
|
||||
fieldMetadataArgs.defaultValue?.[compositeProperty.name],
|
||||
);
|
||||
|
||||
entitySchemaColumnMap[columnName] = {
|
||||
name: columnName,
|
||||
type: columnType as ColumnType,
|
||||
nullable: compositeProperty.isRequired,
|
||||
default: defaultValue,
|
||||
};
|
||||
|
||||
if (isEnumFieldMetadataType(compositeProperty.type)) {
|
||||
throw new Error('Enum composite properties are not yet supported');
|
||||
}
|
||||
}
|
||||
|
||||
return entitySchemaColumnMap;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntitySchemaRelationOptions } from 'typeorm';
|
||||
import { RelationType } from 'typeorm/metadata/types/RelationTypes';
|
||||
|
||||
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
|
||||
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
|
||||
type EntitySchemaRelationMap = {
|
||||
[key: string]: EntitySchemaRelationOptions;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class EntitySchemaRelationFactory {
|
||||
create(
|
||||
relationMetadataArgsCollection: WorkspaceRelationMetadataArgs[],
|
||||
): EntitySchemaRelationMap {
|
||||
const entitySchemaRelationMap: EntitySchemaRelationMap = {};
|
||||
|
||||
for (const relationMetadataArgs of relationMetadataArgsCollection) {
|
||||
const oppositeTarget = relationMetadataArgs.inverseSideTarget();
|
||||
const oppositeObjectName = convertClassNameToObjectMetadataName(
|
||||
oppositeTarget.name,
|
||||
);
|
||||
|
||||
const relationType = this.getRelationType(relationMetadataArgs);
|
||||
|
||||
entitySchemaRelationMap[relationMetadataArgs.name] = {
|
||||
type: relationType,
|
||||
target: oppositeObjectName,
|
||||
inverseSide: relationMetadataArgs.inverseSideFieldKey,
|
||||
joinColumn: relationMetadataArgs.joinColumn
|
||||
? {
|
||||
name: relationMetadataArgs.joinColumn,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return entitySchemaRelationMap;
|
||||
}
|
||||
|
||||
private getRelationType(
|
||||
relationMetadataArgs: WorkspaceRelationMetadataArgs,
|
||||
): RelationType {
|
||||
switch (relationMetadataArgs.type) {
|
||||
case RelationMetadataType.ONE_TO_MANY:
|
||||
return 'one-to-many';
|
||||
case RelationMetadataType.MANY_TO_ONE:
|
||||
return 'many-to-one';
|
||||
case RelationMetadataType.ONE_TO_ONE:
|
||||
return 'one-to-one';
|
||||
case RelationMetadataType.MANY_TO_MANY:
|
||||
return 'many-to-many';
|
||||
default:
|
||||
throw new Error('Invalid relation type');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
import { Injectable, Type } from '@nestjs/common';
|
||||
|
||||
import { EntitySchema } from 'typeorm';
|
||||
|
||||
import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory';
|
||||
import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
|
||||
@Injectable()
|
||||
export class EntitySchemaFactory {
|
||||
constructor(
|
||||
private readonly entitySchemaColumnFactory: EntitySchemaColumnFactory,
|
||||
private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory,
|
||||
) {}
|
||||
|
||||
create<T>(target: Type<T>): EntitySchema {
|
||||
const objectMetadataArgs = metadataArgsStorage.filterObjects(target);
|
||||
|
||||
if (!objectMetadataArgs) {
|
||||
throw new Error('Object metadata args are missing on this target');
|
||||
}
|
||||
|
||||
const fieldMetadataArgsCollection =
|
||||
metadataArgsStorage.filterFields(target);
|
||||
const relationMetadataArgsCollection =
|
||||
metadataArgsStorage.filterRelations(target);
|
||||
|
||||
const columns = this.entitySchemaColumnFactory.create(
|
||||
fieldMetadataArgsCollection,
|
||||
);
|
||||
|
||||
const relations = this.entitySchemaRelationFactory.create(
|
||||
relationMetadataArgsCollection,
|
||||
);
|
||||
|
||||
return new EntitySchema({
|
||||
name: objectMetadataArgs.nameSingular,
|
||||
tableName: objectMetadataArgs.nameSingular,
|
||||
columns,
|
||||
relations,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory';
|
||||
import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory';
|
||||
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
|
||||
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
|
||||
|
||||
export const entitySchemaFactories = [
|
||||
EntitySchemaColumnFactory,
|
||||
EntitySchemaRelationFactory,
|
||||
EntitySchemaFactory,
|
||||
WorkspaceDatasourceFactory,
|
||||
];
|
||||
@ -0,0 +1,58 @@
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
|
||||
import { DataSource, EntitySchema } from 'typeorm';
|
||||
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceStorage } from 'src/engine/twenty-orm/storage/data-source.storage';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class WorkspaceDatasourceFactory {
|
||||
constructor(
|
||||
@Inject(REQUEST) private readonly request: Request,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
public async createWorkspaceDatasource(entities: EntitySchema[]) {
|
||||
const workspace: Workspace = this.request['req']['workspace'];
|
||||
|
||||
if (!workspace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const storedWorkspaceDataSource = DataSourceStorage.getDataSource(
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (storedWorkspaceDataSource) {
|
||||
return storedWorkspaceDataSource;
|
||||
}
|
||||
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const workspaceDataSource = new DataSource({
|
||||
url:
|
||||
dataSourceMetadata.url ??
|
||||
this.environmentService.get('PG_DATABASE_URL'),
|
||||
type: 'postgres',
|
||||
// logging: this.environmentService.get('DEBUG_MODE')
|
||||
// ? ['query', 'error']
|
||||
// : ['error'],
|
||||
logging: 'all',
|
||||
schema: dataSourceMetadata.schema,
|
||||
entities,
|
||||
});
|
||||
|
||||
await workspaceDataSource.initialize();
|
||||
|
||||
DataSourceStorage.setDataSource(workspace.id, workspaceDataSource);
|
||||
|
||||
return workspaceDataSource;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { CompositeMetadataTypes } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
|
||||
// TODO: At the time the composite types are generating union of types instead of a single type for their keys
|
||||
// We need to find a way to fix that
|
||||
export type FlattenCompositeTypes<T> = {
|
||||
[P in keyof T as T[P] extends CompositeMetadataTypes
|
||||
? `${string & P}${Capitalize<string & keyof T[P]>}`
|
||||
: P]: T[P] extends CompositeMetadataTypes ? T[P][keyof T[P]] : T[P];
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export interface Gate {
|
||||
featureFlag: string;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { FactoryProvider, ModuleMetadata, Type } from '@nestjs/common';
|
||||
|
||||
import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
|
||||
export interface TwentyORMOptions {
|
||||
objects: Type<BaseObjectMetadata>[];
|
||||
}
|
||||
|
||||
export type TwentyORMModuleAsyncOptions = {
|
||||
useFactory: (...args: any[]) => TwentyORMOptions | Promise<TwentyORMOptions>;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
@ -0,0 +1,76 @@
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
|
||||
export interface WorkspaceFieldMetadataArgs {
|
||||
/**
|
||||
* Standard id.
|
||||
*/
|
||||
readonly standardId: string;
|
||||
|
||||
/**
|
||||
* Class to which field is applied.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
readonly target: Function | string;
|
||||
|
||||
/**
|
||||
* Field name.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Field label.
|
||||
*/
|
||||
readonly label: string | ((objectMetadata: ObjectMetadataEntity) => string);
|
||||
|
||||
/**
|
||||
* Field type.
|
||||
*/
|
||||
readonly type: FieldMetadataType;
|
||||
|
||||
/**
|
||||
* Field description.
|
||||
*/
|
||||
readonly description?:
|
||||
| string
|
||||
| ((objectMetadata: ObjectMetadataEntity) => string);
|
||||
|
||||
/**
|
||||
* Field icon.
|
||||
*/
|
||||
readonly icon?: string;
|
||||
|
||||
/**
|
||||
* Field default value.
|
||||
*/
|
||||
readonly defaultValue?: FieldMetadataDefaultValue;
|
||||
|
||||
/**
|
||||
* Field options.
|
||||
*/
|
||||
readonly options?: FieldMetadataOptions;
|
||||
|
||||
/**
|
||||
* Is primary field.
|
||||
*/
|
||||
readonly isPrimary?: boolean;
|
||||
|
||||
/**
|
||||
* Is system field.
|
||||
*/
|
||||
readonly isSystem?: boolean;
|
||||
|
||||
/**
|
||||
* Is nullable field.
|
||||
*/
|
||||
readonly isNullable?: boolean;
|
||||
|
||||
/**
|
||||
* Field gate.
|
||||
*/
|
||||
readonly gate?: Gate;
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
||||
|
||||
export interface WorkspaceObjectMetadataArgs {
|
||||
/**
|
||||
* Standard id.
|
||||
*/
|
||||
readonly standardId: string;
|
||||
|
||||
/**
|
||||
* Class to which table is applied.
|
||||
* Function target is a table defined in the class.
|
||||
* String target is a table defined in a json schema.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
readonly target: Function | string;
|
||||
|
||||
/**
|
||||
* Object name.
|
||||
*/
|
||||
readonly nameSingular: string;
|
||||
readonly namePlural: string;
|
||||
|
||||
/**
|
||||
* Object label.
|
||||
*/
|
||||
readonly labelSingular: string;
|
||||
readonly labelPlural: string;
|
||||
|
||||
/**
|
||||
* Object description.
|
||||
*/
|
||||
readonly description?: string;
|
||||
|
||||
/**
|
||||
* Object icon.
|
||||
*/
|
||||
readonly icon?: string;
|
||||
|
||||
/**
|
||||
* Is audit logged.
|
||||
*/
|
||||
readonly isAuditLogged: boolean;
|
||||
|
||||
/**
|
||||
* Is system object.
|
||||
*/
|
||||
readonly isSystem?: boolean;
|
||||
|
||||
/**
|
||||
* Object gate.
|
||||
*/
|
||||
readonly gate?: Gate;
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
import { ObjectType } from 'typeorm';
|
||||
|
||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
RelationMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
|
||||
export interface WorkspaceRelationMetadataArgs {
|
||||
/**
|
||||
* Standard id.
|
||||
*/
|
||||
readonly standardId: string;
|
||||
|
||||
/**
|
||||
* Class to which relation is applied.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
readonly target: Function | string;
|
||||
|
||||
/**
|
||||
* Relation name.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Relation label.
|
||||
*/
|
||||
readonly label: string | ((objectMetadata: ObjectMetadataEntity) => string);
|
||||
|
||||
/**
|
||||
* Relation type.
|
||||
*/
|
||||
readonly type: RelationMetadataType;
|
||||
|
||||
/**
|
||||
* Relation description.
|
||||
*/
|
||||
readonly description?:
|
||||
| string
|
||||
| ((objectMetadata: ObjectMetadataEntity) => string);
|
||||
|
||||
/**
|
||||
* Relation icon.
|
||||
*/
|
||||
readonly icon?: string;
|
||||
|
||||
/**
|
||||
* Relation inverse side target.
|
||||
*/
|
||||
readonly inverseSideTarget: () => ObjectType<object>;
|
||||
|
||||
/**
|
||||
* Relation inverse side field key.
|
||||
*/
|
||||
readonly inverseSideFieldKey?: string;
|
||||
|
||||
/**
|
||||
* Relation on delete action.
|
||||
*/
|
||||
readonly onDelete?: RelationOnDeleteAction;
|
||||
|
||||
/**
|
||||
* Relation join column.
|
||||
*/
|
||||
readonly joinColumn?: string;
|
||||
|
||||
/**
|
||||
* Is primary field.
|
||||
*/
|
||||
readonly isPrimary?: boolean;
|
||||
|
||||
/**
|
||||
* Is system field.
|
||||
*/
|
||||
readonly isSystem?: boolean;
|
||||
|
||||
/**
|
||||
* Is nullable field.
|
||||
*/
|
||||
readonly isNullable?: boolean;
|
||||
|
||||
/**
|
||||
* Field gate.
|
||||
*/
|
||||
readonly gate?: Gate;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { ObjectLiteral, Repository } from 'typeorm';
|
||||
|
||||
import { FlattenCompositeTypes } from 'src/engine/twenty-orm/interfaces/flatten-composite-types.interface';
|
||||
|
||||
export class WorkspaceRepository<
|
||||
Entity extends ObjectLiteral,
|
||||
> extends Repository<FlattenCompositeTypes<Entity>> {}
|
||||
@ -0,0 +1,17 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
export class DataSourceStorage {
|
||||
private static readonly dataSources: Map<string, DataSource> = new Map();
|
||||
|
||||
public static getDataSource(key: string): DataSource | undefined {
|
||||
return this.dataSources.get(key);
|
||||
}
|
||||
|
||||
public static setDataSource(key: string, dataSource: DataSource): void {
|
||||
this.dataSources.set(key, dataSource);
|
||||
}
|
||||
|
||||
public static getAllDataSources(): DataSource[] {
|
||||
return Array.from(this.dataSources.values());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
|
||||
import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
|
||||
import { WorkspaceObjectMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-object-metadata-args.interface';
|
||||
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
|
||||
|
||||
export class MetadataArgsStorage {
|
||||
readonly objects: WorkspaceObjectMetadataArgs[] = [];
|
||||
readonly fields: WorkspaceFieldMetadataArgs[] = [];
|
||||
readonly relations: WorkspaceRelationMetadataArgs[] = [];
|
||||
|
||||
filterObjects(
|
||||
target: Function | string,
|
||||
): WorkspaceObjectMetadataArgs | undefined;
|
||||
|
||||
filterObjects(target: (Function | string)[]): WorkspaceObjectMetadataArgs[];
|
||||
|
||||
filterObjects(
|
||||
target: (Function | string) | (Function | string)[],
|
||||
): WorkspaceObjectMetadataArgs | undefined | WorkspaceObjectMetadataArgs[] {
|
||||
const objects = this.filterByTarget(this.objects, target);
|
||||
|
||||
return Array.isArray(objects) ? objects[0] : objects;
|
||||
}
|
||||
|
||||
filterFields(target: Function | string): WorkspaceFieldMetadataArgs[];
|
||||
|
||||
filterFields(target: (Function | string)[]): WorkspaceFieldMetadataArgs[];
|
||||
|
||||
filterFields(
|
||||
target: (Function | string) | (Function | string)[],
|
||||
): WorkspaceFieldMetadataArgs[] {
|
||||
return this.filterByTarget(this.fields, target);
|
||||
}
|
||||
|
||||
filterRelations(target: Function | string): WorkspaceRelationMetadataArgs[];
|
||||
|
||||
filterRelations(
|
||||
target: (Function | string)[],
|
||||
): WorkspaceRelationMetadataArgs[];
|
||||
|
||||
filterRelations(
|
||||
target: (Function | string) | (Function | string)[],
|
||||
): WorkspaceRelationMetadataArgs[] {
|
||||
return this.filterByTarget(this.relations, target);
|
||||
}
|
||||
|
||||
protected filterByTarget<T extends { target: Function | string }>(
|
||||
array: T[],
|
||||
target: (Function | string) | (Function | string)[],
|
||||
): T[] {
|
||||
if (Array.isArray(target)) {
|
||||
return target.flatMap((targetItem) => {
|
||||
if (typeof targetItem === 'function') {
|
||||
return this.collectFromClass(array, targetItem);
|
||||
}
|
||||
|
||||
return this.collectFromString(array, targetItem);
|
||||
});
|
||||
} else {
|
||||
return typeof target === 'function'
|
||||
? this.collectFromClass(array, target)
|
||||
: this.collectFromString(array, target);
|
||||
}
|
||||
}
|
||||
|
||||
// Private helper to collect metadata from class prototypes
|
||||
private collectFromClass<T extends { target: Function | string }>(
|
||||
array: T[],
|
||||
cls: Function,
|
||||
): T[] {
|
||||
const collectedMetadata: T[] = [];
|
||||
let currentTarget = cls;
|
||||
|
||||
// Collect metadata from the current class and all its parent classes
|
||||
while (currentTarget !== Function.prototype) {
|
||||
collectedMetadata.push(
|
||||
...array.filter((item) => item.target === currentTarget),
|
||||
);
|
||||
currentTarget = Object.getPrototypeOf(currentTarget);
|
||||
}
|
||||
|
||||
return collectedMetadata;
|
||||
}
|
||||
|
||||
// Private helper to collect metadata directly by string comparison
|
||||
private collectFromString<T extends { target: Function | string }>(
|
||||
array: T[],
|
||||
targetString: string,
|
||||
): T[] {
|
||||
return array.filter((item) => item.target === targetString);
|
||||
}
|
||||
}
|
||||
|
||||
export const metadataArgsStorage = new MetadataArgsStorage();
|
||||
@ -0,0 +1,130 @@
|
||||
import {
|
||||
DynamicModule,
|
||||
Global,
|
||||
Logger,
|
||||
Module,
|
||||
OnApplicationShutdown,
|
||||
Provider,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ConfigurableModuleClass,
|
||||
MODULE_OPTIONS_TOKEN,
|
||||
} from '@nestjs/common/cache/cache.module-definition';
|
||||
|
||||
import {
|
||||
TwentyORMModuleAsyncOptions,
|
||||
TwentyORMOptions,
|
||||
} from 'src/engine/twenty-orm/interfaces/twenty-orm-options.interface';
|
||||
|
||||
import { entitySchemaFactories } from 'src/engine/twenty-orm/factories';
|
||||
import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants';
|
||||
import { TwentyORMService } from 'src/engine/twenty-orm/twenty-orm.service';
|
||||
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
|
||||
import { DataSourceStorage } from 'src/engine/twenty-orm/storage/data-source.storage';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [DataSourceModule],
|
||||
providers: [...entitySchemaFactories, TwentyORMService],
|
||||
exports: [EntitySchemaFactory, TwentyORMService],
|
||||
})
|
||||
export class TwentyORMCoreModule
|
||||
extends ConfigurableModuleClass
|
||||
implements OnApplicationShutdown
|
||||
{
|
||||
private readonly logger = new Logger(TwentyORMCoreModule.name);
|
||||
|
||||
static register(options: TwentyORMOptions): DynamicModule {
|
||||
const dynamicModule = super.register(options);
|
||||
const providers: Provider[] = [
|
||||
{
|
||||
provide: TWENTY_ORM_WORKSPACE_DATASOURCE,
|
||||
useFactory: async (
|
||||
entitySchemaFactory: EntitySchemaFactory,
|
||||
workspaceDatasourceFactory: WorkspaceDatasourceFactory,
|
||||
) => {
|
||||
const entities = options.objects.map((entityClass) =>
|
||||
entitySchemaFactory.create(entityClass),
|
||||
);
|
||||
|
||||
const dataSource =
|
||||
await workspaceDatasourceFactory.createWorkspaceDatasource(
|
||||
entities,
|
||||
);
|
||||
|
||||
return dataSource;
|
||||
},
|
||||
inject: [EntitySchemaFactory, WorkspaceDatasourceFactory],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), ...providers],
|
||||
exports: [
|
||||
...(dynamicModule.exports ?? []),
|
||||
TWENTY_ORM_WORKSPACE_DATASOURCE,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
static registerAsync(
|
||||
asyncOptions: TwentyORMModuleAsyncOptions,
|
||||
): DynamicModule {
|
||||
const dynamicModule = super.registerAsync(asyncOptions);
|
||||
const providers: Provider[] = [
|
||||
{
|
||||
provide: TWENTY_ORM_WORKSPACE_DATASOURCE,
|
||||
useFactory: async (
|
||||
entitySchemaFactory: EntitySchemaFactory,
|
||||
workspaceDatasourceFactory: WorkspaceDatasourceFactory,
|
||||
options: TwentyORMOptions,
|
||||
) => {
|
||||
const entities = options.objects.map((entityClass) =>
|
||||
entitySchemaFactory.create(entityClass),
|
||||
);
|
||||
|
||||
const dataSource =
|
||||
await workspaceDatasourceFactory.createWorkspaceDatasource(
|
||||
entities,
|
||||
);
|
||||
|
||||
return dataSource;
|
||||
},
|
||||
inject: [
|
||||
EntitySchemaFactory,
|
||||
WorkspaceDatasourceFactory,
|
||||
MODULE_OPTIONS_TOKEN,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), ...providers],
|
||||
exports: [
|
||||
...(dynamicModule.exports ?? []),
|
||||
TWENTY_ORM_WORKSPACE_DATASOURCE,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys all data sources on application shutdown
|
||||
*/
|
||||
async onApplicationShutdown() {
|
||||
const dataSources = DataSourceStorage.getAllDataSources();
|
||||
|
||||
for (const dataSource of dataSources) {
|
||||
try {
|
||||
if (dataSource && dataSource.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(e?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export const TWENTY_ORM_WORKSPACE_DATASOURCE =
|
||||
'TWENTY_ORM_WORKSPACE_DATASOURCE';
|
||||
@ -0,0 +1,10 @@
|
||||
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
||||
|
||||
import { TwentyORMOptions } from './interfaces/twenty-orm-options.interface';
|
||||
|
||||
export const {
|
||||
ConfigurableModuleClass,
|
||||
MODULE_OPTIONS_TOKEN,
|
||||
OPTIONS_TYPE,
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
} = new ConfigurableModuleBuilder<TwentyORMOptions>().build();
|
||||
@ -0,0 +1,41 @@
|
||||
import { DynamicModule, Global, Module } from '@nestjs/common';
|
||||
import { ConfigurableModuleClass } from '@nestjs/common/cache/cache.module-definition';
|
||||
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
|
||||
|
||||
import {
|
||||
TwentyORMModuleAsyncOptions,
|
||||
TwentyORMOptions,
|
||||
} from 'src/engine/twenty-orm/interfaces/twenty-orm-options.interface';
|
||||
|
||||
import { createTwentyORMProviders } from 'src/engine/twenty-orm/twenty-orm.providers';
|
||||
import { TwentyORMCoreModule } from 'src/engine/twenty-orm/twenty-orm-core.module';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class TwentyORMModule extends ConfigurableModuleClass {
|
||||
static register(options: TwentyORMOptions): DynamicModule {
|
||||
return {
|
||||
module: TwentyORMModule,
|
||||
imports: [TwentyORMCoreModule.register(options)],
|
||||
};
|
||||
}
|
||||
|
||||
static forFeature(objects: EntityClassOrSchema[] = []): DynamicModule {
|
||||
const providers = createTwentyORMProviders(objects);
|
||||
|
||||
return {
|
||||
module: TwentyORMModule,
|
||||
providers: providers,
|
||||
exports: providers,
|
||||
};
|
||||
}
|
||||
|
||||
static registerAsync(
|
||||
asyncOptions: TwentyORMModuleAsyncOptions,
|
||||
): DynamicModule {
|
||||
return {
|
||||
module: TwentyORMModule,
|
||||
imports: [TwentyORMCoreModule.registerAsync(asyncOptions)],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import { Provider, Type } from '@nestjs/common';
|
||||
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
|
||||
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { getWorkspaceRepositoryToken } from 'src/engine/twenty-orm/utils/get-workspace-repository-token.util';
|
||||
import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants';
|
||||
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
|
||||
|
||||
/**
|
||||
* Create providers for the given entities.
|
||||
*/
|
||||
export function createTwentyORMProviders(
|
||||
objects?: EntityClassOrSchema[],
|
||||
): Provider[] {
|
||||
return (objects || []).map((object) => ({
|
||||
provide: getWorkspaceRepositoryToken(object),
|
||||
useFactory: (
|
||||
dataSource: DataSource,
|
||||
entitySchemaFactory: EntitySchemaFactory,
|
||||
) => {
|
||||
const entity = entitySchemaFactory.create(object as Type);
|
||||
|
||||
return dataSource.getRepository(entity);
|
||||
},
|
||||
inject: [TWENTY_ORM_WORKSPACE_DATASOURCE, EntitySchemaFactory],
|
||||
}));
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { Injectable, Type } from '@nestjs/common';
|
||||
|
||||
import { DataSource, ObjectLiteral, Repository } from 'typeorm';
|
||||
|
||||
import { FlattenCompositeTypes } from 'src/engine/twenty-orm/interfaces/flatten-composite-types.interface';
|
||||
|
||||
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
|
||||
import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class TwentyORMService {
|
||||
constructor(
|
||||
@InjectWorkspaceDatasource()
|
||||
private readonly workspaceDataSource: DataSource,
|
||||
private readonly entitySchemaFactory: EntitySchemaFactory,
|
||||
) {}
|
||||
|
||||
getRepository<T extends ObjectLiteral>(
|
||||
entityClass: Type<T>,
|
||||
): Repository<FlattenCompositeTypes<T>> {
|
||||
const entitySchema = this.entitySchemaFactory.create(entityClass);
|
||||
|
||||
return this.workspaceDataSource.getRepository<FlattenCompositeTypes<T>>(
|
||||
entitySchema,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
|
||||
|
||||
import { EntitySchema, Repository } from 'typeorm';
|
||||
|
||||
export function getWorkspaceRepositoryToken(
|
||||
entity: EntityClassOrSchema,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
): Function | string {
|
||||
if (entity === null || entity === undefined) {
|
||||
throw new Error('Circular dependency @InjectWorkspaceRepository()');
|
||||
}
|
||||
|
||||
if (entity instanceof Function && entity.prototype instanceof Repository) {
|
||||
return entity;
|
||||
}
|
||||
|
||||
if (entity instanceof EntitySchema) {
|
||||
return `${
|
||||
entity.options.target ? entity.options.target.name : entity.options.name
|
||||
}WorkspaceRepository`;
|
||||
}
|
||||
|
||||
return `${entity.name}WorkspaceRepository`;
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ATTACHMENT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { WorkspaceObject } from 'src/engine/twenty-orm/decorators/workspace-object.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseObjectMetadata } from 'src/engine/twenty-orm/workspace-object-tests/base.object-metadata';
|
||||
import { WorkspaceMemberObjectMetadata } from 'src/engine/twenty-orm/workspace-object-tests/workspace-member.object-metadata';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
|
||||
@WorkspaceObject({
|
||||
standardId: STANDARD_OBJECT_IDS.attachment,
|
||||
namePlural: 'attachments',
|
||||
labelSingular: 'Attachment',
|
||||
labelPlural: 'Attachments',
|
||||
description: 'An attachment',
|
||||
icon: 'IconFileImport',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
export class AttachmentObjectMetadata extends BaseObjectMetadata {
|
||||
@WorkspaceField({
|
||||
standardId: ATTACHMENT_STANDARD_FIELD_IDS.name,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
description: 'Attachment name',
|
||||
icon: 'IconFileUpload',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: ATTACHMENT_STANDARD_FIELD_IDS.fullPath,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Full path',
|
||||
description: 'Attachment full path',
|
||||
icon: 'IconLink',
|
||||
})
|
||||
fullPath: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: ATTACHMENT_STANDARD_FIELD_IDS.type,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Type',
|
||||
description: 'Attachment type',
|
||||
icon: 'IconList',
|
||||
})
|
||||
type: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: ATTACHMENT_STANDARD_FIELD_IDS.author,
|
||||
label: 'Author',
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
inverseSideTarget: () => WorkspaceMemberObjectMetadata,
|
||||
inverseSideFieldKey: 'authoredAttachments',
|
||||
})
|
||||
author: Relation<WorkspaceMemberObjectMetadata>;
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsPimaryField } from 'src/engine/twenty-orm/decorators/workspace-is-primary-field.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
|
||||
export abstract class BaseObjectMetadata {
|
||||
@WorkspaceField({
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Id',
|
||||
description: 'Id',
|
||||
defaultValue: 'uuid',
|
||||
icon: 'Icon123',
|
||||
})
|
||||
@WorkspaceIsPimaryField()
|
||||
@WorkspaceIsSystem()
|
||||
id: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Creation date',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
defaultValue: 'now',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
createdAt: Date;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Update date',
|
||||
description: 'Update date',
|
||||
icon: 'IconCalendar',
|
||||
defaultValue: 'now',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||
import { LinkMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
RelationMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { COMPANY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { WorkspaceObject } from 'src/engine/twenty-orm/decorators/workspace-object.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { WorkspaceMemberObjectMetadata } from 'src/engine/twenty-orm/workspace-object-tests/workspace-member.object-metadata';
|
||||
import { BaseObjectMetadata } from 'src/engine/twenty-orm/workspace-object-tests/base.object-metadata';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
|
||||
@WorkspaceObject({
|
||||
standardId: STANDARD_OBJECT_IDS.company,
|
||||
namePlural: 'companies',
|
||||
labelSingular: 'Company',
|
||||
labelPlural: 'Companies',
|
||||
description: 'A company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
})
|
||||
export class CompanyObjectMetadata extends BaseObjectMetadata {
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.name,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
description: 'The company name',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.domainName,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Domain Name',
|
||||
description:
|
||||
'The company website URL. We use this url to fetch the company icon',
|
||||
icon: 'IconLink',
|
||||
})
|
||||
domainName?: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.address,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Address',
|
||||
description: 'The company address',
|
||||
icon: 'IconMap',
|
||||
})
|
||||
address: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.employees,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Employees',
|
||||
description: 'Number of employees in the company',
|
||||
icon: 'IconUsers',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
employees: number;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.linkedinLink,
|
||||
type: FieldMetadataType.LINK,
|
||||
label: 'Linkedin',
|
||||
description: 'The company Linkedin account',
|
||||
icon: 'IconBrandLinkedin',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
linkedinLink: LinkMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.xLink,
|
||||
type: FieldMetadataType.LINK,
|
||||
label: 'X',
|
||||
description: 'The company Twitter/X account',
|
||||
icon: 'IconBrandX',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
xLink: LinkMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue,
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
label: 'ARR',
|
||||
description:
|
||||
'Annual Recurring Revenue: The actual or estimated annual revenue of the company',
|
||||
icon: 'IconMoneybag',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
annualRecurringRevenue: CurrencyMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.idealCustomerProfile,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'ICP',
|
||||
description:
|
||||
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
|
||||
icon: 'IconTarget',
|
||||
defaultValue: false,
|
||||
})
|
||||
idealCustomerProfile: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.position,
|
||||
type: FieldMetadataType.POSITION,
|
||||
label: 'Position',
|
||||
description: 'Company record position',
|
||||
icon: 'IconHierarchy2',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNullable()
|
||||
position: number;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.accountOwner,
|
||||
label: 'Account Owner',
|
||||
description:
|
||||
'Your team member responsible for managing the company account',
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
inverseSideTarget: () => WorkspaceMemberObjectMetadata,
|
||||
inverseSideFieldKey: 'accountOwnerForCompanies',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
accountOwner: WorkspaceMemberObjectMetadata;
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
RelationMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { WORKSPACE_MEMBER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { AttachmentObjectMetadata } from 'src/modules/attachment/standard-objects/attachment.object-metadata';
|
||||
import { WorkspaceObject } from 'src/engine/twenty-orm/decorators/workspace-object.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { BaseObjectMetadata } from 'src/engine/twenty-orm/workspace-object-tests/base.object-metadata';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { CompanyObjectMetadata } from 'src/engine/twenty-orm/workspace-object-tests/company.object-metadata';
|
||||
|
||||
@WorkspaceObject({
|
||||
standardId: STANDARD_OBJECT_IDS.workspaceMember,
|
||||
namePlural: 'workspaceMembers',
|
||||
labelSingular: 'Workspace Member',
|
||||
labelPlural: 'Workspace Members',
|
||||
description: 'A workspace member',
|
||||
icon: 'IconUserCircle',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
|
||||
@WorkspaceField({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.name,
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
label: 'Name',
|
||||
description: 'Workspace member name',
|
||||
icon: 'IconCircleUser',
|
||||
})
|
||||
name: FullNameMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.colorScheme,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Color Scheme',
|
||||
description: 'Preferred color scheme',
|
||||
icon: 'IconColorSwatch',
|
||||
defaultValue: "'Light'",
|
||||
})
|
||||
colorScheme: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.locale,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Language',
|
||||
description: 'Preferred language',
|
||||
icon: 'IconLanguage',
|
||||
defaultValue: "'en'",
|
||||
})
|
||||
locale: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.avatarUrl,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Avatar Url',
|
||||
description: 'Workspace member avatar',
|
||||
icon: 'IconFileUpload',
|
||||
})
|
||||
avatarUrl: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.userEmail,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'User Email',
|
||||
description: 'Related user email address',
|
||||
icon: 'IconMail',
|
||||
})
|
||||
userEmail: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.userId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'User Id',
|
||||
description: 'Associated User Id',
|
||||
icon: 'IconCircleUsers',
|
||||
})
|
||||
userId: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.authoredAttachments,
|
||||
label: 'Authored attachments',
|
||||
description: 'Attachments created by the workspace member',
|
||||
icon: 'IconFileImport',
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
inverseSideTarget: () => AttachmentObjectMetadata,
|
||||
inverseSideFieldKey: 'author',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
authoredAttachments: Relation<AttachmentObjectMetadata[]>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.accountOwnerForCompanies,
|
||||
label: 'Account Owner For Companies',
|
||||
description: 'Account owner for companies',
|
||||
icon: 'IconBriefcase',
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
inverseSideTarget: () => CompanyObjectMetadata,
|
||||
inverseSideFieldKey: 'accountOwner',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
accountOwnerForCompanies: Relation<CompanyObjectMetadata[]>;
|
||||
}
|
||||
@ -31,6 +31,7 @@ export function FieldMetadata<T extends FieldMetadataType>(
|
||||
{
|
||||
...restParams,
|
||||
standardId,
|
||||
joinColumn,
|
||||
},
|
||||
fieldKey,
|
||||
isNullable,
|
||||
@ -49,6 +50,7 @@ export function FieldMetadata<T extends FieldMetadataType>(
|
||||
defaultValue: null,
|
||||
options: undefined,
|
||||
settings: undefined,
|
||||
joinColumn,
|
||||
},
|
||||
joinColumn,
|
||||
isNullable,
|
||||
|
||||
@ -6,6 +6,7 @@ import { ReflectDynamicRelationFieldMetadata } from 'src/engine/workspace-manage
|
||||
import { ReflectFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/reflect-field-metadata.interface';
|
||||
import { ReflectObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/reflect-object-metadata.interface';
|
||||
import { ReflectRelationMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface';
|
||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
||||
|
||||
export interface ReflectMetadataTypeMap {
|
||||
objectMetadata: ReflectObjectMetadata;
|
||||
@ -17,6 +18,12 @@ export interface ReflectMetadataTypeMap {
|
||||
isNullable: true;
|
||||
isSystem: true;
|
||||
isAuditLogged: false;
|
||||
|
||||
['workspace:is-nullable-metadata-args']: true;
|
||||
['workspace:gate-metadata-args']: Gate;
|
||||
['workspace:is-system-metadata-args']: true;
|
||||
['workspace:is-audit-logged-metadata-args']: false;
|
||||
['workspace:is-primary-field-metadata-args']: true;
|
||||
}
|
||||
|
||||
export class TypedReflect {
|
||||
|
||||
Reference in New Issue
Block a user