Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-4797 Implement websockets [WIP] #5237

Closed
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ade0ef5
Enable graphql-ws subscription
i-am-chitti Apr 14, 2024
0ad6155
Add feature flag constant
i-am-chitti Apr 14, 2024
c1c98f0
Merge branch 'main' into GH-4797/ws-gql-subscription
i-am-chitti Apr 17, 2024
3aeb60f
Merge branch 'main' into GH-4797/ws-gql-subscription
i-am-chitti Apr 18, 2024
dd51a85
Merge branch 'main' into GH-4797/ws-gql-subscription
i-am-chitti Apr 19, 2024
2d20c00
Add event listeners
i-am-chitti Apr 19, 2024
2e874e2
Add subscription resolver
i-am-chitti Apr 19, 2024
8fd2094
Add subscription module
i-am-chitti Apr 19, 2024
9014583
Add GQL field type in ObjectRecordBaseEvent
i-am-chitti Apr 19, 2024
9b1f5ea
Update core-engine module
i-am-chitti Apr 19, 2024
1196084
Merge branch 'main' into GH-4797/ws-gql-subscription
i-am-chitti Apr 23, 2024
6e75a38
Merge branch 'main' into GH-4797/ws-gql-subscription
i-am-chitti Apr 26, 2024
47d917f
Merge branch 'main' into GH-4797/ws-gql-subscription
i-am-chitti May 1, 2024
3e8fa12
Fix database reset command
FelixMalfait May 1, 2024
2f1e6c4
feat: update links field (#5212)
thaisguigon May 1, 2024
62bd61c
Add relation in CSV exports (#5085)
gitstart-app[bot] May 1, 2024
0983a20
Enable phone field type (#5052)
gitstart-app[bot] May 1, 2024
1737fdb
Bump to 0.10.5
charlesBochet May 2, 2024
a41f813
User workspace middleware throws 401 if token is invalid (#5245)
Weiko May 2, 2024
9f38c94
Constant api version (#5248)
brendanlaschke May 2, 2024
bb9a4b1
[feat] Minor updates to the edit db connection page (#5250)
ijreilly May 2, 2024
ceef884
[calendar] hide calendar settings until implemented (#5252)
Weiko May 2, 2024
7946553
Fix sync metadata script (#5253)
charlesBochet May 2, 2024
ac5cb6e
Bump to 0.10.6
charlesBochet May 2, 2024
b287ddb
fix: fix storybook build script not found by Chromatic (#5235)
thaisguigon May 2, 2024
83849ef
Build stripe integration on backend side (#5246)
thomtrp May 2, 2024
5c23614
fix workspace-member deletion with existing attachments/documents (#5…
Weiko May 2, 2024
6f73094
Quick job update (#5265)
FelixMalfait May 3, 2024
ce1f6bc
Fix token validation on graphql IntrospectionQuery (#5255)
Weiko May 3, 2024
00071a3
Fix filter transform with logic operators (#5269)
Weiko May 3, 2024
089164b
fix: fix storybook coverage task (#5256)
thaisguigon May 3, 2024
726d5d1
4900 multi select field front implement expanded cells (#5151)
martmull May 3, 2024
db2da77
[calendar] Fix calendar sync status (#5272)
Weiko May 3, 2024
347efc4
Fix white screen on token expire (#5271)
charlesBochet May 3, 2024
072fcc9
Fix export with relations (#5279)
thomtrp May 3, 2024
ec3e2e4
Remove isMultiSelect feature flag (#5280)
thomtrp May 3, 2024
6af88e5
Added OG Image (#5251)
ady-beraud May 3, 2024
0b690a0
Fix yoga patch user id cache (#5285)
Weiko May 3, 2024
71c73a5
Create convert record positions to integers command (#5287)
Weiko May 3, 2024
fa88c03
Remove Feature Flag on Calendar (#5288)
charlesBochet May 3, 2024
1db3aae
Fix Filtered index view infinite re-render (#5286)
charlesBochet May 3, 2024
489abfd
Bump versions to 0.11 (#5289)
charlesBochet May 3, 2024
c9db37b
fix: fix storybook:build memory allocation error in CI (#5284)
thaisguigon May 3, 2024
d1313dc
Merge branch 'main' into GH-4797/ws-gql-subscription
i-am-chitti May 4, 2024
23a7ff2
Update event listner name
i-am-chitti May 4, 2024
8980fc8
Merge branch 'main' into GH-4797/ws-gql-subscription
i-am-chitti May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export class GraphQLConfigService
const config: YogaDriverConfig = {
autoSchemaFile: true,
include: [CoreEngineModule],
subscriptions: {
'graphql-ws': true,
},
conditionalSchema: async (context) => {
let user: User | undefined;
let workspace: Workspace | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timel
import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/timeline-calendar-event.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { HealthModule } from 'src/engine/core-modules/health/health.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';

import { ClientConfigModule } from './client-config/client-config.module';
import { FileModule } from './file/file.module';
Expand All @@ -30,6 +31,7 @@ import { AnalyticsModule } from './analytics/analytics.module';
TimelineCalendarEventModule,
UserModule,
WorkspaceModule,
SubscriptionsModule,
],
exports: [
AnalyticsModule,
Expand All @@ -39,6 +41,7 @@ import { AnalyticsModule } from './analytics/analytics.module';
TimelineCalendarEventModule,
UserModule,
WorkspaceModule,
SubscriptionsModule,
],
})
export class CoreEngineModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum FeatureFlagKeys {
IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IsMultiSelectEnabled = 'IS_MULTI_SELECT_ENABLED',
IsRealTimeSyncEnabled = 'IS_REAL_TIME_SYNC_ENABLED',
}

@Entity({ name: 'featureFlag', schema: 'core' })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Field, ObjectType } from '@nestjs/graphql';

import GraphQLJSON from 'graphql-type-json';

import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event';

@ObjectType()
export class ObjectRecordCreateEvent<T> extends ObjectRecordBaseEvent {
@Field(() => GraphQLJSON)
properties: {
after: T;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Field, ObjectType } from '@nestjs/graphql';

import GraphQLJSON from 'graphql-type-json';

import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event';

@ObjectType()
export class ObjectRecordDeleteEvent<T> extends ObjectRecordBaseEvent {
@Field(() => GraphQLJSON)
properties: {
before: T;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Field, ObjectType } from '@nestjs/graphql';

import GraphQLJSON from 'graphql-type-json';

import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event';

@ObjectType()
export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent {
@Field(() => GraphQLJSON)
properties: {
before: T;
after: T;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { Field, ObjectType } from '@nestjs/graphql';

import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';

@ObjectType()
export class ObjectRecordBaseEvent {
@Field(() => String)
name: string;

@Field(() => String)
workspaceId: string;

@Field(() => String)
recordId: string;

@Field(() => String, { nullable: true })
userId?: string;

@Field(() => String, { nullable: true })
workspaceMemberId?: string;

@Field(() => ObjectMetadataInterface)
objectMetadata: ObjectMetadataInterface;
properties: any;
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,48 @@
import { Field, ObjectType } from '@nestjs/graphql';

import GraphQLJSON from 'graphql-type-json';

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 { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';

import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';

export interface FieldMetadataInterface<
@ObjectType()
export class FieldMetadataInterface<
T extends FieldMetadataType | 'default' = 'default',
> {
id: string;
type: FieldMetadataType;
name: string;
label: string;

@Field(() => GraphQLJSON, { nullable: true })
defaultValue?: FieldMetadataDefaultValue<T>;

@Field(() => [GraphQLJSON], { nullable: true })
options?: FieldMetadataOptions<T>;
settings?: FieldMetadataSettings<T>;

@Field(() => String)
objectMetadataId: string;

@Field(() => String, { nullable: true })
workspaceId?: string;

@Field(() => String, { nullable: true })
description?: string;

@Field(() => Boolean, { nullable: true })
isNullable?: boolean;

@Field(() => RelationMetadataEntity, { nullable: true })
fromRelationMetadata?: RelationMetadataEntity;

@Field(() => RelationMetadataEntity, { nullable: true })
toRelationMetadata?: RelationMetadataEntity;

@Field(() => Boolean, { nullable: true })
isCustom?: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
import { Field, ObjectType } from '@nestjs/graphql';

import { RelationMetadataInterface } from './relation-metadata.interface';
import { FieldMetadataInterface } from './field-metadata.interface';

export interface ObjectMetadataInterface {
@ObjectType()
export class ObjectMetadataInterface {
@Field(() => String)
id: string;
standardId?: string | null;

@Field(() => String)
nameSingular: string;

@Field(() => String)
namePlural: string;

@Field(() => String)
labelSingular: string;

@Field(() => String)
labelPlural: string;

@Field(() => String, { nullable: true })
description?: string;

@Field(() => String)
targetTableName: string;

@Field(() => [RelationMetadataInterface], { nullable: true })
fromRelations: RelationMetadataInterface[];

@Field(() => [RelationMetadataInterface], { nullable: true })
toRelations: RelationMetadataInterface[];

@Field(() => [FieldMetadataInterface])
fields: FieldMetadataInterface[];

@Field(() => Boolean)
isSystem: boolean;

@Field(() => Boolean)
isCustom: boolean;

@Field(() => Boolean)
isActive: boolean;

@Field(() => Boolean)
isRemote: boolean;

@Field(() => Boolean)
isAuditLogged: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';

import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';

import { ObjectMetadataInterface } from './object-metadata.interface';
import { FieldMetadataInterface } from './field-metadata.interface';

export interface RelationMetadataInterface {
@ObjectType()
export class RelationMetadataInterface {
@Field(() => ID)
id: string;

@Field(() => RelationMetadataType)
relationType: RelationMetadataType;

@Field(() => String)
fromObjectMetadataId: string;

fromObjectMetadata: ObjectMetadataInterface;

@Field(() => String)
toObjectMetadataId: string;

toObjectMetadata: ObjectMetadataInterface;

@Field(() => String)
fromFieldMetadataId: string;

@Field(() => FieldMetadataInterface)
fromFieldMetadata: FieldMetadataInterface;

@Field(() => String)
toFieldMetadataId: string;

@Field(() => FieldMetadataInterface)
toFieldMetadata: FieldMetadataInterface;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';

import {
Column,
CreateDateColumn,
Expand Down Expand Up @@ -30,11 +32,14 @@ export enum RelationOnDeleteAction {
}

@Entity('relationMetadata')
@ObjectType()
export class RelationMetadataEntity implements RelationMetadataInterface {
@PrimaryGeneratedColumn('uuid')
@Field(() => ID)
id: string;

@Column({ nullable: false })
@Field(() => RelationMetadataType)
relationType: RelationMetadataType;

@Column({
Expand All @@ -43,21 +48,27 @@ export class RelationMetadataEntity implements RelationMetadataInterface {
type: 'enum',
enum: RelationOnDeleteAction,
})
@Field(() => String)
onDeleteAction: RelationOnDeleteAction;

@Column({ nullable: false, type: 'uuid' })
@Field(() => String)
fromObjectMetadataId: string;

@Column({ nullable: false, type: 'uuid' })
@Field(() => String)
toObjectMetadataId: string;

@Column({ nullable: false, type: 'uuid' })
@Field(() => String)
fromFieldMetadataId: string;

@Column({ nullable: false, type: 'uuid' })
@Field(() => String)
toFieldMetadataId: string;

@Column({ nullable: false, type: 'uuid' })
@Field(() => String)
workspaceId: string;

@ManyToOne(
Expand Down Expand Up @@ -92,9 +103,11 @@ export class RelationMetadataEntity implements RelationMetadataInterface {
@JoinColumn()
toFieldMetadata: Relation<FieldMetadataEntity>;

@Field(() => Date)
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;

@Field(() => Date)
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
28 changes: 28 additions & 0 deletions packages/twenty-server/src/engine/subscriptions/events.listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';

import { PubSub } from 'graphql-subscriptions';

import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';

@Injectable()
export class EventsListener {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming feels too generic, every listener is an event listener :)

constructor(@Inject('PUB_SUB') private readonly pubSub: PubSub) {}

@OnEvent('*.created')
async handleCreatedEvent(payload: ObjectRecordCreateEvent<any>) {
this.pubSub.publish('created', { created: payload });
}

@OnEvent('*.updated')
async handleUpdatedEvent(payload: ObjectRecordUpdateEvent<any>) {
this.pubSub.publish('updated', { updated: payload });
}

@OnEvent('*.deleted')
async handleDeletedEvent(payload: ObjectRecordDeleteEvent<any>) {
this.pubSub.publish('deleted', { deleted: payload });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';

import { PubSub } from 'graphql-subscriptions';

import { EventsListener } from 'src/engine/subscriptions/events.listener';
import { SubscriptionsResolver } from 'src/engine/subscriptions/subscriptions.resolver';

@Module({
imports: [],
exports: [],
providers: [
{
provide: 'PUB_SUB',
useFactory: () => {
return new PubSub();
},
},
SubscriptionsResolver,
EventsListener,
],
})
export class SubscriptionsModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Inject } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Resolver, Subscription } from '@nestjs/graphql';

import { PubSub } from 'graphql-subscriptions';

import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';

@Resolver()
export class SubscriptionsResolver {
constructor(
@Inject('PUB_SUB') private readonly pubSub: PubSub,
private readonly eventEmitter: EventEmitter2,
) {}

@Subscription(() => ObjectRecordCreateEvent<any>)
created() {
return this.pubSub.asyncIterator('created');
}

@Subscription(() => ObjectRecordUpdateEvent<any>)
updated() {
return this.pubSub.asyncIterator('updated');
}

@Subscription(() => ObjectRecordDeleteEvent<any>)
deleted() {
return this.pubSub.asyncIterator('deleted');
}
}