Skip to content

Commit

Permalink
feat: manually implement joinColumn (#6022)
Browse files Browse the repository at this point in the history
This PR introduce a new decorator named `@WorkspaceJoinColumn`, the goal
of this one is to manually declare the join columns inside the workspace
entities, so we don't have to rely on `ObjectRecord` type.

This decorator can be used that way:

```typescript
  @WorkspaceRelation({
    standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.company,
    type: RelationMetadataType.MANY_TO_ONE,
    label: 'Company',
    description: 'ActivityTarget company',
    icon: 'IconBuildingSkyscraper',
    inverseSideTarget: () => CompanyWorkspaceEntity,
    inverseSideFieldKey: 'activityTargets',
  })
  @WorkspaceIsNullable()
  company: Relation<CompanyWorkspaceEntity> | null;

  // The argument is the name of the relation above
  @WorkspaceJoinColumn('company')
  companyId: string | null;
```
  • Loading branch information
magrinj authored Jun 27, 2024
1 parent 7eb69a7 commit 95c5602
Show file tree
Hide file tree
Showing 64 changed files with 427 additions and 243 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ export class TimelineCalendarEventService {
const calendarEventIds = await this.calendarEventRepository.find({
where: {
calendarEventParticipants: {
person: {
id: Any(personIds),
},
personId: Any(personIds),
},
},
select: {
Expand Down Expand Up @@ -81,8 +79,8 @@ export class TimelineCalendarEventService {
const participants = event.calendarEventParticipants.map(
(participant) => ({
calendarEventId: event.id,
personId: participant.person?.id ?? null,
workspaceMemberId: participant.workspaceMember?.id ?? null,
personId: participant.personId ?? null,
workspaceMemberId: participant.workspaceMemberId ?? null,
firstName:
participant.person?.name?.firstName ||
participant.workspaceMember?.name.firstName ||
Expand Down Expand Up @@ -135,9 +133,7 @@ export class TimelineCalendarEventService {
): Promise<TimelineCalendarEventsWithTotal> {
const personIds = await this.personRepository.find({
where: {
company: {
id: companyId,
},
companyId,
},
select: {
id: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';

export class UserService extends TypeOrmQueryService<User> {
Expand Down Expand Up @@ -113,8 +112,7 @@ export class UserService extends TypeOrmQueryService<User> {
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember"`,
);
const workspaceMember = workspaceMembers.filter(
(member: ObjectRecord<WorkspaceMemberWorkspaceEntity>) =>
member.userId === userId,
(member: WorkspaceMemberWorkspaceEntity) => member.userId === userId,
)?.[0];

assert(workspaceMember, 'WorkspaceMember not found');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';

export function WorkspaceJoinColumn(
relationPropertyKey: string,
): PropertyDecorator {
return (object, propertyKey) => {
metadataArgsStorage.addJoinColumns({
target: object.constructor,
relationName: relationPropertyKey,
joinColumn: propertyKey.toString(),
});
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,19 @@ import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args
import { TypedReflect } from 'src/utils/typed-reflect';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';

interface WorkspaceBaseRelationOptions<TType, TClass> {
interface WorkspaceRelationOptions<TClass> {
standardId: string;
label: string | ((objectMetadata: ObjectMetadataEntity) => string);
description?: string | ((objectMetadata: ObjectMetadataEntity) => string);
icon?: string;
type: TType;
type: RelationMetadataType;
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>,
options: WorkspaceRelationOptions<TClass>,
): PropertyDecorator {
return (object, propertyKey) => {
const isPrimary =
Expand All @@ -63,14 +47,6 @@ export function WorkspaceRelation<TClass extends object>(
propertyKey.toString(),
);

let joinColumn: string | undefined;

if ('joinColumn' in options) {
joinColumn = options.joinColumn
? options.joinColumn
: `${propertyKey.toString()}Id`;
}

metadataArgsStorage.addRelations({
target: object.constructor,
standardId: options.standardId,
Expand All @@ -82,7 +58,6 @@ export function WorkspaceRelation<TClass extends object>(
inverseSideTarget: options.inverseSideTarget,
inverseSideFieldKey: options.inverseSideFieldKey as string | undefined,
onDelete: options.onDelete,
joinColumn,
isPrimary,
isNullable,
isSystem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ColumnType, EntitySchemaColumnOptions } from 'typeorm';

import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-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';
Expand All @@ -12,6 +13,7 @@ import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-me
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';
import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util';

type EntitySchemaColumnMap = {
[key: string]: EntitySchemaColumnOptions;
Expand All @@ -22,6 +24,7 @@ export class EntitySchemaColumnFactory {
create(
fieldMetadataArgsCollection: WorkspaceFieldMetadataArgs[],
relationMetadataArgsCollection: WorkspaceRelationMetadataArgs[],
joinColumnsMetadataArgsCollection: WorkspaceJoinColumnsMetadataArgs[],
): EntitySchemaColumnMap {
let entitySchemaColumnMap: EntitySchemaColumnMap = {};

Expand Down Expand Up @@ -56,9 +59,14 @@ export class EntitySchemaColumnFactory {
};

for (const relationMetadataArgs of relationMetadataArgsCollection) {
if (relationMetadataArgs.joinColumn) {
entitySchemaColumnMap[relationMetadataArgs.joinColumn] = {
name: relationMetadataArgs.joinColumn,
const joinColumn = getJoinColumn(
joinColumnsMetadataArgsCollection,
relationMetadataArgs,
);

if (joinColumn) {
entitySchemaColumnMap[joinColumn] = {
name: joinColumn,
type: 'uuid',
nullable: relationMetadataArgs.isNullable,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ 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 { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-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';
import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util';

type EntitySchemaRelationMap = {
[key: string]: EntitySchemaRelationOptions;
Expand All @@ -18,6 +20,7 @@ export class EntitySchemaRelationFactory {
// eslint-disable-next-line @typescript-eslint/ban-types
target: Function,
relationMetadataArgsCollection: WorkspaceRelationMetadataArgs[],
joinColumnsMetadataArgsCollection: WorkspaceJoinColumnsMetadataArgs[],
): EntitySchemaRelationMap {
const entitySchemaRelationMap: EntitySchemaRelationMap = {};

Expand All @@ -27,16 +30,19 @@ export class EntitySchemaRelationFactory {
const oppositeObjectName = convertClassNameToObjectMetadataName(
oppositeTarget.name,
);

const relationType = this.getRelationType(relationMetadataArgs);
const joinColumn = getJoinColumn(
joinColumnsMetadataArgsCollection,
relationMetadataArgs,
);

entitySchemaRelationMap[relationMetadataArgs.name] = {
type: relationType,
target: oppositeObjectName,
inverseSide: relationMetadataArgs.inverseSideFieldKey ?? objectName,
joinColumn: relationMetadataArgs.joinColumn
joinColumn: joinColumn
? {
name: relationMetadataArgs.joinColumn,
name: joinColumn,
}
: undefined,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@ export class EntitySchemaFactory {

const fieldMetadataArgsCollection =
metadataArgsStorage.filterFields(target);
const joinColumnsMetadataArgsCollection =
metadataArgsStorage.filterJoinColumns(target);
const relationMetadataArgsCollection =
metadataArgsStorage.filterRelations(target);

const columns = this.entitySchemaColumnFactory.create(
fieldMetadataArgsCollection,
relationMetadataArgsCollection,
joinColumnsMetadataArgsCollection,
);

const relations = this.entitySchemaRelationFactory.create(
target,
relationMetadataArgsCollection,
joinColumnsMetadataArgsCollection,
);

const entitySchema = new EntitySchema({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface WorkspaceJoinColumnsMetadataArgs {
/**
* Class to which relation is applied.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
readonly target: Function;

/**
* Relation name.
*/
readonly relationName: string;

/**
* Relation label.
*/
readonly joinColumn: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,6 @@ export interface WorkspaceRelationMetadataArgs {
*/
readonly onDelete?: RelationOnDeleteAction;

/**
* Relation join column.
*/
readonly joinColumn?: string;

/**
* Is primary field.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/wo
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
import { WorkspaceExtendedEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-extended-entity-metadata-args.interface';
import { WorkspaceIndexMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface';
import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface';

export class MetadataArgsStorage {
private readonly entities: WorkspaceEntityMetadataArgs[] = [];
Expand All @@ -15,6 +16,7 @@ export class MetadataArgsStorage {
private readonly dynamicRelations: WorkspaceDynamicRelationMetadataArgs[] =
[];
private readonly indexes: WorkspaceIndexMetadataArgs[] = [];
private readonly joinColumns: WorkspaceJoinColumnsMetadataArgs[] = [];

addEntities(...entities: WorkspaceEntityMetadataArgs[]): void {
this.entities.push(...entities);
Expand Down Expand Up @@ -44,6 +46,10 @@ export class MetadataArgsStorage {
this.dynamicRelations.push(...dynamicRelations);
}

addJoinColumns(...joinColumns: WorkspaceJoinColumnsMetadataArgs[]): void {
this.joinColumns.push(...joinColumns);
}

filterEntities(
target: Function | string,
): WorkspaceEntityMetadataArgs | undefined;
Expand Down Expand Up @@ -123,6 +129,20 @@ export class MetadataArgsStorage {
return this.filterByTarget(this.dynamicRelations, target);
}

filterJoinColumns(
target: Function | string,
): WorkspaceJoinColumnsMetadataArgs[];

filterJoinColumns(
target: (Function | string)[],
): WorkspaceJoinColumnsMetadataArgs[];

filterJoinColumns(
target: (Function | string) | (Function | string)[],
): WorkspaceJoinColumnsMetadataArgs[] {
return this.filterByTarget(this.joinColumns, target);
}

protected filterByTarget<T extends { target: Function | string }>(
array: T[],
target: (Function | string) | (Function | string)[],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface';
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';

import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';

export const getJoinColumn = (
joinColumnsMetadataArgsCollection: WorkspaceJoinColumnsMetadataArgs[],
relationMetadataArgs: WorkspaceRelationMetadataArgs,
opposite = false,
): string | null => {
if (
relationMetadataArgs.type === RelationMetadataType.ONE_TO_MANY ||
relationMetadataArgs.type === RelationMetadataType.MANY_TO_MANY
) {
return null;
}

const inverseSideTarget = relationMetadataArgs.inverseSideTarget();
const inverseSideJoinColumnsMetadataArgsCollection =
metadataArgsStorage.filterJoinColumns(inverseSideTarget);
const filteredJoinColumnsMetadataArgsCollection =
joinColumnsMetadataArgsCollection.filter(
(joinColumnsMetadataArgs) =>
joinColumnsMetadataArgs.relationName === relationMetadataArgs.name,
);
const oppositeFilteredJoinColumnsMetadataArgsCollection =
inverseSideJoinColumnsMetadataArgsCollection.filter(
(joinColumnsMetadataArgs) =>
joinColumnsMetadataArgs.relationName === relationMetadataArgs.name,
);

if (
filteredJoinColumnsMetadataArgsCollection.length > 0 &&
oppositeFilteredJoinColumnsMetadataArgsCollection.length > 0
) {
throw new Error(
`Join column for ${relationMetadataArgs.name} relation is present on both sides`,
);
}

// If we're in a ONE_TO_ONE relation and there are no join columns, we need to find the join column on the inverse side
if (
relationMetadataArgs.type === RelationMetadataType.ONE_TO_ONE &&
filteredJoinColumnsMetadataArgsCollection.length === 0 &&
!opposite
) {
const inverseSideRelationMetadataArgsCollection =
metadataArgsStorage.filterRelations(inverseSideTarget);
const inverseSideRelationMetadataArgs =
inverseSideRelationMetadataArgsCollection.find(
(inverseSideRelationMetadataArgs) =>
inverseSideRelationMetadataArgs.inverseSideFieldKey ===
relationMetadataArgs.name,
);

if (!inverseSideRelationMetadataArgs) {
throw new Error(
`Inverse side join column of relation ${relationMetadataArgs.name} is missing`,
);
}

return getJoinColumn(
inverseSideJoinColumnsMetadataArgsCollection,
inverseSideRelationMetadataArgs,
// Avoid infinite recursion
true,
);
}

// Check if there are multiple join columns for the relation
if (filteredJoinColumnsMetadataArgsCollection.length > 1) {
throw new Error(
`Multiple join columns found for relation ${relationMetadataArgs.name}`,
);
}

const joinColumnsMetadataArgs = filteredJoinColumnsMetadataArgsCollection[0];

if (!joinColumnsMetadataArgs) {
throw new Error(
`Join column is missing for relation ${relationMetadataArgs.name}`,
);
}

return joinColumnsMetadataArgs.joinColumn;
};
Loading

0 comments on commit 95c5602

Please sign in to comment.