From 9228667a57b93843df5cbe0bcccffc6facf1ab90 Mon Sep 17 00:00:00 2001 From: Pacifique LINJANJA Date: Thu, 20 Jun 2024 18:18:12 +0200 Subject: [PATCH 01/30] Add the support of Empty and Non-Empty filter (#5773) --- .../graphql/types/RecordGqlOperationFilter.ts | 5 + .../MultipleFiltersDropdownContent.tsx | 8 + .../ObjectFilterDropdownOperandSelect.tsx | 22 +- .../getOperandsForFilterType.test.tsx | 40 +- .../utils/getOperandLabel.ts | 8 + .../utils/getOperandsForFilterType.ts | 23 +- ...turnObjectDropdownFilterIntoQueryFilter.ts | 380 ++++++++++++++++-- .../__tests__/useExportTableData.test.ts | 2 +- .../modules/views/types/ViewFilterOperand.ts | 2 + .../generateILikeFiltersForCompositeFields.ts | 24 ++ .../factories/query-runner-args.factory.ts | 20 +- 11 files changed, 478 insertions(+), 56 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts index 66acadc80bee1..fd6de3f7090da 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts @@ -9,6 +9,11 @@ export type UUIDFilter = { is?: IsFilter; }; +export type RelationFilter = { + is?: IsFilter; + in?: UUIDFilterValue[]; +}; + export type BooleanFilter = { eq?: boolean; is?: IsFilter; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index 1bd909b72f98a..cd49fd096b894 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil'; import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput'; @@ -36,6 +37,11 @@ export const MultipleFiltersDropdownContent = ({ const selectedOperandInDropdown = useRecoilValue( selectedOperandInDropdownState, ); + const isEmptyOperand = + selectedOperandInDropdown && + [ViewFilterOperand.IsEmpty, ViewFilterOperand.IsNotEmpty].includes( + selectedOperandInDropdown, + ); return ( <> @@ -43,6 +49,8 @@ export const MultipleFiltersDropdownContent = ({ ) : isObjectFilterDropdownOperandSelectUnfolded ? ( + ) : isEmptyOperand ? ( + ) : ( selectedOperandInDropdown && ( <> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx index 46945d4d3e004..5f500b9164611 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx @@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; @@ -34,10 +35,27 @@ export const ObjectFilterDropdownOperandSelect = () => { filterDefinitionUsedInDropdown?.type, ); - const handleOperangeChange = (newOperand: ViewFilterOperand) => { + const handleOperandChange = (newOperand: ViewFilterOperand) => { + const isEmptyOperand = [ + ViewFilterOperand.IsEmpty, + ViewFilterOperand.IsNotEmpty, + ].includes(newOperand); + setSelectedOperandInDropdown(newOperand); setIsObjectFilterDropdownOperandSelectUnfolded(false); + if (isEmptyOperand) { + selectFilter?.({ + id: v4(), + fieldMetadataId: filterDefinitionUsedInDropdown?.fieldMetadataId ?? '', + displayValue: '', + operand: newOperand, + value: '', + definition: filterDefinitionUsedInDropdown as FilterDefinition, + }); + return; + } + if ( isDefined(filterDefinitionUsedInDropdown) && isDefined(selectedFilter) @@ -63,7 +81,7 @@ export const ObjectFilterDropdownOperandSelect = () => { { - handleOperangeChange(filterOperand); + handleOperandChange(filterOperand); }} text={getOperandLabel(filterOperand)} /> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx index 1644d9472545e..d5eded8bf6667 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx @@ -4,20 +4,34 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { getOperandsForFilterType } from '../getOperandsForFilterType'; describe('getOperandsForFilterType', () => { + const emptyOperands = [ + ViewFilterOperand.IsEmpty, + ViewFilterOperand.IsNotEmpty, + ]; + + const containsOperands = [ + ViewFilterOperand.Contains, + ViewFilterOperand.DoesNotContain, + ]; + + const numberOperands = [ + ViewFilterOperand.GreaterThan, + ViewFilterOperand.LessThan, + ]; + + const relationOperand = [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; + const testCases = [ - ['TEXT', [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]], - ['EMAIL', [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]], - [ - 'FULL_NAME', - [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain], - ], - ['ADDRESS', [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]], - ['LINK', [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]], - ['LINKS', [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]], - ['CURRENCY', [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan]], - ['NUMBER', [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan]], - ['DATE_TIME', [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan]], - ['RELATION', [ViewFilterOperand.Is, ViewFilterOperand.IsNot]], + ['TEXT', [...containsOperands, ...emptyOperands]], + ['EMAIL', [...containsOperands, ...emptyOperands]], + ['FULL_NAME', [...containsOperands, ...emptyOperands]], + ['ADDRESS', [...containsOperands, ...emptyOperands]], + ['LINK', [...containsOperands, ...emptyOperands]], + ['LINKS', [...containsOperands, ...emptyOperands]], + ['CURRENCY', [...numberOperands, ...emptyOperands]], + ['NUMBER', [...numberOperands, ...emptyOperands]], + ['DATE_TIME', [...numberOperands, ...emptyOperands]], + ['RELATION', [...relationOperand, ...emptyOperands]], [undefined, []], [null, []], ['UNKNOWN_TYPE', []], diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts index a58bc9db53c82..9c9e297ef9605 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts @@ -18,6 +18,10 @@ export const getOperandLabel = ( return 'Is not'; case ViewFilterOperand.IsNotNull: return 'Is not null'; + case ViewFilterOperand.IsEmpty: + return 'Is empty'; + case ViewFilterOperand.IsNotEmpty: + return 'Is not empty'; default: return ''; } @@ -35,6 +39,10 @@ export const getOperandLabelShort = ( return ': Not'; case ViewFilterOperand.IsNotNull: return ': NotNull'; + case ViewFilterOperand.IsNotEmpty: + return ': NotEmpty'; + case ViewFilterOperand.IsEmpty: + return ': Empty'; case ViewFilterOperand.GreaterThan: return '\u00A0> '; case ViewFilterOperand.LessThan: diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts index 147e6ee9ecd50..7f189009b41ab 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts @@ -5,6 +5,13 @@ import { FilterType } from '../types/FilterType'; export const getOperandsForFilterType = ( filterType: FilterType | null | undefined, ): ViewFilterOperand[] => { + const emptyOperands = [ + ViewFilterOperand.IsEmpty, + ViewFilterOperand.IsNotEmpty, + ]; + + const relationOperands = [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; + switch (filterType) { case 'TEXT': case 'EMAIL': @@ -12,17 +19,25 @@ export const getOperandsForFilterType = ( case 'ADDRESS': case 'PHONE': case 'LINK': - return [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]; case 'LINKS': - return [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]; + return [ + ViewFilterOperand.Contains, + ViewFilterOperand.DoesNotContain, + ...emptyOperands, + ]; case 'CURRENCY': case 'NUMBER': case 'DATE_TIME': case 'DATE': - return [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan]; + return [ + ViewFilterOperand.GreaterThan, + ViewFilterOperand.LessThan, + ...emptyOperands, + ]; case 'RELATION': + return [...relationOperands, ...emptyOperands]; case 'SELECT': - return [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; + return [...relationOperands]; default: return []; } diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts index 3235ac572dbe7..1b08eace5a6b9 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts @@ -6,10 +6,12 @@ import { DateFilter, FloatFilter, RecordGqlOperationFilter, + RelationFilter, StringFilter, URLFilter, UUIDFilter, } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { Field } from '~/generated/graphql'; @@ -24,6 +26,200 @@ export type ObjectDropdownFilter = Omit & { }; }; +const applyEmptyFilters = ( + operand: ViewFilterOperand, + correspondingField: Pick, + objectRecordFilters: RecordGqlOperationFilter[], + filterType: FilterType, +) => { + let emptyRecordFilter: RecordGqlOperationFilter = {}; + + switch (filterType) { + case 'TEXT': + case 'EMAIL': + case 'PHONE': + emptyRecordFilter = { + or: [ + { [correspondingField.name]: { ilike: '' } as StringFilter }, + { [correspondingField.name]: { is: 'NULL' } as StringFilter }, + ], + }; + break; + case 'CURRENCY': + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { + amountMicros: { is: 'NULL' }, + } as CurrencyFilter, + }, + ], + }; + break; + case 'FULL_NAME': { + const fullNameFilters = generateILikeFiltersForCompositeFields( + '', + correspondingField.name, + ['firstName', 'lastName'], + true, + ); + + emptyRecordFilter = { + and: fullNameFilters, + }; + break; + } + case 'LINK': + emptyRecordFilter = { + or: [ + { [correspondingField.name]: { url: { ilike: '' } } as URLFilter }, + { + [correspondingField.name]: { url: { is: 'NULL' } } as URLFilter, + }, + ], + }; + break; + case 'LINKS': { + const linksFilters = generateILikeFiltersForCompositeFields( + '', + correspondingField.name, + ['primaryLinkLabel', 'primaryLinkUrl'], + true, + ); + + emptyRecordFilter = { + and: linksFilters, + }; + break; + } + case 'ADDRESS': + emptyRecordFilter = { + and: [ + { + or: [ + { + [correspondingField.name]: { + addressStreet1: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressStreet1: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressStreet2: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressStreet2: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressCity: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressCity: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressState: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressState: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressCountry: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressCountry: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressPostcode: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressPostcode: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + ], + }; + break; + case 'NUMBER': + emptyRecordFilter = { + [correspondingField.name]: { is: 'NULL' } as FloatFilter, + }; + break; + case 'DATE_TIME': + emptyRecordFilter = { + [correspondingField.name]: { is: 'NULL' } as DateFilter, + }; + break; + case 'SELECT': + emptyRecordFilter = { + [correspondingField.name]: { is: 'NULL' } as UUIDFilter, + }; + break; + case 'RELATION': + emptyRecordFilter = { + [correspondingField.name + 'Id']: { is: 'NULL' } as RelationFilter, + }; + break; + default: + throw new Error(`Unsupported empty filter type ${filterType}`); + } + + switch (operand) { + case ViewFilterOperand.IsEmpty: + objectRecordFilters.push(emptyRecordFilter); + break; + case ViewFilterOperand.IsNotEmpty: + objectRecordFilters.push({ + not: emptyRecordFilter, + }); + break; + default: + throw new Error(`Unknown operand ${operand} for ${filterType} filter`); + } +}; + export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilters: ObjectDropdownFilter[], fields: Pick[], @@ -35,12 +231,19 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( (field) => field.id === rawUIFilter.fieldMetadataId, ); + const isEmptyOperand = [ + ViewFilterOperand.IsEmpty, + ViewFilterOperand.IsNotEmpty, + ].includes(rawUIFilter.operand); + if (!correspondingField) { continue; } - if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') { - continue; + if (!isEmptyOperand) { + if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') { + continue; + } } switch (rawUIFilter.definition.type) { @@ -64,6 +267,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( }, }); break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; default: throw new Error( `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, @@ -86,6 +298,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } as DateFilter, }); break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; default: throw new Error( `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, @@ -108,6 +329,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } as FloatFilter, }); break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; default: throw new Error( `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, @@ -115,39 +345,57 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } break; case 'RELATION': { - try { - JSON.parse(rawUIFilter.value); - } catch (e) { - throw new Error( - `Cannot parse filter value for RELATION filter : "${rawUIFilter.value}"`, - ); - } + if (!isEmptyOperand) { + try { + JSON.parse(rawUIFilter.value); + } catch (e) { + throw new Error( + `Cannot parse filter value for RELATION filter : "${rawUIFilter.value}"`, + ); + } - const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[]; + const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[]; - if (parsedRecordIds.length > 0) { - switch (rawUIFilter.operand) { - case ViewFilterOperand.Is: - objectRecordFilters.push({ - [correspondingField.name + 'Id']: { - in: parsedRecordIds, - } as UUIDFilter, - }); - break; - case ViewFilterOperand.IsNot: - if (parsedRecordIds.length > 0) { + if (parsedRecordIds.length > 0) { + switch (rawUIFilter.operand) { + case ViewFilterOperand.Is: objectRecordFilters.push({ - not: { - [correspondingField.name + 'Id']: { - in: parsedRecordIds, - } as UUIDFilter, - }, + [correspondingField.name + 'Id']: { + in: parsedRecordIds, + } as RelationFilter, }); - } + break; + case ViewFilterOperand.IsNot: + if (parsedRecordIds.length > 0) { + objectRecordFilters.push({ + not: { + [correspondingField.name + 'Id']: { + in: parsedRecordIds, + } as RelationFilter, + }, + }); + } + break; + default: + throw new Error( + `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, + ); + } + } + } else { + switch (rawUIFilter.operand) { + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); break; default: throw new Error( - `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, + `Unknown empty operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, ); } } @@ -169,6 +417,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } as CurrencyFilter, }); break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; default: throw new Error( `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, @@ -197,6 +454,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( }, }); break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; default: throw new Error( `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, @@ -224,6 +490,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( }), }); break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; default: throw new Error( `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, @@ -231,7 +506,6 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } break; } - case 'FULL_NAME': { const fullNameFilters = generateILikeFiltersForCompositeFields( rawUIFilter.value, @@ -253,6 +527,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( }), }); break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; default: throw new Error( `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, @@ -286,6 +569,27 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( }, } as AddressFilter, }, + { + [correspondingField.name]: { + addressState: { + ilike: `%${rawUIFilter.value}%`, + }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressCountry: { + ilike: `%${rawUIFilter.value}%`, + }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressPostcode: { + ilike: `%${rawUIFilter.value}%`, + }, + } as AddressFilter, + }, ], }); break; @@ -322,6 +626,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( ], }); break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; default: throw new Error( `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, @@ -329,6 +642,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } break; case 'SELECT': { + if (isEmptyOperand) { + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; + } const stringifiedSelectValues = rawUIFilter.value; let parsedOptionValues: string[] = []; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts index 170260764e421..b7dfc586016fe 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts @@ -49,7 +49,7 @@ describe('generateCsv', () => { }, ]; const csv = generateCsv({ columns, rows }); - expect(csv).toEqual(`id,Foo,Empty,Nested Foo,Nested Nested,Relation + expect(csv).toEqual(`Id,Foo,Empty,Nested Foo,Nested Nested,Relation 1,some field,,foo,nested,a relation`); }); }); diff --git a/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts b/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts index fa1a27d681793..025d0085d49d7 100644 --- a/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts +++ b/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts @@ -6,4 +6,6 @@ export enum ViewFilterOperand { GreaterThan = 'greaterThan', Contains = 'contains', DoesNotContain = 'doesNotContain', + IsEmpty = 'isEmpty', + IsNotEmpty = 'isNotEmpty', } diff --git a/packages/twenty-front/src/utils/array/generateILikeFiltersForCompositeFields.ts b/packages/twenty-front/src/utils/array/generateILikeFiltersForCompositeFields.ts index 4d92976934b11..a5a2883e74e78 100644 --- a/packages/twenty-front/src/utils/array/generateILikeFiltersForCompositeFields.ts +++ b/packages/twenty-front/src/utils/array/generateILikeFiltersForCompositeFields.ts @@ -4,7 +4,31 @@ export const generateILikeFiltersForCompositeFields = ( filterString: string, baseFieldName: string, subFields: string[], + emptyCheck = false, ) => { + if (emptyCheck) { + return subFields.map((subField) => { + return { + or: [ + { + [baseFieldName]: { + [subField]: { + is: 'NULL', + }, + }, + }, + { + [baseFieldName]: { + [subField]: { + ilike: '', + }, + }, + }, + ], + }; + }); + } + return filterString .split(' ') .reduce((previousValue: RecordGqlOperationFilter[], currentValue) => { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts index 4a07ee358c5d2..f86ac299df9ed 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts @@ -199,14 +199,20 @@ export class QueryRunnerArgsFactory { if (!fieldMetadata) { return value; } + switch (fieldMetadata.type) { - case 'NUMBER': - return Object.fromEntries( - Object.entries(value).map(([filterKey, filterValue]) => [ - filterKey, - Number(filterValue), - ]), - ); + case 'NUMBER': { + if (value?.is === 'NULL') { + return value; + } else { + return Object.fromEntries( + Object.entries(value).map(([filterKey, filterValue]) => [ + filterKey, + Number(filterValue), + ]), + ); + } + } default: return value; } From 7a0f097df4bf680dc79108f2eff9228060821ae5 Mon Sep 17 00:00:00 2001 From: Us3r-gitHub <58467104+Us3r-gitHub@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:13:27 +0700 Subject: [PATCH 02/30] Fix(view): `Create` Button is not visible when creating `Kanban` View (#5969) Closes #5915 This issue occurs only when there is no select field. The user then creates a new one in settings and returns back to the view picker. And the bug arises, it because `viewPickerKanbanFieldMetadataId` is not being set correctly. When a user navigate to settings, the dirty state should be set to false. As a result, after re-rendering the view picker component, it triggers the effect to set `viewPickerKanbanFieldMetadataId` --------- Co-authored-by: Achsan Co-authored-by: Lucas Bordeau --- .../components/ViewPickerDropdown.tsx | 8 ++++++- .../hooks/useGetAvailableFieldsForKanban.ts | 24 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx index a3461683af1c3..2ead64a8a358f 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { IconChevronDown, IconList, @@ -19,6 +19,7 @@ import { ViewPickerListContent } from '@/views/view-picker/components/ViewPicker import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId'; import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { useViewPickerPersistView } from '@/views/view-picker/hooks/useViewPickerPersistView'; +import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates'; import { isDefined } from '~/utils/isDefined'; import { useViewStates } from '../../hooks/internal/useViewStates'; @@ -52,6 +53,8 @@ export const ViewPickerDropdown = () => { const { entityCountInCurrentViewState } = useViewStates(); + const { viewPickerIsDirtyState } = useViewPickerStates(); + const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); const { handleUpdate } = useViewPickerPersistView(); @@ -60,6 +63,8 @@ export const ViewPickerDropdown = () => { entityCountInCurrentViewState, ); + const setViewPickerIsDirty = useSetRecoilState(viewPickerIsDirtyState); + const { isDropdownOpen: isViewsListDropdownOpen } = useDropdown( VIEW_PICKER_DROPDOWN_ID, ); @@ -70,6 +75,7 @@ export const ViewPickerDropdown = () => { const CurrentViewIcon = getIcon(currentViewWithCombinedFiltersAndSorts?.icon); const handleClickOutside = async () => { + setViewPickerIsDirty(false); if (isViewsListDropdownOpen && viewPickerMode === 'edit') { await handleUpdate(); } diff --git a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts index 21df6d23c2570..a0ea116b63388 100644 --- a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts +++ b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts @@ -1,16 +1,24 @@ import { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useViewStates } from '@/views/hooks/internal/useViewStates'; +import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const useGetAvailableFieldsForKanban = () => { const { viewObjectMetadataIdState } = useViewStates(); + const { viewPickerIsDirtyState } = useViewPickerStates(); const viewObjectMetadataId = useRecoilValue(viewObjectMetadataIdState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const setViewPickerIsDirty = useSetRecoilState(viewPickerIsDirtyState); + const setNavigationMemorizedUrl = useSetRecoilState( + navigationMemorizedUrlState, + ); + const location = useLocation(); const objectMetadataItem = objectMetadataItems.find( (objectMetadata) => objectMetadata.id === viewObjectMetadataId, @@ -24,8 +32,18 @@ export const useGetAvailableFieldsForKanban = () => { const navigate = useNavigate(); const navigateToSelectSettings = useCallback(() => { + setViewPickerIsDirty(false); + + setNavigationMemorizedUrl(location.pathname + location.search); + navigate(`/settings/objects/${objectMetadataItem?.namePlural}`); - }, [navigate, objectMetadataItem?.namePlural]); + }, [ + navigate, + objectMetadataItem?.namePlural, + setViewPickerIsDirty, + setNavigationMemorizedUrl, + location, + ]); return { availableFieldsForKanban, From 68e20c0e8769bd1988978b8d021fc54df7f66d33 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 21 Jun 2024 14:42:48 +0200 Subject: [PATCH 03/30] Add disabled style on non-draggable menu items (#5974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/twentyhq/twenty/issues/5653 Capture d’écran 2024-06-20 à 17 19 44 Capture d’écran 2024-06-20 à 17 20 03 --- .../components/MenuItemDraggable.tsx | 27 ++++++++++++++-- .../__stories__/MenuItemDraggable.stories.tsx | 8 +++++ .../components/MenuItemLeftContent.tsx | 32 +++++++++++++++---- .../components/StyledMenuItemBase.tsx | 8 ++++- .../ViewFieldsVisibilityDropdownSection.tsx | 8 +++-- .../components/ViewPickerListContent.tsx | 4 ++- 6 files changed, 73 insertions(+), 14 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx index 9643b86659484..98c01586b93f9 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx @@ -3,7 +3,10 @@ import { IconComponent } from 'twenty-ui'; import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup'; import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; -import { StyledHoverableMenuItemBase } from '../internals/components/StyledMenuItemBase'; +import { + StyledHoverableMenuItemBase, + StyledMenuItemBase, +} from '../internals/components/StyledMenuItemBase'; import { MenuItemAccent } from '../types/MenuItemAccent'; import { MenuItemIconButton } from './MenuItem'; @@ -15,9 +18,11 @@ export type MenuItemDraggableProps = { isTooltipOpen?: boolean; onClick?: () => void; text: string; - isDragDisabled?: boolean; className?: string; isIconDisplayedOnHoverOnly?: boolean; + showGrip?: boolean; + isDragDisabled?: boolean; + isHoverDisabled?: boolean; }; export const MenuItemDraggable = ({ LeftIcon, @@ -28,9 +33,24 @@ export const MenuItemDraggable = ({ isDragDisabled = false, className, isIconDisplayedOnHoverOnly = true, + showGrip = false, + isHoverDisabled = false, }: MenuItemDraggableProps) => { const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0; + if (isHoverDisabled) { + return ( + + + + ); + } + return ( {showIconButtons && ( { const theme = useTheme(); return ( - {showGrip && ( - - )} + {showGrip && + (isDisabled ? ( + + ) : ( + + + + ))} {LeftIcon && ( )} diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/internals/components/StyledMenuItemBase.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/internals/components/StyledMenuItemBase.tsx index 71cd27eae09e7..a2489def35b98 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/internals/components/StyledMenuItemBase.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/internals/components/StyledMenuItemBase.tsx @@ -7,6 +7,7 @@ import { MenuItemAccent } from '../../types/MenuItemAccent'; export type MenuItemBaseProps = { accent?: MenuItemAccent; isKeySelected?: boolean; + isHoverBackgroundDisabled?: boolean; }; export const StyledMenuItemBase = styled.div` @@ -31,7 +32,8 @@ export const StyledMenuItemBase = styled.div` padding: var(--vertical-padding) var(--horizontal-padding); - ${HOVER_BACKGROUND}; + ${({ isHoverBackgroundDisabled }) => + isHoverBackgroundDisabled ?? HOVER_BACKGROUND}; ${({ theme, accent }) => { switch (accent) { @@ -99,6 +101,10 @@ export const StyledMenuItemRightContent = styled.div` flex-direction: row; `; +export const StyledDraggableItem = styled.div` + cursor: grab; +`; + export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{ isIconDisplayedOnHoverOnly?: boolean; }>` diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx index 9bc30f5a2359b..edbbdf3c0a8da 100644 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx @@ -19,7 +19,6 @@ import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableIt import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader'; -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy'; @@ -101,13 +100,17 @@ export const ViewFieldsVisibilityDropdownSection = ({ )} {nonDraggableItems.map((field, fieldIndex) => ( - ))} {!!draggableItems.length && ( @@ -131,6 +134,7 @@ export const ViewFieldsVisibilityDropdownSection = ({ isTooltipOpen={openToolTipIndex === fieldIndex} text={field.label} className={`${title}-draggable-item-tooltip-anchor-${fieldIndex}`} + showGrip /> } /> diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx index ef0a258b0dfd9..91b18694dd2c0 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx @@ -117,7 +117,7 @@ export const ViewPickerListContent = () => { )} /> {indexView && ( - { onClick={() => handleViewSelect(indexView.id)} LeftIcon={getIcon(indexView.icon)} text={indexView.name} + accent="placeholder" + isDragDisabled /> )} From 51e3454d50ad12dde495926a372767d23430fa60 Mon Sep 17 00:00:00 2001 From: JarWarren <43893126+JarWarren@users.noreply.github.com> Date: Fri, 21 Jun 2024 06:52:36 -0600 Subject: [PATCH 04/30] Update LOGGER_DRIVER env var description (#5968) Update the docs to accurately reflect `LoggerDriverType`. Using `sentry` throws an error on startup. ``` export enum LoggerDriverType { Console = 'console', } ``` Happy to change the wording of course. --- .../src/content/developers/self-hosting/self-hosting-var.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx index 9c4937fb32595..f741ff4759cfc 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx @@ -153,7 +153,7 @@ yarn command:prod cron:messaging:message-list-fetch ### Logging Date: Fri, 21 Jun 2024 09:49:47 -0400 Subject: [PATCH 05/30] Fix: Selected Line Not Fully Highlighted in Blue (#5966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: #5942 Screenshot 2024-06-19 at 5 07 35 PM --------- Co-authored-by: Lucas Bordeau --- .../record-table/components/CheckboxCell.tsx | 1 - .../components/RecordTableHeader.tsx | 1 + .../components/RecordTableRow.tsx | 39 ++++++++++++++++--- .../twenty-ui/src/theme/provider/theme.css | 8 ++++ 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx index 2aff77e1b1620..74af8f6ab5a99 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx @@ -16,7 +16,6 @@ const StyledContainer = styled.div` height: 32px; justify-content: center; - background-color: ${({ theme }) => theme.background.primary}; `; export const CheckboxCell = () => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx index 9fe828dd9750d..b9dbc506b454e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx @@ -76,6 +76,7 @@ export const RecordTableHeader = ({ width: 30, minWidth: 30, maxWidth: 30, + borderRight: 'transparent', }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx index 7fc7160e0b742..2f7112a25538c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx @@ -23,9 +23,17 @@ type RecordTableRowProps = { isPendingRow?: boolean; }; -export const StyledTd = styled.td` +export const StyledTd = styled.td<{ isSelected?: boolean }>` + background: ${({ theme }) => theme.background.primary}; position: relative; user-select: none; + + ${({ isSelected, theme }) => + isSelected && + ` + background: ${theme.accent.quaternary}; + + `} `; export const StyledTr = styled.tr<{ isDragging: boolean }>` @@ -33,7 +41,6 @@ export const StyledTr = styled.tr<{ isDragging: boolean }>` transition: border-left-color 0.2s ease-in-out; td:nth-of-type(-n + 2) { - background-color: ${({ theme }) => theme.background.primary}; border-right-color: ${({ theme }) => theme.background.primary}; } @@ -58,6 +65,20 @@ export const StyledTr = styled.tr<{ isDragging: boolean }>` `} `; +const SelectableStyledTd = ({ + isSelected, + children, + style, +}: { + isSelected: boolean; + children?: React.ReactNode; + style?: React.CSSProperties; +}) => ( + + {children} + +); + export const RecordTableRow = ({ recordId, rowIndex, @@ -127,9 +148,12 @@ export const RecordTableRow = ({ > - + {!draggableSnapshot.isDragging && } - + {inView || draggableSnapshot.isDragging ? visibleTableColumns.map((column, columnIndex) => ( )) : visibleTableColumns.map((column) => ( - + ))} - + )} diff --git a/packages/twenty-ui/src/theme/provider/theme.css b/packages/twenty-ui/src/theme/provider/theme.css index 076090f6370f2..8ef007905c606 100644 --- a/packages/twenty-ui/src/theme/provider/theme.css +++ b/packages/twenty-ui/src/theme/provider/theme.css @@ -1,3 +1,11 @@ +/* + +!! DEPRECATED !! + +Please do not use those variables anymore. They are deprecated and will be removed soon. + +*/ + :root { --twentycrm-spacing-multiplicator: 4; --twentycrm-border-radius-sm: 4px; From 732653034efcc6873169fa93f950550b1b0467d4 Mon Sep 17 00:00:00 2001 From: Akilesh Praveen Date: Fri, 21 Jun 2024 07:01:27 -0700 Subject: [PATCH 06/30] fix: background colors for record table (#5967) # Summary - Address issue #5959 - Update background color on hover & click for record table # Test Plan ![Kapture 2024-06-19 at 20 32 44](https://github.com/twentyhq/twenty/assets/10789158/18a58c09-040a-47e6-953d-aac6f3803486) --- .../record-table/components/RecordTableHeaderCell.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx index 698939c85b8cb..9330ff2c54dd9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx @@ -32,7 +32,10 @@ const StyledColumnHeaderCell = styled.th<{ ${({ theme }) => { return ` &:hover { - background: ${theme.background.quaternary}; + background: ${theme.background.secondary}; + }; + &:active { + background: ${theme.background.tertiary}; }; `; }}; From 9a4a2e4ca91a1391127184906e8951904a0b7a40 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 21 Jun 2024 16:15:17 +0200 Subject: [PATCH 07/30] Fix links chip design (#5963) Fix https://github.com/twentyhq/twenty/issues/5938 and https://github.com/twentyhq/twenty/issues/5655 - Make sure chip count is displayed - Fix padding - Fix background colors, border - Add hover and active states --------- Co-authored-by: Lucas Bordeau --- .../field/display/components/LinksDisplay.tsx | 2 +- .../link/components/RoundedLink.tsx | 47 ++++++++++++++----- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx index ee58d124fbda1..339859dfe62a9 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx @@ -59,7 +59,7 @@ export const LinksDisplay = ({ value, isFocused }: LinksDisplayProps) => { ); return isFocused ? ( - + {links.map(({ url, label, type }, index) => type === LinkType.LinkedIn || type === LinkType.Twitter ? ( diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/RoundedLink.tsx b/packages/twenty-front/src/modules/ui/navigation/link/components/RoundedLink.tsx index 4b63ca09029bf..61480f17fd2a9 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/RoundedLink.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/link/components/RoundedLink.tsx @@ -1,7 +1,7 @@ -import { MouseEvent } from 'react'; +import { MouseEvent, useContext } from 'react'; import { styled } from '@linaria/react'; import { isNonEmptyString } from '@sniptt/guards'; -import { FONT_COMMON, THEME_COMMON } from 'twenty-ui'; +import { FONT_COMMON, THEME_COMMON, ThemeContext } from 'twenty-ui'; type RoundedLinkProps = { href: string; @@ -11,17 +11,23 @@ type RoundedLinkProps = { const fontSizeMd = FONT_COMMON.size.md; const spacing1 = THEME_COMMON.spacing(1); -const spacing3 = THEME_COMMON.spacing(3); +const spacing2 = THEME_COMMON.spacing(2); const spacingMultiplicator = THEME_COMMON.spacingMultiplicator; -const StyledLink = styled.a` +const StyledLink = styled.a<{ + color: string; + background: string; + backgroundHover: string; + backgroundActive: string; + border: string; +}>` align-items: center; - background-color: var(--twentycrm-background-transparent-light); - border: 1px solid var(--twentycrm-border-color-medium); + background-color: ${({ background }) => background}; + border: 1px solid ${({ border }) => border}; border-radius: 50px; - color: var(--twentycrm-font-color-primary); + color: ${({ color }) => color}; cursor: pointer; display: inline-flex; @@ -29,25 +35,39 @@ const StyledLink = styled.a` gap: ${spacing1}; - height: ${spacing3}; + height: 10px; justify-content: center; max-width: calc(100% - ${spacingMultiplicator} * 2px); - max-width: 100%; - min-width: fit-content; overflow: hidden; - padding: ${spacing1} ${spacing1}; + padding: ${spacing1} ${spacing2}; text-decoration: none; text-overflow: ellipsis; user-select: none; white-space: nowrap; + + &:hover { + background-color: ${({ backgroundHover }) => backgroundHover}; + } + + &:active { + background-color: ${({ backgroundActive }) => backgroundActive}; + } `; export const RoundedLink = ({ label, href, onClick }: RoundedLinkProps) => { + const { theme } = useContext(ThemeContext); + + const background = theme.background.transparent.lighter; + const backgroundHover = theme.background.transparent.light; + const backgroundActive = theme.background.transparent.medium; + const border = theme.border.color.strong; + const color = theme.font.color.primary; + if (!isNonEmptyString(label)) { return <>; } @@ -64,6 +84,11 @@ export const RoundedLink = ({ label, href, onClick }: RoundedLinkProps) => { target="_blank" rel="noreferrer" onClick={handleClick} + color={color} + background={background} + backgroundHover={backgroundHover} + backgroundActive={backgroundActive} + border={border} > {label} From d126b148a19bc32a7cb52e7ed04d2470bf8be6b5 Mon Sep 17 00:00:00 2001 From: Ymir <36711026+Ymirke@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:49:48 +0200 Subject: [PATCH 08/30] Navigation Panel UI Sizing Changes (#5964) ## Fixes #5902 : - [x] Navigation items' height should be risen to 28px. > For clarity: - [x] Also increased the height of NavigationDrawerSectionTitle to 28px to match navigation item. - [x] The gap between sections should be reduced to 12px > Was already completed it seems. - [x] The workspace switcher should be aligned with the navigation items --------- Co-authored-by: Lucas Bordeau --- .../components/MultiWorkspaceDropdownButton.tsx | 7 ++++--- .../navigation-drawer/components/NavigationDrawer.tsx | 2 +- .../components/NavigationDrawerCollapseButton.tsx | 2 +- .../components/NavigationDrawerHeader.tsx | 4 +--- .../navigation-drawer/components/NavigationDrawerItem.tsx | 1 + .../components/NavigationDrawerSectionTitle.tsx | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx index 0249de11c093a..23d356aa4fea9 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx @@ -33,10 +33,12 @@ const StyledContainer = styled.div` border: 1px solid transparent; display: flex; justify-content: space-between; - height: ${({ theme }) => theme.spacing(7)}; - padding: 0 ${({ theme }) => theme.spacing(2)}; + height: ${({ theme }) => theme.spacing(5)}; + padding: calc(${({ theme }) => theme.spacing(1)} - 1px); width: 100%; + gap: ${({ theme }) => theme.spacing(1)}; + &:hover { background-color: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -46,7 +48,6 @@ const StyledContainer = styled.div` const StyledLabel = styled.div` align-items: center; display: flex; - gap: ${({ theme }) => theme.spacing(2)}; `; const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>` diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx index 524e879476911..406402377646a 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx @@ -53,7 +53,7 @@ const StyledContainer = styled.div<{ isSubMenu?: boolean }>` const StyledItemsContainer = styled.div` display: flex; flex-direction: column; - gap: ${({ theme }) => theme.spacing(8)}; + gap: ${({ theme }) => theme.spacing(3)}; margin-bottom: auto; overflow-y: auto; `; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx index 856cb3041a886..d555a43599d96 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx @@ -14,7 +14,7 @@ const StyledCollapseButton = styled.div` color: ${({ theme }) => theme.font.color.light}; cursor: pointer; display: flex; - height: ${({ theme }) => theme.spacing(6)}; + height: ${({ theme }) => theme.spacing(5)}; justify-content: center; user-select: none; width: ${({ theme }) => theme.spacing(6)}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx index eae27e8c73d38..72b384fb0c910 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx @@ -12,9 +12,7 @@ import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton const StyledContainer = styled.div` align-items: center; display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - height: ${({ theme }) => theme.spacing(6)}; - padding: ${({ theme }) => theme.spacing(1)}; + height: ${({ theme }) => theme.spacing(7)}; user-select: none; `; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx index 8f0dd6ed2f8bc..d4f33fb82a1b8 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx @@ -39,6 +39,7 @@ const StyledItem = styled('div', { align-items: center; background: ${(props) => props.active ? props.theme.background.transparent.light : 'inherit'}; + height: ${({ theme }) => theme.spacing(5)}; border: none; border-radius: ${({ theme }) => theme.border.radius.sm}; text-decoration: none; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx index 3d3f693870666..4f8386e12d0f8 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx @@ -16,7 +16,7 @@ const StyledTitle = styled.div<{ onClick?: () => void }>` display: flex; font-size: ${({ theme }) => theme.font.size.xs}; font-weight: ${({ theme }) => theme.font.weight.semiBold}; - height: ${({ theme }) => theme.spacing(4)}; + height: ${({ theme }) => theme.spacing(5)}; padding: ${({ theme }) => theme.spacing(1)}; ${({ onClick, theme }) => From 91b0c2bb8e8200a1a308780ccb6d7a65fa729795 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Rodrigues <11232497+vitorhugoro1@users.noreply.github.com> Date: Sat, 22 Jun 2024 03:38:52 -0300 Subject: [PATCH 09/30] feat: add brazilian real currency (#5989) Added support to Brazilian Real currency code, added the new code on `CurrencyCode.ts` and on `SETTINGS_FIELD_CURRENCY_CODES` --- .../modules/object-record/record-field/types/CurrencyCode.ts | 1 + .../data-model/constants/SettingsFieldCurrencyCodes.ts | 5 +++++ .../twenty-ui/src/display/icon/components/TablerIcons.ts | 1 + 3 files changed, 7 insertions(+) diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts b/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts index 673b6b7cb2642..7d3fad5846c89 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts @@ -15,4 +15,5 @@ export enum CurrencyCode { QAR = 'QAR', AED = 'AED', KRW = 'KRW', + BRL = 'BRL', } diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts index da235b1100dd4..6f302e6ea90e9 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts @@ -8,6 +8,7 @@ import { IconCurrencyKroneCzech, IconCurrencyKroneSwedish, IconCurrencyPound, + IconCurrencyReal, IconCurrencyRiyal, IconCurrencyWon, IconCurrencyYen, @@ -84,4 +85,8 @@ export const SETTINGS_FIELD_CURRENCY_CODES: Record< label: 'South Korean won', Icon: IconCurrencyWon, }, + BRL: { + label: 'Brazilian real', + Icon: IconCurrencyReal, + } }; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index c4262154cf3f7..9ff7c5008bbf9 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -60,6 +60,7 @@ export { IconCurrencyKroneCzech, IconCurrencyKroneSwedish, IconCurrencyPound, + IconCurrencyReal, IconCurrencyRiyal, IconCurrencyWon, IconCurrencyYen, From 0b4bfce324be1ac206da642348de6d4303b4cbd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Sat, 22 Jun 2024 09:26:58 +0200 Subject: [PATCH 10/30] feat: drop calendar repository (#5824) This PR is replacing and removing all the raw queries and repositories with the new `TwentyORM` and injection system using `@InjectWorkspaceRepository`. Some logic that was contained inside repositories has been moved to the services. In this PR we're only replacing repositories for calendar feature. --------- Co-authored-by: Weiko Co-authored-by: bosiraphael Co-authored-by: Charles Bochet --- .../analytics/analytics.service.ts | 4 +- .../engine/core-modules/auth/auth.module.ts | 5 +- .../engine/core-modules/auth/auth.resolver.ts | 1 + .../google-apis-auth.controller.ts | 29 +- .../auth/services/google-apis.service.ts | 121 +++---- .../auth/services/token.service.ts | 4 + .../core-modules/billing/billing.service.ts | 4 +- .../billing/jobs/update-subscription.job.ts | 9 +- ...timeline-calendar-event-participant.dto.ts | 4 +- .../timeline-calendar-event.service.ts | 12 +- .../onboarding/onboarding.service.ts | 2 +- .../user-workspace/user-workspace.module.ts | 3 + .../user-workspace/user-workspace.service.ts | 27 +- .../engine/core-modules/user/user.resolver.ts | 10 +- .../message-queue/drivers/pg-boss.driver.ts | 6 +- .../message-queue/message-queue.explorer.ts | 2 +- .../metadata-to-repository.mapping.ts | 13 - .../load-service-with-workspace.context.ts | 39 ++ .../twenty-orm/custom.workspace-entity.ts | 2 +- .../datasource/workspace.datasource.ts | 2 +- .../factories/workspace-datasource.factory.ts | 7 +- .../repository/workspace.repository.ts | 187 ++++++++-- .../twenty-orm/twenty-orm-core.module.ts | 13 +- .../types/object-record.ts | 33 +- .../repositories/comment.repository.ts | 21 -- .../activity-target.workspace-entity.ts | 8 +- .../activity.workspace-entity.ts | 10 +- .../api-key.workspace-entity.ts | 2 +- .../repositories/attachment.repository.ts | 21 -- .../attachment.workspace-entity.ts | 8 +- .../jobs/match-participant.job.ts | 7 +- .../jobs/unmatch-participant.job.ts | 7 +- .../commands/calendar-commands.module.ts | 11 +- .../jobs/google-calendar-sync.cron.job.ts | 6 +- ...ocklist-item-delete-calendar-events.job.ts | 61 +++- .../blocklist-reimport-calendar-events.job.ts | 7 +- ...eate-company-and-contact-after-sync.job.ts | 74 ++-- .../calendar/jobs/calendar-job.module.ts | 5 +- .../calendar/jobs/google-calendar-sync.job.ts | 7 +- .../calendar-event-participant.listener.ts | 4 +- ...calendar-event-find-many.pre-query.hook.ts | 47 ++- .../calendar-event-find-one.pre-query-hook.ts | 46 ++- .../can-access-calendar-event.service.ts | 28 +- .../query-hooks/calendar-query-hook.module.ts | 5 +- ...ar-channel-event-association.repository.ts | 205 ----------- .../calendar-channel.repository.ts | 135 ------- .../calendar-event-participant.repository.ts | 305 ---------------- .../repositories/calendar-event.repository.ts | 227 ------------ .../calendar-event-cleaner.module.ts | 6 +- .../calendar-event-cleaner.service.ts | 33 +- .../calendar-event-participant.module.ts | 3 + .../calendar-event-participant.service.ts | 84 +++-- .../google-calendar-sync.module.ts | 7 +- .../google-calendar-sync.service.ts | 341 +++++++++++------- .../workspace-google-calendar-sync.module.ts | 6 +- .../workspace-google-calendar-sync.service.ts | 13 +- ...ndar-event-participant.workspace-entity.ts | 4 +- .../company.workspace-entity.ts | 12 +- ...-companies-and-contacts-creation.module.ts | 2 - .../jobs/create-company-and-contact.job.ts | 3 +- .../create-company-and-contact.service.ts | 21 +- ...google-api-refresh-access-token.service.ts | 6 + .../connected-account.workspace-entity.ts | 4 +- .../favorite.workspace-entity.ts | 6 +- ...ging-blocklist-item-delete-messages.job.ts | 6 + .../can-access-message-thread.service.ts | 5 +- .../messaging-error-handling.service.ts | 6 + .../messaging-message-participant.service.ts | 2 +- ...es-and-enqueue-contact-creation.service.ts | 2 +- ...el-message-association.workspace-entity.ts | 10 +- .../message-channel.workspace-entity.ts | 6 +- .../message-participant.workspace-entity.ts | 4 +- .../message.workspace-entity.ts | 4 +- .../jobs/messaging-message-list-fetch.job.ts | 7 +- .../jobs/messaging-messages-import.job.ts | 7 +- .../listeners/message-participant.listener.ts | 4 +- ...age-channel-sync-status-monitoring.cron.ts | 3 + .../opportunity.workspace-entity.ts | 10 +- .../person.workspace-entity.ts | 10 +- .../timeline-activity.repository.ts | 4 +- .../audit-log.workspace-entity.ts | 8 +- .../behavioral-event.workspace-entity.ts | 6 +- .../timeline-activity.workspace-entity.ts | 14 +- .../view-field.workspace-entity.ts | 2 +- .../view-filter.workspace-entity.ts | 2 +- .../view-sort.workspace-entity.ts | 2 +- ...kspace-member-delete-one.pre-query.hook.ts | 33 +- .../workspace-member-query-hook.module.ts | 4 +- .../twenty-server/src/utils/is-defined.ts | 3 + .../twenty-server/src/utils/typed-reflect.ts | 1 + 90 files changed, 975 insertions(+), 1537 deletions(-) create mode 100644 packages/twenty-server/src/engine/twenty-orm/context/load-service-with-workspace.context.ts delete mode 100644 packages/twenty-server/src/modules/activity/repositories/comment.repository.ts delete mode 100644 packages/twenty-server/src/modules/attachment/repositories/attachment.repository.ts delete mode 100644 packages/twenty-server/src/modules/calendar/repositories/calendar-channel-event-association.repository.ts delete mode 100644 packages/twenty-server/src/modules/calendar/repositories/calendar-channel.repository.ts delete mode 100644 packages/twenty-server/src/modules/calendar/repositories/calendar-event-participant.repository.ts delete mode 100644 packages/twenty-server/src/modules/calendar/repositories/calendar-event.repository.ts create mode 100644 packages/twenty-server/src/utils/is-defined.ts diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts index fd685851c584e..73851f71ea5b1 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts @@ -19,8 +19,8 @@ export class AnalyticsService { async create( createEventInput: CreateEventInput, - userId: string | undefined, - workspaceId: string | undefined, + userId: string | null | undefined, + workspaceId: string | null | undefined, workspaceDisplayName: string | undefined, workspaceDomainName: string | undefined, hostName: string | undefined, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 999d3175074f1..6a4d1c9d26784 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -28,6 +28,8 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { AuthResolver } from './auth.resolver'; @@ -60,11 +62,12 @@ const jwtModule = JwtModule.registerAsync({ ObjectMetadataRepositoryModule.forFeature([ ConnectedAccountWorkspaceEntity, MessageChannelWorkspaceEntity, - CalendarChannelWorkspaceEntity, ]), HttpModule, UserWorkspaceModule, OnboardingModule, + TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]), + WorkspaceDataSourceModule, ], controllers: [ GoogleAuthController, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 2c6466eeea317..13ecbec165dce 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -138,6 +138,7 @@ export class AuthResolver { } const transientToken = await this.tokenService.generateTransientToken( workspaceMember.id, + user.id, user.defaultWorkspace.id, ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts index 0a36fe5d0132e..da56c086e1e5a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts @@ -16,9 +16,7 @@ import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google- import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; -import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; @Controller('auth/google-apis') export class GoogleAPIsAuthController { @@ -27,8 +25,7 @@ export class GoogleAPIsAuthController { private readonly tokenService: TokenService, private readonly environmentService: EnvironmentService, private readonly onboardingService: OnboardingService, - @InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity) - private readonly workspaceMemberService: WorkspaceMemberRepository, + private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext, ) {} @Get() @@ -56,7 +53,7 @@ export class GoogleAPIsAuthController { messageVisibility, } = user; - const { workspaceMemberId, workspaceId } = + const { workspaceMemberId, userId, workspaceId } = await this.tokenService.verifyTransientToken(transientToken); const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS'); @@ -71,7 +68,13 @@ export class GoogleAPIsAuthController { throw new Error('Workspace not found'); } - await this.googleAPIsService.refreshGoogleRefreshToken({ + const googleAPIsServiceInstance = + await this.loadServiceWithWorkspaceContext.load( + this.googleAPIsService, + workspaceId, + ); + + await googleAPIsServiceInstance.refreshGoogleRefreshToken({ handle: email, workspaceMemberId: workspaceMemberId, workspaceId: workspaceId, @@ -81,12 +84,14 @@ export class GoogleAPIsAuthController { messageVisibility, }); - const userId = ( - await this.workspaceMemberService.find(workspaceMemberId, workspaceId) - )?.userId; - if (userId) { - await this.onboardingService.skipSyncEmailOnboardingStep( + const onboardingServiceInstance = + await this.loadServiceWithWorkspaceContext.load( + this.onboardingService, + workspaceId, + ); + + await onboardingServiceInstance.skipSyncEmailOnboardingStep( userId, workspaceId, ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts index f580a34df3580..665dea7037421 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts @@ -3,17 +3,14 @@ import { Injectable } from '@nestjs/common'; import { EntityManager } from 'typeorm'; import { v4 } from 'uuid'; -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { GoogleCalendarSyncJobData, GoogleCalendarSyncJob, } from 'src/modules/calendar/jobs/google-calendar-sync.job'; -import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository'; import { CalendarChannelWorkspaceEntity, CalendarChannelVisibility, @@ -35,12 +32,16 @@ import { MessagingMessageListFetchJobData, } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; @Injectable() export class GoogleAPIsService { constructor( - private readonly dataSourceService: DataSourceService, - private readonly typeORMService: TypeORMService, + @InjectWorkspaceDatasource() + private readonly workspaceDataSource: WorkspaceDataSource, @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, @InjectMessageQueue(MessageQueue.calendarQueue) @@ -50,8 +51,8 @@ export class GoogleAPIsService { private readonly connectedAccountRepository: ConnectedAccountRepository, @InjectObjectMetadataRepository(MessageChannelWorkspaceEntity) private readonly messageChannelRepository: MessageChannelRepository, - @InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity) - private readonly calendarChannelRepository: CalendarChannelRepository, + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, ) {} async refreshGoogleRefreshToken(input: { @@ -71,14 +72,6 @@ export class GoogleAPIsService { messageVisibility, } = input; - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - workspaceId, - ); - - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - const isCalendarEnabled = this.environmentService.get( 'CALENDAR_PROVIDER_GOOGLE_ENABLED', ); @@ -93,65 +86,67 @@ export class GoogleAPIsService { const existingAccountId = connectedAccounts?.[0]?.id; const newOrExistingConnectedAccountId = existingAccountId ?? v4(); - await workspaceDataSource?.transaction(async (manager: EntityManager) => { - if (!existingAccountId) { - await this.connectedAccountRepository.create( - { - id: newOrExistingConnectedAccountId, - handle, - provider: ConnectedAccountProvider.GOOGLE, - accessToken: input.accessToken, - refreshToken: input.refreshToken, - accountOwnerId: workspaceMemberId, - }, - workspaceId, - manager, - ); - - await this.messageChannelRepository.create( - { - id: v4(), - connectedAccountId: newOrExistingConnectedAccountId, - type: MessageChannelType.EMAIL, - handle, - visibility: - messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING, - syncStatus: MessageChannelSyncStatus.ONGOING, - }, - workspaceId, - manager, - ); + await this.workspaceDataSource.transaction( + async (manager: EntityManager) => { + if (!existingAccountId) { + await this.connectedAccountRepository.create( + { + id: newOrExistingConnectedAccountId, + handle, + provider: ConnectedAccountProvider.GOOGLE, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + accountOwnerId: workspaceMemberId, + }, + workspaceId, + manager, + ); - if (isCalendarEnabled) { - await this.calendarChannelRepository.create( + await this.messageChannelRepository.create( { id: v4(), connectedAccountId: newOrExistingConnectedAccountId, + type: MessageChannelType.EMAIL, handle, visibility: - calendarVisibility || - CalendarChannelVisibility.SHARE_EVERYTHING, + messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING, + syncStatus: MessageChannelSyncStatus.ONGOING, }, workspaceId, manager, ); - } - } else { - await this.connectedAccountRepository.updateAccessTokenAndRefreshToken( - input.accessToken, - input.refreshToken, - newOrExistingConnectedAccountId, - workspaceId, - manager, - ); - await this.messageChannelRepository.resetSync( - newOrExistingConnectedAccountId, - workspaceId, - manager, - ); - } - }); + if (isCalendarEnabled) { + await this.calendarChannelRepository.save( + { + id: v4(), + connectedAccountId: newOrExistingConnectedAccountId, + handle, + visibility: + calendarVisibility || + CalendarChannelVisibility.SHARE_EVERYTHING, + }, + {}, + manager, + ); + } + } else { + await this.connectedAccountRepository.updateAccessTokenAndRefreshToken( + input.accessToken, + input.refreshToken, + newOrExistingConnectedAccountId, + workspaceId, + manager, + ); + + await this.messageChannelRepository.resetSync( + newOrExistingConnectedAccountId, + workspaceId, + manager, + ); + } + }, + ); if (this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED')) { const messageChannels = diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts index c8a8a85dfe12b..85bfcebc79746 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts @@ -147,6 +147,7 @@ export class TokenService { async generateTransientToken( workspaceMemberId: string, + userId: string, workspaceId: string, ): Promise { const secret = this.environmentService.get('LOGIN_TOKEN_SECRET'); @@ -158,6 +159,7 @@ export class TokenService { const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn)); const jwtPayload = { sub: workspaceMemberId, + userId, workspaceId, }; @@ -234,6 +236,7 @@ export class TokenService { async verifyTransientToken(transientToken: string): Promise<{ workspaceMemberId: string; + userId: string; workspaceId: string; }> { const transientTokenSecret = @@ -243,6 +246,7 @@ export class TokenService { return { workspaceMemberId: payload.sub, + userId: payload.userId, workspaceId: payload.workspaceId, }; } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.service.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.service.ts index 8228788f4637a..f4494c0eaa403 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.service.ts @@ -203,9 +203,7 @@ export class BillingService { : frontBaseUrl; const quantity = - (await this.userWorkspaceService.getWorkspaceMemberCount( - user.defaultWorkspaceId, - )) || 1; + (await this.userWorkspaceService.getWorkspaceMemberCount()) || 1; const stripeCustomerId = ( await this.billingSubscriptionRepository.findOneBy({ diff --git a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts index 33f690fdddbe0..84388027d3ede 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts @@ -1,4 +1,4 @@ -import { Logger } from '@nestjs/common'; +import { Logger, Scope } from '@nestjs/common'; import { BillingService } from 'src/engine/core-modules/billing/billing.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; @@ -8,7 +8,10 @@ import { MessageQueue } from 'src/engine/integrations/message-queue/message-queu import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; export type UpdateSubscriptionJobData = { workspaceId: string }; -@Processor(MessageQueue.billingQueue) +@Processor({ + queueName: MessageQueue.billingQueue, + scope: Scope.REQUEST, +}) export class UpdateSubscriptionJob { protected readonly logger = new Logger(UpdateSubscriptionJob.name); @@ -21,7 +24,7 @@ export class UpdateSubscriptionJob { @Process(UpdateSubscriptionJob.name) async handle(data: UpdateSubscriptionJobData): Promise { const workspaceMembersCount = - await this.userWorkspaceService.getWorkspaceMemberCount(data.workspaceId); + await this.userWorkspaceService.getWorkspaceMemberCount(); if (!workspaceMembersCount || workspaceMembersCount <= 0) { return; diff --git a/packages/twenty-server/src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto.ts b/packages/twenty-server/src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto.ts index 1d5d997208994..6e9a3189a285e 100644 --- a/packages/twenty-server/src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto.ts @@ -5,10 +5,10 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/ @ObjectType('TimelineCalendarEventParticipant') export class TimelineCalendarEventParticipant { @Field(() => UUIDScalarType, { nullable: true }) - personId: string; + personId: string | null; @Field(() => UUIDScalarType, { nullable: true }) - workspaceMemberId: string; + workspaceMemberId: string | null; @Field() firstName: string; diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts index 810869e2a74e0..1a2b903a9961e 100644 --- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts +++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts @@ -81,19 +81,19 @@ export class TimelineCalendarEventService { const participants = event.calendarEventParticipants.map( (participant) => ({ calendarEventId: event.id, - personId: participant.person?.id, - workspaceMemberId: participant.workspaceMember?.id, + personId: participant.person?.id ?? null, + workspaceMemberId: participant.workspaceMember?.id ?? null, firstName: - participant.person?.name.firstName || + participant.person?.name?.firstName || participant.workspaceMember?.name.firstName || '', lastName: - participant.person?.name.lastName || + participant.person?.name?.lastName || participant.workspaceMember?.name.lastName || '', displayName: - participant.person?.name.firstName || - participant.person?.name.lastName || + participant.person?.name?.firstName || + participant.person?.name?.lastName || participant.workspaceMember?.name.firstName || participant.workspaceMember?.name.lastName || '', diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts index 1709f3d789b90..2b6fa3674b054 100644 --- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts +++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts @@ -56,7 +56,7 @@ export class OnboardingService { const isInviteTeamSkipped = inviteTeamValue === OnboardingStepValues.SKIPPED; const workspaceMemberCount = - await this.userWorkspaceService.getWorkspaceMemberCount(workspace.id); + await this.userWorkspaceService.getWorkspaceMemberCount(); return ( !isInviteTeamSkipped && diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts index 09d3c621fc11f..1fd86d27cbe28 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts @@ -9,6 +9,8 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Module({ imports: [ @@ -21,6 +23,7 @@ import { User } from 'src/engine/core-modules/user/user.entity'; ], services: [UserWorkspaceService], }), + TwentyORMModule.forFeature([WorkspaceMemberWorkspaceEntity]), ], exports: [UserWorkspaceService], providers: [UserWorkspaceService], diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index bdeaa75357352..1ba4e66269db6 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -8,11 +8,12 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { assert } from 'src/utils/assert'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; export class UserWorkspaceService extends TypeOrmQueryService { constructor( @@ -20,9 +21,10 @@ export class UserWorkspaceService extends TypeOrmQueryService { private readonly userWorkspaceRepository: Repository, @InjectRepository(User, 'core') private readonly userRepository: Repository, + @InjectWorkspaceRepository(WorkspaceMemberWorkspaceEntity) + private readonly workspaceMemberRepository: WorkspaceRepository, private readonly dataSourceService: DataSourceService, private readonly typeORMService: TypeORMService, - private readonly workspaceDataSourceService: WorkspaceDataSourceService, private eventEmitter: EventEmitter2, ) { super(userWorkspaceRepository); @@ -99,23 +101,10 @@ export class UserWorkspaceService extends TypeOrmQueryService { }); } - public async getWorkspaceMemberCount( - workspaceId: string, - ): Promise { - try { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return ( - await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."workspaceMember"`, - [], - workspaceId, - ) - ).length; - } catch { - return undefined; - } + public async getWorkspaceMemberCount(): Promise { + const workspaceMemberCount = await this.workspaceMemberRepository.count(); + + return workspaceMemberCount; } async checkUserWorkspaceExists( diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index ef596d19d0a56..154405d8eed2e 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -29,6 +29,7 @@ import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; +import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; const getHMACKey = (email?: string, key?: string | null) => { if (!email || !key) return null; @@ -48,6 +49,7 @@ export class UserResolver { private readonly environmentService: EnvironmentService, private readonly fileUploadService: FileUploadService, private readonly onboardingService: OnboardingService, + private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext, ) {} @Query(() => User) @@ -122,9 +124,11 @@ export class UserResolver { return null; } - return this.onboardingService.getOnboardingStep( - user, - user.defaultWorkspace, + const contextInstance = await this.loadServiceWithWorkspaceContext.load( + this.onboardingService, + user.defaultWorkspaceId, ); + + return contextInstance.getOnboardingStep(user, user.defaultWorkspace); } } diff --git a/packages/twenty-server/src/engine/integrations/message-queue/drivers/pg-boss.driver.ts b/packages/twenty-server/src/engine/integrations/message-queue/drivers/pg-boss.driver.ts index 8f875f75f1e02..ca3a0cd5f945f 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/drivers/pg-boss.driver.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/drivers/pg-boss.driver.ts @@ -47,7 +47,11 @@ export class PgBossDriver } : {}, async (job) => { - await handler({ data: job.data, id: job.id, name: job.name }); + await handler({ + data: job.data, + id: job.id, + name: job.name.split('.')[1], + }); }, ); } diff --git a/packages/twenty-server/src/engine/integrations/message-queue/message-queue.explorer.ts b/packages/twenty-server/src/engine/integrations/message-queue/message-queue.explorer.ts index 838143f09d2f5..1d5cbf473bf3d 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/message-queue.explorer.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/message-queue.explorer.ts @@ -156,7 +156,7 @@ export class MessageQueueExplorer implements OnModuleInit { }), ); - if (isRequestScoped) { + if (isRequestScoped && job.data) { const contextId = createContextId(); if (this.moduleRef.registerRequestByContextId) { diff --git a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts index 17b4d0e8028c9..0d264fcd37703 100644 --- a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts +++ b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts @@ -1,15 +1,9 @@ -import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository'; -import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository'; -import { CalendarEventParticipantRepository } from 'src/modules/calendar/repositories/calendar-event-participant.repository'; -import { CalendarEventRepository } from 'src/modules/calendar/repositories/calendar-event.repository'; import { CompanyRepository } from 'src/modules/company/repositories/company.repository'; import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository'; import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository'; import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository'; import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; -import { AttachmentRepository } from 'src/modules/attachment/repositories/attachment.repository'; -import { CommentRepository } from 'src/modules/activity/repositories/comment.repository'; import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository'; import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository'; import { MessageParticipantRepository } from 'src/modules/messaging/common/repositories/message-participant.repository'; @@ -20,11 +14,6 @@ import { PersonRepository } from 'src/modules/person/repositories/person.reposit export const metadataToRepositoryMapping = { AuditLogWorkspaceEntity: AuditLogRepository, BlocklistWorkspaceEntity: BlocklistRepository, - CalendarChannelEventAssociationWorkspaceEntity: - CalendarChannelEventAssociationRepository, - CalendarChannelWorkspaceEntity: CalendarChannelRepository, - CalendarEventParticipantWorkspaceEntity: CalendarEventParticipantRepository, - CalendarEventWorkspaceEntity: CalendarEventRepository, CompanyWorkspaceEntity: CompanyRepository, ConnectedAccountWorkspaceEntity: ConnectedAccountRepository, MessageChannelMessageAssociationWorkspaceEntity: @@ -36,6 +25,4 @@ export const metadataToRepositoryMapping = { PersonWorkspaceEntity: PersonRepository, TimelineActivityWorkspaceEntity: TimelineActivityRepository, WorkspaceMemberWorkspaceEntity: WorkspaceMemberRepository, - AttachmentWorkspaceEntity: AttachmentRepository, - CommentWorkspaceEntity: CommentRepository, }; diff --git a/packages/twenty-server/src/engine/twenty-orm/context/load-service-with-workspace.context.ts b/packages/twenty-server/src/engine/twenty-orm/context/load-service-with-workspace.context.ts new file mode 100644 index 0000000000000..02af978050447 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/context/load-service-with-workspace.context.ts @@ -0,0 +1,39 @@ +import { Inject, Type } from '@nestjs/common'; +import { ModuleRef, createContextId } from '@nestjs/core'; +import { Injector } from '@nestjs/core/injector/injector'; + +export class LoadServiceWithWorkspaceContext { + private readonly injector = new Injector(); + + constructor( + @Inject(ModuleRef) + private readonly moduleRef: ModuleRef, + ) {} + + async load(service: T, workspaceId: string): Promise { + const modules = this.moduleRef['container'].getModules(); + const host = [...modules.values()].find((module) => + module.providers.has((service as Type).constructor), + ); + + if (!host) { + throw new Error('Host module not found for the service'); + } + + const contextId = createContextId(); + + if (this.moduleRef.registerRequestByContextId) { + this.moduleRef.registerRequestByContextId( + { req: { workspaceId } }, + contextId, + ); + } + + return this.injector.loadPerContext( + service, + host, + new Map(host.providers), + contextId, + ); + } +} diff --git a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts index ee7740f065ee1..56f9238d249f2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts +++ b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts @@ -36,7 +36,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsNullable() @WorkspaceIsSystem() - position: number; + position: number | null; @WorkspaceRelation({ standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.activityTargets, diff --git a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts index ab51a2492706c..71ca2190903ca 100644 --- a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts @@ -6,8 +6,8 @@ import { QueryRunner, } from 'typeorm'; -import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/entity.manager'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; export class WorkspaceDataSource extends DataSource { readonly manager: WorkspaceEntityManager; diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index 27aead734ab49..f7447a1318576 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -32,10 +32,9 @@ export class WorkspaceDatasourceFactory { dataSourceMetadata.url ?? this.environmentService.get('PG_DATABASE_URL'), type: 'postgres', - // logging: this.environmentService.get('DEBUG_MODE') - // ? ['query', 'error'] - // : ['error'], - logging: 'all', + logging: this.environmentService.get('DEBUG_MODE') + ? ['query', 'error'] + : ['error'], schema: dataSourceMetadata.schema, entities, ssl: this.environmentService.get('PG_SSL_ALLOW_SELF_SIGNED') diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index f03123e0734f2..0f336768589d8 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -1,6 +1,7 @@ import { DeepPartial, DeleteResult, + EntityManager, FindManyOptions, FindOneOptions, FindOptionsWhere, @@ -29,9 +30,13 @@ export class WorkspaceRepository< /** * FIND METHODS */ - override async find(options?: FindManyOptions): Promise { + override async find( + options?: FindManyOptions, + entityManager?: EntityManager, + ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions(options); - const result = await super.find(computedOptions); + const result = await manager.find(this.target, computedOptions); const formattedResult = this.formatResult(result); return formattedResult; @@ -39,9 +44,11 @@ export class WorkspaceRepository< override async findBy( where: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - const result = await super.findBy(computedOptions.where); + const result = await manager.findBy(this.target, computedOptions.where); const formattedResult = this.formatResult(result); return formattedResult; @@ -49,9 +56,11 @@ export class WorkspaceRepository< override async findAndCount( options?: FindManyOptions, + entityManager?: EntityManager, ): Promise<[Entity[], number]> { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions(options); - const result = await super.findAndCount(computedOptions); + const result = await manager.findAndCount(this.target, computedOptions); const formattedResult = this.formatResult(result); return formattedResult; @@ -59,9 +68,14 @@ export class WorkspaceRepository< override async findAndCountBy( where: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise<[Entity[], number]> { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - const result = await super.findAndCountBy(computedOptions.where); + const result = await manager.findAndCountBy( + this.target, + computedOptions.where, + ); const formattedResult = this.formatResult(result); return formattedResult; @@ -69,9 +83,11 @@ export class WorkspaceRepository< override async findOne( options: FindOneOptions, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions(options); - const result = await super.findOne(computedOptions); + const result = await manager.findOne(this.target, computedOptions); const formattedResult = this.formatResult(result); return formattedResult; @@ -79,9 +95,11 @@ export class WorkspaceRepository< override async findOneBy( where: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - const result = await super.findOneBy(computedOptions.where); + const result = await manager.findOneBy(this.target, computedOptions.where); const formattedResult = this.formatResult(result); return formattedResult; @@ -89,9 +107,11 @@ export class WorkspaceRepository< override async findOneOrFail( options: FindOneOptions, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions(options); - const result = await super.findOneOrFail(computedOptions); + const result = await manager.findOneOrFail(this.target, computedOptions); const formattedResult = this.formatResult(result); return formattedResult; @@ -99,9 +119,14 @@ export class WorkspaceRepository< override async findOneByOrFail( where: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - const result = await super.findOneByOrFail(computedOptions.where); + const result = await manager.findOneByOrFail( + this.target, + computedOptions.where, + ); const formattedResult = this.formatResult(result); return formattedResult; @@ -113,29 +138,40 @@ export class WorkspaceRepository< override save>( entities: T[], options: SaveOptions & { reload: false }, + entityManager?: EntityManager, ): Promise; override save>( entities: T[], options?: SaveOptions, + entityManager?: EntityManager, ): Promise<(T & Entity)[]>; override save>( entity: T, options: SaveOptions & { reload: false }, + entityManager?: EntityManager, ): Promise; override save>( entity: T, options?: SaveOptions, + entityManager?: EntityManager, ): Promise; override async save>( entityOrEntities: T | T[], options?: SaveOptions, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const formattedEntityOrEntities = this.formatData(entityOrEntities); - const result = await super.save(formattedEntityOrEntities as any, options); + const result = await manager.save( + this.target, + formattedEntityOrEntities as any, + options, + ); + const formattedResult = this.formatResult(result); return formattedResult; @@ -147,15 +183,27 @@ export class WorkspaceRepository< override remove( entities: Entity[], options?: RemoveOptions, + entityManager?: EntityManager, ): Promise; - override remove(entity: Entity, options?: RemoveOptions): Promise; + override remove( + entity: Entity, + options?: RemoveOptions, + entityManager?: EntityManager, + ): Promise; override async remove( entityOrEntities: Entity | Entity[], + options?: RemoveOptions, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const formattedEntityOrEntities = this.formatData(entityOrEntities); - const result = await super.remove(formattedEntityOrEntities as any); + const result = await manager.remove( + this.target, + formattedEntityOrEntities, + options, + ); const formattedResult = this.formatResult(result); return formattedResult; @@ -172,40 +220,50 @@ export class WorkspaceRepository< | ObjectId | ObjectId[] | FindOptionsWhere, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; + if (typeof criteria === 'object' && 'where' in criteria) { criteria = this.transformOptions(criteria); } - return this.delete(criteria); + return manager.delete(this.target, criteria); } override softRemove>( entities: T[], options: SaveOptions & { reload: false }, + entityManager?: EntityManager, ): Promise; override softRemove>( entities: T[], options?: SaveOptions, + entityManager?: EntityManager, ): Promise<(T & Entity)[]>; override softRemove>( entity: T, options: SaveOptions & { reload: false }, + entityManager?: EntityManager, ): Promise; override softRemove>( entity: T, options?: SaveOptions, + entityManager?: EntityManager, ): Promise; override async softRemove>( entityOrEntities: T | T[], options?: SaveOptions, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const formattedEntityOrEntities = this.formatData(entityOrEntities); - const result = await super.softRemove( + const result = await manager.softRemove( + this.target, formattedEntityOrEntities as any, options, ); @@ -225,12 +283,15 @@ export class WorkspaceRepository< | ObjectId | ObjectId[] | FindOptionsWhere, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; + if (typeof criteria === 'object' && 'where' in criteria) { criteria = this.transformOptions(criteria); } - return this.softDelete(criteria); + return manager.softDelete(this.target, criteria); } /** @@ -239,29 +300,36 @@ export class WorkspaceRepository< override recover>( entities: T[], options: SaveOptions & { reload: false }, + entityManager?: EntityManager, ): Promise; override recover>( entities: T[], options?: SaveOptions, + entityManager?: EntityManager, ): Promise<(T & Entity)[]>; override recover>( entity: T, options: SaveOptions & { reload: false }, + entityManager?: EntityManager, ): Promise; override recover>( entity: T, options?: SaveOptions, + entityManager?: EntityManager, ): Promise; override async recover>( entityOrEntities: T | T[], options?: SaveOptions, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const formattedEntityOrEntities = this.formatData(entityOrEntities); - const result = await super.recover( + const result = await manager.recover( + this.target, formattedEntityOrEntities as any, options, ); @@ -281,12 +349,15 @@ export class WorkspaceRepository< | ObjectId | ObjectId[] | FindOptionsWhere, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; + if (typeof criteria === 'object' && 'where' in criteria) { criteria = this.transformOptions(criteria); } - return this.restore(criteria); + return manager.restore(this.target, criteria); } /** @@ -294,9 +365,11 @@ export class WorkspaceRepository< */ override async insert( entity: QueryDeepPartialEntity | QueryDeepPartialEntity[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const formatedEntity = this.formatData(entity); - const result = await super.insert(formatedEntity); + const result = await manager.insert(this.target, formatedEntity); const formattedResult = this.formatResult(result); return formattedResult; @@ -317,12 +390,15 @@ export class WorkspaceRepository< | ObjectId[] | FindOptionsWhere, partialEntity: QueryDeepPartialEntity, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; + if (typeof criteria === 'object' && 'where' in criteria) { criteria = this.transformOptions(criteria); } - return this.update(criteria, partialEntity); + return manager.update(this.target, criteria, partialEntity); } override upsert( @@ -330,50 +406,63 @@ export class WorkspaceRepository< | QueryDeepPartialEntity | QueryDeepPartialEntity[], conflictPathsOrOptions: string[] | UpsertOptions, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; + const formattedEntityOrEntities = this.formatData(entityOrEntities); - return this.upsert(formattedEntityOrEntities, conflictPathsOrOptions); + return manager.upsert( + this.target, + formattedEntityOrEntities, + conflictPathsOrOptions, + ); } /** * EXIST METHODS */ - override exist(options?: FindManyOptions): Promise { - const computedOptions = this.transformOptions(options); - - return super.exist(computedOptions); - } - - override exists(options?: FindManyOptions): Promise { + override exists( + options?: FindManyOptions, + entityManager?: EntityManager, + ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions(options); - return super.exists(computedOptions); + return manager.exists(this.target, computedOptions); } override existsBy( where: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - return super.existsBy(computedOptions.where); + return manager.existsBy(this.target, computedOptions.where); } /** * COUNT METHODS */ - override count(options?: FindManyOptions): Promise { + override count( + options?: FindManyOptions, + entityManager?: EntityManager, + ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions(options); - return super.count(computedOptions); + return manager.count(this.target, computedOptions); } override countBy( where: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - return super.countBy(computedOptions.where); + return manager.countBy(this.target, computedOptions.where); } /** @@ -382,57 +471,79 @@ export class WorkspaceRepository< override sum( columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - return super.sum(columnName, computedOptions.where); + return manager.sum(this.target, columnName, computedOptions.where); } override average( columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - return super.average(columnName, computedOptions.where); + return manager.average(this.target, columnName, computedOptions.where); } override minimum( columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - return super.minimum(columnName, computedOptions.where); + return manager.minimum(this.target, columnName, computedOptions.where); } override maximum( columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedOptions = this.transformOptions({ where }); - return super.maximum(columnName, computedOptions.where); + return manager.maximum(this.target, columnName, computedOptions.where); } override increment( conditions: FindOptionsWhere, propertyPath: string, value: number | string, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedConditions = this.transformOptions({ where: conditions }); - return this.increment(computedConditions.where, propertyPath, value); + return manager.increment( + this.target, + computedConditions.where, + propertyPath, + value, + ); } override decrement( conditions: FindOptionsWhere, propertyPath: string, value: number | string, + entityManager?: EntityManager, ): Promise { + const manager = entityManager || this.manager; const computedConditions = this.transformOptions({ where: conditions }); - return this.decrement(computedConditions.where, propertyPath, value); + return manager.decrement( + this.target, + computedConditions.where, + propertyPath, + value, + ); } /** diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts index 353c3e5a5005a..efed8ca7bcae4 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts @@ -30,12 +30,21 @@ import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, } from 'src/engine/twenty-orm/twenty-orm.module-definition'; +import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; @Global() @Module({ imports: [DataSourceModule], - providers: [...entitySchemaFactories, TwentyORMManager], - exports: [EntitySchemaFactory, TwentyORMManager], + providers: [ + ...entitySchemaFactories, + TwentyORMManager, + LoadServiceWithWorkspaceContext, + ], + exports: [ + EntitySchemaFactory, + TwentyORMManager, + LoadServiceWithWorkspaceContext, + ], }) export class TwentyORMCoreModule extends ConfigurableModuleClass diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/types/object-record.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/types/object-record.ts index d517a6aeda882..659f072560641 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/types/object-record.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/types/object-record.ts @@ -1,19 +1,20 @@ -import { ObjectLiteral } from 'typeorm'; - import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; -export type ObjectRecord = { - [K in keyof T as T[K] extends BaseWorkspaceEntity - ? `${Extract}Id` - : K]: T[K] extends BaseWorkspaceEntity - ? string - : T[K] extends BaseWorkspaceEntity[] - ? string[] - : T[K]; -} & { - [K in keyof T]: T[K] extends BaseWorkspaceEntity - ? ObjectRecord - : T[K] extends BaseWorkspaceEntity[] - ? ObjectRecord[] - : T[K]; +type RelationKeys = { + [K in keyof T]: NonNullable extends BaseWorkspaceEntity ? K : never; +}[keyof T]; + +type ForeignKeyMap = { + [K in RelationKeys as `${K & string}Id`]: string; }; + +type RecursiveObjectRecord = { + [P in keyof T]: NonNullable extends BaseWorkspaceEntity + ? ObjectRecord> & ForeignKeyMap> + : T[P]; +}; + +// TODO: We should get rid of that it's causing too much issues +// Some relations can be null or undefined because they're not mendatory and other cannot +// This utility type put as defined all the joinColumn, so it's not well typed +export type ObjectRecord = RecursiveObjectRecord & ForeignKeyMap; diff --git a/packages/twenty-server/src/modules/activity/repositories/comment.repository.ts b/packages/twenty-server/src/modules/activity/repositories/comment.repository.ts deleted file mode 100644 index e6bf07b2b8a59..0000000000000 --- a/packages/twenty-server/src/modules/activity/repositories/comment.repository.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; - -@Injectable() -export class CommentRepository { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - ) {} - - async deleteByAuthorId(authorId: string, workspaceId: string): Promise { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `DELETE FROM ${dataSourceSchema}."comment" WHERE "authorId" = $1`, - [authorId], - workspaceId, - ); - } -} diff --git a/packages/twenty-server/src/modules/activity/standard-objects/activity-target.workspace-entity.ts b/packages/twenty-server/src/modules/activity/standard-objects/activity-target.workspace-entity.ts index c99325d137f5a..a72330bfd2019 100644 --- a/packages/twenty-server/src/modules/activity/standard-objects/activity-target.workspace-entity.ts +++ b/packages/twenty-server/src/modules/activity/standard-objects/activity-target.workspace-entity.ts @@ -36,7 +36,7 @@ export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'activityTargets', }) @WorkspaceIsNullable() - activity: Relation; + activity: Relation | null; @WorkspaceRelation({ standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.person, @@ -49,7 +49,7 @@ export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'activityTargets', }) @WorkspaceIsNullable() - person: Relation; + person: Relation | null; @WorkspaceRelation({ standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.company, @@ -62,7 +62,7 @@ export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'activityTargets', }) @WorkspaceIsNullable() - company: Relation; + company: Relation | null; @WorkspaceRelation({ standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.opportunity, @@ -75,7 +75,7 @@ export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'activityTargets', }) @WorkspaceIsNullable() - opportunity: Relation; + opportunity: Relation | null; @WorkspaceDynamicRelation({ type: RelationMetadataType.MANY_TO_ONE, diff --git a/packages/twenty-server/src/modules/activity/standard-objects/activity.workspace-entity.ts b/packages/twenty-server/src/modules/activity/standard-objects/activity.workspace-entity.ts index 8c6460eb45e3a..d27fd026f5edd 100644 --- a/packages/twenty-server/src/modules/activity/standard-objects/activity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/activity/standard-objects/activity.workspace-entity.ts @@ -64,7 +64,7 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconCalendarEvent', }) @WorkspaceIsNullable() - reminderAt: Date; + reminderAt: Date | null; @WorkspaceField({ standardId: ACTIVITY_STANDARD_FIELD_IDS.dueAt, @@ -74,7 +74,7 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconCalendarEvent', }) @WorkspaceIsNullable() - dueAt: Date; + dueAt: Date | null; @WorkspaceField({ standardId: ACTIVITY_STANDARD_FIELD_IDS.completedAt, @@ -84,7 +84,7 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconCheck', }) @WorkspaceIsNullable() - completedAt: Date; + completedAt: Date | null; @WorkspaceRelation({ standardId: ACTIVITY_STANDARD_FIELD_IDS.activityTargets, @@ -134,7 +134,7 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity { joinColumn: 'authorId', }) @WorkspaceIsNullable() - author: Relation; + author: Relation | null; @WorkspaceRelation({ standardId: ACTIVITY_STANDARD_FIELD_IDS.assignee, @@ -148,5 +148,5 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity { joinColumn: 'assigneeId', }) @WorkspaceIsNullable() - assignee: Relation; + assignee: Relation | null; } diff --git a/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts b/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts index 40159e093a855..82ca9d107d6b5 100644 --- a/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts +++ b/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts @@ -45,5 +45,5 @@ export class ApiKeyWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconCalendar', }) @WorkspaceIsNullable() - revokedAt?: Date; + revokedAt?: Date | null; } diff --git a/packages/twenty-server/src/modules/attachment/repositories/attachment.repository.ts b/packages/twenty-server/src/modules/attachment/repositories/attachment.repository.ts deleted file mode 100644 index 2d340d2cba0d7..0000000000000 --- a/packages/twenty-server/src/modules/attachment/repositories/attachment.repository.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; - -@Injectable() -export class AttachmentRepository { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - ) {} - - async deleteByAuthorId(authorId: string, workspaceId: string): Promise { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `DELETE FROM ${dataSourceSchema}."attachment" WHERE "authorId" = $1`, - [authorId], - workspaceId, - ); - } -} diff --git a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts index 42af628b9e030..3b4b9f9afca10 100644 --- a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts +++ b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts @@ -80,7 +80,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'attachments', }) @WorkspaceIsNullable() - activity: Relation; + activity: Relation | null; @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.person, @@ -93,7 +93,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'attachments', }) @WorkspaceIsNullable() - person: Relation; + person: Relation | null; @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.company, @@ -106,7 +106,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'attachments', }) @WorkspaceIsNullable() - company: Relation; + company: Relation | null; @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.opportunity, @@ -119,7 +119,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'attachments', }) @WorkspaceIsNullable() - opportunity: Relation; + opportunity: Relation | null; @WorkspaceDynamicRelation({ type: RelationMetadataType.MANY_TO_ONE, diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/match-participant.job.ts b/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/match-participant.job.ts index 97f639e8d7a23..f8ccadfa44250 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/match-participant.job.ts +++ b/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/match-participant.job.ts @@ -1,3 +1,5 @@ +import { Scope } from '@nestjs/common'; + import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; @@ -11,7 +13,10 @@ export type MatchParticipantJobData = { workspaceMemberId?: string; }; -@Processor(MessageQueue.messagingQueue) +@Processor({ + queueName: MessageQueue.messagingQueue, + scope: Scope.REQUEST, +}) export class MatchParticipantJob { constructor( private readonly messageParticipantService: MessagingMessageParticipantService, diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/unmatch-participant.job.ts b/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/unmatch-participant.job.ts index 34b0e06f31ef7..a2b8c9044a0ac 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/unmatch-participant.job.ts +++ b/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/unmatch-participant.job.ts @@ -1,3 +1,5 @@ +import { Scope } from '@nestjs/common'; + import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { CalendarEventParticipantService } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service'; @@ -11,7 +13,10 @@ export type UnmatchParticipantJobData = { workspaceMemberId?: string; }; -@Processor(MessageQueue.messagingQueue) +@Processor({ + queueName: MessageQueue.messagingQueue, + scope: Scope.REQUEST, +}) export class UnmatchParticipantJob { constructor( private readonly messageParticipantService: MessagingMessageParticipantService, diff --git a/packages/twenty-server/src/modules/calendar/commands/calendar-commands.module.ts b/packages/twenty-server/src/modules/calendar/commands/calendar-commands.module.ts index 20b187004c8b6..0c0acf812888e 100644 --- a/packages/twenty-server/src/modules/calendar/commands/calendar-commands.module.ts +++ b/packages/twenty-server/src/modules/calendar/commands/calendar-commands.module.ts @@ -1,19 +1,10 @@ import { Module } from '@nestjs/common'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { GoogleCalendarSyncCommand } from 'src/modules/calendar/commands/google-calendar-sync.command'; import { WorkspaceGoogleCalendarSyncModule } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module'; -import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Module({ - imports: [ - ObjectMetadataRepositoryModule.forFeature([ - ConnectedAccountWorkspaceEntity, - CalendarChannelWorkspaceEntity, - ]), - WorkspaceGoogleCalendarSyncModule, - ], + imports: [WorkspaceGoogleCalendarSyncModule], providers: [GoogleCalendarSyncCommand], }) export class CalendarCommandsModule {} diff --git a/packages/twenty-server/src/modules/calendar/crons/jobs/google-calendar-sync.cron.job.ts b/packages/twenty-server/src/modules/calendar/crons/jobs/google-calendar-sync.cron.job.ts index f9b39cfaba8d9..b2c6e43da7fdf 100644 --- a/packages/twenty-server/src/modules/calendar/crons/jobs/google-calendar-sync.cron.job.ts +++ b/packages/twenty-server/src/modules/calendar/crons/jobs/google-calendar-sync.cron.job.ts @@ -1,4 +1,5 @@ import { InjectRepository } from '@nestjs/typeorm'; +import { Scope } from '@nestjs/common'; import { Repository, In } from 'typeorm'; @@ -10,7 +11,10 @@ import { MessageQueue } from 'src/engine/integrations/message-queue/message-queu import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; -@Processor(MessageQueue.cronQueue) +@Processor({ + queueName: MessageQueue.cronQueue, + scope: Scope.REQUEST, +}) export class GoogleCalendarSyncCronJob { constructor( @InjectRepository(Workspace, 'core') diff --git a/packages/twenty-server/src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job.ts b/packages/twenty-server/src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job.ts index 5b9b9ddc9d7c7..6dc9eef1ceb1c 100644 --- a/packages/twenty-server/src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job.ts +++ b/packages/twenty-server/src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job.ts @@ -1,35 +1,38 @@ -import { Logger } from '@nestjs/common'; +import { Logger, Scope } from '@nestjs/common'; + +import { Any, ILike } from 'typeorm'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository'; -import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository'; import { CalendarEventCleanerService } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity'; import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository'; import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; export type BlocklistItemDeleteCalendarEventsJobData = { workspaceId: string; blocklistItemId: string; }; -@Processor(MessageQueue.calendarQueue) +@Processor({ + queueName: MessageQueue.calendarQueue, + scope: Scope.REQUEST, +}) export class BlocklistItemDeleteCalendarEventsJob { private readonly logger = new Logger( BlocklistItemDeleteCalendarEventsJob.name, ); constructor( - @InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity) - private readonly calendarChannelRepository: CalendarChannelRepository, - @InjectObjectMetadataRepository( - CalendarChannelEventAssociationWorkspaceEntity, - ) - private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository, + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, + @InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity) + private readonly calendarChannelEventAssociationRepository: WorkspaceRepository, @InjectObjectMetadataRepository(BlocklistWorkspaceEntity) private readonly blocklistRepository: BlocklistRepository, private readonly calendarEventCleanerService: CalendarEventCleanerService, @@ -58,19 +61,39 @@ export class BlocklistItemDeleteCalendarEventsJob { `Deleting calendar events from ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId}`, ); - const calendarChannels = - await this.calendarChannelRepository.getIdsByWorkspaceMemberId( - workspaceMemberId, - workspaceId, + if (!workspaceMemberId) { + throw new Error( + `Workspace member ID is undefined for blocklist item ${blocklistItemId} in workspace ${workspaceId}`, ); + } + + const calendarChannels = await this.calendarChannelRepository.find({ + where: { + connectedAccount: { + accountOwner: { + id: workspaceMemberId, + }, + }, + }, + relations: ['connectedAccount.accountOwner'], + }); const calendarChannelIds = calendarChannels.map(({ id }) => id); - await this.calendarChannelEventAssociationRepository.deleteByCalendarEventParticipantHandleAndCalendarChannelIds( - handle, - calendarChannelIds, - workspaceId, - ); + const isHandleDomain = handle.startsWith('@'); + + await this.calendarChannelEventAssociationRepository.delete({ + calendarEvent: { + calendarEventParticipants: { + handle: isHandleDomain ? ILike(`%${handle}`) : handle, + }, + calendarChannelEventAssociations: { + calendarChannel: { + id: Any(calendarChannelIds), + }, + }, + }, + }); await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents( workspaceId, diff --git a/packages/twenty-server/src/modules/calendar/jobs/blocklist-reimport-calendar-events.job.ts b/packages/twenty-server/src/modules/calendar/jobs/blocklist-reimport-calendar-events.job.ts index 73069a36a88bf..7c34e0898c99d 100644 --- a/packages/twenty-server/src/modules/calendar/jobs/blocklist-reimport-calendar-events.job.ts +++ b/packages/twenty-server/src/modules/calendar/jobs/blocklist-reimport-calendar-events.job.ts @@ -1,4 +1,4 @@ -import { Logger } from '@nestjs/common'; +import { Logger, Scope } from '@nestjs/common'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; @@ -14,7 +14,10 @@ export type BlocklistReimportCalendarEventsJobData = { handle: string; }; -@Processor(MessageQueue.calendarQueue) +@Processor({ + queueName: MessageQueue.calendarQueue, + scope: Scope.REQUEST, +}) export class BlocklistReimportCalendarEventsJob { private readonly logger = new Logger(BlocklistReimportCalendarEventsJob.name); diff --git a/packages/twenty-server/src/modules/calendar/jobs/calendar-create-company-and-contact-after-sync.job.ts b/packages/twenty-server/src/modules/calendar/jobs/calendar-create-company-and-contact-after-sync.job.ts index 0dec9f8e8f997..77cf65b231f97 100644 --- a/packages/twenty-server/src/modules/calendar/jobs/calendar-create-company-and-contact-after-sync.job.ts +++ b/packages/twenty-server/src/modules/calendar/jobs/calendar-create-company-and-contact-after-sync.job.ts @@ -1,35 +1,35 @@ -import { Logger } from '@nestjs/common'; +import { Logger, Scope } from '@nestjs/common'; + +import { IsNull } from 'typeorm'; -import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository'; -import { CalendarEventParticipantRepository } from 'src/modules/calendar/repositories/calendar-event-participant.repository'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity'; import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity'; import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; export type CalendarCreateCompanyAndContactAfterSyncJobData = { workspaceId: string; calendarChannelId: string; }; -@Processor(MessageQueue.calendarQueue) +@Processor({ + queueName: MessageQueue.calendarQueue, + scope: Scope.REQUEST, +}) export class CalendarCreateCompanyAndContactAfterSyncJob { private readonly logger = new Logger( CalendarCreateCompanyAndContactAfterSyncJob.name, ); constructor( private readonly createCompanyAndContactService: CreateCompanyAndContactService, - @InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity) - private readonly calendarChannelService: CalendarChannelRepository, - @InjectObjectMetadataRepository(CalendarEventParticipantWorkspaceEntity) - private readonly calendarEventParticipantRepository: CalendarEventParticipantRepository, - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, + @InjectWorkspaceRepository(CalendarEventParticipantWorkspaceEntity) + private readonly calendarEventParticipantRepository: WorkspaceRepository, ) {} @Process(CalendarCreateCompanyAndContactAfterSyncJob.name) @@ -41,40 +41,52 @@ export class CalendarCreateCompanyAndContactAfterSyncJob { ); const { workspaceId, calendarChannelId } = data; - const calendarChannels = await this.calendarChannelService.getByIds( - [calendarChannelId], - workspaceId, - ); + const calendarChannel = await this.calendarChannelRepository.findOne({ + where: { + id: calendarChannelId, + }, + relations: ['connectedAccount.accountOwner'], + }); - if (calendarChannels.length === 0) { + if (!calendarChannel) { throw new Error( `Calendar channel with id ${calendarChannelId} not found in workspace ${workspaceId}`, ); } - const { handle, isContactAutoCreationEnabled, connectedAccountId } = - calendarChannels[0]; + const { handle, isContactAutoCreationEnabled, connectedAccount } = + calendarChannel; if (!isContactAutoCreationEnabled || !handle) { return; } - const connectedAccount = await this.connectedAccountRepository.getById( - connectedAccountId, - workspaceId, - ); - if (!connectedAccount) { throw new Error( - `Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`, + `Connected account not found in workspace ${workspaceId}`, ); } const calendarEventParticipantsWithoutPersonIdAndWorkspaceMemberId = - await this.calendarEventParticipantRepository.getByCalendarChannelIdWithoutPersonIdAndWorkspaceMemberId( - calendarChannelId, - workspaceId, - ); + await this.calendarEventParticipantRepository.find({ + where: { + calendarEvent: { + calendarChannelEventAssociations: { + calendarChannel: { + id: calendarChannelId, + }, + }, + calendarEventParticipants: { + person: IsNull(), + workspaceMember: IsNull(), + }, + }, + }, + relations: [ + 'calendarEvent.calendarChannelEventAssociations', + 'calendarEvent.calendarEventParticipants', + ], + }); await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants( connectedAccount, diff --git a/packages/twenty-server/src/modules/calendar/jobs/calendar-job.module.ts b/packages/twenty-server/src/modules/calendar/jobs/calendar-job.module.ts index 1c3abdaa0593c..b419764970209 100644 --- a/packages/twenty-server/src/modules/calendar/jobs/calendar-job.module.ts +++ b/packages/twenty-server/src/modules/calendar/jobs/calendar-job.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { BlocklistItemDeleteCalendarEventsJob } from 'src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job'; import { BlocklistReimportCalendarEventsJob } from 'src/modules/calendar/jobs/blocklist-reimport-calendar-events.job'; import { CalendarCreateCompanyAndContactAfterSyncJob } from 'src/modules/calendar/jobs/calendar-create-company-and-contact-after-sync.job'; @@ -18,10 +19,12 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s @Module({ imports: [ - ObjectMetadataRepositoryModule.forFeature([ + TwentyORMModule.forFeature([ CalendarChannelWorkspaceEntity, CalendarChannelEventAssociationWorkspaceEntity, CalendarEventParticipantWorkspaceEntity, + ]), + ObjectMetadataRepositoryModule.forFeature([ ConnectedAccountWorkspaceEntity, BlocklistWorkspaceEntity, ]), diff --git a/packages/twenty-server/src/modules/calendar/jobs/google-calendar-sync.job.ts b/packages/twenty-server/src/modules/calendar/jobs/google-calendar-sync.job.ts index 46240cca07629..38f90f8b98689 100644 --- a/packages/twenty-server/src/modules/calendar/jobs/google-calendar-sync.job.ts +++ b/packages/twenty-server/src/modules/calendar/jobs/google-calendar-sync.job.ts @@ -1,4 +1,4 @@ -import { Logger } from '@nestjs/common'; +import { Logger, Scope } from '@nestjs/common'; import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.service'; import { GoogleCalendarSyncService } from 'src/modules/calendar/services/google-calendar-sync/google-calendar-sync.service'; @@ -11,7 +11,10 @@ export type GoogleCalendarSyncJobData = { connectedAccountId: string; }; -@Processor(MessageQueue.calendarQueue) +@Processor({ + queueName: MessageQueue.calendarQueue, + scope: Scope.REQUEST, +}) export class GoogleCalendarSyncJob { private readonly logger = new Logger(GoogleCalendarSyncJob.name); diff --git a/packages/twenty-server/src/modules/calendar/listeners/calendar-event-participant.listener.ts b/packages/twenty-server/src/modules/calendar/listeners/calendar-event-participant.listener.ts index 79025878c586d..7f8e6e3c0e432 100644 --- a/packages/twenty-server/src/modules/calendar/listeners/calendar-event-participant.listener.ts +++ b/packages/twenty-server/src/modules/calendar/listeners/calendar-event-participant.listener.ts @@ -25,7 +25,7 @@ export class CalendarEventParticipantListener { @OnEvent('calendarEventParticipant.matched') public async handleCalendarEventParticipantMatchedEvent(payload: { workspaceId: string; - userId: string; + workspaceMemberId: string; calendarEventParticipants: ObjectRecord[]; }): Promise { const calendarEventParticipants = payload.calendarEventParticipants ?? []; @@ -59,7 +59,7 @@ export class CalendarEventParticipantListener { properties: null, objectName: 'calendarEvent', recordId: participant.personId, - workspaceMemberId: payload.userId, + workspaceMemberId: payload.workspaceMemberId, workspaceId: payload.workspaceId, linkedObjectMetadataId: calendarEventObjectMetadata.id, linkedRecordId: participant.calendarEventId, diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts index a5c8b06aa6491..ee20809427d30 100644 --- a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts @@ -1,26 +1,20 @@ -import { - BadRequestException, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity'; -import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository'; import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @Injectable() export class CalendarEventFindManyPreQueryHook implements WorkspacePreQueryHook { constructor( - @InjectObjectMetadataRepository( - CalendarChannelEventAssociationWorkspaceEntity, - ) - private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository, + @InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity) + private readonly calendarChannelEventAssociationRepository: WorkspaceRepository, private readonly canAccessCalendarEventService: CanAccessCalendarEventService, ) {} @@ -33,20 +27,25 @@ export class CalendarEventFindManyPreQueryHook throw new BadRequestException('id filter is required'); } - const calendarChannelCalendarEventAssociations = - await this.calendarChannelEventAssociationRepository.getByCalendarEventIds( - [payload?.filter?.id?.eq], - workspaceId, - ); + // TODO: Re-implement this using twenty ORM + // const calendarChannelCalendarEventAssociations = + // await this.calendarChannelEventAssociationRepository.find({ + // where: { + // calendarEvent: { + // id: payload?.filter?.id?.eq, + // }, + // }, + // relations: ['calendarChannel.connectedAccount'], + // }); - if (calendarChannelCalendarEventAssociations.length === 0) { - throw new NotFoundException(); - } + // if (calendarChannelCalendarEventAssociations.length === 0) { + // throw new NotFoundException(); + // } - await this.canAccessCalendarEventService.canAccessCalendarEvent( - userId, - workspaceId, - calendarChannelCalendarEventAssociations, - ); + // await this.canAccessCalendarEventService.canAccessCalendarEvent( + // userId, + // workspaceId, + // calendarChannelCalendarEventAssociations, + // ); } } diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts index 082e1b280185f..34642b128e48a 100644 --- a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts @@ -1,24 +1,18 @@ -import { - BadRequestException, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service'; -import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity'; @Injectable() export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook { constructor( - @InjectObjectMetadataRepository( - CalendarChannelEventAssociationWorkspaceEntity, - ) - private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository, + @InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity) + private readonly calendarChannelEventAssociationRepository: WorkspaceRepository, private readonly canAccessCalendarEventService: CanAccessCalendarEventService, ) {} @@ -31,20 +25,24 @@ export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook { throw new BadRequestException('id filter is required'); } - const calendarChannelCalendarEventAssociations = - await this.calendarChannelEventAssociationRepository.getByCalendarEventIds( - [payload?.filter?.id?.eq], - workspaceId, - ); + // TODO: Re-implement this using twenty ORM + // const calendarChannelCalendarEventAssociations = + // await this.calendarChannelEventAssociationRepository.find({ + // where: { + // calendarEvent: { + // id: payload?.filter?.id?.eq, + // }, + // }, + // }); - if (calendarChannelCalendarEventAssociations.length === 0) { - throw new NotFoundException(); - } + // if (calendarChannelCalendarEventAssociations.length === 0) { + // throw new NotFoundException(); + // } - await this.canAccessCalendarEventService.canAccessCalendarEvent( - userId, - workspaceId, - calendarChannelCalendarEventAssociations, - ); + // await this.canAccessCalendarEventService.canAccessCalendarEvent( + // userId, + // workspaceId, + // calendarChannelCalendarEventAssociations, + // ); } } diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts index 0a971c32dcbf7..b86a3ca1cda63 100644 --- a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts @@ -1,10 +1,11 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import groupBy from 'lodash.groupby'; +import { Any } from 'typeorm'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; -import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelWorkspaceEntity, @@ -18,8 +19,8 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Injectable() export class CanAccessCalendarEventService { constructor( - @InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity) - private readonly calendarChannelRepository: CalendarChannelRepository, + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) private readonly connectedAccountRepository: ConnectedAccountRepository, @InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity) @@ -29,14 +30,17 @@ export class CanAccessCalendarEventService { public async canAccessCalendarEvent( userId: string, workspaceId: string, - calendarChannelCalendarEventAssociations: ObjectRecord[], + calendarChannelCalendarEventAssociations: CalendarChannelEventAssociationWorkspaceEntity[], ) { - const calendarChannels = await this.calendarChannelRepository.getByIds( - calendarChannelCalendarEventAssociations.map( - (association) => association.calendarChannelId, - ), - workspaceId, - ); + const calendarChannels = await this.calendarChannelRepository.find({ + where: { + id: Any( + calendarChannelCalendarEventAssociations.map( + (association) => association.calendarChannel.id, + ), + ), + }, + }); const calendarChannelsGroupByVisibility = groupBy( calendarChannels, @@ -56,7 +60,7 @@ export class CanAccessCalendarEventService { const calendarChannelsConnectedAccounts = await this.connectedAccountRepository.getByIds( - calendarChannels.map((channel) => channel.connectedAccountId), + calendarChannels.map((channel) => channel.connectedAccount.id), workspaceId, ); diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts index 0bed2914aa0db..555b10719f7f8 100644 --- a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts @@ -8,12 +8,15 @@ import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-ob import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook'; import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook'; import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; @Module({ imports: [ - ObjectMetadataRepositoryModule.forFeature([ + TwentyORMModule.forFeature([ CalendarChannelEventAssociationWorkspaceEntity, CalendarChannelWorkspaceEntity, + ]), + ObjectMetadataRepositoryModule.forFeature([ ConnectedAccountWorkspaceEntity, WorkspaceMemberWorkspaceEntity, ]), diff --git a/packages/twenty-server/src/modules/calendar/repositories/calendar-channel-event-association.repository.ts b/packages/twenty-server/src/modules/calendar/repositories/calendar-channel-event-association.repository.ts deleted file mode 100644 index 89a9a28b486f8..0000000000000 --- a/packages/twenty-server/src/modules/calendar/repositories/calendar-channel-event-association.repository.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { EntityManager } from 'typeorm'; - -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; -import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity'; -import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/get-flattened-values-and-values-string-for-batch-raw-query.util'; - -@Injectable() -export class CalendarChannelEventAssociationRepository { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - ) {} - - public async getByEventExternalIdsAndCalendarChannelId( - eventExternalIds: string[], - calendarChannelId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - if (eventExternalIds.length === 0) { - return []; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarChannelEventAssociation" - WHERE "eventExternalId" = ANY($1) AND "calendarChannelId" = $2`, - [eventExternalIds, calendarChannelId], - workspaceId, - transactionManager, - ); - } - - public async deleteByEventExternalIdsAndCalendarChannelId( - eventExternalIds: string[], - calendarChannelId: string, - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `DELETE FROM ${dataSourceSchema}."calendarChannelEventAssociation" WHERE "eventExternalId" = ANY($1) AND "calendarChannelId" = $2`, - [eventExternalIds, calendarChannelId], - workspaceId, - transactionManager, - ); - } - - public async getByCalendarChannelIds( - calendarChannelIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - if (calendarChannelIds.length === 0) { - return []; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarChannelEventAssociation" - WHERE "calendarChannelId" = ANY($1)`, - [calendarChannelIds], - workspaceId, - transactionManager, - ); - } - - public async deleteByCalendarChannelIds( - calendarChannelIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ) { - if (calendarChannelIds.length === 0) { - return; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `DELETE FROM ${dataSourceSchema}."calendarChannelEventAssociation" WHERE "calendarChannelId" = ANY($1)`, - [calendarChannelIds], - workspaceId, - transactionManager, - ); - } - - public async deleteByIds( - ids: string[], - workspaceId: string, - transactionManager?: EntityManager, - ) { - if (ids.length === 0) { - return; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `DELETE FROM ${dataSourceSchema}."calendarChannelEventAssociation" WHERE "id" = ANY($1)`, - [ids], - workspaceId, - transactionManager, - ); - } - - public async getByCalendarEventIds( - calendarEventIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - if (calendarEventIds.length === 0) { - return []; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarChannelEventAssociation" - WHERE "calendarEventId" = ANY($1)`, - [calendarEventIds], - workspaceId, - transactionManager, - ); - } - - public async saveCalendarChannelEventAssociations( - calendarChannelEventAssociations: Omit< - ObjectRecord, - 'id' | 'createdAt' | 'updatedAt' | 'calendarChannel' | 'calendarEvent' - >[], - workspaceId: string, - transactionManager?: EntityManager, - ) { - if (calendarChannelEventAssociations.length === 0) { - return; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const { - flattenedValues: calendarChannelEventAssociationValues, - valuesString, - } = getFlattenedValuesAndValuesStringForBatchRawQuery( - calendarChannelEventAssociations, - { - calendarChannelId: 'uuid', - calendarEventId: 'uuid', - eventExternalId: 'text', - }, - ); - - await this.workspaceDataSourceService.executeRawQuery( - `INSERT INTO ${dataSourceSchema}."calendarChannelEventAssociation" ("calendarChannelId", "calendarEventId", "eventExternalId") - VALUES ${valuesString}`, - calendarChannelEventAssociationValues, - workspaceId, - transactionManager, - ); - } - - public async deleteByCalendarEventParticipantHandleAndCalendarChannelIds( - calendarEventParticipantHandle: string, - calendarChannelIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const isHandleDomain = calendarEventParticipantHandle.startsWith('@'); - - await this.workspaceDataSourceService.executeRawQuery( - `DELETE FROM ${dataSourceSchema}."calendarChannelEventAssociation" - WHERE "id" IN ( - SELECT "calendarChannelEventAssociation"."id" - FROM ${dataSourceSchema}."calendarChannelEventAssociation" "calendarChannelEventAssociation" - JOIN ${dataSourceSchema}."calendarEvent" "calendarEvent" ON "calendarChannelEventAssociation"."calendarEventId" = "calendarEvent"."id" - JOIN ${dataSourceSchema}."calendarEventParticipant" "calendarEventParticipant" ON "calendarEvent"."id" = "calendarEventParticipant"."calendarEventId" - WHERE "calendarEventParticipant"."handle" ${ - isHandleDomain ? 'ILIKE' : '=' - } $1 AND "calendarChannelEventAssociation"."calendarChannelId" = ANY($2) - )`, - [ - isHandleDomain - ? `%${calendarEventParticipantHandle}` - : calendarEventParticipantHandle, - calendarChannelIds, - ], - workspaceId, - transactionManager, - ); - } -} diff --git a/packages/twenty-server/src/modules/calendar/repositories/calendar-channel.repository.ts b/packages/twenty-server/src/modules/calendar/repositories/calendar-channel.repository.ts deleted file mode 100644 index d0cde3ca82fd2..0000000000000 --- a/packages/twenty-server/src/modules/calendar/repositories/calendar-channel.repository.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { EntityManager } from 'typeorm'; - -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity'; -import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; - -@Injectable() -export class CalendarChannelRepository { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - ) {} - - public async getAll( - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarChannel"`, - [], - workspaceId, - transactionManager, - ); - } - - public async create( - calendarChannel: Pick< - ObjectRecord, - 'id' | 'connectedAccountId' | 'handle' | 'visibility' - >, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `INSERT INTO ${dataSourceSchema}."calendarChannel" (id, "connectedAccountId", "handle", "visibility") VALUES ($1, $2, $3, $4)`, - [ - calendarChannel.id, - calendarChannel.connectedAccountId, - calendarChannel.handle, - calendarChannel.visibility, - ], - workspaceId, - transactionManager, - ); - } - - public async getByConnectedAccountId( - connectedAccountId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarChannel" WHERE "connectedAccountId" = $1 LIMIT 1`, - [connectedAccountId], - workspaceId, - transactionManager, - ); - } - - public async getFirstByConnectedAccountId( - connectedAccountId: string, - workspaceId: string, - ): Promise | undefined> { - const calendarChannels = await this.getByConnectedAccountId( - connectedAccountId, - workspaceId, - ); - - return calendarChannels[0]; - } - - public async getByIds( - ids: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarChannel" WHERE "id" = ANY($1)`, - [ids], - workspaceId, - transactionManager, - ); - } - - public async getIdsByWorkspaceMemberId( - workspaceMemberId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const calendarChannelIds = - await this.workspaceDataSourceService.executeRawQuery( - `SELECT "calendarChannel".id FROM ${dataSourceSchema}."calendarChannel" "calendarChannel" - JOIN ${dataSourceSchema}."connectedAccount" ON "calendarChannel"."connectedAccountId" = ${dataSourceSchema}."connectedAccount"."id" - WHERE ${dataSourceSchema}."connectedAccount"."accountOwnerId" = $1`, - [workspaceMemberId], - workspaceId, - transactionManager, - ); - - return calendarChannelIds; - } - - public async updateSyncCursor( - syncCursor: string | null, - calendarChannelId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."calendarChannel" SET "syncCursor" = $1 WHERE "id" = $2`, - [syncCursor || '', calendarChannelId], - workspaceId, - transactionManager, - ); - } -} diff --git a/packages/twenty-server/src/modules/calendar/repositories/calendar-event-participant.repository.ts b/packages/twenty-server/src/modules/calendar/repositories/calendar-event-participant.repository.ts deleted file mode 100644 index f095e100e7d0b..0000000000000 --- a/packages/twenty-server/src/modules/calendar/repositories/calendar-event-participant.repository.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { EntityManager } from 'typeorm'; -import differenceWith from 'lodash.differencewith'; - -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity'; -import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/get-flattened-values-and-values-string-for-batch-raw-query.util'; -import { - CalendarEventParticipant, - CalendarEventParticipantWithId, -} from 'src/modules/calendar/types/calendar-event'; - -@Injectable() -export class CalendarEventParticipantRepository { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - ) {} - - public async getByHandles( - handles: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarEventParticipant" WHERE "handle" = ANY($1)`, - [handles], - workspaceId, - transactionManager, - ); - } - - public async updateParticipantsPersonId( - participantIds: string[], - personId: string, - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "personId" = $1 WHERE "id" = ANY($2)`, - [personId, participantIds], - workspaceId, - transactionManager, - ); - } - - public async updateParticipantsPersonIdAndReturn( - participantIds: string[], - personId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "personId" = $1 WHERE "id" = ANY($2) RETURNING *`, - [personId, participantIds], - workspaceId, - transactionManager, - ); - } - - public async updateParticipantsWorkspaceMemberId( - participantIds: string[], - workspaceMemberId: string, - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "workspaceMemberId" = $1 WHERE "id" = ANY($2)`, - [workspaceMemberId, participantIds], - workspaceId, - transactionManager, - ); - } - - public async removePersonIdByHandle( - handle: string, - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "personId" = NULL WHERE "handle" = $1`, - [handle], - workspaceId, - transactionManager, - ); - } - - public async removeWorkspaceMemberIdByHandle( - handle: string, - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "workspaceMemberId" = NULL WHERE "handle" = $1`, - [handle], - workspaceId, - transactionManager, - ); - } - - public async getByIds( - calendarEventParticipantIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - if (calendarEventParticipantIds.length === 0) { - return []; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarEventParticipant" WHERE "id" = ANY($1)`, - [calendarEventParticipantIds], - workspaceId, - transactionManager, - ); - } - - public async getByCalendarEventIds( - calendarEventIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - if (calendarEventIds.length === 0) { - return []; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarEventParticipant" WHERE "calendarEventId" = ANY($1)`, - [calendarEventIds], - workspaceId, - transactionManager, - ); - } - - public async deleteByIds( - calendarEventParticipantIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - if (calendarEventParticipantIds.length === 0) { - return; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `DELETE FROM ${dataSourceSchema}."calendarEventParticipant" WHERE "id" = ANY($1)`, - [calendarEventParticipantIds], - workspaceId, - transactionManager, - ); - } - - public async updateCalendarEventParticipantsAndReturnNewOnes( - calendarEventParticipants: CalendarEventParticipant[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - if (calendarEventParticipants.length === 0) { - return []; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const existingCalendarEventParticipants = await this.getByCalendarEventIds( - calendarEventParticipants.map( - (calendarEventParticipant) => calendarEventParticipant.calendarEventId, - ), - workspaceId, - transactionManager, - ); - - const calendarEventParticipantsToDelete = differenceWith( - existingCalendarEventParticipants, - calendarEventParticipants, - (existingCalendarEventParticipant, calendarEventParticipant) => - existingCalendarEventParticipant.handle === - calendarEventParticipant.handle, - ); - - const newCalendarEventParticipants = differenceWith( - calendarEventParticipants, - existingCalendarEventParticipants, - (calendarEventParticipant, existingCalendarEventParticipant) => - calendarEventParticipant.handle === - existingCalendarEventParticipant.handle, - ); - - await this.deleteByIds( - calendarEventParticipantsToDelete.map( - (calendarEventParticipant) => calendarEventParticipant.id, - ), - workspaceId, - transactionManager, - ); - - const { flattenedValues, valuesString } = - getFlattenedValuesAndValuesStringForBatchRawQuery( - calendarEventParticipants, - { - calendarEventId: 'uuid', - handle: 'text', - displayName: 'text', - isOrganizer: 'boolean', - responseStatus: `${dataSourceSchema}."calendarEventParticipant_responseStatus_enum"`, - }, - ); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant" - SET "displayName" = "newValues"."displayName", - "isOrganizer" = "newValues"."isOrganizer", - "responseStatus" = "newValues"."responseStatus" - FROM (VALUES ${valuesString}) AS "newValues"("calendarEventId", "handle", "displayName", "isOrganizer", "responseStatus") - WHERE "calendarEventParticipant"."handle" = "newValues"."handle" - AND "calendarEventParticipant"."calendarEventId" = "newValues"."calendarEventId"`, - flattenedValues, - workspaceId, - transactionManager, - ); - - return newCalendarEventParticipants; - } - - public async getWithoutPersonIdAndWorkspaceMemberId( - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - if (!workspaceId) { - throw new Error('WorkspaceId is required'); - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const calendarEventParticipants: CalendarEventParticipantWithId[] = - await this.workspaceDataSourceService.executeRawQuery( - `SELECT "calendarEventParticipant".* - FROM ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant" - WHERE "calendarEventParticipant"."personId" IS NULL - AND "calendarEventParticipant"."workspaceMemberId" IS NULL`, - [], - workspaceId, - transactionManager, - ); - - return calendarEventParticipants; - } - - public async getByCalendarChannelIdWithoutPersonIdAndWorkspaceMemberId( - calendarChannelId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - if (!workspaceId) { - throw new Error('WorkspaceId is required'); - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const calendarEventParticipants: CalendarEventParticipantWithId[] = - await this.workspaceDataSourceService.executeRawQuery( - `SELECT "calendarEventParticipant".* - FROM ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant" - LEFT JOIN ${dataSourceSchema}."calendarEvent" AS "calendarEvent" ON "calendarEventParticipant"."calendarEventId" = "calendarEvent"."id" - LEFT JOIN ${dataSourceSchema}."calendarChannelEventAssociation" AS "calendarChannelEventAssociation" ON "calendarEvent"."id" = "calendarChannelEventAssociation"."calendarEventId" - WHERE "calendarChannelEventAssociation"."calendarChannelId" = $1 - AND "calendarEventParticipant"."personId" IS NULL - AND "calendarEventParticipant"."workspaceMemberId" IS NULL`, - [calendarChannelId], - workspaceId, - transactionManager, - ); - - return calendarEventParticipants; - } -} diff --git a/packages/twenty-server/src/modules/calendar/repositories/calendar-event.repository.ts b/packages/twenty-server/src/modules/calendar/repositories/calendar-event.repository.ts deleted file mode 100644 index f6f28918dd1da..0000000000000 --- a/packages/twenty-server/src/modules/calendar/repositories/calendar-event.repository.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { EntityManager } from 'typeorm'; - -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; -import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity'; -import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/get-flattened-values-and-values-string-for-batch-raw-query.util'; -import { CalendarEvent } from 'src/modules/calendar/types/calendar-event'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity'; - -@Injectable() -export class CalendarEventRepository { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - ) {} - - public async getByIds( - calendarEventIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - if (calendarEventIds.length === 0) { - return []; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarEvent" WHERE "id" = ANY($1)`, - [calendarEventIds], - workspaceId, - transactionManager, - ); - } - - public async getByICalUIDs( - iCalUIDs: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - if (iCalUIDs.length === 0) { - return []; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."calendarEvent" WHERE "iCalUID" = ANY($1)`, - [iCalUIDs], - workspaceId, - transactionManager, - ); - } - - public async deleteByIds( - calendarEventIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - if (calendarEventIds.length === 0) { - return; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `DELETE FROM ${dataSourceSchema}."calendarEvent" WHERE "id" = ANY($1)`, - [calendarEventIds], - workspaceId, - transactionManager, - ); - } - - public async getNonAssociatedCalendarEventIdsPaginated( - limit: number, - offset: number, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise[]> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const nonAssociatedCalendarEvents = - await this.workspaceDataSourceService.executeRawQuery( - `SELECT m.id FROM ${dataSourceSchema}."calendarEvent" m - LEFT JOIN ${dataSourceSchema}."calendarChannelEventAssociation" ccea - ON m.id = ccea."calendarEventId" - WHERE ccea.id IS NULL - LIMIT $1 OFFSET $2`, - [limit, offset], - workspaceId, - transactionManager, - ); - - return nonAssociatedCalendarEvents.map(({ id }) => id); - } - - public async getICalUIDCalendarEventIdMap( - iCalUIDs: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise> { - if (iCalUIDs.length === 0) { - return new Map(); - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const calendarEvents: - | { - id: string; - iCalUID: string; - }[] - | undefined = await this.workspaceDataSourceService.executeRawQuery( - `SELECT id, "iCalUID" FROM ${dataSourceSchema}."calendarEvent" WHERE "iCalUID" = ANY($1)`, - [iCalUIDs], - workspaceId, - transactionManager, - ); - - const iCalUIDsCalendarEventIdsMap = new Map(); - - calendarEvents?.forEach((calendarEvent) => { - iCalUIDsCalendarEventIdsMap.set(calendarEvent.iCalUID, calendarEvent.id); - }); - - return iCalUIDsCalendarEventIdsMap; - } - - public async saveCalendarEvents( - calendarEvents: CalendarEvent[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - if (calendarEvents.length === 0) { - return; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const { flattenedValues, valuesString } = - getFlattenedValuesAndValuesStringForBatchRawQuery(calendarEvents, { - id: 'uuid', - title: 'text', - isCanceled: 'boolean', - isFullDay: 'boolean', - startsAt: 'timestamptz', - endsAt: 'timestamptz', - externalCreatedAt: 'timestamptz', - externalUpdatedAt: 'timestamptz', - description: 'text', - location: 'text', - iCalUID: 'text', - conferenceSolution: 'text', - conferenceLinkLabel: 'text', - conferenceLinkUrl: 'text', - recurringEventExternalId: 'text', - }); - - await this.workspaceDataSourceService.executeRawQuery( - `INSERT INTO ${dataSourceSchema}."calendarEvent" ("id", "title", "isCanceled", "isFullDay", "startsAt", "endsAt", "externalCreatedAt", "externalUpdatedAt", "description", "location", "iCalUID", "conferenceSolution", "conferenceLinkLabel", "conferenceLinkUrl", "recurringEventExternalId") VALUES ${valuesString}`, - flattenedValues, - workspaceId, - transactionManager, - ); - } - - public async updateCalendarEvents( - calendarEvents: CalendarEvent[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - if (calendarEvents.length === 0) { - return; - } - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const { flattenedValues, valuesString } = - getFlattenedValuesAndValuesStringForBatchRawQuery(calendarEvents, { - title: 'text', - isCanceled: 'boolean', - isFullDay: 'boolean', - startsAt: 'timestamptz', - endsAt: 'timestamptz', - externalCreatedAt: 'timestamptz', - externalUpdatedAt: 'timestamptz', - description: 'text', - location: 'text', - iCalUID: 'text', - conferenceSolution: 'text', - conferenceLinkLabel: 'text', - conferenceLinkUrl: 'text', - recurringEventExternalId: 'text', - }); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."calendarEvent" AS "calendarEvent" - SET "title" = "newData"."title", - "isCanceled" = "newData"."isCanceled", - "isFullDay" = "newData"."isFullDay", - "startsAt" = "newData"."startsAt", - "endsAt" = "newData"."endsAt", - "externalCreatedAt" = "newData"."externalCreatedAt", - "externalUpdatedAt" = "newData"."externalUpdatedAt", - "description" = "newData"."description", - "location" = "newData"."location", - "conferenceSolution" = "newData"."conferenceSolution", - "conferenceLinkLabel" = "newData"."conferenceLinkLabel", - "conferenceLinkUrl" = "newData"."conferenceLinkUrl", - "recurringEventExternalId" = "newData"."recurringEventExternalId" - FROM (VALUES ${valuesString}) - AS "newData"("title", "isCanceled", "isFullDay", "startsAt", "endsAt", "externalCreatedAt", "externalUpdatedAt", "description", "location", "iCalUID", "conferenceSolution", "conferenceLinkLabel", "conferenceLinkUrl", "recurringEventExternalId") - WHERE "calendarEvent"."iCalUID" = "newData"."iCalUID"`, - flattenedValues, - workspaceId, - transactionManager, - ); - } -} diff --git a/packages/twenty-server/src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.module.ts b/packages/twenty-server/src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.module.ts index 644371db16f69..12f92a4ec8df4 100644 --- a/packages/twenty-server/src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.module.ts +++ b/packages/twenty-server/src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.module.ts @@ -1,13 +1,11 @@ import { Module } from '@nestjs/common'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { CalendarEventCleanerService } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service'; import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity'; @Module({ - imports: [ - ObjectMetadataRepositoryModule.forFeature([CalendarEventWorkspaceEntity]), - ], + imports: [TwentyORMModule.forFeature([CalendarEventWorkspaceEntity])], providers: [CalendarEventCleanerService], exports: [CalendarEventCleanerService], }) diff --git a/packages/twenty-server/src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service.ts b/packages/twenty-server/src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service.ts index 9f3f5d5202989..77a347a0d6c73 100644 --- a/packages/twenty-server/src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service.ts +++ b/packages/twenty-server/src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service.ts @@ -1,27 +1,40 @@ import { Injectable } from '@nestjs/common'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { CalendarEventRepository } from 'src/modules/calendar/repositories/calendar-event.repository'; +import { Any, IsNull } from 'typeorm'; + +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity'; import { deleteUsingPagination } from 'src/modules/messaging/message-cleaner/utils/delete-using-pagination.util'; @Injectable() export class CalendarEventCleanerService { constructor( - @InjectObjectMetadataRepository(CalendarEventWorkspaceEntity) - private readonly calendarEventRepository: CalendarEventRepository, + @InjectWorkspaceRepository(CalendarEventWorkspaceEntity) + private readonly calendarEventRepository: WorkspaceRepository, ) {} public async cleanWorkspaceCalendarEvents(workspaceId: string) { await deleteUsingPagination( workspaceId, 500, - this.calendarEventRepository.getNonAssociatedCalendarEventIdsPaginated.bind( - this.calendarEventRepository, - ), - this.calendarEventRepository.deleteByIds.bind( - this.calendarEventRepository, - ), + async (limit, offset) => { + const nonAssociatedCalendarEvents = + await this.calendarEventRepository.find({ + where: { + calendarChannelEventAssociations: { + id: IsNull(), + }, + }, + take: limit, + skip: offset, + }); + + return nonAssociatedCalendarEvents.map(({ id }) => id); + }, + async (ids) => { + await this.calendarEventRepository.delete({ id: Any(ids) }); + }, ); } } diff --git a/packages/twenty-server/src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module.ts b/packages/twenty-server/src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module.ts index 4eabeedb997a1..212a087dbf44f 100644 --- a/packages/twenty-server/src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module.ts +++ b/packages/twenty-server/src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module.ts @@ -1,14 +1,17 @@ import { Module } from '@nestjs/common'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { AddPersonIdAndWorkspaceMemberIdModule } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.module'; import { CalendarEventParticipantService } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service'; +import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; @Module({ imports: [ WorkspaceDataSourceModule, + TwentyORMModule.forFeature([CalendarEventParticipantWorkspaceEntity]), ObjectMetadataRepositoryModule.forFeature([PersonWorkspaceEntity]), AddPersonIdAndWorkspaceMemberIdModule, ], diff --git a/packages/twenty-server/src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service.ts b/packages/twenty-server/src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service.ts index 4b524de0dedc2..dee6a2aeadb09 100644 --- a/packages/twenty-server/src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service.ts +++ b/packages/twenty-server/src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { EntityManager } from 'typeorm'; +import { Any, EntityManager } from 'typeorm'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { PersonRepository } from 'src/modules/person/repositories/person.repository'; @@ -9,17 +9,18 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/get-flattened-values-and-values-string-for-batch-raw-query.util'; import { CalendarEventParticipant } from 'src/modules/calendar/types/calendar-event'; -import { CalendarEventParticipantRepository } from 'src/modules/calendar/repositories/calendar-event-participant.repository'; import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity'; import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @Injectable() export class CalendarEventParticipantService { constructor( private readonly workspaceDataSourceService: WorkspaceDataSourceService, - @InjectObjectMetadataRepository(CalendarEventParticipantWorkspaceEntity) - private readonly calendarEventParticipantRepository: CalendarEventParticipantRepository, + @InjectWorkspaceRepository(CalendarEventParticipantWorkspaceEntity) + private readonly calendarEventParticipantRepository: WorkspaceRepository, @InjectObjectMetadataRepository(PersonWorkspaceEntity) private readonly personRepository: PersonRepository, private readonly addPersonIdAndWorkspaceMemberIdService: AddPersonIdAndWorkspaceMemberIdService, @@ -31,11 +32,11 @@ export class CalendarEventParticipantService { workspaceId: string, transactionManager?: EntityManager, ): Promise[]> { - const participants = - await this.calendarEventParticipantRepository.getByHandles( - createdPeople.map((person) => person.email), - workspaceId, - ); + const participants = await this.calendarEventParticipantRepository.find({ + where: { + handle: Any(createdPeople.map((person) => person.email)), + }, + }); if (!participants) return []; @@ -132,33 +133,50 @@ export class CalendarEventParticipantService { workspaceMemberId?: string, ) { const calendarEventParticipantsToUpdate = - await this.calendarEventParticipantRepository.getByHandles( - [email], - workspaceId, - ); + await this.calendarEventParticipantRepository.find({ + where: { + handle: email, + }, + }); const calendarEventParticipantIdsToUpdate = calendarEventParticipantsToUpdate.map((participant) => participant.id); if (personId) { + await this.calendarEventParticipantRepository.update( + { + id: Any(calendarEventParticipantIdsToUpdate), + }, + { + person: { + id: personId, + }, + }, + ); + const updatedCalendarEventParticipants = - await this.calendarEventParticipantRepository.updateParticipantsPersonIdAndReturn( - calendarEventParticipantIdsToUpdate, - personId, - workspaceId, - ); + await this.calendarEventParticipantRepository.find({ + where: { + id: Any(calendarEventParticipantIdsToUpdate), + }, + }); this.eventEmitter.emit(`calendarEventParticipant.matched`, { workspaceId, - userId: null, + workspaceMemberId: null, calendarEventParticipants: updatedCalendarEventParticipants, }); } if (workspaceMemberId) { - await this.calendarEventParticipantRepository.updateParticipantsWorkspaceMemberId( - calendarEventParticipantIdsToUpdate, - workspaceMemberId, - workspaceId, + await this.calendarEventParticipantRepository.update( + { + id: Any(calendarEventParticipantIdsToUpdate), + }, + { + workspaceMember: { + id: workspaceMemberId, + }, + }, ); } } @@ -170,15 +188,23 @@ export class CalendarEventParticipantService { workspaceMemberId?: string, ) { if (personId) { - await this.calendarEventParticipantRepository.removePersonIdByHandle( - handle, - workspaceId, + await this.calendarEventParticipantRepository.update( + { + handle, + }, + { + person: null, + }, ); } if (workspaceMemberId) { - await this.calendarEventParticipantRepository.removeWorkspaceMemberIdByHandle( - handle, - workspaceId, + await this.calendarEventParticipantRepository.update( + { + handle, + }, + { + workspaceMember: null, + }, ); } } diff --git a/packages/twenty-server/src/modules/calendar/services/google-calendar-sync/google-calendar-sync.module.ts b/packages/twenty-server/src/modules/calendar/services/google-calendar-sync/google-calendar-sync.module.ts index fcd5e495ce19c..11017c7f4cf3d 100644 --- a/packages/twenty-server/src/modules/calendar/services/google-calendar-sync/google-calendar-sync.module.ts +++ b/packages/twenty-server/src/modules/calendar/services/google-calendar-sync/google-calendar-sync.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { CalendarEventCleanerModule } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.module'; import { CalendarEventParticipantModule } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module'; @@ -20,12 +21,14 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Module({ imports: [ CalendarProvidersModule, - ObjectMetadataRepositoryModule.forFeature([ - ConnectedAccountWorkspaceEntity, + TwentyORMModule.forFeature([ CalendarEventWorkspaceEntity, CalendarChannelWorkspaceEntity, CalendarChannelEventAssociationWorkspaceEntity, CalendarEventParticipantWorkspaceEntity, + ]), + ObjectMetadataRepositoryModule.forFeature([ + ConnectedAccountWorkspaceEntity, BlocklistWorkspaceEntity, PersonWorkspaceEntity, WorkspaceMemberWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/calendar/services/google-calendar-sync/google-calendar-sync.service.ts b/packages/twenty-server/src/modules/calendar/services/google-calendar-sync/google-calendar-sync.service.ts index 17681f17e3502..a7d653967ac48 100644 --- a/packages/twenty-server/src/modules/calendar/services/google-calendar-sync/google-calendar-sync.service.ts +++ b/packages/twenty-server/src/modules/calendar/services/google-calendar-sync/google-calendar-sync.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { Repository } from 'typeorm'; +import { Any, Repository } from 'typeorm'; import { calendar_v3 as calendarV3 } from 'googleapis'; import { GaxiosError } from 'gaxios'; @@ -13,12 +13,7 @@ import { FeatureFlagKeys, } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { GoogleCalendarClientProvider } from 'src/modules/calendar/services/providers/google-calendar/google-calendar.provider'; -import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository'; -import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { CalendarEventRepository } from 'src/modules/calendar/repositories/calendar-event.repository'; import { formatGoogleCalendarEvent } from 'src/modules/calendar/utils/format-google-calendar-event.util'; -import { CalendarEventParticipantRepository } from 'src/modules/calendar/repositories/calendar-event-participant.repository'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity'; @@ -28,7 +23,10 @@ import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/st import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity'; import { CalendarEventCleanerService } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service'; import { CalendarEventParticipantService } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service'; -import { CalendarEventWithParticipants } from 'src/modules/calendar/types/calendar-event'; +import { + CalendarEventParticipant, + CalendarEventWithParticipants, +} from 'src/modules/calendar/types/calendar-event'; import { filterOutBlocklistedEvents } from 'src/modules/calendar/utils/filter-out-blocklisted-events.util'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; @@ -37,7 +35,12 @@ import { CreateCompanyAndContactJob, CreateCompanyAndContactJobData, } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; +import { isDefined } from 'src/utils/is-defined'; +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; @Injectable() export class GoogleCalendarSyncService { @@ -47,21 +50,20 @@ export class GoogleCalendarSyncService { private readonly googleCalendarClientProvider: GoogleCalendarClientProvider, @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) private readonly connectedAccountRepository: ConnectedAccountRepository, - @InjectObjectMetadataRepository(CalendarEventWorkspaceEntity) - private readonly calendarEventRepository: CalendarEventRepository, - @InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity) - private readonly calendarChannelRepository: CalendarChannelRepository, - @InjectObjectMetadataRepository( - CalendarChannelEventAssociationWorkspaceEntity, - ) - private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository, - @InjectObjectMetadataRepository(CalendarEventParticipantWorkspaceEntity) - private readonly calendarEventParticipantsRepository: CalendarEventParticipantRepository, + @InjectWorkspaceRepository(CalendarEventWorkspaceEntity) + private readonly calendarEventRepository: WorkspaceRepository, + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, + @InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity) + private readonly calendarChannelEventAssociationRepository: WorkspaceRepository, + @InjectWorkspaceRepository(CalendarEventParticipantWorkspaceEntity) + private readonly calendarEventParticipantsRepository: WorkspaceRepository, @InjectObjectMetadataRepository(BlocklistWorkspaceEntity) private readonly blocklistRepository: BlocklistRepository, @InjectRepository(FeatureFlagEntity, 'core') private readonly featureFlagRepository: Repository, - private readonly workspaceDataSourceService: WorkspaceDataSourceService, + @InjectWorkspaceDatasource() + private readonly workspaceDataSource: WorkspaceDataSource, private readonly calendarEventCleanerService: CalendarEventCleanerService, private readonly calendarEventParticipantsService: CalendarEventParticipantService, @InjectMessageQueue(MessageQueue.contactCreationQueue) @@ -92,11 +94,11 @@ export class GoogleCalendarSyncService { ); } - const calendarChannel = - await this.calendarChannelRepository.getFirstByConnectedAccountId( - connectedAccountId, - workspaceId, - ); + const calendarChannel = await this.calendarChannelRepository.findOneBy({ + connectedAccount: { + id: connectedAccountId, + }, + }); const syncToken = calendarChannel?.syncCursor || undefined; @@ -122,6 +124,12 @@ export class GoogleCalendarSyncService { return; } + if (!workspaceMemberId) { + throw new Error( + `Workspace member ID is undefined for connected account ${connectedAccountId} in workspace ${workspaceId}`, + ); + } + const blocklist = await this.getBlocklist(workspaceMemberId, workspaceId); let filteredEvents = filterOutBlocklistedEvents( @@ -143,11 +151,18 @@ export class GoogleCalendarSyncService { .filter((event) => event.status === 'cancelled') .map((event) => event.id as string); - const iCalUIDCalendarEventIdMap = - await this.calendarEventRepository.getICalUIDCalendarEventIdMap( - filteredEvents.map((calendarEvent) => calendarEvent.iCalUID as string), - workspaceId, - ); + const existingCalendarEvents = await this.calendarEventRepository.find({ + where: { + iCalUID: Any(filteredEvents.map((event) => event.iCalUID as string)), + }, + }); + + const iCalUIDCalendarEventIdMap = new Map( + existingCalendarEvents.map((calendarEvent) => [ + calendarEvent.iCalUID, + calendarEvent.id, + ]), + ); const formattedEvents = filteredEvents.map((event) => formatGoogleCalendarEvent(event, iCalUIDCalendarEventIdMap), @@ -157,31 +172,34 @@ export class GoogleCalendarSyncService { let startTime = Date.now(); - const existingEvents = await this.calendarEventRepository.getByICalUIDs( - formattedEvents.map((event) => event.iCalUID), - workspaceId, + const existingEventsICalUIDs = existingCalendarEvents.map( + (calendarEvent) => calendarEvent.iCalUID, ); - const existingEventsICalUIDs = existingEvents.map((event) => event.iCalUID); - let endTime = Date.now(); const eventsToSave = formattedEvents.filter( - (event) => !existingEventsICalUIDs.includes(event.iCalUID), + (calendarEvent) => + !existingEventsICalUIDs.includes(calendarEvent.iCalUID), ); - const eventsToUpdate = formattedEvents.filter((event) => - existingEventsICalUIDs.includes(event.iCalUID), + const eventsToUpdate = formattedEvents.filter((calendarEvent) => + existingEventsICalUIDs.includes(calendarEvent.iCalUID), ); startTime = Date.now(); const existingCalendarChannelEventAssociations = - await this.calendarChannelEventAssociationRepository.getByEventExternalIdsAndCalendarChannelId( - formattedEvents.map((event) => event.externalId), - calendarChannelId, - workspaceId, - ); + await this.calendarChannelEventAssociationRepository.find({ + where: { + eventExternalId: Any( + formattedEvents.map((calendarEvent) => calendarEvent.id), + ), + calendarChannel: { + id: calendarChannelId, + }, + }, + }); endTime = Date.now(); @@ -193,14 +211,14 @@ export class GoogleCalendarSyncService { const calendarChannelEventAssociationsToSave = formattedEvents .filter( - (event) => + (calendarEvent) => !existingCalendarChannelEventAssociations.some( - (association) => association.eventExternalId === event.id, + (association) => association.eventExternalId === calendarEvent.id, ), ) - .map((event) => ({ - calendarEventId: event.id, - eventExternalId: event.externalId, + .map((calendarEvent) => ({ + calendarEventId: calendarEvent.id, + eventExternalId: calendarEvent.externalId, calendarChannelId, })); @@ -216,11 +234,12 @@ export class GoogleCalendarSyncService { startTime = Date.now(); - await this.calendarChannelEventAssociationRepository.deleteByEventExternalIdsAndCalendarChannelId( - cancelledEventExternalIds, - calendarChannelId, - workspaceId, - ); + await this.calendarChannelEventAssociationRepository.delete({ + eventExternalId: Any(cancelledEventExternalIds), + calendarChannel: { + id: calendarChannelId, + }, + }); endTime = Date.now(); @@ -257,10 +276,13 @@ export class GoogleCalendarSyncService { startTime = Date.now(); - await this.calendarChannelRepository.updateSyncCursor( - nextSyncToken, - calendarChannel.id, - workspaceId, + await this.calendarChannelRepository.update( + { + id: calendarChannel.id, + }, + { + syncCursor: nextSyncToken, + }, ); endTime = Date.now(); @@ -337,10 +359,13 @@ export class GoogleCalendarSyncService { throw error; } - await this.calendarChannelRepository.updateSyncCursor( - null, - connectedAccountId, - workspaceId, + await this.calendarChannelRepository.update( + { + id: connectedAccountId, + }, + { + syncCursor: '', + }, ); this.logger.log( @@ -395,11 +420,6 @@ export class GoogleCalendarSyncService { calendarChannel: CalendarChannelWorkspaceEntity, workspaceId: string, ): Promise { - const dataSourceMetadata = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); - const participantsToSave = eventsToSave.flatMap( (event) => event.participants, ); @@ -415,103 +435,154 @@ export class GoogleCalendarSyncService { []; try { - await dataSourceMetadata?.transaction(async (transactionManager) => { - startTime = Date.now(); + await this.workspaceDataSource?.transaction( + async (transactionManager) => { + startTime = Date.now(); - await this.calendarEventRepository.saveCalendarEvents( - eventsToSave, - workspaceId, - transactionManager, - ); + await this.calendarEventRepository.save( + eventsToSave, + {}, + transactionManager, + ); - endTime = Date.now(); + endTime = Date.now(); - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: saving ${eventsToSave.length} events in ${endTime - startTime}ms.`, - ); + this.logger.log( + `google calendar sync for workspace ${workspaceId} and account ${ + connectedAccount.id + }: saving ${eventsToSave.length} events in ${ + endTime - startTime + }ms.`, + ); - startTime = Date.now(); + startTime = Date.now(); - await this.calendarEventRepository.updateCalendarEvents( - eventsToUpdate, - workspaceId, - transactionManager, - ); + await this.calendarChannelRepository.save( + eventsToUpdate, + {}, + transactionManager, + ); - endTime = Date.now(); + endTime = Date.now(); - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: updating ${eventsToUpdate.length} events in ${ - endTime - startTime - }ms.`, - ); + this.logger.log( + `google calendar sync for workspace ${workspaceId} and account ${ + connectedAccount.id + }: updating ${eventsToUpdate.length} events in ${ + endTime - startTime + }ms.`, + ); - startTime = Date.now(); + startTime = Date.now(); - await this.calendarChannelEventAssociationRepository.saveCalendarChannelEventAssociations( - calendarChannelEventAssociationsToSave, - workspaceId, - transactionManager, - ); + await this.calendarChannelEventAssociationRepository.save( + calendarChannelEventAssociationsToSave, + {}, + transactionManager, + ); - endTime = Date.now(); + endTime = Date.now(); - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: saving calendar channel event associations in ${ - endTime - startTime - }ms.`, - ); + this.logger.log( + `google calendar sync for workspace ${workspaceId} and account ${ + connectedAccount.id + }: saving calendar channel event associations in ${ + endTime - startTime + }ms.`, + ); - startTime = Date.now(); + startTime = Date.now(); + + const existingCalendarEventParticipants = + await this.calendarEventParticipantsRepository.find({ + where: { + calendarEvent: { + id: Any( + participantsToUpdate + .map((participant) => participant.calendarEventId) + .filter(isDefined), + ), + }, + }, + }); + + const { + calendarEventParticipantsToDelete, + newCalendarEventParticipants, + } = participantsToUpdate.reduce( + (acc, calendarEventParticipant) => { + const existingCalendarEventParticipant = + existingCalendarEventParticipants.find( + (existingCalendarEventParticipant) => + existingCalendarEventParticipant.handle === + calendarEventParticipant.handle, + ); + + if (existingCalendarEventParticipant) { + acc.calendarEventParticipantsToDelete.push( + existingCalendarEventParticipant, + ); + } else { + acc.newCalendarEventParticipants.push(calendarEventParticipant); + } + + return acc; + }, + { + calendarEventParticipantsToDelete: + [] as CalendarEventParticipantWorkspaceEntity[], + newCalendarEventParticipants: [] as CalendarEventParticipant[], + }, + ); + + await this.calendarEventParticipantsRepository.delete({ + id: Any( + calendarEventParticipantsToDelete.map( + (calendarEventParticipant) => calendarEventParticipant.id, + ), + ), + }); - const newCalendarEventParticipants = - await this.calendarEventParticipantsRepository.updateCalendarEventParticipantsAndReturnNewOnes( + await this.calendarEventParticipantsRepository.save( participantsToUpdate, - workspaceId, - transactionManager, ); - endTime = Date.now(); + endTime = Date.now(); - participantsToSave.push(...newCalendarEventParticipants); + participantsToSave.push(...newCalendarEventParticipants); - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: updating participants in ${endTime - startTime}ms.`, - ); + this.logger.log( + `google calendar sync for workspace ${workspaceId} and account ${ + connectedAccount.id + }: updating participants in ${endTime - startTime}ms.`, + ); - startTime = Date.now(); + startTime = Date.now(); - const savedCalendarEventParticipants = - await this.calendarEventParticipantsService.saveCalendarEventParticipants( - participantsToSave, - workspaceId, - transactionManager, - ); + const savedCalendarEventParticipants = + await this.calendarEventParticipantsService.saveCalendarEventParticipants( + participantsToSave, + workspaceId, + transactionManager, + ); - savedCalendarEventParticipantsToEmit.push( - ...savedCalendarEventParticipants, - ); + savedCalendarEventParticipantsToEmit.push( + ...savedCalendarEventParticipants, + ); - endTime = Date.now(); + endTime = Date.now(); - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: saving participants in ${endTime - startTime}ms.`, - ); - }); + this.logger.log( + `google calendar sync for workspace ${workspaceId} and account ${ + connectedAccount.id + }: saving participants in ${endTime - startTime}ms.`, + ); + }, + ); this.eventEmitter.emit(`calendarEventParticipant.matched`, { workspaceId, - userId: connectedAccount.accountOwnerId, + workspaceMemberId: connectedAccount.accountOwnerId, calendarEventParticipants: savedCalendarEventParticipantsToEmit, }); diff --git a/packages/twenty-server/src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module.ts b/packages/twenty-server/src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module.ts index 877de36e89d5d..f2c009caf22ea 100644 --- a/packages/twenty-server/src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module.ts +++ b/packages/twenty-server/src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module.ts @@ -1,13 +1,11 @@ import { Module } from '@nestjs/common'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceGoogleCalendarSyncService } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.service'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity'; @Module({ - imports: [ - ObjectMetadataRepositoryModule.forFeature([CalendarChannelWorkspaceEntity]), - ], + imports: [TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity])], providers: [WorkspaceGoogleCalendarSyncService], exports: [WorkspaceGoogleCalendarSyncService], }) diff --git a/packages/twenty-server/src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.service.ts b/packages/twenty-server/src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.service.ts index 8d68c01d88c8a..9b2fd6c5aff2c 100644 --- a/packages/twenty-server/src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.service.ts +++ b/packages/twenty-server/src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.service.ts @@ -3,19 +3,19 @@ import { Injectable } from '@nestjs/common'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { GoogleCalendarSyncJobData, GoogleCalendarSyncJob, } from 'src/modules/calendar/jobs/google-calendar-sync.job'; -import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity'; @Injectable() export class WorkspaceGoogleCalendarSyncService { constructor( - @InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity) - private readonly calendarChannelRepository: CalendarChannelRepository, + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, @InjectMessageQueue(MessageQueue.calendarQueue) private readonly messageQueueService: MessageQueueService, ) {} @@ -23,8 +23,7 @@ export class WorkspaceGoogleCalendarSyncService { public async startWorkspaceGoogleCalendarSync( workspaceId: string, ): Promise { - const calendarChannels = - await this.calendarChannelRepository.getAll(workspaceId); + const calendarChannels = await this.calendarChannelRepository.find({}); for (const calendarChannel of calendarChannels) { if (!calendarChannel?.isSyncEnabled) { @@ -35,7 +34,7 @@ export class WorkspaceGoogleCalendarSyncService { GoogleCalendarSyncJob.name, { workspaceId, - connectedAccountId: calendarChannel.connectedAccountId, + connectedAccountId: calendarChannel.connectedAccount.id, }, { retryLimit: 2, diff --git a/packages/twenty-server/src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity.ts index ee271b437998f..6e6d81f2252ef 100644 --- a/packages/twenty-server/src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity.ts @@ -120,7 +120,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity inverseSideFieldKey: 'calendarEventParticipants', }) @WorkspaceIsNullable() - person: Relation; + person: Relation | null; @WorkspaceRelation({ standardId: CALENDAR_EVENT_PARTICIPANT_STANDARD_FIELD_IDS.workspaceMember, @@ -133,5 +133,5 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity inverseSideFieldKey: 'calendarEventParticipants', }) @WorkspaceIsNullable() - workspaceMember: Relation; + workspaceMember: Relation | null; } diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index 867b11e023e95..490a33919c289 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -68,7 +68,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconUsers', }) @WorkspaceIsNullable() - employees: number; + employees: number | null; @WorkspaceField({ standardId: COMPANY_STANDARD_FIELD_IDS.linkedinLink, @@ -78,7 +78,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconBrandLinkedin', }) @WorkspaceIsNullable() - linkedinLink: LinkMetadata; + linkedinLink: LinkMetadata | null; @WorkspaceField({ standardId: COMPANY_STANDARD_FIELD_IDS.xLink, @@ -88,7 +88,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconBrandX', }) @WorkspaceIsNullable() - xLink: LinkMetadata; + xLink: LinkMetadata | null; @WorkspaceField({ standardId: COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue, @@ -99,7 +99,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconMoneybag', }) @WorkspaceIsNullable() - annualRecurringRevenue: CurrencyMetadata; + annualRecurringRevenue: CurrencyMetadata | null; @WorkspaceField({ standardId: COMPANY_STANDARD_FIELD_IDS.idealCustomerProfile, @@ -121,7 +121,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsSystem() @WorkspaceIsNullable() - position: number; + position: number | null; // Relations @WorkspaceRelation({ @@ -149,7 +149,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { onDelete: RelationOnDeleteAction.SET_NULL, }) @WorkspaceIsNullable() - accountOwner: Relation; + accountOwner: Relation | null; @WorkspaceRelation({ standardId: COMPANY_STANDARD_FIELD_IDS.activityTargets, diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module.ts index 5e6d0fa68a73b..15a6131d1dffc 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module.ts @@ -8,7 +8,6 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity'; import { CalendarEventParticipantModule } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; @@ -20,7 +19,6 @@ import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-co ObjectMetadataRepositoryModule.forFeature([ PersonWorkspaceEntity, WorkspaceMemberWorkspaceEntity, - CalendarEventParticipantWorkspaceEntity, ]), MessagingCommonModule, WorkspaceDataSourceModule, diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job.ts index 744dcf294c2b0..7124d1467d8d2 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job.ts @@ -1,13 +1,12 @@ import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; export type CreateCompanyAndContactJobData = { workspaceId: string; - connectedAccount: ObjectRecord; + connectedAccount: ConnectedAccountWorkspaceEntity; contactsToCreate: { displayName: string; handle: string; diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service.ts index 7fd46851031f1..abe0f2603a910 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service.ts @@ -15,14 +15,15 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { getUniqueContactsAndHandles } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-unique-contacts-and-handles.util'; import { Contacts } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { CalendarEventParticipantService } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service'; import { filterOutContactsFromCompanyOrWorkspace } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util'; import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record'; import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity'; +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Injectable() export class CreateCompanyAndContactService { @@ -33,7 +34,8 @@ export class CreateCompanyAndContactService { private readonly personRepository: PersonRepository, @InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity) private readonly workspaceMemberRepository: WorkspaceMemberRepository, - private readonly workspaceDataSourceService: WorkspaceDataSourceService, + @InjectWorkspaceDatasource() + private readonly workspaceDataSource: WorkspaceDataSource, private readonly messageParticipantService: MessagingMessageParticipantService, private readonly calendarEventParticipantService: CalendarEventParticipantService, private readonly eventEmitter: EventEmitter2, @@ -130,21 +132,16 @@ export class CreateCompanyAndContactService { } async createCompaniesAndContactsAndUpdateParticipants( - connectedAccount: ObjectRecord, + connectedAccount: ConnectedAccountWorkspaceEntity, contactsToCreate: Contacts, workspaceId: string, ) { - const { dataSource: workspaceDataSource } = - await this.workspaceDataSourceService.connectedToWorkspaceDataSourceAndReturnMetadata( - workspaceId, - ); - let updatedMessageParticipants: ObjectRecord[] = []; let updatedCalendarEventParticipants: ObjectRecord[] = []; - await workspaceDataSource?.transaction( + await this.workspaceDataSource?.transaction( async (transactionManager: EntityManager) => { const createdPeople = await this.createCompaniesAndPeople( connectedAccount.handle, @@ -171,13 +168,13 @@ export class CreateCompanyAndContactService { this.eventEmitter.emit(`messageParticipant.matched`, { workspaceId, - userId: connectedAccount.accountOwnerId, + workspaceMemberId: connectedAccount.accountOwnerId, messageParticipants: updatedMessageParticipants, }); this.eventEmitter.emit(`calendarEventParticipant.matched`, { workspaceId, - userId: connectedAccount.accountOwnerId, + workspaceMemberId: connectedAccount.accountOwnerId, calendarEventParticipants: updatedCalendarEventParticipants, }); } diff --git a/packages/twenty-server/src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.service.ts b/packages/twenty-server/src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.service.ts index dc687e1a4c194..31fc052ce654c 100644 --- a/packages/twenty-server/src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.service.ts +++ b/packages/twenty-server/src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.service.ts @@ -80,6 +80,12 @@ export class GoogleAPIRefreshAccessTokenService { workspaceId, ); + if (!messageChannel.connectedAccountId) { + throw new Error( + `No connected account ID found for message channel ${messageChannel.id} in workspace ${workspaceId}`, + ); + } + await this.connectedAccountRepository.updateAuthFailedAt( messageChannel.connectedAccountId, workspaceId, diff --git a/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts b/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts index 481f319703e36..5302d08ee9ee7 100644 --- a/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts +++ b/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts @@ -86,7 +86,7 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconX', }) @WorkspaceIsNullable() - authFailedAt: Date; + authFailedAt: Date | null; @WorkspaceRelation({ standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.accountOwner, @@ -100,6 +100,8 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity { }) accountOwner: Relation; + accountOwnerId: string; + @WorkspaceRelation({ standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.messageChannels, type: RelationMetadataType.ONE_TO_MANY, diff --git a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts index 6c1f81b6b3b33..9995a4af7fdfb 100644 --- a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts +++ b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts @@ -63,7 +63,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'favorites', }) @WorkspaceIsNullable() - person: Relation; + person: Relation | null; @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.company, @@ -76,7 +76,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'favorites', }) @WorkspaceIsNullable() - company: Relation; + company: Relation | null; @WorkspaceRelation({ standardId: FAVORITE_STANDARD_FIELD_IDS.opportunity, @@ -89,7 +89,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'favorites', }) @WorkspaceIsNullable() - opportunity: Relation; + opportunity: Relation | null; @WorkspaceDynamicRelation({ type: RelationMetadataType.MANY_TO_ONE, diff --git a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts index 89b7200406aff..00f146f7ecd36 100644 --- a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts +++ b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts @@ -56,6 +56,12 @@ export class BlocklistItemDeleteMessagesJob { `Deleting messages from ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId}`, ); + if (!workspaceMemberId) { + throw new Error( + `Workspace member ID is not defined for blocklist item ${blocklistItemId} in workspace ${workspaceId}`, + ); + } + const messageChannels = await this.messageChannelRepository.getIdsByWorkspaceMemberId( workspaceMemberId, diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/can-access-message-thread.service.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/can-access-message-thread.service.ts index 095523ee63582..f79c972c61004 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/can-access-message-thread.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/can-access-message-thread.service.ts @@ -9,6 +9,7 @@ import { MessageChannelRepository } from 'src/modules/messaging/common/repositor import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { isDefined } from 'src/utils/is-defined'; export class CanAccessMessageThreadService { constructor( @@ -46,7 +47,9 @@ export class CanAccessMessageThreadService { const messageChannelsConnectedAccounts = await this.connectedAccountRepository.getByIds( - messageChannels.map((channel) => channel.connectedAccountId), + messageChannels + .map((channel) => channel.connectedAccountId) + .filter(isDefined), workspaceId, ); diff --git a/packages/twenty-server/src/modules/messaging/common/services/messaging-error-handling.service.ts b/packages/twenty-server/src/modules/messaging/common/services/messaging-error-handling.service.ts index c8cdce1d02a98..fb3c7b99d21c1 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/messaging-error-handling.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/services/messaging-error-handling.service.ts @@ -211,6 +211,12 @@ export class MessagingErrorHandlingService { workspaceId, ); + if (!messageChannel.connectedAccountId) { + throw new Error( + `Connected account ID is not defined for message channel ${messageChannel.id} in workspace ${workspaceId}`, + ); + } + await this.connectedAccountRepository.updateAuthFailedAt( messageChannel.connectedAccountId, workspaceId, diff --git a/packages/twenty-server/src/modules/messaging/common/services/messaging-message-participant.service.ts b/packages/twenty-server/src/modules/messaging/common/services/messaging-message-participant.service.ts index 78a5623bbaace..527a3951c2e77 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/messaging-message-participant.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/services/messaging-message-participant.service.ts @@ -149,7 +149,7 @@ export class MessagingMessageParticipantService { this.eventEmitter.emit(`messageParticipant.matched`, { workspaceId, - userId: null, + workspaceMemberId: null, messageParticipants: updatedMessageParticipants, }); } diff --git a/packages/twenty-server/src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service.ts b/packages/twenty-server/src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service.ts index 9b288566a9950..1493acb8a559a 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service.ts @@ -107,7 +107,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService { this.eventEmitter.emit(`messageParticipant.matched`, { workspaceId, - userId: connectedAccount.accountOwnerId, + workspaceMemberId: connectedAccount.accountOwnerId, messageParticipants: savedMessageParticipants, }); diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts index 91e9fa828209f..8792293d6e1a5 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts @@ -35,7 +35,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa icon: 'IconHash', }) @WorkspaceIsNullable() - messageExternalId: string; + messageExternalId: string | null; @WorkspaceField({ standardId: @@ -46,7 +46,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa icon: 'IconHash', }) @WorkspaceIsNullable() - messageThreadExternalId: string; + messageThreadExternalId: string | null; @WorkspaceRelation({ standardId: @@ -60,7 +60,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa inverseSideFieldKey: 'messageChannelMessageAssociations', }) @WorkspaceIsNullable() - messageChannel: Relation; + messageChannel: Relation | null; @WorkspaceRelation({ standardId: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.message, @@ -73,7 +73,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa inverseSideFieldKey: 'messageChannelMessageAssociations', }) @WorkspaceIsNullable() - message: Relation; + message: Relation | null; @WorkspaceRelation({ standardId: @@ -87,5 +87,5 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa inverseSideFieldKey: 'messageChannelMessageAssociations', }) @WorkspaceIsNullable() - messageThread: Relation; + messageThread: Relation | null; } diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts index 92175c6afbf84..b2bf40b1bc9fc 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts @@ -162,7 +162,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconHistory', }) @WorkspaceIsNullable() - syncedAt: string; + syncedAt: string | null; @WorkspaceField({ standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncStatus, @@ -224,7 +224,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity { ], }) @WorkspaceIsNullable() - syncStatus: MessageChannelSyncStatus; + syncStatus: MessageChannelSyncStatus | null; @WorkspaceField({ standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncStage, @@ -282,7 +282,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconHistory', }) @WorkspaceIsNullable() - syncStageStartedAt: string; + syncStageStartedAt: string | null; @WorkspaceField({ standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.throttleFailureCount, diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts index b8c1e09f6447e..32068791b1355 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts @@ -83,7 +83,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'messageParticipants', }) @WorkspaceIsNullable() - person: Relation; + person: Relation | null; @WorkspaceRelation({ standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.workspaceMember, @@ -96,5 +96,5 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'messageParticipants', }) @WorkspaceIsNullable() - workspaceMember: Relation; + workspaceMember: Relation | null; } diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts index a3ad1ea48c8cc..3b2c899ca26d0 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts @@ -78,7 +78,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconCalendar', }) @WorkspaceIsNullable() - receivedAt: string; + receivedAt: string | null; @WorkspaceRelation({ standardId: MESSAGE_STANDARD_FIELD_IDS.messageThread, @@ -92,7 +92,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity { onDelete: RelationOnDeleteAction.CASCADE, }) @WorkspaceIsNullable() - messageThread: Relation; + messageThread: Relation | null; @WorkspaceRelation({ standardId: MESSAGE_STANDARD_FIELD_IDS.messageParticipants, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts index a22126de9e01f..90ee6dbf5d79e 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts @@ -1,4 +1,4 @@ -import { Logger } from '@nestjs/common'; +import { Logger, Scope } from '@nestjs/common'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; @@ -21,7 +21,10 @@ export type MessagingMessageListFetchJobData = { workspaceId: string; }; -@Processor(MessageQueue.messagingQueue) +@Processor({ + queueName: MessageQueue.messagingQueue, + scope: Scope.REQUEST, +}) export class MessagingMessageListFetchJob { private readonly logger = new Logger(MessagingMessageListFetchJob.name); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts index f0a543d8070fc..46f3b89dcdfd0 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts @@ -1,3 +1,5 @@ +import { Scope } from '@nestjs/common'; + import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; @@ -18,7 +20,10 @@ export type MessagingMessagesImportJobData = { workspaceId: string; }; -@Processor(MessageQueue.messagingQueue) +@Processor({ + queueName: MessageQueue.messagingQueue, + scope: Scope.REQUEST, +}) export class MessagingMessagesImportJob { constructor( @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) diff --git a/packages/twenty-server/src/modules/messaging/message-participants-manager/listeners/message-participant.listener.ts b/packages/twenty-server/src/modules/messaging/message-participants-manager/listeners/message-participant.listener.ts index 92853ba5d345a..346dcf64fb599 100644 --- a/packages/twenty-server/src/modules/messaging/message-participants-manager/listeners/message-participant.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-participants-manager/listeners/message-participant.listener.ts @@ -25,7 +25,7 @@ export class MessageParticipantListener { @OnEvent('messageParticipant.matched') public async handleMessageParticipantMatched(payload: { workspaceId: string; - userId: string; + workspaceMemberId: string; messageParticipants: ObjectRecord[]; }): Promise { const messageParticipants = payload.messageParticipants ?? []; @@ -60,7 +60,7 @@ export class MessageParticipantListener { properties: null, objectName: 'message', recordId: participant.personId, - workspaceMemberId: payload.userId, + workspaceMemberId: payload.workspaceMemberId, workspaceId: payload.workspaceId, linkedObjectMetadataId: messageObjectMetadata.id, linkedRecordId: participant.messageId, diff --git a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.ts b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.ts index ee420467c5297..584792ac4f2ef 100644 --- a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.ts +++ b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.ts @@ -67,6 +67,9 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob { await this.messageChannelRepository.getAll(workspaceId); for (const messageChannel of messageChannels) { + if (!messageChannel.syncStatus) { + continue; + } await this.messagingTelemetryService.track({ eventName: `message_channel.monitoring.sync_status.${snakeCase( messageChannel.syncStatus, diff --git a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts index 5ec506eb57c98..8592632616964 100644 --- a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts @@ -49,7 +49,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconCurrencyDollar', }) @WorkspaceIsNullable() - amount: CurrencyMetadata; + amount: CurrencyMetadata | null; @WorkspaceField({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.closeDate, @@ -59,7 +59,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconCalendarEvent', }) @WorkspaceIsNullable() - closeDate: Date; + closeDate: Date | null; @WorkspaceField({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.probability, @@ -102,7 +102,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsSystem() @WorkspaceIsNullable() - position: number; + position: number | null; @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.pointOfContact, @@ -116,7 +116,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { onDelete: RelationOnDeleteAction.SET_NULL, }) @WorkspaceIsNullable() - pointOfContact: Relation; + pointOfContact: Relation | null; @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.company, @@ -130,7 +130,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { onDelete: RelationOnDeleteAction.SET_NULL, }) @WorkspaceIsNullable() - company: Relation; + company: Relation | null; @WorkspaceRelation({ standardId: OPPORTUNITY_STANDARD_FIELD_IDS.favorites, diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index 83353993dfb14..6340c84297e80 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -41,7 +41,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconUser', }) @WorkspaceIsNullable() - name: FullNameMetadata; + name: FullNameMetadata | null; @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.email, @@ -60,7 +60,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconBrandLinkedin', }) @WorkspaceIsNullable() - linkedinLink: LinkMetadata; + linkedinLink: LinkMetadata | null; @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.xLink, @@ -70,7 +70,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconBrandX', }) @WorkspaceIsNullable() - xLink: LinkMetadata; + xLink: LinkMetadata | null; @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.jobTitle, @@ -118,7 +118,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsSystem() @WorkspaceIsNullable() - position: number; + position: number | null; // Relations @WorkspaceRelation({ @@ -132,7 +132,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'people', }) @WorkspaceIsNullable() - company: Relation; + company: Relation | null; @WorkspaceRelation({ standardId: PERSON_STANDARD_FIELD_IDS.pointOfContactForOpportunities, diff --git a/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts b/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts index 8d9a99c7d98ee..a311dd1f8aed8 100644 --- a/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts +++ b/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts @@ -151,9 +151,9 @@ export class TimelineActivityRepository { name: string; properties: Record | null; workspaceMemberId: string | undefined; - recordId: string; + recordId: string | null; linkedRecordCachedName: string; - linkedRecordId: string | undefined; + linkedRecordId: string | null | undefined; linkedObjectMetadataId: string | undefined; }[], workspaceId: string, diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts index d7b42a75b0897..b0b801db8c0f6 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts @@ -39,7 +39,7 @@ export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconListDetails', }) @WorkspaceIsNullable() - properties: JSON; + properties: JSON | null; @WorkspaceField({ standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.context, @@ -50,7 +50,7 @@ export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconListDetails', }) @WorkspaceIsNullable() - context: JSON; + context: JSON | null; @WorkspaceField({ standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.objectName, @@ -78,7 +78,7 @@ export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconAbc', }) @WorkspaceIsNullable() - recordId: string; + recordId: string | null; @WorkspaceRelation({ standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.workspaceMember, @@ -91,5 +91,5 @@ export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'auditLogs', }) @WorkspaceIsNullable() - workspaceMember: Relation; + workspaceMember: Relation | null; } diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.workspace-entity.ts index 3460768e4f818..a74069685bbcb 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.workspace-entity.ts @@ -56,7 +56,7 @@ export class BehavioralEventWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconListDetails', }) @WorkspaceIsNullable() - properties: JSON; + properties: JSON | null; @WorkspaceField({ standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.context, @@ -67,7 +67,7 @@ export class BehavioralEventWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconListDetails', }) @WorkspaceIsNullable() - context: JSON; + context: JSON | null; @WorkspaceField({ standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.objectName, @@ -86,5 +86,5 @@ export class BehavioralEventWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconAbc', }) @WorkspaceIsNullable() - recordId: string; + recordId: string | null; } diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts index 40cd49307684e..2e366378ff26d 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts @@ -56,7 +56,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconListDetails', }) @WorkspaceIsNullable() - properties: JSON; + properties: JSON | null; // Special objects that don't have their own timeline and are 'link' to the main object @WorkspaceField({ @@ -76,7 +76,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconAbc', }) @WorkspaceIsNullable() - linkedRecordId: string; + linkedRecordId: string | null; @WorkspaceField({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.linkedObjectMetadataId, @@ -86,7 +86,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconAbc', }) @WorkspaceIsNullable() - linkedObjectMetadataId: string; + linkedObjectMetadataId: string | null; // Who made the action @WorkspaceRelation({ @@ -100,7 +100,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'timelineActivities', }) @WorkspaceIsNullable() - workspaceMember: Relation; + workspaceMember: Relation | null; @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.person, @@ -113,7 +113,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'timelineActivities', }) @WorkspaceIsNullable() - person: Relation; + person: Relation | null; @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.company, @@ -126,7 +126,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'timelineActivities', }) @WorkspaceIsNullable() - company: Relation; + company: Relation | null; @WorkspaceRelation({ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.opportunity, @@ -139,7 +139,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'timelineActivities', }) @WorkspaceIsNullable() - opportunity: Relation; + opportunity: Relation | null; @WorkspaceDynamicRelation({ type: RelationMetadataType.MANY_TO_ONE, diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts index 424f4299ce5cc..474fedb8251fb 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts @@ -72,5 +72,5 @@ export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity { joinColumn: 'viewId', }) @WorkspaceIsNullable() - view?: ViewWorkspaceEntity; + view?: ViewWorkspaceEntity | null; } diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts index bce3e944aff4a..5d0f5598fdaee 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts @@ -68,5 +68,5 @@ export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'viewFilters', }) @WorkspaceIsNullable() - view: Relation; + view: Relation | null; } diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts index 458d4d316881d..6ecc1b4fe34c1 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts @@ -53,5 +53,5 @@ export class ViewSortWorkspaceEntity extends BaseWorkspaceEntity { inverseSideFieldKey: 'viewSorts', }) @WorkspaceIsNullable() - view: Relation; + view: Relation | null; } diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts index efa6cd863cc00..67f7d3f9e64f6 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts @@ -3,10 +3,9 @@ import { Injectable } from '@nestjs/common'; import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; import { DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { CommentRepository } from 'src/modules/activity/repositories/comment.repository'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity'; -import { AttachmentRepository } from 'src/modules/attachment/repositories/attachment.repository'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; @Injectable() @@ -14,10 +13,10 @@ export class WorkspaceMemberDeleteOnePreQueryHook implements WorkspacePreQueryHook { constructor( - @InjectObjectMetadataRepository(AttachmentWorkspaceEntity) - private readonly attachmentRepository: AttachmentRepository, - @InjectObjectMetadataRepository(CommentWorkspaceEntity) - private readonly commentRepository: CommentRepository, + @InjectWorkspaceRepository(AttachmentWorkspaceEntity) + private readonly attachmentRepository: WorkspaceRepository, + @InjectWorkspaceRepository(CommentWorkspaceEntity) + private readonly commentRepository: WorkspaceRepository, ) {} // There is no need to validate the user's access to the workspace member since we don't have permission yet. @@ -26,16 +25,18 @@ export class WorkspaceMemberDeleteOnePreQueryHook workspaceId: string, payload: DeleteOneResolverArgs, ): Promise { - const workspaceMemberId = payload.id; + const authorId = payload.id; - await this.attachmentRepository.deleteByAuthorId( - workspaceMemberId, - workspaceId, - ); + await this.attachmentRepository.delete({ + author: { + id: authorId, + }, + }); - await this.commentRepository.deleteByAuthorId( - workspaceMemberId, - workspaceId, - ); + await this.commentRepository.delete({ + author: { + id: authorId, + }, + }); } } diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts index 14c1ef5e53840..44f2362c839ca 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { WorkspaceMemberDeleteManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook'; @@ -8,7 +8,7 @@ import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-memb @Module({ imports: [ - ObjectMetadataRepositoryModule.forFeature([ + TwentyORMModule.forFeature([ AttachmentWorkspaceEntity, CommentWorkspaceEntity, ]), diff --git a/packages/twenty-server/src/utils/is-defined.ts b/packages/twenty-server/src/utils/is-defined.ts new file mode 100644 index 0000000000000..1be478d76dd0c --- /dev/null +++ b/packages/twenty-server/src/utils/is-defined.ts @@ -0,0 +1,3 @@ +export const isDefined = (value: T | null | undefined): value is T => { + return value !== null && value !== undefined; +}; diff --git a/packages/twenty-server/src/utils/typed-reflect.ts b/packages/twenty-server/src/utils/typed-reflect.ts index 6e9fdb819385e..c8a25cee93659 100644 --- a/packages/twenty-server/src/utils/typed-reflect.ts +++ b/packages/twenty-server/src/utils/typed-reflect.ts @@ -8,6 +8,7 @@ export interface ReflectMetadataTypeMap { ['workspace:is-system-metadata-args']: true; ['workspace:is-audit-logged-metadata-args']: false; ['workspace:is-primary-field-metadata-args']: true; + ['workspace:join-column']: true; } export class TypedReflect { From e13dc7a1fc47b638903cd6f67ca3db0c8be1f4a0 Mon Sep 17 00:00:00 2001 From: Weiko Date: Sat, 22 Jun 2024 12:39:57 +0200 Subject: [PATCH 11/30] [FlexibleSchema] Add IndexMetadata decorator (#5981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Our Flexible Schema engine dynamically generates entities/tables/APIs for us but was not flexible enough to build indexes in the DB. With more and more features involving heavy queries such as Messaging, we are now adding a new WorkspaceIndex() decorator for our standard objects (will come later for custom objects). This decorator will give enough information to the workspace sync metadata manager to generate the proper migrations that will create or drop indexes on demand. To be aligned with the rest of the engine, we are adding 2 new tables: IndexMetadata and IndexFieldMetadata, that will store the info of our indexes. ## Implementation ```typescript @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.person, namePlural: 'people', labelSingular: 'Person', labelPlural: 'People', description: 'A person', icon: 'IconUser', }) export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.email, type: FieldMetadataType.EMAIL, label: 'Email', description: 'Contact’s Email', icon: 'IconMail', }) @WorkspaceIndex() email: string; ``` By simply adding the WorkspaceIndex decorator, sync-metadata command will create a new index for that column. We can also add composite indexes, note that the order is important for PSQL. ```typescript @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.person, namePlural: 'people', labelSingular: 'Person', labelPlural: 'People', description: 'A person', icon: 'IconUser', }) @WorkspaceIndex(['phone', 'email']) export class PersonWorkspaceEntity extends BaseWorkspaceEntity { ``` Currently composite fields and relation fields are not handled by @WorkspaceIndex() and you will need to use this notation instead ```typescript @WorkspaceIndex(['companyId', 'nameFirstName']) export class PersonWorkspaceEntity extends BaseWorkspaceEntity { ``` Screenshot 2024-06-21 at 15 15 45 Next step: We might need to implement more complex index expressions, this is why we have an expression column in IndexMetadata. What I had in mind for the decorator, still open to discussion ```typescript @WorkspaceIndex(['nameFirstName', 'nameLastName'], { expression: "$1 || ' ' || $2"}) export class PersonWorkspaceEntity extends BaseWorkspaceEntity { ``` --------- Co-authored-by: Charles Bochet --- .../1718985664968-addIndexMetadataTable.ts | 28 ++++ .../field-metadata/field-metadata.entity.ts | 12 ++ .../index-field-metadata.entity.ts | 46 ++++++ .../index-field-metadata.module.ts | 11 ++ .../index-metadata/index-metadata.entity.ts | 43 +++++ .../index-metadata/index-metadata.module.ts | 11 ++ .../generate-deterministic-index-name.ts | 11 ++ .../object-metadata/object-metadata.entity.ts | 6 + .../workspace-migration.entity.ts | 13 ++ .../decorators/workspace-index.decorator.ts | 37 +++++ ...workspace-index-metadata-args.interface.ts | 17 ++ .../storage/metadata-args.storage.ts | 16 ++ .../factories/index.ts | 3 + .../workspace-migration-index.factory.ts | 156 ++++++++++++++++++ .../workspace-migration-runner.service.ts | 41 +++++ .../sync-workspace-metadata.command.ts | 9 +- .../comparators/index.ts | 3 + .../comparators/workspace-index.comparator.ts | 90 ++++++++++ .../factories/index.ts | 3 + .../factories/standard-index.factory.ts | 69 ++++++++ .../interfaces/comparator.interface.ts | 6 + .../partial-index-metadata.interface.ts | 8 + .../workspace-metadata-updater.service.ts | 65 ++++++++ .../workspace-sync-field-metadata.service.ts | 4 +- .../workspace-sync-index-metadata.service.ts | 119 +++++++++++++ .../workspace-sync-object-metadata.service.ts | 4 + .../storage/workspace-sync.storage.ts | 34 +++- .../workspace-sync-metadata.module.ts | 2 + .../workspace-sync-metadata.service.ts | 12 ++ 29 files changed, 870 insertions(+), 9 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1718985664968-addIndexMetadataTable.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/index-field-metadata/index-field-metadata.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1718985664968-addIndexMetadataTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1718985664968-addIndexMetadataTable.ts new file mode 100644 index 0000000000000..c4d5afca948d6 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1718985664968-addIndexMetadataTable.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddIndexMetadataTable1718985664968 implements MigrationInterface { + name = 'AddIndexMetadataTable1718985664968'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "metadata"."indexMetadata" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "workspaceId" character varying, "objectMetadataId" uuid NOT NULL, CONSTRAINT "PK_f73bb3c3678aee204e341f0ca4e" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "metadata"."indexFieldMetadata" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "indexMetadataId" uuid NOT NULL, "fieldMetadataId" uuid NOT NULL, "order" integer NOT NULL, CONSTRAINT "PK_5928f67e43eff7d95aa79fd96fd" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."indexMetadata" ADD CONSTRAINT "FK_051487e9b745cb175950130b63f" FOREIGN KEY ("objectMetadataId") REFERENCES "metadata"."objectMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."indexFieldMetadata" ADD CONSTRAINT "FK_b20192c432612eb710801dd5664" FOREIGN KEY ("indexMetadataId") REFERENCES "metadata"."indexMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."indexFieldMetadata" ADD CONSTRAINT "FK_be0950612a54b58c72bd62d629e" FOREIGN KEY ("fieldMetadataId") REFERENCES "metadata"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "metadata"."indexFieldMetadata"`); + await queryRunner.query(`DROP TABLE "metadata"."indexMetadata"`); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index dd24719ad6906..44cdf69d1d033 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -9,6 +9,7 @@ import { CreateDateColumn, UpdateDateColumn, Relation, + OneToMany, } from 'typeorm'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; @@ -18,6 +19,7 @@ import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadat import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity'; export enum FieldMetadataType { UUID = 'UUID', @@ -119,6 +121,16 @@ export class FieldMetadataEntity< ) toRelationMetadata: Relation; + @OneToMany( + () => IndexFieldMetadataEntity, + (indexFieldMetadata: IndexFieldMetadataEntity) => + indexFieldMetadata.fieldMetadata, + { + cascade: true, + }, + ) + indexFieldMetadatas: Relation; + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity.ts new file mode 100644 index 0000000000000..79ce331c7bfef --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity.ts @@ -0,0 +1,46 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + JoinColumn, + ManyToOne, + Relation, +} from 'typeorm'; + +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +@Entity('indexFieldMetadata') +export class IndexFieldMetadataEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false }) + indexMetadataId: string; + + @ManyToOne( + () => IndexMetadataEntity, + (indexMetadata) => indexMetadata.indexFieldMetadatas, + { + onDelete: 'CASCADE', + }, + ) + @JoinColumn() + indexMetadata: Relation; + + @Column({ nullable: false }) + fieldMetadataId: string; + + @ManyToOne( + () => FieldMetadataEntity, + (fieldMetadata) => fieldMetadata.indexFieldMetadatas, + { + onDelete: 'CASCADE', + }, + ) + @JoinColumn() + fieldMetadata: Relation; + + @Column({ nullable: false }) + order: number; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/index-field-metadata/index-field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/index-field-metadata/index-field-metadata.module.ts new file mode 100644 index 0000000000000..937851df1ed9c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/index-field-metadata/index-field-metadata.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([IndexFieldMetadataEntity], 'metadata')], + providers: [], + exports: [], +}) +export class IndexFieldMetadataModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts new file mode 100644 index 0000000000000..d006a092e31b0 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts @@ -0,0 +1,43 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + JoinColumn, + ManyToOne, + Relation, + OneToMany, +} from 'typeorm'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity'; + +@Entity('indexMetadata') +export class IndexMetadataEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false }) + name: string; + + @Column({ nullable: true }) + workspaceId: string; + + @Column({ nullable: false, type: 'uuid' }) + objectMetadataId: string; + + @ManyToOne(() => ObjectMetadataEntity, (object) => object.indexes, { + onDelete: 'CASCADE', + }) + @JoinColumn() + objectMetadata: Relation; + + @OneToMany( + () => IndexFieldMetadataEntity, + (indexFieldMetadata: IndexFieldMetadataEntity) => + indexFieldMetadata.indexMetadata, + { + cascade: true, + }, + ) + indexFieldMetadatas: Relation; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.module.ts new file mode 100644 index 0000000000000..01cb4a3e6a923 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([IndexMetadataEntity], 'metadata')], + providers: [], + exports: [], +}) +export class IndexMetadataModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name.ts new file mode 100644 index 0000000000000..ebf84d17d2dae --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name.ts @@ -0,0 +1,11 @@ +import { createHash } from 'crypto'; + +export const generateDeterministicIndexName = (columns: string[]): string => { + const hash = createHash('sha256'); + + columns.forEach((column) => { + hash.update(column); + }); + + return hash.digest('hex').slice(0, 27); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts index d4cf45c14165b..cc64c231b7047 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts @@ -15,6 +15,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; @Entity('objectMetadata') @Unique('IndexOnNameSingularAndWorkspaceIdUnique', [ @@ -82,6 +83,11 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface { }) fields: Relation; + @OneToMany(() => FieldMetadataEntity, (field) => field.object, { + cascade: true, + }) + indexes: Relation; + @OneToMany( () => RelationMetadataEntity, (relation: RelationMetadataEntity) => relation.fromObjectMetadata, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts index 8116e0b3dc9e5..12084f80f1aad 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts @@ -18,6 +18,11 @@ export enum WorkspaceMigrationColumnActionType { export type WorkspaceMigrationRenamedEnum = { from: string; to: string }; export type WorkspaceMigrationEnum = string | WorkspaceMigrationRenamedEnum; +export enum WorkspaceMigrationIndexActionType { + CREATE = 'CREATE', + DROP = 'DROP', +} + export interface WorkspaceMigrationColumnDefinition { columnName: string; columnType: string; @@ -27,6 +32,12 @@ export interface WorkspaceMigrationColumnDefinition { defaultValue?: any; } +export interface WorkspaceMigrationIndexAction { + action: WorkspaceMigrationIndexActionType; + name: string; + columns: string[]; +} + export interface WorkspaceMigrationColumnCreate extends WorkspaceMigrationColumnDefinition { action: WorkspaceMigrationColumnActionType.CREATE; @@ -105,6 +116,7 @@ export enum WorkspaceMigrationTableActionType { CREATE_FOREIGN_TABLE = 'create_foreign_table', DROP_FOREIGN_TABLE = 'drop_foreign_table', ALTER_FOREIGN_TABLE = 'alter_foreign_table', + ALTER_INDEXES = 'alter_indexes', } export type WorkspaceMigrationTableAction = { @@ -113,6 +125,7 @@ export type WorkspaceMigrationTableAction = { action: WorkspaceMigrationTableActionType; columns?: WorkspaceMigrationColumnAction[]; foreignTable?: WorkspaceMigrationForeignTable; + indexes?: WorkspaceMigrationIndexAction[]; }; @Entity('workspaceMigration') diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts new file mode 100644 index 0000000000000..e74394c2ccc65 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts @@ -0,0 +1,37 @@ +import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name'; +import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; + +export interface WorkspaceIndexOptions { + columns?: string[]; +} + +export function WorkspaceIndex(): PropertyDecorator; +export function WorkspaceIndex(columns: string[]): ClassDecorator; +export function WorkspaceIndex( + columns?: string[], +): PropertyDecorator | ClassDecorator { + return (target: any, propertyKey: string | symbol) => { + if (propertyKey === undefined && columns === undefined) { + throw new Error('Class level WorkspaceIndex should be used with columns'); + } + + // TODO: handle composite field metadata types + // TODO: handle relation field metadata types + + if (Array.isArray(columns) && columns.length > 0) { + metadataArgsStorage.addIndexes({ + name: `IDX_${generateDeterministicIndexName(columns)}`, + columns, + target: target, + }); + + return; + } + + metadataArgsStorage.addIndexes({ + name: `IDX_${generateDeterministicIndexName([propertyKey.toString()])}`, + columns: [propertyKey.toString()], + target: target.constructor, + }); + }; +} diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface.ts new file mode 100644 index 0000000000000..add4c89e7476a --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface.ts @@ -0,0 +1,17 @@ +export interface WorkspaceIndexMetadataArgs { + /** + * Class to which index is applied. + */ + // eslint-disable-next-line @typescript-eslint/ban-types + readonly target: Function; + + /* + * Index name. + */ + name: string; + + /* + * Index columns. + */ + columns: string[]; +} diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/metadata-args.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/metadata-args.storage.ts index 9fb114be94f2c..f228116b562d5 100644 --- a/packages/twenty-server/src/engine/twenty-orm/storage/metadata-args.storage.ts +++ b/packages/twenty-server/src/engine/twenty-orm/storage/metadata-args.storage.ts @@ -5,6 +5,7 @@ import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/wor import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface'; 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'; export class MetadataArgsStorage { private readonly entities: WorkspaceEntityMetadataArgs[] = []; @@ -13,6 +14,7 @@ export class MetadataArgsStorage { private readonly relations: WorkspaceRelationMetadataArgs[] = []; private readonly dynamicRelations: WorkspaceDynamicRelationMetadataArgs[] = []; + private readonly indexes: WorkspaceIndexMetadataArgs[] = []; addEntities(...entities: WorkspaceEntityMetadataArgs[]): void { this.entities.push(...entities); @@ -32,6 +34,10 @@ export class MetadataArgsStorage { this.relations.push(...relations); } + addIndexes(...indexes: WorkspaceIndexMetadataArgs[]): void { + this.indexes.push(...indexes); + } + addDynamicRelations( ...dynamicRelations: WorkspaceDynamicRelationMetadataArgs[] ): void { @@ -93,6 +99,16 @@ export class MetadataArgsStorage { return this.filterByTarget(this.relations, target); } + filterIndexes(target: Function | string): WorkspaceIndexMetadataArgs[]; + + filterIndexes(target: (Function | string)[]): WorkspaceIndexMetadataArgs[]; + + filterIndexes( + target: (Function | string) | (Function | string)[], + ): WorkspaceIndexMetadataArgs[] { + return this.filterByTarget(this.indexes, target); + } + filterDynamicRelations( target: Function | string, ): WorkspaceDynamicRelationMetadataArgs[]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/index.ts index 50f96d3948291..5d5e66ac5310b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/index.ts @@ -1,3 +1,5 @@ +import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory'; + import { WorkspaceMigrationObjectFactory } from './workspace-migration-object.factory'; import { WorkspaceMigrationFieldFactory } from './workspace-migration-field.factory'; import { WorkspaceMigrationRelationFactory } from './workspace-migration-relation.factory'; @@ -6,4 +8,5 @@ export const workspaceMigrationBuilderFactories = [ WorkspaceMigrationObjectFactory, WorkspaceMigrationFieldFactory, WorkspaceMigrationRelationFactory, + WorkspaceMigrationIndexFactory, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory.ts new file mode 100644 index 0000000000000..0392041edc93d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory.ts @@ -0,0 +1,156 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { + WorkspaceMigrationEntity, + WorkspaceMigrationIndexActionType, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; + +@Injectable() +export class WorkspaceMigrationIndexFactory { + constructor() {} + + async create( + originalObjectMetadataCollection: ObjectMetadataEntity[], + indexMetadataCollection: IndexMetadataEntity[], + action: WorkspaceMigrationBuilderAction, + ): Promise[]> { + const originalObjectMetadataMap = Object.fromEntries( + originalObjectMetadataCollection.map((obj) => [obj.id, obj]), + ); + + const indexMetadataByObjectMetadataMap = new Map< + ObjectMetadataEntity, + IndexMetadataEntity[] + >(); + + indexMetadataCollection.forEach((currentIndexMetadata) => { + const objectMetadata = + originalObjectMetadataMap[currentIndexMetadata.objectMetadataId]; + + if (!objectMetadata) { + throw new Error( + `Object metadata with id ${currentIndexMetadata.objectMetadataId} not found`, + ); + } + + if (!indexMetadataByObjectMetadataMap.has(objectMetadata)) { + indexMetadataByObjectMetadataMap.set(objectMetadata, []); + } + + indexMetadataByObjectMetadataMap + ?.get(objectMetadata) + ?.push(currentIndexMetadata); + }); + + switch (action) { + case WorkspaceMigrationBuilderAction.CREATE: + return this.createIndexMigration(indexMetadataByObjectMetadataMap); + case WorkspaceMigrationBuilderAction.DELETE: + return this.deleteIndexMigration(indexMetadataByObjectMetadataMap); + default: + return []; + } + } + + private async createIndexMigration( + indexMetadataByObjectMetadataMap: Map< + ObjectMetadataEntity, + IndexMetadataEntity[] + >, + ): Promise[]> { + const workspaceMigrations: Partial[] = []; + + for (const [ + objectMetadata, + indexMetadataCollection, + ] of indexMetadataByObjectMetadataMap) { + const targetTable = computeObjectTargetTable(objectMetadata); + + const fieldsById = Object.fromEntries( + objectMetadata.fields.map((field) => [field.id, field]), + ); + + const indexes = indexMetadataCollection.map((indexMetadata) => ({ + name: indexMetadata.name, + action: WorkspaceMigrationIndexActionType.CREATE, + columns: indexMetadata.indexFieldMetadatas + .sort((a, b) => a.order - b.order) + .map((indexFieldMetadata) => { + const fieldMetadata = + fieldsById[indexFieldMetadata.fieldMetadataId]; + + if (!fieldMetadata) { + throw new Error( + `Field metadata with id ${indexFieldMetadata.fieldMetadataId} not found in object metadata with id ${objectMetadata.id}`, + ); + } + + return fieldMetadata.name; + }), + })); + + workspaceMigrations.push({ + workspaceId: objectMetadata.workspaceId, + name: generateMigrationName( + `create-${objectMetadata.nameSingular}-indexes`, + ), + isCustom: false, + migrations: [ + { + name: targetTable, + action: WorkspaceMigrationTableActionType.ALTER_INDEXES, + indexes, + }, + ], + }); + } + + return workspaceMigrations; + } + + private async deleteIndexMigration( + indexMetadataByObjectMetadataMap: Map< + ObjectMetadataEntity, + IndexMetadataEntity[] + >, + ): Promise[]> { + const workspaceMigrations: Partial[] = []; + + for (const [ + objectMetadata, + indexMetadataCollection, + ] of indexMetadataByObjectMetadataMap) { + const targetTable = computeObjectTargetTable(objectMetadata); + + const indexes = indexMetadataCollection.map((indexMetadata) => ({ + name: indexMetadata.name, + action: WorkspaceMigrationIndexActionType.DROP, + columns: [], + })); + + workspaceMigrations.push({ + workspaceId: objectMetadata.workspaceId, + name: generateMigrationName( + `delete-${objectMetadata.nameSingular}-indexes`, + ), + isCustom: false, + migrations: [ + { + name: targetTable, + action: WorkspaceMigrationTableActionType.ALTER_INDEXES, + indexes, + }, + ], + }); + } + + return workspaceMigrations; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts index 560d9fff4995d..c239462a2dbc3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts @@ -5,6 +5,7 @@ import { Table, TableColumn, TableForeignKey, + TableIndex, TableUnique, } from 'typeorm'; @@ -20,6 +21,8 @@ import { WorkspaceMigrationColumnDropRelation, WorkspaceMigrationTableActionType, WorkspaceMigrationForeignTable, + WorkspaceMigrationIndexAction, + WorkspaceMigrationIndexActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { WorkspaceMigrationEnumService } from 'src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service'; @@ -137,6 +140,7 @@ export class WorkspaceMigrationRunnerService { tableMigration.columns, ); } + break; } case WorkspaceMigrationTableActionType.DROP: @@ -163,6 +167,17 @@ export class WorkspaceMigrationRunnerService { tableMigration.columns, ); break; + + case WorkspaceMigrationTableActionType.ALTER_INDEXES: + if (tableMigration.indexes && tableMigration.indexes.length > 0) { + await this.handleIndexesChanges( + queryRunner, + schemaName, + tableMigration.newName ?? tableMigration.name, + tableMigration.indexes, + ); + } + break; default: throw new Error( `Migration table action ${tableMigration.action} not supported`, @@ -170,6 +185,32 @@ export class WorkspaceMigrationRunnerService { } } + private async handleIndexesChanges( + queryRunner: QueryRunner, + schemaName: string, + tableName: string, + indexes: WorkspaceMigrationIndexAction[], + ) { + for (const index of indexes) { + switch (index.action) { + case WorkspaceMigrationIndexActionType.CREATE: + await queryRunner.createIndex( + `${schemaName}.${tableName}`, + new TableIndex({ + name: index.name, + columnNames: index.columns, + }), + ); + break; + case WorkspaceMigrationIndexActionType.DROP: + await queryRunner.dropIndex(`${schemaName}.${tableName}`, index.name); + break; + default: + throw new Error(`Migration index action not supported`); + } + } + } + /** * Creates a table for a given schema and table name * diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts index b6d15d677bd57..106a0d8760454 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts @@ -5,7 +5,6 @@ import { Command, CommandRunner, Option } from 'nest-commander'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service'; import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service'; -import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service'; @@ -28,7 +27,6 @@ export class SyncWorkspaceMetadataCommand extends CommandRunner { private readonly workspaceHealthService: WorkspaceHealthService, private readonly dataSourceService: DataSourceService, private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService, - private readonly workspaceService: WorkspaceService, ) { super(); } @@ -37,9 +35,8 @@ export class SyncWorkspaceMetadataCommand extends CommandRunner { _passedParam: string[], options: RunWorkspaceMigrationsOptions, ): Promise { - const workspaceIds = options.workspaceId - ? [options.workspaceId] - : await this.workspaceService.getWorkspaceIds(); + // TODO: re-implement load index from workspaceService, this is breaking the logger + const workspaceIds = options.workspaceId ? [options.workspaceId] : []; for (const workspaceId of workspaceIds) { try { @@ -105,7 +102,7 @@ export class SyncWorkspaceMetadataCommand extends CommandRunner { @Option({ flags: '-w, --workspace-id [workspace_id]', description: 'workspace id', - required: false, + required: true, }) parseWorkspaceId(value: string): string { return value; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts index 3c95f7babe3fe..2f29fbf41f5be 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts @@ -1,3 +1,5 @@ +import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator'; + import { WorkspaceFieldComparator } from './workspace-field.comparator'; import { WorkspaceObjectComparator } from './workspace-object.comparator'; import { WorkspaceRelationComparator } from './workspace-relation.comparator'; @@ -6,4 +8,5 @@ export const workspaceSyncMetadataComparators = [ WorkspaceFieldComparator, WorkspaceObjectComparator, WorkspaceRelationComparator, + WorkspaceIndexComparator, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator.ts new file mode 100644 index 0000000000000..5fbf2ad9756da --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; + +import diff from 'microdiff'; + +import { + IndexComparatorResult, + ComparatorAction, +} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; + +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; + +const propertiesToIgnore = ['createdAt', 'updatedAt', 'indexFieldMetadatas']; + +@Injectable() +export class WorkspaceIndexComparator { + constructor() {} + + compare( + originalIndexMetadataCollection: IndexMetadataEntity[], + standardIndexMetadataCollection: Partial[], + ): IndexComparatorResult[] { + const results: IndexComparatorResult[] = []; + + // Create a map of standard relations + const standardIndexMetadataMap = transformMetadataForComparison( + standardIndexMetadataCollection, + { + keyFactory(indexMetadata) { + return `${indexMetadata.name}`; + }, + }, + ); + + const originalIndexMetadataCollectionWithColumns = + originalIndexMetadataCollection.map((indexMetadata) => { + return { + ...indexMetadata, + columns: indexMetadata.indexFieldMetadatas.map( + (indexFieldMetadata) => indexFieldMetadata.fieldMetadata.name, + ), + indexFieldMetadatas: undefined, + }; + }); + + // Create a filtered map of original relations + // We filter out 'id' later because we need it to remove the relation from DB + const originalIndexMetadataMap = transformMetadataForComparison( + originalIndexMetadataCollectionWithColumns, + { + shouldIgnoreProperty: (property) => + propertiesToIgnore.includes(property), + keyFactory(indexMetadata) { + return `${indexMetadata.name}`; + }, + }, + ); + + // Compare indexes + const indexesDifferences = diff( + originalIndexMetadataMap, + standardIndexMetadataMap, + ); + + for (const difference of indexesDifferences) { + switch (difference.type) { + case 'CREATE': { + results.push({ + action: ComparatorAction.CREATE, + object: difference.value, + }); + break; + } + case 'REMOVE': { + if (difference.path[difference.path.length - 1] !== 'id') { + results.push({ + action: ComparatorAction.DELETE, + object: difference.oldValue, + }); + } + break; + } + default: + break; + } + } + + return results; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts index 72185436922b0..958bf346cae7c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts @@ -1,3 +1,5 @@ +import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory'; + import { FeatureFlagFactory } from './feature-flags.factory'; import { StandardFieldFactory } from './standard-field.factory'; import { StandardObjectFactory } from './standard-object.factory'; @@ -8,4 +10,5 @@ export const workspaceSyncMetadataFactories = [ StandardFieldFactory, StandardObjectFactory, StandardRelationFactory, + StandardIndexFactory, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts new file mode 100644 index 0000000000000..8ad2c89d18013 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@nestjs/common'; + +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; +import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; +import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface'; + +import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-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'; + +@Injectable() +export class StandardIndexFactory { + create( + standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[], + context: WorkspaceSyncContext, + originalObjectMetadataMap: Record, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): Partial[] { + return standardObjectMetadataDefinitions.flatMap((standardObjectMetadata) => + this.createIndexMetadata( + standardObjectMetadata, + context, + originalObjectMetadataMap, + workspaceFeatureFlagsMap, + ), + ); + } + + private createIndexMetadata( + target: typeof BaseWorkspaceEntity, + context: WorkspaceSyncContext, + originalObjectMetadataMap: Record, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): Partial[] { + const workspaceEntity = metadataArgsStorage.filterEntities(target); + + if (!workspaceEntity) { + throw new Error( + `Object metadata decorator not found, can't parse ${target.name}`, + ); + } + + const workspaceIndexMetadataArgsCollection = + metadataArgsStorage.filterIndexes(target); + + return workspaceIndexMetadataArgsCollection.map( + (workspaceIndexMetadataArgs) => { + const objectMetadata = + originalObjectMetadataMap[workspaceEntity.nameSingular]; + + if (!objectMetadata) { + throw new Error( + `Object metadata not found for ${workspaceEntity.nameSingular}`, + ); + } + + const indexMetadata: PartialIndexMetadata = { + workspaceId: context.workspaceId, + objectMetadataId: objectMetadata.id, + name: workspaceIndexMetadataArgs.name, + columns: workspaceIndexMetadataArgs.columns, + }; + + return indexMetadata; + }, + ); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts index efc061bc73acd..ab40fdfa79c0c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts @@ -1,5 +1,6 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { ComputedPartialFieldMetadata } from './partial-field-metadata.interface'; import { ComputedPartialWorkspaceEntity } from './partial-object-metadata.interface'; @@ -49,3 +50,8 @@ export type RelationComparatorResult = | ComparatorCreateResult> | ComparatorDeleteResult | ComparatorUpdateResult>; + +export type IndexComparatorResult = + | ComparatorCreateResult> + | ComparatorUpdateResult> + | ComparatorDeleteResult; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface.ts new file mode 100644 index 0000000000000..7b174d6fb180c --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface.ts @@ -0,0 +1,8 @@ +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; + +export type PartialIndexMetadata = Omit< + IndexMetadataEntity, + 'id' | 'objectMetadata' | 'indexFieldMetadatas' +> & { + columns: string[]; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts index 5ba73cbcc2034..4e6c0e11795b0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts @@ -11,6 +11,7 @@ import { v4 as uuidV4 } from 'uuid'; import { DeepPartial } from 'typeorm/common/DeepPartial'; import { PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; +import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { @@ -22,6 +23,7 @@ import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-me import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage'; import { FieldMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; import { ObjectMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; @Injectable() export class WorkspaceMetadataUpdaterService { @@ -230,6 +232,69 @@ export class WorkspaceMetadataUpdaterService { }; } + async updateIndexMetadata( + manager: EntityManager, + storage: WorkspaceSyncStorage, + originalObjectMetadataCollection: ObjectMetadataEntity[], + ): Promise<{ + createdIndexMetadataCollection: IndexMetadataEntity[]; + }> { + const indexMetadataRepository = manager.getRepository(IndexMetadataEntity); + + const convertIndexMetadataForSaving = ( + indexMetadata: PartialIndexMetadata, + ) => { + const convertIndexFieldMetadataForSaving = ( + column: string, + order: number, + ) => { + const fieldMetadata = originalObjectMetadataCollection + .find((object) => object.id === indexMetadata.objectMetadataId) + ?.fields.find((field) => column === field.name); + + if (!fieldMetadata) { + throw new Error(` + Field metadata not found for column ${column} in object ${indexMetadata.objectMetadataId} + `); + } + + return { + fieldMetadataId: fieldMetadata.id, + order, + }; + }; + + return { + ...indexMetadata, + indexFieldMetadatas: indexMetadata.columns.map((column, index) => + convertIndexFieldMetadataForSaving(column, index), + ), + }; + }; + + /** + * Create index metadata + */ + const createdIndexMetadataCollection = await indexMetadataRepository.save( + storage.indexMetadataCreateCollection.map(convertIndexMetadataForSaving), + ); + + /** + * Delete index metadata + */ + if (storage.indexMetadataDeleteCollection.length > 0) { + await indexMetadataRepository.delete( + storage.indexMetadataDeleteCollection.map( + (indexMetadata) => indexMetadata.id, + ), + ); + } + + return { + createdIndexMetadataCollection, + }; + } + /** * Update entities in the database * @param manager EntityManager diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts index 4fe25b628ce21..e590900bc8282 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts @@ -48,7 +48,7 @@ export class WorkspaceSyncFieldMetadataService { relations: ['dataSource', 'fields'], }); - // Filter out custom objects + // Filter out non-custom objects const customObjectMetadataCollection = originalObjectMetadataCollection.filter( (objectMetadata) => objectMetadata.isCustom, @@ -61,7 +61,7 @@ export class WorkspaceSyncFieldMetadataService { workspaceFeatureFlagsMap, ); - // Loop over all standard objects and compare them with the objects in DB + // Loop over all custom objects from the DB and compare their fields with standard fields for (const customObjectMetadata of customObjectMetadataCollection) { // Also, maybe it's better to refactor a bit and move generation part into a separate module ? const standardObjectMetadata = computeStandardObject( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts new file mode 100644 index 0000000000000..4f7a30ef9d25f --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts @@ -0,0 +1,119 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; +import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; +import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; +import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; + +import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage'; +import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; +import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator'; +import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service'; +import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory'; + +@Injectable() +export class WorkspaceSyncIndexMetadataService { + private readonly logger = new Logger(WorkspaceSyncIndexMetadataService.name); + + constructor( + private readonly standardIndexFactory: StandardIndexFactory, + private readonly workspaceIndexComparator: WorkspaceIndexComparator, + private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService, + private readonly workspaceMigrationIndexFactory: WorkspaceMigrationIndexFactory, + ) {} + + async synchronize( + context: WorkspaceSyncContext, + manager: EntityManager, + storage: WorkspaceSyncStorage, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): Promise[]> { + this.logger.log('Syncing index metadata'); + + const objectMetadataRepository = + manager.getRepository(ObjectMetadataEntity); + + // Retrieve object metadata collection from DB + const originalObjectMetadataCollection = + await objectMetadataRepository.find({ + where: { + workspaceId: context.workspaceId, + // We're only interested in standard fields + fields: { isCustom: false }, + isCustom: false, + }, + relations: ['dataSource', 'fields', 'indexes'], + }); + + // Create map of object metadata & field metadata by unique identifier + const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier( + originalObjectMetadataCollection, + // Relation are based on the singular name + (objectMetadata) => objectMetadata.nameSingular, + ); + + const indexMetadataRepository = manager.getRepository(IndexMetadataEntity); + + const originalIndexMetadataCollection = await indexMetadataRepository.find({ + where: { + workspaceId: context.workspaceId, + }, + relations: ['indexFieldMetadatas.fieldMetadata'], + }); + + // Generate index metadata from models + const standardIndexMetadataCollection = this.standardIndexFactory.create( + standardObjectMetadataDefinitions, + context, + originalObjectMetadataMap, + workspaceFeatureFlagsMap, + ); + + const indexComparatorResults = this.workspaceIndexComparator.compare( + originalIndexMetadataCollection, + standardIndexMetadataCollection, + ); + + for (const indexComparatorResult of indexComparatorResults) { + if (indexComparatorResult.action === ComparatorAction.CREATE) { + storage.addCreateIndexMetadata(indexComparatorResult.object); + } else if (indexComparatorResult.action === ComparatorAction.DELETE) { + storage.addDeleteIndexMetadata(indexComparatorResult.object); + } + } + + const metadataIndexUpdaterResult = + await this.workspaceMetadataUpdaterService.updateIndexMetadata( + manager, + storage, + originalObjectMetadataCollection, + ); + + // Create migrations + const createIndexWorkspaceMigrations = + await this.workspaceMigrationIndexFactory.create( + originalObjectMetadataCollection, + metadataIndexUpdaterResult.createdIndexMetadataCollection, + WorkspaceMigrationBuilderAction.CREATE, + ); + + const deleteIndexWorkspaceMigrations = + await this.workspaceMigrationIndexFactory.create( + originalObjectMetadataCollection, + storage.indexMetadataDeleteCollection, + WorkspaceMigrationBuilderAction.DELETE, + ); + + return [ + ...createIndexWorkspaceMigrations, + ...deleteIndexWorkspaceMigrations, + ]; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts index 147389b18918a..7d809b3f80869 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts @@ -112,6 +112,10 @@ export class WorkspaceSyncObjectMetadataService { /** * COMPARE FIELD METADATA + * NOTE: This should be moved to WorkspaceSyncFieldMetadataService for more clarity since + * this code only adds field metadata to the storage but it's actually used in the other service. + * NOTE2: WorkspaceSyncFieldMetadataService has been added for custom fields sync, it should be refactored to handle + * both custom and non-custom fields. */ const fieldComparatorResults = this.workspaceFieldComparator.compare( originalObjectMetadata, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts index 6d0a7a9d39aec..5cf0fd60ab92a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts @@ -4,6 +4,7 @@ import { ComputedPartialFieldMetadata } from 'src/engine/workspace-manager/works import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; export class WorkspaceSyncStorage { // Object metadata @@ -25,10 +26,17 @@ export class WorkspaceSyncStorage { // Relation metadata private readonly _relationMetadataCreateCollection: Partial[] = []; + private readonly _relationMetadataUpdateCollection: Partial[] = + []; private readonly _relationMetadataDeleteCollection: RelationMetadataEntity[] = []; - private readonly _relationMetadataUpdateCollection: Partial[] = + + // Index metadata + private readonly _indexMetadataCreateCollection: Partial[] = + []; + private readonly _indexMetadataUpdateCollection: Partial[] = []; + private readonly _indexMetadataDeleteCollection: IndexMetadataEntity[] = []; constructor() {} @@ -68,6 +76,18 @@ export class WorkspaceSyncStorage { return this._relationMetadataDeleteCollection; } + get indexMetadataCreateCollection() { + return this._indexMetadataCreateCollection; + } + + get indexMetadataUpdateCollection() { + return this._indexMetadataUpdateCollection; + } + + get indexMetadataDeleteCollection() { + return this._indexMetadataDeleteCollection; + } + addCreateObjectMetadata(object: ComputedPartialWorkspaceEntity) { this._objectMetadataCreateCollection.push(object); } @@ -107,4 +127,16 @@ export class WorkspaceSyncStorage { addDeleteRelationMetadata(relation: RelationMetadataEntity) { this._relationMetadataDeleteCollection.push(relation); } + + addCreateIndexMetadata(index: Partial) { + this._indexMetadataCreateCollection.push(index); + } + + addUpdateIndexMetadata(index: Partial) { + this._indexMetadataUpdateCollection.push(index); + } + + addDeleteIndexMetadata(index: IndexMetadataEntity) { + this._indexMetadataDeleteCollection.push(index); + } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts index 7e8d046cbe7ea..951d9e74a0b0e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts @@ -16,6 +16,7 @@ import { WorkspaceSyncRelationMetadataService } from 'src/engine/workspace-manag import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service'; import { WorkspaceMigrationBuilderModule } from 'src/engine/workspace-manager/workspace-migration-builder/workspace-migration-builder.module'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; +import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service'; @Module({ imports: [ @@ -41,6 +42,7 @@ import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspa WorkspaceSyncRelationMetadataService, WorkspaceSyncFieldMetadataService, WorkspaceSyncMetadataService, + WorkspaceSyncIndexMetadataService, ], exports: [...workspaceSyncMetadataFactories, WorkspaceSyncMetadataService], }) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts index 8b4db8d36ae46..b233be4e37471 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts @@ -13,6 +13,7 @@ import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/ import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; +import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service'; interface SynchronizeOptions { applyChanges?: boolean; @@ -31,6 +32,7 @@ export class WorkspaceSyncMetadataService { private readonly workspaceSyncRelationMetadataService: WorkspaceSyncRelationMetadataService, private readonly workspaceSyncFieldMetadataService: WorkspaceSyncFieldMetadataService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, + private readonly workspaceSyncIndexMetadataService: WorkspaceSyncIndexMetadataService, ) {} /** @@ -97,11 +99,21 @@ export class WorkspaceSyncMetadataService { workspaceFeatureFlagsMap, ); + // 4 - Sync standard indexes on standard objects + const workspaceIndexMigrations = + await this.workspaceSyncIndexMetadataService.synchronize( + context, + manager, + storage, + workspaceFeatureFlagsMap, + ); + // Save workspace migrations into the database workspaceMigrations = await workspaceMigrationRepository.save([ ...workspaceObjectMigrations, ...workspaceFieldMigrations, ...workspaceRelationMigrations, + ...workspaceIndexMigrations, ]); // If we're running a dry run, rollback the transaction and do not execute migrations From 158e7a31f4ea3d02367db2599f43e9176d95a078 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sun, 23 Jun 2024 20:12:18 +0200 Subject: [PATCH 12/30] Improve tests (#5994) Our tests on FE are red, which is a threat to code quality. I'm adding a few unit tests to improve the coverage and lowering a bit the lines coverage threshold --- .../src/generated/graphql.tsx | 2 +- packages/twenty-front/jest.config.ts | 2 +- .../src/generated-metadata/graphql.ts | 2 +- .../twenty-front/src/generated/graphql.tsx | 2 +- .../CaptchaProviderScriptLoaderEffect.tsx | 2 +- .../hooks/useRequestFreshCaptchaToken.ts | 2 +- .../__tests__/getCaptchaUrlByProvider.test.ts | 38 +++++++++++++++ .../captcha/utils/getCaptchaUrlByProvider.ts | 20 +++++--- .../getForeignDataWrapperType.test.ts | 15 ++++++ .../utils/__tests__/indexAppPath.test.ts | 10 ++++ .../constants/SettingsFieldCurrencyCodes.ts | 2 +- .../getSettingsIntegrationAll.test.ts | 48 +++++++++++++++++++ .../src/testing/mock-data/config.ts | 2 +- .../getImageAbsoluteURIOrBase64.test.ts | 27 +++++++++++ .../integrations/captcha/captcha.module.ts | 2 +- .../captcha/interfaces/captcha.interface.ts | 8 ++-- 16 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 packages/twenty-front/src/modules/captcha/utils/__tests__/getCaptchaUrlByProvider.test.ts create mode 100644 packages/twenty-front/src/modules/databases/utils/__tests__/getForeignDataWrapperType.test.ts create mode 100644 packages/twenty-front/src/modules/navigation/utils/__tests__/indexAppPath.test.ts create mode 100644 packages/twenty-front/src/modules/settings/integrations/utils/__tests__/getSettingsIntegrationAll.test.ts create mode 100644 packages/twenty-front/src/utils/image/__tests__/getImageAbsoluteURIOrBase64.test.ts diff --git a/packages/twenty-chrome-extension/src/generated/graphql.tsx b/packages/twenty-chrome-extension/src/generated/graphql.tsx index 69e043e63d650..33b20dd349842 100644 --- a/packages/twenty-chrome-extension/src/generated/graphql.tsx +++ b/packages/twenty-chrome-extension/src/generated/graphql.tsx @@ -1640,7 +1640,7 @@ export type Captcha = { }; export enum CaptchaDriverType { - GoogleRecatpcha = 'GoogleRecatpcha', + GoogleRecaptcha = 'GoogleRecaptcha', Turnstile = 'Turnstile' } diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index 63c026a8c4ae5..e21e9cbaf0a7e 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -25,7 +25,7 @@ const jestConfig: JestConfigWithTsJest = { coverageThreshold: { global: { statements: 65, - lines: 65, + lines: 64, functions: 55, }, }, diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 05155eaa108e7..7f5c5c4ddedbe 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -136,7 +136,7 @@ export type Captcha = { }; export enum CaptchaDriverType { - GoogleRecatpcha = 'GoogleRecatpcha', + GoogleRecaptcha = 'GoogleRecaptcha', Turnstile = 'Turnstile' } diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index d153c73c0adaf..9da680560f024 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -130,7 +130,7 @@ export type Captcha = { }; export enum CaptchaDriverType { - GoogleRecatpcha = 'GoogleRecatpcha', + GoogleRecaptcha = 'GoogleRecaptcha', Turnstile = 'Turnstile' } diff --git a/packages/twenty-front/src/modules/captcha/components/CaptchaProviderScriptLoaderEffect.tsx b/packages/twenty-front/src/modules/captcha/components/CaptchaProviderScriptLoaderEffect.tsx index 59dedab8fee39..aae90964f1dd8 100644 --- a/packages/twenty-front/src/modules/captcha/components/CaptchaProviderScriptLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/captcha/components/CaptchaProviderScriptLoaderEffect.tsx @@ -32,7 +32,7 @@ export const CaptchaProviderScriptLoaderEffect = () => { scriptElement = document.createElement('script'); scriptElement.src = scriptUrl; scriptElement.onload = () => { - if (captchaProvider.provider === CaptchaDriverType.GoogleRecatpcha) { + if (captchaProvider.provider === CaptchaDriverType.GoogleRecaptcha) { window.grecaptcha?.ready(() => { setIsCaptchaScriptLoaded(true); }); diff --git a/packages/twenty-front/src/modules/captcha/hooks/useRequestFreshCaptchaToken.ts b/packages/twenty-front/src/modules/captcha/hooks/useRequestFreshCaptchaToken.ts index f2e2821fc27f4..63e6ee7254800 100644 --- a/packages/twenty-front/src/modules/captcha/hooks/useRequestFreshCaptchaToken.ts +++ b/packages/twenty-front/src/modules/captcha/hooks/useRequestFreshCaptchaToken.ts @@ -35,7 +35,7 @@ export const useRequestFreshCaptchaToken = () => { let captchaWidget: any; switch (captchaProvider.provider) { - case CaptchaDriverType.GoogleRecatpcha: + case CaptchaDriverType.GoogleRecaptcha: window.grecaptcha .execute(captchaProvider.siteKey, { action: 'submit', diff --git a/packages/twenty-front/src/modules/captcha/utils/__tests__/getCaptchaUrlByProvider.test.ts b/packages/twenty-front/src/modules/captcha/utils/__tests__/getCaptchaUrlByProvider.test.ts new file mode 100644 index 0000000000000..40cba1f430818 --- /dev/null +++ b/packages/twenty-front/src/modules/captcha/utils/__tests__/getCaptchaUrlByProvider.test.ts @@ -0,0 +1,38 @@ +import { expect } from '@storybook/test'; + +import { CaptchaDriverType } from '~/generated/graphql'; + +import { getCaptchaUrlByProvider } from '../getCaptchaUrlByProvider'; + +describe('getCaptchaUrlByProvider', () => { + it('handles GoogleRecaptcha', async () => { + const captchaUrl = getCaptchaUrlByProvider( + CaptchaDriverType.GoogleRecaptcha, + 'siteKey', + ); + + expect(captchaUrl).toEqual( + 'https://www.google.com/recaptcha/api.js?render=siteKey', + ); + + expect(() => + getCaptchaUrlByProvider(CaptchaDriverType.GoogleRecaptcha, ''), + ).toThrow( + 'SiteKey must be provided while generating url for GoogleRecaptcha provider', + ); + }); + + it('handles Turnstile', async () => { + const captchaUrl = getCaptchaUrlByProvider(CaptchaDriverType.Turnstile, ''); + + expect(captchaUrl).toEqual( + 'https://challenges.cloudflare.com/turnstile/v0/api.js', + ); + }); + + it('handles unknown provider', async () => { + expect(() => + getCaptchaUrlByProvider('Unknown' as CaptchaDriverType, ''), + ).toThrow('Unknown captcha provider'); + }); +}); diff --git a/packages/twenty-front/src/modules/captcha/utils/getCaptchaUrlByProvider.ts b/packages/twenty-front/src/modules/captcha/utils/getCaptchaUrlByProvider.ts index 5c0abe89e8a1c..b3c1874ea8973 100644 --- a/packages/twenty-front/src/modules/captcha/utils/getCaptchaUrlByProvider.ts +++ b/packages/twenty-front/src/modules/captcha/utils/getCaptchaUrlByProvider.ts @@ -1,16 +1,22 @@ -import { CaptchaDriverType } from '~/generated-metadata/graphql'; +import { isNonEmptyString } from '@sniptt/guards'; -export const getCaptchaUrlByProvider = (name: string, siteKey: string) => { - if (!name) { - return ''; - } +import { CaptchaDriverType } from '~/generated-metadata/graphql'; +export const getCaptchaUrlByProvider = ( + name: CaptchaDriverType, + siteKey: string, +) => { switch (name) { - case CaptchaDriverType.GoogleRecatpcha: + case CaptchaDriverType.GoogleRecaptcha: + if (!isNonEmptyString(siteKey)) { + throw new Error( + 'SiteKey must be provided while generating url for GoogleRecaptcha provider', + ); + } return `https://www.google.com/recaptcha/api.js?render=${siteKey}`; case CaptchaDriverType.Turnstile: return 'https://challenges.cloudflare.com/turnstile/v0/api.js'; default: - return ''; + throw new Error('Unknown captcha provider'); } }; diff --git a/packages/twenty-front/src/modules/databases/utils/__tests__/getForeignDataWrapperType.test.ts b/packages/twenty-front/src/modules/databases/utils/__tests__/getForeignDataWrapperType.test.ts new file mode 100644 index 0000000000000..3db265dd94444 --- /dev/null +++ b/packages/twenty-front/src/modules/databases/utils/__tests__/getForeignDataWrapperType.test.ts @@ -0,0 +1,15 @@ +import { getForeignDataWrapperType } from '../getForeignDataWrapperType'; + +describe('getForeignDataWrapperType', () => { + it('should handle postgres', () => { + expect(getForeignDataWrapperType('postgresql')).toBe('postgres_fdw'); + }); + + it('should handle stripe', () => { + expect(getForeignDataWrapperType('stripe')).toBe('stripe_fdw'); + }); + + it('should return null for unknown', () => { + expect(getForeignDataWrapperType('unknown')).toBeNull(); + }); +}); diff --git a/packages/twenty-front/src/modules/navigation/utils/__tests__/indexAppPath.test.ts b/packages/twenty-front/src/modules/navigation/utils/__tests__/indexAppPath.test.ts new file mode 100644 index 0000000000000..8920f6fa7445f --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/utils/__tests__/indexAppPath.test.ts @@ -0,0 +1,10 @@ +import { AppPath } from '@/types/AppPath'; + +import indexAppPath from '../indexAppPath'; + +describe('getIndexAppPath', () => { + it('returns the index app path', () => { + const { getIndexAppPath } = indexAppPath; + expect(getIndexAppPath()).toEqual(AppPath.Index); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts index 6f302e6ea90e9..ef2ea0b158268 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts @@ -88,5 +88,5 @@ export const SETTINGS_FIELD_CURRENCY_CODES: Record< BRL: { label: 'Brazilian real', Icon: IconCurrencyReal, - } + }, }; diff --git a/packages/twenty-front/src/modules/settings/integrations/utils/__tests__/getSettingsIntegrationAll.test.ts b/packages/twenty-front/src/modules/settings/integrations/utils/__tests__/getSettingsIntegrationAll.test.ts new file mode 100644 index 0000000000000..f8815dab20f13 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/integrations/utils/__tests__/getSettingsIntegrationAll.test.ts @@ -0,0 +1,48 @@ +import { getSettingsIntegrationAll } from '../getSettingsIntegrationAll'; + +describe('getSettingsIntegrationAll', () => { + it('should return null if imageUrl is null', () => { + expect( + getSettingsIntegrationAll({ + isAirtableIntegrationActive: true, + isAirtableIntegrationEnabled: true, + isPostgresqlIntegrationActive: true, + isPostgresqlIntegrationEnabled: true, + isStripeIntegrationActive: true, + isStripeIntegrationEnabled: true, + }), + ).toStrictEqual({ + integrations: [ + { + from: { + image: '/images/integrations/airtable-logo.png', + key: 'airtable', + }, + link: '/settings/integrations/airtable', + text: 'Airtable', + type: 'Active', + }, + { + from: { + image: '/images/integrations/postgresql-logo.png', + key: 'postgresql', + }, + link: '/settings/integrations/postgresql', + text: 'PostgreSQL', + type: 'Active', + }, + { + from: { + image: '/images/integrations/stripe-logo.png', + key: 'stripe', + }, + link: '/settings/integrations/stripe', + text: 'Stripe', + type: 'Active', + }, + ], + key: 'all', + title: 'All', + }); + }); +}); diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index b3eb74af804ce..d85622bd190e3 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -34,7 +34,7 @@ export const mockedClientConfig: ClientConfig = { __typename: 'Billing', }, captcha: { - provider: CaptchaDriverType.GoogleRecatpcha, + provider: CaptchaDriverType.GoogleRecaptcha, siteKey: 'MOCKED_SITE_KEY', __typename: 'Captcha', }, diff --git a/packages/twenty-front/src/utils/image/__tests__/getImageAbsoluteURIOrBase64.test.ts b/packages/twenty-front/src/utils/image/__tests__/getImageAbsoluteURIOrBase64.test.ts new file mode 100644 index 0000000000000..226aa6a0e598f --- /dev/null +++ b/packages/twenty-front/src/utils/image/__tests__/getImageAbsoluteURIOrBase64.test.ts @@ -0,0 +1,27 @@ +import { getImageAbsoluteURIOrBase64 } from '../getImageAbsoluteURIOrBase64'; + +describe('getImageAbsoluteURIOrBase64', () => { + it('should return null if imageUrl is null', () => { + const imageUrl = null; + const result = getImageAbsoluteURIOrBase64(imageUrl); + expect(result).toBeNull(); + }); + + it('should return base64 encoded string if prefixed with data', () => { + const imageUrl = 'data:XXX'; + const result = getImageAbsoluteURIOrBase64(imageUrl); + expect(result).toBe(imageUrl); + }); + + it('should return absolute url if the imageUrl is an absolute url', () => { + const imageUrl = 'https://XXX'; + const result = getImageAbsoluteURIOrBase64(imageUrl); + expect(result).toBe(imageUrl); + }); + + it('should return fully formed url if imageUrl is a relative url', () => { + const imageUrl = 'XXX'; + const result = getImageAbsoluteURIOrBase64(imageUrl); + expect(result).toBe('http://localhost:3000/files/XXX'); + }); +}); diff --git a/packages/twenty-server/src/engine/integrations/captcha/captcha.module.ts b/packages/twenty-server/src/engine/integrations/captcha/captcha.module.ts index 891bed2e5e12a..506e51f42b2fd 100644 --- a/packages/twenty-server/src/engine/integrations/captcha/captcha.module.ts +++ b/packages/twenty-server/src/engine/integrations/captcha/captcha.module.ts @@ -22,7 +22,7 @@ export class CaptchaModule { } switch (config.type) { - case CaptchaDriverType.GoogleRecatpcha: + case CaptchaDriverType.GoogleRecaptcha: return new GoogleRecaptchaDriver(config.options); case CaptchaDriverType.Turnstile: return new TurnstileDriver(config.options); diff --git a/packages/twenty-server/src/engine/integrations/captcha/interfaces/captcha.interface.ts b/packages/twenty-server/src/engine/integrations/captcha/interfaces/captcha.interface.ts index e7f0ca5620bd6..f19e15fa29851 100644 --- a/packages/twenty-server/src/engine/integrations/captcha/interfaces/captcha.interface.ts +++ b/packages/twenty-server/src/engine/integrations/captcha/interfaces/captcha.interface.ts @@ -2,7 +2,7 @@ import { FactoryProvider, ModuleMetadata } from '@nestjs/common'; import { registerEnumType } from '@nestjs/graphql'; export enum CaptchaDriverType { - GoogleRecatpcha = 'google-recaptcha', + GoogleRecaptcha = 'google-recaptcha', Turnstile = 'turnstile', } @@ -15,8 +15,8 @@ export type CaptchaDriverOptions = { secretKey: string; }; -export interface GoogleRecatpchaDriverFactoryOptions { - type: CaptchaDriverType.GoogleRecatpcha; +export interface GoogleRecaptchaDriverFactoryOptions { + type: CaptchaDriverType.GoogleRecaptcha; options: CaptchaDriverOptions; } @@ -26,7 +26,7 @@ export interface TurnstileDriverFactoryOptions { } export type CaptchaModuleOptions = - | GoogleRecatpchaDriverFactoryOptions + | GoogleRecaptchaDriverFactoryOptions | TurnstileDriverFactoryOptions; export type CaptchaModuleAsyncOptions = { From e8f386ce4360ded3b1cc4230a63f0521affbd306 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 24 Jun 2024 11:20:16 +0200 Subject: [PATCH 13/30] Fix infinite scroll issue on table (#5996) We had an issue on infinite scroll on table view. The fetch more logic was modifying isTableLastRowVisible state (which is wrong, how could it know)? This was done to prevent loading too much data at once. This was causing some race condition on isTableLastRowVisible (as the table itself was also changing it depending on the real visibility of the line) I have remove this hacky usage of isTableLastRowVisible and replaced it by a setTimeout to let the user some time to scroll and introduce a throttle logic. --- .../hooks/useLoadRecordIndexTable.ts | 5 +--- .../components/RecordTableBodyEffect.tsx | 30 ++++++++----------- .../components/RecordTableBodyLoading.tsx | 1 + 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index c6a26daada1da..99f613521b2dc 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -1,4 +1,4 @@ -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -41,8 +41,6 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => { const { setRecordTableData, setIsRecordTableInitialLoading } = useRecordTable(); - const { tableLastRowVisibleState } = useRecordTableStates(); - const setLastRowVisible = useSetRecoilState(tableLastRowVisibleState); const currentWorkspace = useRecoilValue(currentWorkspaceState); const params = useFindManyParams(objectNameSingular); @@ -58,7 +56,6 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => { ...params, recordGqlFields, onCompleted: () => { - setLastRowVisible(false); setIsRecordTableInitialLoading(false); }, onError: () => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx index 4bea69f3d4467..8d3422a215a8c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; @@ -18,20 +18,18 @@ export const RecordTableBodyEffect = ({ records, totalCount, setRecordTableData, - queryStateIdentifier, loading, + queryStateIdentifier, } = useLoadRecordIndexTable(objectNameSingular); - const { tableLastRowVisibleState } = useRecordTableStates(); - - const [tableLastRowVisible, setTableLastRowVisible] = useRecoilState( - tableLastRowVisibleState, - ); - const isFetchingMoreObjects = useRecoilValue( isFetchingMoreRecordsFamilyState(queryStateIdentifier), ); + const { tableLastRowVisibleState } = useRecordTableStates(); + + const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState); + const rowHeight = 32; const viewportHeight = records.length * rowHeight; @@ -44,15 +42,13 @@ export const RecordTableBodyEffect = ({ }, [records, totalCount, setRecordTableData, loading]); useEffect(() => { - if (tableLastRowVisible && !isFetchingMoreObjects) { - fetchMoreObjects(); - } - }, [ - fetchMoreObjects, - isFetchingMoreObjects, - setTableLastRowVisible, - tableLastRowVisible, - ]); + // We are adding a setTimeout here to give the user some room to scroll if they want to within this throttle window + setTimeout(async () => { + if (!isFetchingMoreObjects && tableLastRowVisible) { + await fetchMoreObjects(); + } + }, 100); + }, [fetchMoreObjects, isFetchingMoreObjects, tableLastRowVisible]); return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyLoading.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyLoading.tsx index d8df4058b7844..0f89384ec0e53 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyLoading.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyLoading.tsx @@ -20,6 +20,7 @@ export const RecordTableBodyLoading = () => { isDragging={false} data-testid={`row-id-${rowIndex}`} data-selectable-id={`row-id-${rowIndex}`} + key={rowIndex} > From 2e4ba9ca7bdc559e1e43d6ea8c22119900f884a3 Mon Sep 17 00:00:00 2001 From: Raunak Singh Jolly Date: Mon, 24 Jun 2024 15:33:46 +0530 Subject: [PATCH 14/30] Remove Right-Edge Gap in Table Cell Display (#5992) fixes #5941 ![Screenshot from 2024-06-23 17-24-24](https://github.com/twentyhq/twenty/assets/59247136/ae67603a-824d-4e6b-b873-2d58e6296341) --------- Co-authored-by: Lucas Bordeau --- .../record-table/components/RecordTable.tsx | 2 ++ .../components/RecordTableCellContainer.module.css | 2 +- .../RecordTableCellDisplayContainer.module.css | 9 +++------ .../components/RecordTableCellDisplayContainer.tsx | 2 +- .../components/RecordTableCellDisplayMode.tsx | 5 +++-- .../components/RecordTableCellSoftFocusMode.tsx | 1 + 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 2afb86e9875c3..84a8e13cbdb31 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -53,6 +53,8 @@ const StyledTable = styled.table<{ color: ${({ theme }) => theme.font.color.primary}; border-right: 1px solid ${({ theme }) => theme.border.color.light}; + padding: 0; + text-align: left; :last-child { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.module.css b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.module.css index eef506c67a98f..ed8e3802356b8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.module.css +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.module.css @@ -27,6 +27,6 @@ .cell-base-container-soft-focus { background: var(--twentycrm-background-transparent-secondary); border-radius: var(--twentycrm-border-radius-sm); - outline: 1px solid var(--twentycrm-font-color-extra-light); + border: 1px solid var(--twentycrm-font-color-extra-light); } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.module.css b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.module.css index d2184fcda0ba6..381efe436ff1c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.module.css +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.module.css @@ -3,15 +3,12 @@ display: flex; height: 100%; overflow: hidden; - padding-left: 8px; - padding-right: 4px; + padding-left: 6px; width: 100%; } -.cell-display-outer-container-soft-focus { - background: var(--twentycrm-background-transparent-secondary); - border-radius: var(--twentycrm-border-radius-sm); - outline: 1px solid var(--twentycrm-font-color-extra-light); +.cell-display-with-soft-focus { + margin: -1px; } .cell-display-inner-container { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx index f2db2901da0e4..e47277cc1d501 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx @@ -23,7 +23,7 @@ export const RecordTableCellDisplayContainer = ({ onClick={onClick} className={clsx({ [styles.cellDisplayOuterContainer]: true, - [styles.cellDisplayOuterContainerSoftFocus]: softFocus, + [styles.cellDisplayWithSoftFocus]: softFocus, })} ref={scrollRef} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx index 566be79baf32e..f562348e79740 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx @@ -4,7 +4,8 @@ import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContain export const RecordTableCellDisplayMode = ({ children, -}: React.PropsWithChildren) => { + softFocus, +}: React.PropsWithChildren<{ softFocus?: boolean }>) => { const isEmpty = useIsFieldEmpty(); if (isEmpty) { @@ -12,7 +13,7 @@ export const RecordTableCellDisplayMode = ({ } return ( - + {children} ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx index e7f3a03599e3f..6dd6387a15af3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx @@ -159,6 +159,7 @@ export const RecordTableCellSoftFocusMode = ({ {editModeContentOnly ? editModeContent : nonEditModeContent} From 498e4ff6ba16bf1306b2485305eb31cb8022f8d0 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 24 Jun 2024 13:45:07 +0200 Subject: [PATCH 15/30] Refactor infiniteScoll to use debouncing (#5999) Same as https://github.com/twentyhq/twenty/pull/5996 but with useDebounced as asked in review --- .../components/RecordTableBodyEffect.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx index 8d3422a215a8c..508c2c8c8ffd8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; +import { useDebouncedCallback } from 'use-debounce'; import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; @@ -41,14 +42,20 @@ export const RecordTableBodyEffect = ({ } }, [records, totalCount, setRecordTableData, loading]); + const fetchMoreDebouncedIfRequested = useDebouncedCallback(async () => { + // We are debouncing here to give the user some room to scroll if they want to within this throttle window + await fetchMoreObjects(); + }, 100); + useEffect(() => { - // We are adding a setTimeout here to give the user some room to scroll if they want to within this throttle window - setTimeout(async () => { - if (!isFetchingMoreObjects && tableLastRowVisible) { - await fetchMoreObjects(); - } - }, 100); - }, [fetchMoreObjects, isFetchingMoreObjects, tableLastRowVisible]); + if (!isFetchingMoreObjects && tableLastRowVisible) { + fetchMoreDebouncedIfRequested(); + } + }, [ + fetchMoreDebouncedIfRequested, + isFetchingMoreObjects, + tableLastRowVisible, + ]); return <>; }; From 77f9f6473b4fbea75355b3503903ad854bea3ddc Mon Sep 17 00:00:00 2001 From: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:54:52 +0200 Subject: [PATCH 16/30] Create feature flag for calendar V2 (#5998) Create feature flag for calendar V2 --- .../src/database/typeorm-seeds/core/feature-flags.ts | 5 +++++ .../engine/core-modules/feature-flag/feature-flag.entity.ts | 1 + .../commands/add-standard-id.command.ts | 2 ++ 3 files changed, 8 insertions(+) diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index 3a36ad753ff77..f20528d23d1d3 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -45,6 +45,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKeys.IsGoogleCalendarSyncV2Enabled, + workspaceId: workspaceId, + value: true, + }, ]) .execute(); }; diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts index 954461abdc70a..6214f0f7a84bb 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts @@ -22,6 +22,7 @@ export enum FeatureFlagKeys { IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED', IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED', IsContactCreationForSentAndReceivedEmailsEnabled = 'IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED', + IsGoogleCalendarSyncV2Enabled = 'IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED', } @Entity({ name: 'featureFlag', schema: 'core' }) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts index be810ced1e81e..d89a8357ce466 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts @@ -59,6 +59,7 @@ export class AddStandardIdCommand extends CommandRunner { IS_POSTGRESQL_INTEGRATION_ENABLED: true, IS_STRIPE_INTEGRATION_ENABLED: false, IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED: true, + IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true, }, ); const standardFieldMetadataCollection = this.standardFieldFactory.create( @@ -74,6 +75,7 @@ export class AddStandardIdCommand extends CommandRunner { IS_POSTGRESQL_INTEGRATION_ENABLED: true, IS_STRIPE_INTEGRATION_ENABLED: false, IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED: true, + IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true, }, ); From 28c8f0df32972cbca83359116f23b83a11365ac7 Mon Sep 17 00:00:00 2001 From: Atharv Parlikar <58113282+atharvParlikar@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:21:18 +0530 Subject: [PATCH 17/30] Turned on tooltip on kanban cards with shortDelay (#5991) fixes: #5982 Demo: https://github.com/twentyhq/twenty/assets/58113282/6593381c-c01a-4259-9caa-8612247a9e95 --------- Co-authored-by: Lucas Bordeau --- .../record-board-card/components/RecordBoardCard.tsx | 2 +- .../components/RecordInlineCellContainer.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index 0ad27183ca463..0072081f1dba2 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -262,7 +262,7 @@ export const RecordBoardCard = () => { recoilScopeId: recordId + fieldDefinition.fieldMetadataId, isLabelIdentifier: false, fieldDefinition: { - disableTooltip: true, + disableTooltip: false, fieldMetadataId: fieldDefinition.fieldMetadataId, label: fieldDefinition.label, iconName: fieldDefinition.iconName, diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx index d93b2b309b788..2cb77520fa322 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx @@ -1,7 +1,7 @@ import React, { ReactElement, useContext } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { AppTooltip, IconComponent } from 'twenty-ui'; +import { AppTooltip, IconComponent, TooltipDelay } from 'twenty-ui'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; @@ -136,6 +136,7 @@ export const RecordInlineCellContainer = ({ noArrow place="bottom" positionStrategy="fixed" + delay={TooltipDelay.shortDelay} /> )} From 901ef655451f1f69767638dc6f8cbdb2b750d1cd Mon Sep 17 00:00:00 2001 From: Rob Luke Date: Mon, 24 Jun 2024 22:55:37 +1000 Subject: [PATCH 18/30] feat: add australian dollar currency (#5990) Hi Twenty team, I'd love to have Australian dollar as an option in Twenty! Please let me me know if I have missed anything I need to change to enable this. Thanks for a a great product --------- Co-authored-by: Lucas Bordeau --- .../modules/object-record/record-field/types/CurrencyCode.ts | 1 + .../data-model/constants/SettingsFieldCurrencyCodes.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts b/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts index 7d3fad5846c89..9e86b3c87939a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts @@ -16,4 +16,5 @@ export enum CurrencyCode { AED = 'AED', KRW = 'KRW', BRL = 'BRL', + AUD = 'AUD', } diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts index ef2ea0b158268..5e013b50ca26f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts @@ -89,4 +89,8 @@ export const SETTINGS_FIELD_CURRENCY_CODES: Record< label: 'Brazilian real', Icon: IconCurrencyReal, }, + AUD: { + label: 'Australian dollar', + Icon: IconCurrencyDollar, + }, }; From 57bbd7c129b3e843e121311b9691247e9d027684 Mon Sep 17 00:00:00 2001 From: Aakarshan Thapa <63531478+akarsanth@users.noreply.github.com> Date: Mon, 24 Jun 2024 09:01:21 -0400 Subject: [PATCH 19/30] Add update chevron (#5988) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #5986 1. Added right chevron to Fields Menu Item Screenshot 2024-06-21 at 5 59 46 PM 2. Changed color of Hidden fields menu item chevron and stroke of left chevron Screenshot 2024-06-21 at 6 21 30 PM --------- Co-authored-by: Lucas Bordeau --- .../RecordIndexOptionsDropdownContent.tsx | 1 + .../ui/input/button/components/LightIconButton.tsx | 2 +- .../ui/navigation/menu-item/components/MenuItem.tsx | 13 ++++++++++++- .../menu-item/components/MenuItemNavigate.tsx | 5 ++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx index 73149fb6eba51..c93c8bfdadf1c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx @@ -132,6 +132,7 @@ export const RecordIndexOptionsDropdownContent = ({ onClick={() => handleSelectMenu('fields')} LeftIcon={IconTag} text="Fields" + hasSubMenu /> openRecordSpreadsheetImport()} diff --git a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx b/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx index 5f03c700e0c9a..868ed0c534a7d 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx @@ -105,7 +105,7 @@ export const LightIconButton = ({ active={active} title={title} > - {Icon && } + {Icon && } ); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx index 4e40759f97503..a8571280723c4 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx @@ -1,5 +1,6 @@ import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react'; -import { IconComponent } from 'twenty-ui'; +import { useTheme } from '@emotion/react'; +import { IconChevronRight, IconComponent } from 'twenty-ui'; import { LightIconButtonProps } from '@/ui/input/button/components/LightIconButton'; import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup'; @@ -30,6 +31,7 @@ export type MenuItemProps = { onMouseLeave?: (event: MouseEvent) => void; testId?: string; text: ReactNode; + hasSubMenu?: boolean; }; export const MenuItem = ({ @@ -43,7 +45,9 @@ export const MenuItem = ({ onMouseLeave, testId, text, + hasSubMenu = false, }: MenuItemProps) => { + const theme = useTheme(); const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0; const handleMenuItemClick = (event: MouseEvent) => { @@ -72,6 +76,13 @@ export const MenuItem = ({ )} + + {hasSubMenu && ( + + )} ); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemNavigate.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemNavigate.tsx index 07f5ff1e03a69..dd847b3048d62 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemNavigate.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemNavigate.tsx @@ -27,7 +27,10 @@ export const MenuItemNavigate = ({ - + ); }; From 24c31f9b39529483517e6fc0b0890c541ef8859b Mon Sep 17 00:00:00 2001 From: Us3r-gitHub <58467104+Us3r-gitHub@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:05:40 +0700 Subject: [PATCH 20/30] Fix(view): Show Kanban View Creation (#5985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # This PR - Revise my previous work (PR #5969) Because it would break the current logic and cause unexpected behavior. (Issue #5979) - Solve (Issue #5915) with another way @lucasbordeau What do you think about my current approach? @JarWarren Please check it out—I'd love to get your feedback too! --------- Co-authored-by: Achsan Co-authored-by: Lucas Bordeau --- .../ViewPickerCreateOrEditContentEffect.tsx | 24 ++++++++++++------- .../components/ViewPickerDropdown.tsx | 8 +------ .../hooks/useGetAvailableFieldsForKanban.ts | 6 ----- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateOrEditContentEffect.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateOrEditContentEffect.tsx index 6124063e002c4..45ba1209c18d6 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateOrEditContentEffect.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateOrEditContentEffect.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; @@ -22,9 +22,8 @@ export const ViewPickerCreateOrEditContentEffect = () => { ); const setViewPickerInputName = useSetRecoilState(viewPickerInputNameState); - const setViewPickerKanbanFieldMetadataId = useSetRecoilState( - viewPickerKanbanFieldMetadataIdState, - ); + const [viewPickerKanbanFieldMetadataId, setViewPickerKanbanFieldMetadataId] = + useRecoilState(viewPickerKanbanFieldMetadataIdState); const setViewPickerType = useSetRecoilState(viewPickerTypeState); const viewPickerReferenceViewId = useRecoilValue( @@ -50,13 +49,11 @@ export const ViewPickerCreateOrEditContentEffect = () => { ) { setViewPickerSelectedIcon(referenceView.icon); setViewPickerInputName(referenceView.name); - setViewPickerKanbanFieldMetadataId(referenceView.kanbanFieldMetadataId); setViewPickerType(referenceView.type); } }, [ referenceView, setViewPickerInputName, - setViewPickerKanbanFieldMetadataId, setViewPickerSelectedIcon, setViewPickerType, viewPickerIsPersisting, @@ -64,13 +61,22 @@ export const ViewPickerCreateOrEditContentEffect = () => { ]); useEffect(() => { - if (availableFieldsForKanban.length > 0 && !viewPickerIsDirty) { - setViewPickerKanbanFieldMetadataId(availableFieldsForKanban[0].id); + if ( + isDefined(referenceView) && + availableFieldsForKanban.length > 0 && + viewPickerKanbanFieldMetadataId === '' + ) { + setViewPickerKanbanFieldMetadataId( + referenceView.kanbanFieldMetadataId !== '' + ? referenceView.kanbanFieldMetadataId + : availableFieldsForKanban[0].id, + ); } }, [ + referenceView, availableFieldsForKanban, + viewPickerKanbanFieldMetadataId, setViewPickerKanbanFieldMetadataId, - viewPickerIsDirty, ]); return <>; diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx index 2ead64a8a358f..a3461683af1c3 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { IconChevronDown, IconList, @@ -19,7 +19,6 @@ import { ViewPickerListContent } from '@/views/view-picker/components/ViewPicker import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId'; import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { useViewPickerPersistView } from '@/views/view-picker/hooks/useViewPickerPersistView'; -import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates'; import { isDefined } from '~/utils/isDefined'; import { useViewStates } from '../../hooks/internal/useViewStates'; @@ -53,8 +52,6 @@ export const ViewPickerDropdown = () => { const { entityCountInCurrentViewState } = useViewStates(); - const { viewPickerIsDirtyState } = useViewPickerStates(); - const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); const { handleUpdate } = useViewPickerPersistView(); @@ -63,8 +60,6 @@ export const ViewPickerDropdown = () => { entityCountInCurrentViewState, ); - const setViewPickerIsDirty = useSetRecoilState(viewPickerIsDirtyState); - const { isDropdownOpen: isViewsListDropdownOpen } = useDropdown( VIEW_PICKER_DROPDOWN_ID, ); @@ -75,7 +70,6 @@ export const ViewPickerDropdown = () => { const CurrentViewIcon = getIcon(currentViewWithCombinedFiltersAndSorts?.icon); const handleClickOutside = async () => { - setViewPickerIsDirty(false); if (isViewsListDropdownOpen && viewPickerMode === 'edit') { await handleUpdate(); } diff --git a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts index a0ea116b63388..d5044bd196397 100644 --- a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts +++ b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts @@ -5,16 +5,13 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useViewStates } from '@/views/hooks/internal/useViewStates'; -import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const useGetAvailableFieldsForKanban = () => { const { viewObjectMetadataIdState } = useViewStates(); - const { viewPickerIsDirtyState } = useViewPickerStates(); const viewObjectMetadataId = useRecoilValue(viewObjectMetadataIdState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const setViewPickerIsDirty = useSetRecoilState(viewPickerIsDirtyState); const setNavigationMemorizedUrl = useSetRecoilState( navigationMemorizedUrlState, ); @@ -32,15 +29,12 @@ export const useGetAvailableFieldsForKanban = () => { const navigate = useNavigate(); const navigateToSelectSettings = useCallback(() => { - setViewPickerIsDirty(false); - setNavigationMemorizedUrl(location.pathname + location.search); navigate(`/settings/objects/${objectMetadataItem?.namePlural}`); }, [ navigate, objectMetadataItem?.namePlural, - setViewPickerIsDirty, setNavigationMemorizedUrl, location, ]); From ad61efe6ed0e7a7f1872e7d399362b2850d6cc5e Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 24 Jun 2024 16:39:17 +0200 Subject: [PATCH 21/30] Remove multi select usage (#6004) As per title! Also, I'm removing an incorrect logic in the enum migration runner that takes care of the case where we have no defaultValue but non nullable which is not a valid business case. --- .../utils/formatFieldMetadataItemsAsFilterDefinitions.ts | 1 - .../services/workspace-migration-enum.service.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts index c5df10c85c8f4..222070f21b450 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts @@ -22,7 +22,6 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({ FieldMetadataType.Address, FieldMetadataType.Relation, FieldMetadataType.Select, - FieldMetadataType.MultiSelect, FieldMetadataType.Currency, ].includes(field.type) ) { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service.ts index 05ebc0370482b..03dd078fa6a66 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service.ts @@ -49,10 +49,6 @@ export class WorkspaceMigrationEnumService { typeof enumValue !== 'string', ); - if (!columnDefinition.isNullable && !columnDefinition.defaultValue) { - columnDefinition.defaultValue = serializeDefaultValue(enumValues[0]); - } - const oldColumnName = `${columnDefinition.columnName}_old_${v4()}`; // Rename old column From f3701281e9ec8bc080705cff969e5a297c0f5c6f Mon Sep 17 00:00:00 2001 From: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:39:56 +0200 Subject: [PATCH 22/30] Create new sync statuses and stages for calendar (#5997) Create fields: - syncStatus - syncStage - syncStageStartedAt --- .../constants/standard-field-ids.ts | 3 + .../calendar-channel.workspace-entity.ts | 119 ++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 21910d0a758e3..402fc905f9ee2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -71,6 +71,9 @@ export const CALENDAR_CHANNEL_STANDARD_FIELD_IDS = { syncCursor: '20202020-bac2-4852-a5cb-7a7898992b70', calendarChannelEventAssociations: '20202020-afb0-4a9f-979f-2d5087d71d09', throttleFailureCount: '20202020-525c-4b76-b9bd-0dd57fd11d61', + syncStatus: '20202020-7116-41da-8b4b-035975c4eb6a', + syncStage: '20202020-6246-42e6-b5cd-003bd921782c', + syncStageStartedAt: '20202020-a934-46f1-a8e7-9568b1e3a53e', }; export const CALENDAR_EVENT_PARTICIPANT_STANDARD_FIELD_IDS = { diff --git a/packages/twenty-server/src/modules/calendar/standard-objects/calendar-channel.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/standard-objects/calendar-channel.workspace-entity.ts index 85aed3b43c9e0..c72af80f87fbd 100644 --- a/packages/twenty-server/src/modules/calendar/standard-objects/calendar-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/standard-objects/calendar-channel.workspace-entity.ts @@ -15,12 +15,30 @@ import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; +import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; export enum CalendarChannelVisibility { METADATA = 'METADATA', SHARE_EVERYTHING = 'SHARE_EVERYTHING', } +export enum CalendarChannelSyncStatus { + NOT_SYNCED = 'NOT_SYNCED', + ONGOING = 'ONGOING', + ACTIVE = 'ACTIVE', + FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS', + FAILED_UNKNOWN = 'FAILED_UNKNOWN', +} + +export enum CalendarChannelSyncStage { + FULL_CALENDAR_EVENT_LIST_FETCH_PENDING = 'FULL_CALENDAR_EVENT_LIST_FETCH_PENDING', + PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING = 'PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING', + CALENDAR_EVENT_LIST_FETCH_ONGOING = 'CALENDAR_EVENT_LIST_FETCH_ONGOING', + CALENDAR_EVENTS_IMPORT_PENDING = 'CALENDAR_EVENTS_IMPORT_PENDING', + CALENDAR_EVENTS_IMPORT_ONGOING = 'CALENDAR_EVENTS_IMPORT_ONGOING', + FAILED = 'FAILED', +} + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.calendarChannel, namePlural: 'calendarChannels', @@ -41,6 +59,97 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity { }) handle: string; + @WorkspaceField({ + standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncStatus, + type: FieldMetadataType.SELECT, + label: 'Sync status', + description: 'Sync status', + icon: 'IconStatusChange', + options: [ + { + value: CalendarChannelSyncStatus.ONGOING, + label: 'Ongoing', + position: 1, + color: 'yellow', + }, + { + value: CalendarChannelSyncStatus.NOT_SYNCED, + label: 'Not Synced', + position: 2, + color: 'blue', + }, + { + value: CalendarChannelSyncStatus.ACTIVE, + label: 'Active', + position: 3, + color: 'green', + }, + { + value: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS, + label: 'Failed Insufficient Permissions', + position: 4, + color: 'red', + }, + { + value: CalendarChannelSyncStatus.FAILED_UNKNOWN, + label: 'Failed Unknown', + position: 5, + color: 'red', + }, + ], + }) + @WorkspaceIsNullable() + syncStatus: CalendarChannelSyncStatus | null; + + @WorkspaceField({ + standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncStage, + type: FieldMetadataType.SELECT, + label: 'Sync stage', + description: 'Sync stage', + icon: 'IconStatusChange', + options: [ + { + value: CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING, + label: 'Full calendar event list fetch pending', + position: 0, + color: 'blue', + }, + { + value: + CalendarChannelSyncStage.PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING, + label: 'Partial calendar event list fetch pending', + position: 1, + color: 'blue', + }, + { + value: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, + label: 'Calendar event list fetch ongoing', + position: 2, + color: 'orange', + }, + { + value: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING, + label: 'Calendar events import pending', + position: 3, + color: 'blue', + }, + { + value: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, + label: 'Calendar events import ongoing', + position: 4, + color: 'orange', + }, + { + value: CalendarChannelSyncStage.FAILED, + label: 'Failed', + position: 5, + color: 'red', + }, + ], + defaultValue: `'${CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING}'`, + }) + syncStage: CalendarChannelSyncStage; + @WorkspaceField({ standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.visibility, type: FieldMetadataType.SELECT, @@ -96,6 +205,16 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity { }) syncCursor: string; + @WorkspaceField({ + standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncStageStartedAt, + type: FieldMetadataType.DATE_TIME, + label: 'Sync stage started at', + description: 'Sync stage started at', + icon: 'IconHistory', + }) + @WorkspaceIsNullable() + syncStageStartedAt: string | null; + @WorkspaceField({ standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.throttleFailureCount, type: FieldMetadataType.NUMBER, From a001bf1514b7b806bd179c5e3b01a4f2734165ef Mon Sep 17 00:00:00 2001 From: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:01:22 +0200 Subject: [PATCH 23/30] 5951 create a command to trigger the import of a single message (#5962) Closes #5951 --------- Co-authored-by: Charles Bochet --- .../messaging-fetch-by-batch.service.ts | 24 ++- ...messaging-single-message-import.command.ts | 69 +++++++ ...gmail-fetch-messages-by-batches.service.ts | 192 +++++++++--------- ...messaging-gmail-messages-import.service.ts | 9 +- ...-single-message-to-cache-for-import.job.ts | 32 +++ .../messaging-import-manager.module.ts | 4 + 6 files changed, 224 insertions(+), 106 deletions(-) create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job.ts diff --git a/packages/twenty-server/src/modules/messaging/common/services/messaging-fetch-by-batch.service.ts b/packages/twenty-server/src/modules/messaging/common/services/messaging-fetch-by-batch.service.ts index 354ab92659f03..0af4b578e8559 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/messaging-fetch-by-batch.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/services/messaging-fetch-by-batch.service.ts @@ -5,25 +5,31 @@ import { AxiosResponse } from 'axios'; import { GmailMessageParsedResponse } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message-parsed-response'; import { BatchQueries } from 'src/modules/messaging/message-import-manager/types/batch-queries'; +import { createQueriesFromMessageIds } from 'src/modules/messaging/message-import-manager/utils/create-queries-from-message-ids.util'; @Injectable() export class MessagingFetchByBatchesService { constructor(private readonly httpService: HttpService) {} async fetchAllByBatches( - queries: BatchQueries, + messageIds: string[], accessToken: string, boundary: string, - ): Promise[]> { + ): Promise<{ + messageIdsByBatch: string[][]; + batchResponses: AxiosResponse[]; + }> { const batchLimit = 50; let batchOffset = 0; let batchResponses: AxiosResponse[] = []; - while (batchOffset < queries.length) { + const messageIdsByBatch: string[][] = []; + + while (batchOffset < messageIds.length) { const batchResponse = await this.fetchBatch( - queries, + messageIds, accessToken, batchOffset, batchLimit, @@ -32,19 +38,25 @@ export class MessagingFetchByBatchesService { batchResponses = batchResponses.concat(batchResponse); + messageIdsByBatch.push( + messageIds.slice(batchOffset, batchOffset + batchLimit), + ); + batchOffset += batchLimit; } - return batchResponses; + return { messageIdsByBatch, batchResponses }; } async fetchBatch( - queries: BatchQueries, + messageIds: string[], accessToken: string, batchOffset: number, batchLimit: number, boundary: string, ): Promise> { + const queries = createQueriesFromMessageIds(messageIds); + const limitedQueries = queries.slice(batchOffset, batchOffset + batchLimit); const response = await this.httpService.axiosRef.post( diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command.ts new file mode 100644 index 0000000000000..c55465f7dedcf --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command.ts @@ -0,0 +1,69 @@ +import { Command, CommandRunner, Option } from 'nest-commander'; + +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { + MessagingAddSingleMessageToCacheForImportJob, + MessagingAddSingleMessageToCacheForImportJobData, +} from 'src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job'; + +type MessagingSingleMessageImportCommandOptions = { + messageExternalId: string; + messageChannelId: string; + workspaceId: string; +}; + +@Command({ + name: 'messaging:single-message-import', + description: 'Enqueue a job to schedule the import of a single message', +}) +export class MessagingSingleMessageImportCommand extends CommandRunner { + constructor( + @InjectMessageQueue(MessageQueue.messagingQueue) + private readonly messageQueueService: MessageQueueService, + ) { + super(); + } + + async run( + _passedParam: string[], + options: MessagingSingleMessageImportCommandOptions, + ): Promise { + await this.messageQueueService.add( + MessagingAddSingleMessageToCacheForImportJob.name, + { + messageExternalId: options.messageExternalId, + messageChannelId: options.messageChannelId, + workspaceId: options.workspaceId, + }, + ); + } + + @Option({ + flags: '-m, --message-external-id [message_external_id]', + description: 'Message external ID', + required: true, + }) + parseMessageId(value: string): string { + return value; + } + + @Option({ + flags: '-M, --message-channel-id [message_channel_id]', + description: 'Message channel ID', + required: true, + }) + parseMessageChannelId(value: string): string { + return value; + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: 'Workspace ID', + required: true, + }) + parseWorkspaceId(value: string): string { + return value; + } +} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-fetch-messages-by-batches.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-fetch-messages-by-batches.service.ts index 1e0c0c6e979c8..f6cbb064df28b 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-fetch-messages-by-batches.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-fetch-messages-by-batches.service.ts @@ -7,7 +7,6 @@ import { gmail_v1 } from 'googleapis'; import { assert, assertNotNull } from 'src/utils/assert'; import { GmailMessage } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message'; -import { MessageQuery } from 'src/modules/messaging/message-import-manager/types/message-or-thread-query'; import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util'; import { MessagingFetchByBatchesService } from 'src/modules/messaging/common/services/messaging-fetch-by-batch.service'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; @@ -27,7 +26,7 @@ export class MessagingGmailFetchMessagesByBatchesService { ) {} async fetchAllMessages( - queries: MessageQuery[], + messageIds: string[], connectedAccountId: string, workspaceId: string, ): Promise { @@ -46,22 +45,24 @@ export class MessagingGmailFetchMessagesByBatchesService { const accessToken = connectedAccount.accessToken; - const batchResponses = await this.fetchByBatchesService.fetchAllByBatches( - queries, - accessToken, - 'batch_gmail_messages', - ); + const { messageIdsByBatch, batchResponses } = + await this.fetchByBatchesService.fetchAllByBatches( + messageIds, + accessToken, + 'batch_gmail_messages', + ); let endTime = Date.now(); this.logger.log( `Messaging import for workspace ${workspaceId} and account ${connectedAccountId} fetching ${ - queries.length + messageIds.length } messages in ${endTime - startTime}ms`, ); startTime = Date.now(); const formattedResponse = this.formatBatchResponsesAsGmailMessages( + messageIdsByBatch, batchResponses, workspaceId, connectedAccountId, @@ -71,7 +72,7 @@ export class MessagingGmailFetchMessagesByBatchesService { this.logger.log( `Messaging import for workspace ${workspaceId} and account ${connectedAccountId} formatting ${ - queries.length + messageIds.length } messages in ${endTime - startTime}ms`, ); @@ -79,6 +80,7 @@ export class MessagingGmailFetchMessagesByBatchesService { } private formatBatchResponseAsGmailMessage( + messageIds: string[], responseCollection: AxiosResponse, workspaceId: string, connectedAccountId: string, @@ -90,94 +92,92 @@ export class MessagingGmailFetchMessagesByBatchesService { return str.replace(/\0/g, ''); }; - const formattedResponse = parsedResponses.map( - (response): GmailMessage | null => { - if ('error' in response) { - if (response.error.code === 404) { - return null; - } - - throw response.error; - } - - const { - historyId, - id, - threadId, - internalDate, - subject, - from, - to, - cc, - bcc, - headerMessageId, - text, - attachments, - deliveredTo, - } = this.parseGmailMessage(response); - - if (!from) { - this.logger.log( - `From value is missing while importing message #${id} in workspace ${workspaceId} and account ${connectedAccountId}`, - ); - - return null; - } - - if (!to && !deliveredTo && !bcc && !cc) { - this.logger.log( - `To, Delivered-To, Bcc or Cc value is missing while importing message #${id} in workspace ${workspaceId} and account ${connectedAccountId}`, - ); - - return null; - } - - if (!headerMessageId) { - this.logger.log( - `Message-ID is missing while importing message #${id} in workspace ${workspaceId} and account ${connectedAccountId}`, - ); - - return null; - } - - if (!threadId) { - this.logger.log( - `Thread Id is missing while importing message #${id} in workspace ${workspaceId} and account ${connectedAccountId}`, - ); - + const formattedResponse = parsedResponses.map((response, index) => { + if ('error' in response) { + if (response.error.code === 404) { return null; } - const participants = [ - ...formatAddressObjectAsParticipants(from, 'from'), - ...formatAddressObjectAsParticipants(to ?? deliveredTo, 'to'), - ...formatAddressObjectAsParticipants(cc, 'cc'), - ...formatAddressObjectAsParticipants(bcc, 'bcc'), - ]; - - let textWithoutReplyQuotations = text; - - if (text) { - textWithoutReplyQuotations = planer.extractFrom(text, 'text/plain'); - } - - const messageFromGmail: GmailMessage = { - historyId, - externalId: id, - headerMessageId, - subject: subject || '', - messageThreadExternalId: threadId, - internalDate, - fromHandle: from[0].address || '', - fromDisplayName: from[0].name || '', - participants, - text: sanitizeString(textWithoutReplyQuotations || ''), - attachments, - }; - - return messageFromGmail; - }, - ); + throw { ...response.error, messageId: messageIds[index] }; + } + + const { + historyId, + id, + threadId, + internalDate, + subject, + from, + to, + cc, + bcc, + headerMessageId, + text, + attachments, + deliveredTo, + } = this.parseGmailMessage(response); + + if (!from) { + this.logger.log( + `From value is missing while importing message #${id} in workspace ${workspaceId} and account ${connectedAccountId}`, + ); + + return null; + } + + if (!to && !deliveredTo && !bcc && !cc) { + this.logger.log( + `To, Delivered-To, Bcc or Cc value is missing while importing message #${id} in workspace ${workspaceId} and account ${connectedAccountId}`, + ); + + return null; + } + + if (!headerMessageId) { + this.logger.log( + `Message-ID is missing while importing message #${id} in workspace ${workspaceId} and account ${connectedAccountId}`, + ); + + return null; + } + + if (!threadId) { + this.logger.log( + `Thread Id is missing while importing message #${id} in workspace ${workspaceId} and account ${connectedAccountId}`, + ); + + return null; + } + + const participants = [ + ...formatAddressObjectAsParticipants(from, 'from'), + ...formatAddressObjectAsParticipants(to ?? deliveredTo, 'to'), + ...formatAddressObjectAsParticipants(cc, 'cc'), + ...formatAddressObjectAsParticipants(bcc, 'bcc'), + ]; + + let textWithoutReplyQuotations = text; + + if (text) { + textWithoutReplyQuotations = planer.extractFrom(text, 'text/plain'); + } + + const messageFromGmail: GmailMessage = { + historyId, + externalId: id, + headerMessageId, + subject: subject || '', + messageThreadExternalId: threadId, + internalDate, + fromHandle: from[0].address || '', + fromDisplayName: from[0].name || '', + participants, + text: sanitizeString(textWithoutReplyQuotations || ''), + attachments, + }; + + return messageFromGmail; + }); const filteredMessages = formattedResponse.filter((message) => assertNotNull(message), @@ -187,12 +187,14 @@ export class MessagingGmailFetchMessagesByBatchesService { } private formatBatchResponsesAsGmailMessages( + messageIdsByBatch: string[][], batchResponses: AxiosResponse[], workspaceId: string, connectedAccountId: string, ): GmailMessage[] { - const messageBatches = batchResponses.map((response) => { + const messageBatches = batchResponses.map((response, index) => { return this.formatBatchResponseAsGmailMessage( + messageIdsByBatch[index], response, workspaceId, connectedAccountId, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service.ts index bf3dec6cddad5..083ff2d522af1 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service.ts @@ -14,7 +14,6 @@ import { MessageChannelWorkspaceEntity, MessageChannelSyncStage, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { createQueriesFromMessageIds } from 'src/modules/messaging/message-import-manager/utils/create-queries-from-message-ids.util'; import { filterEmails } from 'src/modules/messaging/message-import-manager/utils/filter-emails.util'; import { MessagingChannelSyncStatusService } from 'src/modules/messaging/common/services/messaging-channel-sync-status.service'; import { MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-users-messages-get-batch-size.constant'; @@ -95,12 +94,10 @@ export class MessagingGmailMessagesImportService { ); } - const messageQueries = createQueriesFromMessageIds(messageIdsToFetch); - try { const allMessages = await this.fetchMessagesByBatchesService.fetchAllMessages( - messageQueries, + messageIdsToFetch, connectedAccount.id, workspaceId, ); @@ -153,7 +150,9 @@ export class MessagingGmailMessagesImportService { ); } catch (error) { this.logger.log( - `Messaging import for workspace ${workspaceId} and connected account ${ + `Messaging import for messageId ${ + error.messageId + }, workspace ${workspaceId} and connected account ${ connectedAccount.id } failed with error: ${JSON.stringify(error)}`, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job.ts new file mode 100644 index 0000000000000..d81bba9cd0963 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job.ts @@ -0,0 +1,32 @@ +import { CacheStorageService } from 'src/engine/integrations/cache-storage/cache-storage.service'; +import { InjectCacheStorage } from 'src/engine/integrations/cache-storage/decorators/cache-storage.decorator'; +import { CacheStorageNamespace } from 'src/engine/integrations/cache-storage/types/cache-storage-namespace.enum'; +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; + +export type MessagingAddSingleMessageToCacheForImportJobData = { + messageExternalId: string; + messageChannelId: string; + workspaceId: string; +}; + +@Processor(MessageQueue.messagingQueue) +export class MessagingAddSingleMessageToCacheForImportJob { + constructor( + @InjectCacheStorage(CacheStorageNamespace.Messaging) + private readonly cacheStorage: CacheStorageService, + ) {} + + @Process(MessagingAddSingleMessageToCacheForImportJob.name) + async handle( + data: MessagingAddSingleMessageToCacheForImportJobData, + ): Promise { + const { messageExternalId, messageChannelId, workspaceId } = data; + + await this.cacheStorage.setAdd( + `messages-to-import:${workspaceId}:gmail:${messageChannelId}`, + [messageExternalId], + ); + } +} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts index 494edba8fb6db..38d0e8db58cbf 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts @@ -4,11 +4,13 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; +import { MessagingSingleMessageImportCommand } from 'src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command'; import { MessagingMessageListFetchCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-message-list-fetch.cron.command'; import { MessagingMessagesImportCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-messages-import.cron.command'; import { MessagingMessageListFetchCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job'; import { MessagingMessagesImportCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job'; import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module'; +import { MessagingAddSingleMessageToCacheForImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job'; import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job'; import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job'; @@ -22,10 +24,12 @@ import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import providers: [ MessagingMessageListFetchCronCommand, MessagingMessagesImportCronCommand, + MessagingSingleMessageImportCommand, MessagingMessageListFetchJob, MessagingMessagesImportJob, MessagingMessageListFetchCronJob, MessagingMessagesImportCronJob, + MessagingAddSingleMessageToCacheForImportJob, ], exports: [], }) From 797c2f4b6e5b1ae52c22508ff2196260f7e1dc09 Mon Sep 17 00:00:00 2001 From: Hanch Han <51526347+hanchchch@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:59:31 +0900 Subject: [PATCH 24/30] Add calendar cron command on self-hosting-var.mdx (#6009) To enable Google Calendar integration, you need to run `yarn command:prod cron:calendar:google-calendar-sync` in the worker container. However, currently, the self-hosting guide does not tell you how to do it. If you just follow the guide, only Gmail integration will be enabled. So I added the command for calendar sync cron on self-hosting-var.mdx. --- .../src/content/developers/self-hosting/self-hosting-var.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx index f741ff4759cfc..88ae2e5119e48 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx @@ -13,6 +13,7 @@ Twenty offers integrations with Gmail and Google Calendar. To enable these featu # from your worker container yarn command:prod cron:messaging:messages-import yarn command:prod cron:messaging:message-list-fetch +yarn command:prod cron:calendar:google-calendar-sync ``` # Setup Environment Variables @@ -208,4 +209,4 @@ yarn command:prod cron:messaging:message-list-fetch ['CAPTCHA_SECRET_KEY', '', 'The captcha secret key'], ]}> - \ No newline at end of file + From 7fb5c9b60f7d1935297954b49684a35a465d72c6 Mon Sep 17 00:00:00 2001 From: martmull Date: Tue, 25 Jun 2024 10:30:19 +0200 Subject: [PATCH 25/30] Remove useless api position parameter (#6010) - remove buggy addition of position parameter - check created records are in first position by default --- .../query-builder/factories/create-variables.factory.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-variables.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-variables.factory.ts index 02bc3f9d6acd0..cb907164a2582 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-variables.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-variables.factory.ts @@ -7,14 +7,8 @@ import { QueryVariables } from 'src/engine/api/rest/core/types/query-variables.t @Injectable() export class CreateVariablesFactory { create(request: Request): QueryVariables { - const data = Array.isArray(request.body) - ? request.body.map((recordData) => { - return { position: 'first', ...recordData }; - }) - : { position: 'first', ...request.body }; - return { - data, + data: request.body, }; } } From f8c057deea3e42dfda223863a32edfc46bbc9779 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 25 Jun 2024 10:59:07 +0200 Subject: [PATCH 26/30] Fix sign up broken because of missing workspace schema (#6013) Allow workspace datasource factory to return null if the workspace schema has not been created yet --- .../data-source/data-source.service.ts | 9 +++++++++ .../factories/workspace-datasource.factory.ts | 11 +++++++++-- .../src/engine/twenty-orm/twenty-orm.manager.ts | 5 +++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.service.ts b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.service.ts index 226b5a9ca830b..6e6f46a4ca490 100644 --- a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.service.ts @@ -46,6 +46,15 @@ export class DataSourceService { }); } + async getLastDataSourceMetadataFromWorkspaceId( + workspaceId: string, + ): Promise { + return this.dataSourceMetadataRepository.findOne({ + where: { workspaceId }, + order: { createdAt: 'DESC' }, + }); + } + async getLastDataSourceMetadataFromWorkspaceIdOrFail( workspaceId: string, ): Promise { diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index f7447a1318576..2c733961a265e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -14,7 +14,10 @@ export class WorkspaceDatasourceFactory { private readonly environmentService: EnvironmentService, ) {} - public async create(entities: EntitySchema[], workspaceId: string) { + public async create( + entities: EntitySchema[], + workspaceId: string, + ): Promise { const storedWorkspaceDataSource = DataSourceStorage.getDataSource(workspaceId); @@ -23,10 +26,14 @@ export class WorkspaceDatasourceFactory { } const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( workspaceId, ); + if (!dataSourceMetadata) { + return null; + } + const workspaceDataSource = new WorkspaceDataSource({ url: dataSourceMetadata.url ?? diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts index caaa4c21646d1..c2e12ea456c38 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts @@ -35,6 +35,11 @@ export class TwentyORMManager { entities, workspaceId, ); + + if (!workspaceDataSource) { + throw new Error('Workspace data source not found'); + } + const entitySchema = this.entitySchemaFactory.create(entityClass); return workspaceDataSource.getRepository(entitySchema); From 4dfca45fd329d254fed3d464f516f3acdae98c0d Mon Sep 17 00:00:00 2001 From: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:57:02 +0200 Subject: [PATCH 27/30] 5615 create messageongoingstalecron (#6005) Closes #5615 --- .../messaging-ongoing-stale.cron.command.ts | 32 ++++++++ .../messaging-messages-import.cron.job.ts | 2 +- .../jobs/messaging-ongoing-stale.cron.job.ts | 62 ++++++++++++++++ .../jobs/messaging-ongoing-stale.job.ts | 74 +++++++++++++++++++ .../messaging-import-manager.module.ts | 9 +++ .../__tests__/is-sync-stale.util.spec.ts | 34 +++++++++ .../utils/is-sync-stale.util.ts | 13 ++++ 7 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/crons/commands/messaging-ongoing-stale.cron.command.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/is-sync-stale.util.spec.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/utils/is-sync-stale.util.ts diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/commands/messaging-ongoing-stale.cron.command.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/commands/messaging-ongoing-stale.cron.command.ts new file mode 100644 index 0000000000000..ed77d79555b80 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/commands/messaging-ongoing-stale.cron.command.ts @@ -0,0 +1,32 @@ +import { Command, CommandRunner } from 'nest-commander'; + +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { MessagingOngoingStaleCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job'; + +const MESSAGING_ONGOING_STALE_CRON_PATTERN = '0 * * * *'; + +@Command({ + name: 'cron:messaging:ongoing-stale', + description: + 'Starts a cron job to check for stale ongoing message imports and put them back to pending', +}) +export class MessagingOngoingStaleCronCommand extends CommandRunner { + constructor( + @InjectMessageQueue(MessageQueue.cronQueue) + private readonly messageQueueService: MessageQueueService, + ) { + super(); + } + + async run(): Promise { + await this.messageQueueService.addCron( + MessagingOngoingStaleCronJob.name, + undefined, + { + repeat: { pattern: MESSAGING_ONGOING_STALE_CRON_PATTERN }, + }, + ); + } +} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts index 3c4352a921530..53c9d86d90ba9 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, In } from 'typeorm'; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job.ts new file mode 100644 index 0000000000000..b9666c5ec5671 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job.ts @@ -0,0 +1,62 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository, In } from 'typeorm'; + +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { + MessagingOngoingStaleJobData, + MessagingOngoingStaleJob, +} from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job'; + +@Processor(MessageQueue.cronQueue) +export class MessagingOngoingStaleCronJob { + constructor( + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + @InjectRepository(DataSourceEntity, 'metadata') + private readonly dataSourceRepository: Repository, + private readonly environmentService: EnvironmentService, + @InjectMessageQueue(MessageQueue.messagingQueue) + private readonly messageQueueService: MessageQueueService, + ) {} + + @Process(MessagingOngoingStaleCronJob.name) + async handle(): Promise { + const workspaceIds = ( + await this.workspaceRepository.find({ + where: this.environmentService.get('IS_BILLING_ENABLED') + ? { + subscriptionStatus: In(['active', 'trialing', 'past_due']), + } + : {}, + select: ['id'], + }) + ).map((workspace) => workspace.id); + + const dataSources = await this.dataSourceRepository.find({ + where: { + workspaceId: In(workspaceIds), + }, + }); + + const workspaceIdsWithDataSources = new Set( + dataSources.map((dataSource) => dataSource.workspaceId), + ); + + for (const workspaceId of workspaceIdsWithDataSources) { + await this.messageQueueService.add( + MessagingOngoingStaleJob.name, + { + workspaceId, + }, + ); + } + } +} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts new file mode 100644 index 0000000000000..0eb3c2aa12f81 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts @@ -0,0 +1,74 @@ +import { Logger, Scope } from '@nestjs/common'; + +import { In } from 'typeorm'; + +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { + MessageChannelSyncStage, + MessageChannelWorkspaceEntity, +} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { isSyncStale } from 'src/modules/messaging/message-import-manager/utils/is-sync-stale.util'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { MessagingChannelSyncStatusService } from 'src/modules/messaging/common/services/messaging-channel-sync-status.service'; + +export type MessagingOngoingStaleJobData = { + workspaceId: string; +}; + +@Processor({ + queueName: MessageQueue.messagingQueue, + scope: Scope.REQUEST, +}) +export class MessagingOngoingStaleJob { + private readonly logger = new Logger(MessagingOngoingStaleJob.name); + constructor( + @InjectWorkspaceRepository(MessageChannelWorkspaceEntity) + private readonly messageChannelRepository: WorkspaceRepository, + private readonly messagingChannelSyncStatusService: MessagingChannelSyncStatusService, + ) {} + + @Process(MessagingOngoingStaleJob.name) + async handle(data: MessagingOngoingStaleJobData): Promise { + const { workspaceId } = data; + + const messageChannels = await this.messageChannelRepository.find({ + where: { + syncStage: In([ + MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, + MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING, + ]), + }, + }); + + for (const messageChannel of messageChannels) { + if ( + messageChannel.syncStageStartedAt && + isSyncStale(messageChannel.syncStageStartedAt) + ) { + this.logger.log( + `Sync for message channel ${messageChannel.id} and workspace ${workspaceId} is stale. Setting sync stage to MESSAGES_IMPORT_PENDING`, + ); + + switch (messageChannel.syncStage) { + case MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING: + await this.messagingChannelSyncStatusService.schedulePartialMessageListFetch( + messageChannel.id, + workspaceId, + ); + break; + case MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING: + await this.messagingChannelSyncStatusService.scheduleMessagesImport( + messageChannel.id, + workspaceId, + ); + break; + default: + break; + } + } + } + } +} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts index 38d0e8db58cbf..87cce001913c7 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts @@ -3,16 +3,21 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; +import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingSingleMessageImportCommand } from 'src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command'; import { MessagingMessageListFetchCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-message-list-fetch.cron.command'; import { MessagingMessagesImportCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-messages-import.cron.command'; +import { MessagingOngoingStaleCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-ongoing-stale.cron.command'; import { MessagingMessageListFetchCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job'; import { MessagingMessagesImportCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job'; +import { MessagingOngoingStaleCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job'; import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module'; import { MessagingAddSingleMessageToCacheForImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job'; import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job'; import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job'; +import { MessagingOngoingStaleJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job'; @Module({ imports: [ @@ -20,15 +25,19 @@ import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import MessagingCommonModule, TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([DataSourceEntity], 'metadata'), + TwentyORMModule.forFeature([MessageChannelWorkspaceEntity]), ], providers: [ MessagingMessageListFetchCronCommand, MessagingMessagesImportCronCommand, + MessagingOngoingStaleCronCommand, MessagingSingleMessageImportCommand, MessagingMessageListFetchJob, MessagingMessagesImportJob, + MessagingOngoingStaleJob, MessagingMessageListFetchCronJob, MessagingMessagesImportCronJob, + MessagingOngoingStaleCronJob, MessagingAddSingleMessageToCacheForImportJob, ], exports: [], diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/is-sync-stale.util.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/is-sync-stale.util.spec.ts new file mode 100644 index 0000000000000..9935eb610812c --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/is-sync-stale.util.spec.ts @@ -0,0 +1,34 @@ +import { MESSAGING_IMPORT_ONGOING_SYNC_TIMEOUT } from 'src/modules/messaging/message-import-manager/constants/messaging-import-ongoing-sync-timeout.constant'; +import { isSyncStale } from 'src/modules/messaging/message-import-manager/utils/is-sync-stale.util'; + +jest.useFakeTimers().setSystemTime(new Date('2024-01-01')); + +describe('isSyncStale', () => { + it('should return true if sync is stale', () => { + const syncStageStartedAt = new Date( + Date.now() - MESSAGING_IMPORT_ONGOING_SYNC_TIMEOUT - 1, + ).toISOString(); + + const result = isSyncStale(syncStageStartedAt); + + expect(result).toBe(true); + }); + + it('should return false if sync is not stale', () => { + const syncStageStartedAt = new Date( + Date.now() - MESSAGING_IMPORT_ONGOING_SYNC_TIMEOUT + 1, + ).toISOString(); + + const result = isSyncStale(syncStageStartedAt); + + expect(result).toBe(false); + }); + + it('should return false if syncStageStartedAt is invalid', () => { + const syncStageStartedAt = 'invalid-date'; + + expect(() => { + isSyncStale(syncStageStartedAt); + }).toThrow('Invalid date format'); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/is-sync-stale.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/is-sync-stale.util.ts new file mode 100644 index 0000000000000..7a1ae5e2029f8 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/is-sync-stale.util.ts @@ -0,0 +1,13 @@ +import { MESSAGING_IMPORT_ONGOING_SYNC_TIMEOUT } from 'src/modules/messaging/message-import-manager/constants/messaging-import-ongoing-sync-timeout.constant'; + +export const isSyncStale = (syncStageStartedAt: string): boolean => { + const syncStageStartedTime = new Date(syncStageStartedAt).getTime(); + + if (isNaN(syncStageStartedTime)) { + throw new Error('Invalid date format'); + } + + return ( + Date.now() - syncStageStartedTime > MESSAGING_IMPORT_ONGOING_SYNC_TIMEOUT + ); +}; From 7c2e745b45e9139baab936bdef81a40dc598190d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Tue, 25 Jun 2024 12:41:46 +0200 Subject: [PATCH 28/30] feat: Dynamic hook registration for WorkspaceQueryHooks (#6008) #### Overview This PR introduces a new API for dynamically registering and executing pre and post query hooks in the Workspace Query Hook system using the `@WorkspaceQueryHook` decorator. This approach eliminates the need for manual provider registration, and fix the issue of `undefined` or `null` repository using `@InjectWorkspaceRepository`. #### New API **Define a Hook** Use the `@WorkspaceQueryHook` decorator to define pre or post hooks: ```typescript @WorkspaceQueryHook({ key: `calendarEvent.findMany`, scope: Scope.REQUEST, }) export class CalendarEventFindManyPreQueryHook implements WorkspaceQueryHookInstance { async execute(userId: string, workspaceId: string, payload: FindManyResolverArgs): Promise { if (!payload?.filter?.id?.eq) { throw new BadRequestException('id filter is required'); } // Implement hook logic here } } ``` This API simplifies the registration and execution of query hooks, providing a more flexible and maintainable approach. --------- Co-authored-by: Weiko --- .../graphql-config/graphql-config.service.ts | 6 +- .../workspace-pre-query-hook.config.ts | 31 ---- .../workspace-pre-query-hook.module.ts | 19 --- .../workspace-pre-query-hook.service.ts | 34 ----- .../workspace-query-hook.decorator.ts | 40 +++++ .../workspace-query-hook.interface.ts} | 2 +- .../storage/workspace-query-hook.storage.ts | 59 ++++++++ .../types/workspace-query-hook.type.ts | 24 +-- .../workspace-query-hook-metadata.accessor.ts | 25 ++++ .../workspace-query-hook.constants.ts | 3 + .../workspace-query-hook.explorer.ts | 139 ++++++++++++++++++ .../workspace-query-hook.module.ts | 29 ++++ .../workspace-query-hook.service.ts | 43 ++++++ .../workspace-query-runner.module.ts | 4 +- .../workspace-query-runner.service.ts | 20 +-- .../message-queue/drivers/pg-boss.driver.ts | 9 +- .../scoped-workspace-datasource.factory.ts | 2 +- ...calendar-event-find-many.pre-query.hook.ts | 47 +++--- .../calendar-event-find-one.pre-query-hook.ts | 47 +++--- .../can-access-calendar-event.service.ts | 2 +- .../query-hooks/calendar-query-hook.module.ts | 10 +- .../calendar-channel.workspace-entity.ts | 2 + .../blocklist-create-many.pre-query.hook.ts | 11 +- .../blocklist-update-many.pre-query.hook.ts | 12 +- .../blocklist-update-one.pre-query.hook.ts | 11 +- .../connected-account-query-hook.module.ts | 15 +- .../message-find-many.pre-query.hook.ts | 13 +- .../message-find-one.pre-query-hook.ts | 9 +- .../messaging-query-hook.module.ts | 10 +- ...space-member-delete-many.pre-query.hook.ts | 10 +- ...kspace-member-delete-one.pre-query.hook.ts | 9 +- .../workspace-member-query-hook.module.ts | 10 +- 32 files changed, 472 insertions(+), 235 deletions(-) delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator.ts rename packages/twenty-server/src/engine/api/graphql/workspace-query-runner/{workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface.ts => workspace-query-hook/interfaces/workspace-query-hook.interface.ts} (84%) create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage.ts rename packages/twenty-server/src/engine/api/graphql/workspace-query-runner/{workspace-pre-query-hook => workspace-query-hook}/types/workspace-query-hook.type.ts (73%) create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts index b71ff69d4dd86..266dbf2ce48cc 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts @@ -141,8 +141,10 @@ export class GraphQLConfigService // Create a new contextId for each request const contextId = ContextIdFactory.create(); - // Register the request in the contextId - this.moduleRef.registerRequestByContextId(context.req, contextId); + if (this.moduleRef.registerRequestByContextId) { + // Register the request in the contextId + this.moduleRef.registerRequestByContextId(context.req, contextId); + } // Resolve the WorkspaceSchemaFactory for the contextId const workspaceFactory = await this.moduleRef.resolve( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts deleted file mode 100644 index 518e147241a1e..0000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type'; -import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook'; -import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook'; -import { BlocklistCreateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook'; -import { BlocklistUpdateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook'; -import { BlocklistUpdateOnePreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-update-one.pre-query.hook'; -import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook'; -import { WorkspaceMemberDeleteManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook'; -import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook'; -import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook'; - -// TODO: move to a decorator -export const workspacePreQueryHooks: WorkspaceQueryHook = { - message: { - findOne: [MessageFindOnePreQueryHook.name], - findMany: [MessageFindManyPreQueryHook.name], - }, - calendarEvent: { - findOne: [CalendarEventFindOnePreQueryHook.name], - findMany: [CalendarEventFindManyPreQueryHook.name], - }, - blocklist: { - createMany: [BlocklistCreateManyPreQueryHook.name], - updateMany: [BlocklistUpdateManyPreQueryHook.name], - updateOne: [BlocklistUpdateOnePreQueryHook.name], - }, - workspaceMember: { - deleteOne: [WorkspaceMemberDeleteOnePreQueryHook.name], - deleteMany: [WorkspaceMemberDeleteManyPreQueryHook.name], - }, -}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts deleted file mode 100644 index 8ea6b6a6e11b7..0000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service'; -import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module'; -import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module'; -import { MessagingQueryHookModule } from 'src/modules/messaging/common/query-hooks/messaging-query-hook.module'; -import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/query-hooks/workspace-member-query-hook.module'; - -@Module({ - imports: [ - MessagingQueryHookModule, - CalendarQueryHookModule, - ConnectedAccountQueryHookModule, - WorkspaceMemberQueryHookModule, - ], - providers: [WorkspacePreQueryHookService], - exports: [WorkspacePreQueryHookService], -}) -export class WorkspacePreQueryHookModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service.ts deleted file mode 100644 index 28681e0b965b0..0000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; - -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; - -import { - ExecutePreHookMethod, - WorkspacePreQueryHookPayload, -} from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type'; -import { workspacePreQueryHooks } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config'; - -@Injectable() -export class WorkspacePreQueryHookService { - constructor(private readonly workspaceQueryHookModuleRef: ModuleRef) {} - - public async executePreHooks( - userId: string | undefined, - workspaceId: string, - objectName: string, - method: T, - payload: WorkspacePreQueryHookPayload, - ): Promise { - const hooks = workspacePreQueryHooks[objectName] || []; - - for (const hookName of Object.values(hooks[method] ?? [])) { - const hook: WorkspacePreQueryHook = - await this.workspaceQueryHookModuleRef.get(hookName, { - strict: false, - }); - - await hook.execute(userId, workspaceId, payload); - } - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator.ts new file mode 100644 index 0000000000000..d879794186385 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator.ts @@ -0,0 +1,40 @@ +import { Scope, SetMetadata } from '@nestjs/common'; +import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants'; + +import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { WORKSPACE_QUERY_HOOK_METADATA } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants'; +import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; + +export type WorkspaceQueryHookKey = + `${string}.${WorkspaceResolverBuilderMethodNames}`; + +export interface WorkspaceQueryHookOptions { + key: WorkspaceQueryHookKey; + type?: WorkspaceQueryHookType; + scope?: Scope; +} + +export function WorkspaceQueryHook(key: WorkspaceQueryHookKey): ClassDecorator; +export function WorkspaceQueryHook( + options: WorkspaceQueryHookOptions, +): ClassDecorator; +export function WorkspaceQueryHook( + keyOrOptions: WorkspaceQueryHookKey | WorkspaceQueryHookOptions, +): ClassDecorator { + const options: WorkspaceQueryHookOptions = + keyOrOptions && typeof keyOrOptions === 'object' + ? keyOrOptions + : { key: keyOrOptions }; + + // Default to PreHook + if (!options.type) { + options.type = WorkspaceQueryHookType.PreHook; + } + + // eslint-disable-next-line @typescript-eslint/ban-types + return (target: Function) => { + SetMetadata(SCOPE_OPTIONS_METADATA, options)(target); + SetMetadata(WORKSPACE_QUERY_HOOK_METADATA, options)(target); + }; +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface.ts similarity index 84% rename from packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface.ts rename to packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface.ts index 60a782e8c4432..93c3964c83b53 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface.ts @@ -1,6 +1,6 @@ import { ResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -export interface WorkspacePreQueryHook { +export interface WorkspaceQueryHookInstance { execute( userId: string | undefined, workspaceId: string, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage.ts new file mode 100644 index 0000000000000..18b25d1d964d6 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage.ts @@ -0,0 +1,59 @@ +// hook-registry.service.ts +import { Injectable } from '@nestjs/common'; +import { Module } from '@nestjs/core/injector/module'; + +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; + +import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; + +interface WorkspaceQueryHookData { + instance: T; + host: Module; + isRequestScoped: boolean; +} + +@Injectable() +export class WorkspaceQueryHookStorage { + private preHookInstances = new Map< + WorkspaceQueryHookKey, + WorkspaceQueryHookData[] + >(); + private postHookInstances = new Map< + WorkspaceQueryHookKey, + WorkspaceQueryHookData[] + >(); + + registerWorkspaceQueryPreHookInstance( + key: WorkspaceQueryHookKey, + data: WorkspaceQueryHookData, + ) { + if (!this.preHookInstances.has(key)) { + this.preHookInstances.set(key, []); + } + + this.preHookInstances.get(key)?.push(data); + } + + getWorkspaceQueryPreHookInstances( + key: WorkspaceQueryHookKey, + ): WorkspaceQueryHookData[] | undefined { + return this.preHookInstances.get(key); + } + + registerWorkspaceQueryPostHookInstance( + key: WorkspaceQueryHookKey, + data: WorkspaceQueryHookData, + ) { + if (!this.postHookInstances.has(key)) { + this.postHookInstances.set(key, []); + } + + this.postHookInstances.get(key)?.push(data); + } + + getWorkspaceQueryPostHookInstances( + key: WorkspaceQueryHookKey, + ): WorkspaceQueryHookData[] | undefined { + return this.postHookInstances.get(key); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts similarity index 73% rename from packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type.ts rename to packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts index 84d75b1358b95..40ec1df6b4d4b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts @@ -10,26 +10,10 @@ import { UpdateOneResolverArgs, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -export type ExecutePreHookMethod = - | 'createMany' - | 'createOne' - | 'deleteMany' - | 'deleteOne' - | 'findMany' - | 'findOne' - | 'findDuplicates' - | 'updateMany' - | 'updateOne'; - -export type ObjectName = string; - -export type HookName = string; - -export type WorkspaceQueryHook = { - [key in ObjectName]: { - [key in ExecutePreHookMethod]?: HookName[]; - }; -}; +export enum WorkspaceQueryHookType { + PreHook = 'PreHook', + PostHook = 'PostHook', +} export type WorkspacePreQueryHookPayload = T extends 'createMany' ? CreateManyResolverArgs diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor.ts new file mode 100644 index 0000000000000..229e0b0bf3db2 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor.ts @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { Injectable, Type } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; + +import { WORKSPACE_QUERY_HOOK_METADATA } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants'; +import { WorkspaceQueryHookOptions } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; + +@Injectable() +export class WorkspaceQueryHookMetadataAccessor { + constructor(private readonly reflector: Reflector) {} + + isWorkspaceQueryHook(target: Type | Function): boolean { + if (!target) { + return false; + } + + return !!this.reflector.get(WORKSPACE_QUERY_HOOK_METADATA, target); + } + + getWorkspaceQueryHookMetadata( + target: Type | Function, + ): WorkspaceQueryHookOptions | undefined { + return this.reflector.get(WORKSPACE_QUERY_HOOK_METADATA, target); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants.ts new file mode 100644 index 0000000000000..ea1aadc490065 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants.ts @@ -0,0 +1,3 @@ +export const WORKSPACE_QUERY_HOOK_METADATA = Symbol( + 'workspace-query-hook:query-hook-metadata', +); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts new file mode 100644 index 0000000000000..87bce8248bd11 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts @@ -0,0 +1,139 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { DiscoveryService, ModuleRef, createContextId } from '@nestjs/core'; +import { Module } from '@nestjs/core/injector/module'; +import { Injector } from '@nestjs/core/injector/injector'; + +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; + +import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor'; +import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage'; +import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; +import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; + +@Injectable() +export class WorkspaceQueryHookExplorer implements OnModuleInit { + private readonly logger = new Logger('WorkspaceQueryHookModule'); + private readonly injector = new Injector(); + + constructor( + private readonly moduleRef: ModuleRef, + private readonly discoveryService: DiscoveryService, + private readonly metadataAccessor: WorkspaceQueryHookMetadataAccessor, + private readonly workspaceQueryHookStorage: WorkspaceQueryHookStorage, + ) {} + + onModuleInit() { + this.explore(); + } + + async explore() { + const hooks = this.discoveryService + .getProviders() + .filter((wrapper) => + this.metadataAccessor.isWorkspaceQueryHook( + !wrapper.metatype || wrapper.inject + ? wrapper.instance?.constructor + : wrapper.metatype, + ), + ); + + for (const hook of hooks) { + const { instance, metatype } = hook; + + const { key, type } = + this.metadataAccessor.getWorkspaceQueryHookMetadata( + instance.constructor || metatype, + ) ?? {}; + + if (!key || !type) { + this.logger.error( + `PreHook ${hook.name} is missing key or type metadata`, + ); + continue; + } + + if (!hook.host) { + this.logger.error(`PreHook ${hook.name} is missing host metadata`); + + continue; + } + + this.registerWorkspaceQueryHook( + key, + type, + instance, + hook.host, + !hook.isDependencyTreeStatic(), + ); + } + } + + async handleHook( + payload: Parameters, + instance: object, + host: Module, + isRequestScoped: boolean, + ) { + const methodName = 'execute'; + + if (isRequestScoped) { + const contextId = createContextId(); + + if (this.moduleRef.registerRequestByContextId) { + this.moduleRef.registerRequestByContextId( + { + req: { + workspaceId: payload?.[1], + }, + }, + contextId, + ); + } + + const contextInstance = await this.injector.loadPerContext( + instance, + host, + host.providers, + contextId, + ); + + await contextInstance[methodName].call(contextInstance, ...payload); + } else { + await instance[methodName].call(instance, ...payload); + } + } + + private registerWorkspaceQueryHook( + key: WorkspaceQueryHookKey, + type: WorkspaceQueryHookType, + instance: object, + host: Module, + isRequestScoped: boolean, + ) { + switch (type) { + case WorkspaceQueryHookType.PreHook: + this.workspaceQueryHookStorage.registerWorkspaceQueryPreHookInstance( + key, + { + instance: instance as WorkspaceQueryHookInstance, + host, + isRequestScoped, + }, + ); + break; + case WorkspaceQueryHookType.PostHook: + this.workspaceQueryHookStorage.registerWorkspaceQueryPostHookInstance( + key, + { + instance: instance as WorkspaceQueryHookInstance, + host, + isRequestScoped, + }, + ); + break; + default: + this.logger.error(`Unknown WorkspaceQueryHookType: ${type}`); + break; + } + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module.ts new file mode 100644 index 0000000000000..f018238835a2b --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { DiscoveryModule } from '@nestjs/core'; + +import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage'; +import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor'; +import { WorkspaceQueryHookExplorer } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer'; +import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; +import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module'; +import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module'; +import { MessagingQueryHookModule } from 'src/modules/messaging/common/query-hooks/messaging-query-hook.module'; +import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/query-hooks/workspace-member-query-hook.module'; + +@Module({ + imports: [ + MessagingQueryHookModule, + CalendarQueryHookModule, + ConnectedAccountQueryHookModule, + WorkspaceMemberQueryHookModule, + DiscoveryModule, + ], + providers: [ + WorkspaceQueryHookService, + WorkspaceQueryHookExplorer, + WorkspaceQueryHookMetadataAccessor, + WorkspaceQueryHookStorage, + ], + exports: [WorkspaceQueryHookService], +}) +export class WorkspaceQueryHookModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts new file mode 100644 index 0000000000000..adfd90f4261d5 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage'; +import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; +import { WorkspaceQueryHookExplorer } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer'; +import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; + +@Injectable() +export class WorkspaceQueryHookService { + constructor( + private readonly workspaceQueryHookStorage: WorkspaceQueryHookStorage, + private readonly workspaceQueryHookExplorer: WorkspaceQueryHookExplorer, + ) {} + + public async executePreQueryHooks< + T extends WorkspaceResolverBuilderMethodNames, + >( + userId: string | undefined, + workspaceId: string, + objectName: string, + methodName: T, + payload: WorkspacePreQueryHookPayload, + ): Promise { + const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`; + const preHookInstances = + this.workspaceQueryHookStorage.getWorkspaceQueryPreHookInstances(key); + + if (!preHookInstances) { + return; + } + + for (const preHookInstance of preHookInstances) { + await this.workspaceQueryHookExplorer.handleHook( + [userId, workspaceId, payload], + preHookInstance.instance, + preHookInstance.host, + preHookInstance.isRequestScoped, + ); + } + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index f7358021b849b..d81f5b7e2441b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { WorkspacePreQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module'; +import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -20,7 +20,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen AuthModule, WorkspaceQueryBuilderModule, WorkspaceDataSourceModule, - WorkspacePreQueryHookModule, + WorkspaceQueryHookModule, ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]), AnalyticsModule, ], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts index d5ea042a16066..6c4bb35e9805a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts @@ -42,7 +42,7 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event'; import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event'; -import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service'; +import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { NotFoundError } from 'src/engine/utils/graphql-errors.util'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; @@ -75,7 +75,7 @@ export class WorkspaceQueryRunnerService { @InjectMessageQueue(MessageQueue.webhookQueue) private readonly messageQueueService: MessageQueueService, private readonly eventEmitter: EventEmitter2, - private readonly workspacePreQueryHookService: WorkspacePreQueryHookService, + private readonly workspaceQueryHookService: WorkspaceQueryHookService, private readonly environmentService: EnvironmentService, ) {} @@ -101,7 +101,7 @@ export class WorkspaceQueryRunnerService { options, ); - await this.workspacePreQueryHookService.executePreHooks( + await this.workspaceQueryHookService.executePreQueryHooks( userId, workspaceId, objectMetadataItem.nameSingular, @@ -148,7 +148,7 @@ export class WorkspaceQueryRunnerService { options, ); - await this.workspacePreQueryHookService.executePreHooks( + await this.workspaceQueryHookService.executePreQueryHooks( userId, workspaceId, objectMetadataItem.nameSingular, @@ -223,7 +223,7 @@ export class WorkspaceQueryRunnerService { existingRecord, ); - await this.workspacePreQueryHookService.executePreHooks( + await this.workspaceQueryHookService.executePreQueryHooks( userId, workspaceId, objectMetadataItem.nameSingular, @@ -260,7 +260,7 @@ export class WorkspaceQueryRunnerService { ResolverArgsType.CreateMany, )) as CreateManyResolverArgs; - await this.workspacePreQueryHookService.executePreHooks( + await this.workspaceQueryHookService.executePreQueryHooks( userId, workspaceId, objectMetadataItem.nameSingular, @@ -333,7 +333,7 @@ export class WorkspaceQueryRunnerService { options, ); - await this.workspacePreQueryHookService.executePreHooks( + await this.workspaceQueryHookService.executePreQueryHooks( userId, workspaceId, objectMetadataItem.nameSingular, @@ -389,7 +389,7 @@ export class WorkspaceQueryRunnerService { atMost: maximumRecordAffected, }); - await this.workspacePreQueryHookService.executePreHooks( + await this.workspaceQueryHookService.executePreQueryHooks( userId, workspaceId, objectMetadataItem.nameSingular, @@ -441,7 +441,7 @@ export class WorkspaceQueryRunnerService { atMost: maximumRecordAffected, }); - await this.workspacePreQueryHookService.executePreHooks( + await this.workspaceQueryHookService.executePreQueryHooks( userId, workspaceId, objectMetadataItem.nameSingular, @@ -509,7 +509,7 @@ export class WorkspaceQueryRunnerService { ); // TODO END - await this.workspacePreQueryHookService.executePreHooks( + await this.workspaceQueryHookService.executePreQueryHooks( userId, workspaceId, objectMetadataItem.nameSingular, diff --git a/packages/twenty-server/src/engine/integrations/message-queue/drivers/pg-boss.driver.ts b/packages/twenty-server/src/engine/integrations/message-queue/drivers/pg-boss.driver.ts index ca3a0cd5f945f..98631bc881215 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/drivers/pg-boss.driver.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/drivers/pg-boss.driver.ts @@ -47,10 +47,17 @@ export class PgBossDriver } : {}, async (job) => { + // PGBoss work with wildcard job name + const jobName = job.name.split('.')?.[1]; + + if (!jobName) { + throw new Error('Job name could not be splited from the job.'); + } + await handler({ data: job.data, id: job.id, - name: job.name.split('.')[1], + name: jobName, }); }, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-datasource.factory.ts index 62166e536c51d..e040f6ddc46a3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-datasource.factory.ts @@ -14,7 +14,7 @@ export class ScopedWorkspaceDatasourceFactory { public async create(entities: EntitySchema[]) { const workspaceId: string | undefined = - this.request['req']?.['workspaceId']; + this.request?.['req']?.['workspaceId']; if (!workspaceId) { return null; diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts index ee20809427d30..d7c67d409efc9 100644 --- a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook.ts @@ -1,16 +1,20 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, NotFoundException, Scope } from '@nestjs/common'; -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity'; import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service'; import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; -@Injectable() +@WorkspaceQueryHook({ + key: `calendarEvent.findMany`, + scope: Scope.REQUEST, +}) export class CalendarEventFindManyPreQueryHook - implements WorkspacePreQueryHook + implements WorkspaceQueryHookInstance { constructor( @InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity) @@ -27,25 +31,24 @@ export class CalendarEventFindManyPreQueryHook throw new BadRequestException('id filter is required'); } - // TODO: Re-implement this using twenty ORM - // const calendarChannelCalendarEventAssociations = - // await this.calendarChannelEventAssociationRepository.find({ - // where: { - // calendarEvent: { - // id: payload?.filter?.id?.eq, - // }, - // }, - // relations: ['calendarChannel.connectedAccount'], - // }); + const calendarChannelCalendarEventAssociations = + await this.calendarChannelEventAssociationRepository.find({ + where: { + calendarEvent: { + id: payload?.filter?.id?.eq, + }, + }, + relations: ['calendarChannel.connectedAccount'], + }); - // if (calendarChannelCalendarEventAssociations.length === 0) { - // throw new NotFoundException(); - // } + if (calendarChannelCalendarEventAssociations.length === 0) { + throw new NotFoundException(); + } - // await this.canAccessCalendarEventService.canAccessCalendarEvent( - // userId, - // workspaceId, - // calendarChannelCalendarEventAssociations, - // ); + await this.canAccessCalendarEventService.canAccessCalendarEvent( + userId, + workspaceId, + calendarChannelCalendarEventAssociations, + ); } } diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts index 34642b128e48a..4deb02b2c97fe 100644 --- a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts @@ -1,15 +1,21 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, NotFoundException, Scope } from '@nestjs/common'; -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity'; -@Injectable() -export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook { +@WorkspaceQueryHook({ + key: `calendarEvent.findOne`, + scope: Scope.REQUEST, +}) +export class CalendarEventFindOnePreQueryHook + implements WorkspaceQueryHookInstance +{ constructor( @InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity) private readonly calendarChannelEventAssociationRepository: WorkspaceRepository, @@ -26,23 +32,24 @@ export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook { } // TODO: Re-implement this using twenty ORM - // const calendarChannelCalendarEventAssociations = - // await this.calendarChannelEventAssociationRepository.find({ - // where: { - // calendarEvent: { - // id: payload?.filter?.id?.eq, - // }, - // }, - // }); + const calendarChannelCalendarEventAssociations = + await this.calendarChannelEventAssociationRepository.find({ + where: { + calendarEvent: { + id: payload?.filter?.id?.eq, + }, + }, + relations: ['calendarChannel.connectedAccount'], + }); - // if (calendarChannelCalendarEventAssociations.length === 0) { - // throw new NotFoundException(); - // } + if (calendarChannelCalendarEventAssociations.length === 0) { + throw new NotFoundException(); + } - // await this.canAccessCalendarEventService.canAccessCalendarEvent( - // userId, - // workspaceId, - // calendarChannelCalendarEventAssociations, - // ); + await this.canAccessCalendarEventService.canAccessCalendarEvent( + userId, + workspaceId, + calendarChannelCalendarEventAssociations, + ); } } diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts index b86a3ca1cda63..80528e04004ff 100644 --- a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service.ts @@ -60,7 +60,7 @@ export class CanAccessCalendarEventService { const calendarChannelsConnectedAccounts = await this.connectedAccountRepository.getByIds( - calendarChannels.map((channel) => channel.connectedAccount.id), + calendarChannels.map((channel) => channel.connectedAccountId), workspaceId, ); diff --git a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts index 555b10719f7f8..c85bf53c7391c 100644 --- a/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts +++ b/packages/twenty-server/src/modules/calendar/query-hooks/calendar-query-hook.module.ts @@ -23,14 +23,8 @@ import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; ], providers: [ CanAccessCalendarEventService, - { - provide: CalendarEventFindOnePreQueryHook.name, - useClass: CalendarEventFindOnePreQueryHook, - }, - { - provide: CalendarEventFindManyPreQueryHook.name, - useClass: CalendarEventFindManyPreQueryHook, - }, + CalendarEventFindOnePreQueryHook, + CalendarEventFindManyPreQueryHook, ], }) export class CalendarQueryHookModule {} diff --git a/packages/twenty-server/src/modules/calendar/standard-objects/calendar-channel.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/standard-objects/calendar-channel.workspace-entity.ts index c72af80f87fbd..3e63dcbe26dac 100644 --- a/packages/twenty-server/src/modules/calendar/standard-objects/calendar-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/standard-objects/calendar-channel.workspace-entity.ts @@ -237,6 +237,8 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity { }) connectedAccount: Relation; + connectedAccountId: string; + @WorkspaceRelation({ standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.calendarChannelEventAssociations, diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook.ts index 30e025de1079a..9acb2b4bfa1c7 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook.ts @@ -1,15 +1,16 @@ -import { Injectable } from '@nestjs/common'; - -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { BlocklistItem, BlocklistValidationService, } from 'src/modules/connected-account/services/blocklist/blocklist-validation.service'; -@Injectable() -export class BlocklistCreateManyPreQueryHook implements WorkspacePreQueryHook { +@WorkspaceQueryHook(`blocklist.createMany`) +export class BlocklistCreateManyPreQueryHook + implements WorkspaceQueryHookInstance +{ constructor( private readonly blocklistValidationService: BlocklistValidationService, ) {} diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook.ts index 1ce12a5350dfd..28be74cfc6dc7 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook.ts @@ -1,9 +1,13 @@ -import { Injectable, MethodNotAllowedException } from '@nestjs/common'; +import { MethodNotAllowedException } from '@nestjs/common'; -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; -@Injectable() -export class BlocklistUpdateManyPreQueryHook implements WorkspacePreQueryHook { +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; + +@WorkspaceQueryHook(`blocklist.updateMany`) +export class BlocklistUpdateManyPreQueryHook + implements WorkspaceQueryHookInstance +{ constructor() {} async execute(): Promise { diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-one.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-one.pre-query.hook.ts index 28c39a43b2e91..cbb3f0fae5626 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-one.pre-query.hook.ts @@ -1,15 +1,16 @@ -import { Injectable } from '@nestjs/common'; - -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { BlocklistItem, BlocklistValidationService, } from 'src/modules/connected-account/services/blocklist/blocklist-validation.service'; -@Injectable() -export class BlocklistUpdateOnePreQueryHook implements WorkspacePreQueryHook { +@WorkspaceQueryHook(`blocklist.updateOne`) +export class BlocklistUpdateOnePreQueryHook + implements WorkspaceQueryHookInstance +{ constructor( private readonly blocklistValidationService: BlocklistValidationService, ) {} diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts index c711b4b567c88..d6fb5b4347356 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts @@ -8,18 +8,9 @@ import { BlocklistValidationModule } from 'src/modules/connected-account/service @Module({ imports: [BlocklistValidationModule], providers: [ - { - provide: BlocklistCreateManyPreQueryHook.name, - useClass: BlocklistCreateManyPreQueryHook, - }, - { - provide: BlocklistUpdateManyPreQueryHook.name, - useClass: BlocklistUpdateManyPreQueryHook, - }, - { - provide: BlocklistUpdateOnePreQueryHook.name, - useClass: BlocklistUpdateOnePreQueryHook, - }, + BlocklistCreateManyPreQueryHook, + BlocklistUpdateManyPreQueryHook, + BlocklistUpdateOnePreQueryHook, ], }) export class ConnectedAccountQueryHookModule {} diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook.ts index 8e82c578afe21..0e2f2a5d79a71 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook.ts @@ -1,19 +1,16 @@ -import { - BadRequestException, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service'; import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository'; import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; -@Injectable() -export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook { +@WorkspaceQueryHook(`message.findMany`) +export class MessageFindManyPreQueryHook implements WorkspaceQueryHookInstance { constructor( @InjectObjectMetadataRepository( MessageChannelMessageAssociationWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook.ts index 2140ac01081a1..713e024894f6a 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook.ts @@ -1,16 +1,17 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { NotFoundException } from '@nestjs/common'; -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service'; import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository'; import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; -@Injectable() -export class MessageFindOnePreQueryHook implements WorkspacePreQueryHook { +@WorkspaceQueryHook(`message.findOne`) +export class MessageFindOnePreQueryHook implements WorkspaceQueryHookInstance { constructor( @InjectObjectMetadataRepository( MessageChannelMessageAssociationWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts index f27adf462af72..4268de828017f 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts @@ -20,14 +20,8 @@ import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/stan ], providers: [ CanAccessMessageThreadService, - { - provide: MessageFindOnePreQueryHook.name, - useClass: MessageFindOnePreQueryHook, - }, - { - provide: MessageFindManyPreQueryHook.name, - useClass: MessageFindManyPreQueryHook, - }, + MessageFindOnePreQueryHook, + MessageFindManyPreQueryHook, ], }) export class MessagingQueryHookModule {} diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook.ts index e0650b7e2548b..9effd33285b72 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook.ts @@ -1,10 +1,12 @@ -import { Injectable, MethodNotAllowedException } from '@nestjs/common'; +import { MethodNotAllowedException } from '@nestjs/common'; -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; -@Injectable() +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; + +@WorkspaceQueryHook(`workspaceMember.deleteMany`) export class WorkspaceMemberDeleteManyPreQueryHook - implements WorkspacePreQueryHook + implements WorkspaceQueryHookInstance { constructor() {} diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts index 67f7d3f9e64f6..d5edd6ff5b320 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts @@ -1,16 +1,15 @@ -import { Injectable } from '@nestjs/common'; - -import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; -@Injectable() +@WorkspaceQueryHook(`workspaceMember.deleteOne`) export class WorkspaceMemberDeleteOnePreQueryHook - implements WorkspacePreQueryHook + implements WorkspaceQueryHookInstance { constructor( @InjectWorkspaceRepository(AttachmentWorkspaceEntity) diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts index 44f2362c839ca..051354c6becbe 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts @@ -14,14 +14,8 @@ import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-memb ]), ], providers: [ - { - provide: WorkspaceMemberDeleteOnePreQueryHook.name, - useClass: WorkspaceMemberDeleteOnePreQueryHook, - }, - { - provide: WorkspaceMemberDeleteManyPreQueryHook.name, - useClass: WorkspaceMemberDeleteManyPreQueryHook, - }, + WorkspaceMemberDeleteOnePreQueryHook, + WorkspaceMemberDeleteManyPreQueryHook, ], }) export class WorkspaceMemberQueryHookModule {} From 78865ee73ec422884d7007be37a9a79cf41b82e3 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 25 Jun 2024 15:51:01 +0200 Subject: [PATCH 29/30] Fix billing signup when workspace does not exist (#6018) --- .../core-modules/user-workspace/user-workspace.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 1ba4e66269db6..561357304374a 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -102,6 +102,11 @@ export class UserWorkspaceService extends TypeOrmQueryService { } public async getWorkspaceMemberCount(): Promise { + // TODO: to refactor, this could happen today for the first signup since the workspace does not exist yet + if (!this.workspaceMemberRepository) { + return undefined; + } + const workspaceMemberCount = await this.workspaceMemberRepository.count(); return workspaceMemberCount; From 3b7901b49a452213fd84a11279148e8263507439 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 25 Jun 2024 15:53:31 +0200 Subject: [PATCH 30/30] Removed performance optimization and put back previous system with recoil states for edit mode and soft focus to avoid side effects. (#6019) Fixes https://github.com/twentyhq/twenty/issues/6016 This was another side effect of the optimization made on RecordTableCellContainer to avoid using recoil states, but which causes too many unpredictable side effects. I just put back the previous system which works well. We'll see how to optimize it again later. --- .../useCloseCurrentTableCellInEditMode.ts | 7 -- .../internal/useMoveEditModeToCellPosition.ts | 14 --- .../hooks/internal/useSetSoftFocusPosition.ts | 14 --- .../components/RecordTableCellContainer.tsx | 87 +++++++------------ .../RecordTableCellSoftFocusMode.tsx | 6 -- 5 files changed, 33 insertions(+), 95 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts index b894046ce057b..a9976aa05fac4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts @@ -21,13 +21,6 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => { isTableCellInEditModeFamilyState(currentTableCellInEditModePosition), false, ); - - document.dispatchEvent( - new CustomEvent( - `edit-mode-change-${currentTableCellInEditModePosition.row}:${currentTableCellInEditModePosition.column}`, - { detail: false }, - ), - ); }; }, [currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState], diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition.ts index c51be2fa6bdbf..02e04a259d31f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition.ts @@ -24,23 +24,9 @@ export const useMoveEditModeToTableCellPosition = (recordTableId?: string) => { false, ); - document.dispatchEvent( - new CustomEvent( - `edit-mode-change-${currentTableCellInEditModePosition.row}:${currentTableCellInEditModePosition.column}`, - { detail: false }, - ), - ); - set(currentTableCellInEditModePositionState, newPosition); set(isTableCellInEditModeFamilyState(newPosition), true); - - document.dispatchEvent( - new CustomEvent( - `edit-mode-change-${newPosition.row}:${newPosition.column}`, - { detail: true }, - ), - ); }; }, [currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState], diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetSoftFocusPosition.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetSoftFocusPosition.ts index d645c7bb9036d..edf8f7a904e02 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetSoftFocusPosition.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetSoftFocusPosition.ts @@ -24,23 +24,9 @@ export const useSetSoftFocusPosition = (recordTableId?: string) => { set(isSoftFocusOnTableCellFamilyState(currentPosition), false); - document.dispatchEvent( - new CustomEvent( - `soft-focus-move-${currentPosition.row}:${currentPosition.column}`, - { detail: false }, - ), - ); - set(softFocusPositionState, newPosition); set(isSoftFocusOnTableCellFamilyState(newPosition), true); - - document.dispatchEvent( - new CustomEvent( - `soft-focus-move-${newPosition.row}:${newPosition.column}`, - { detail: true }, - ), - ); }; }, [ diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx index 3ef55faf1032f..8b817cd267132 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx @@ -1,14 +1,20 @@ -import React, { ReactElement, useContext, useEffect, useState } from 'react'; +import React, { ReactElement, useContext } from 'react'; import { clsx } from 'clsx'; +import { useRecoilValue } from 'recoil'; -import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableCellSoftFocusMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode'; import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition'; import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell'; +import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; +import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState'; +import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; +import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; +import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState'; import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext'; import { TableHotkeyScope } from '../../types/TableHotkeyScope'; @@ -40,20 +46,36 @@ export const RecordTableCellContainer = ({ const { setIsFocused } = useFieldFocus(); const { openTableCell } = useOpenRecordTableCellFromCell(); - const { isSelected, recordId, isPendingRow } = useContext( - RecordTableRowContext, + const { isSelected, recordId } = useContext(RecordTableRowContext); + + const { onMoveSoftFocusToCell, onContextMenu, onCellMouseEnter } = + useContext(RecordTableContext); + + const tableScopeId = useAvailableScopeIdOrThrow( + RecordTableScopeInternalContext, + getScopeIdOrUndefinedFromComponentId(), ); - const { isLabelIdentifier } = useContext(FieldContext); - const { onContextMenu, onCellMouseEnter } = useContext(RecordTableContext); - const shouldBeInitiallyInEditMode = - isPendingRow === true && isLabelIdentifier; + const isTableCellInEditModeFamilyState = extractComponentFamilyState( + isTableCellInEditModeComponentFamilyState, + tableScopeId, + ); - const [hasSoftFocus, setHasSoftFocus] = useState(false); - const [isInEditMode, setIsInEditMode] = useState(shouldBeInitiallyInEditMode); + const isSoftFocusOnTableCellFamilyState = extractComponentFamilyState( + isSoftFocusOnTableCellComponentFamilyState, + tableScopeId, + ); const cellPosition = useCurrentTableCellPosition(); + const isInEditMode = useRecoilValue( + isTableCellInEditModeFamilyState(cellPosition), + ); + + const hasSoftFocus = useRecoilValue( + isSoftFocusOnTableCellFamilyState(cellPosition), + ); + const handleContextMenu = (event: React.MouseEvent) => { onContextMenu(event, recordId); }; @@ -67,59 +89,16 @@ export const RecordTableCellContainer = ({ }; const handleContainerMouseLeave = () => { - setHasSoftFocus(false); setIsFocused(false); }; const handleContainerClick = () => { if (!hasSoftFocus) { + onMoveSoftFocusToCell(cellPosition); openTableCell(); } }; - useEffect(() => { - const customEventListener = (event: any) => { - event.stopPropagation(); - - const newHasSoftFocus = event.detail; - - setHasSoftFocus(newHasSoftFocus); - setIsFocused(newHasSoftFocus); - }; - - document.addEventListener( - `soft-focus-move-${cellPosition.row}:${cellPosition.column}`, - customEventListener, - ); - - return () => { - document.removeEventListener( - `soft-focus-move-${cellPosition.row}:${cellPosition.column}`, - customEventListener, - ); - }; - }, [cellPosition, setIsFocused]); - - useEffect(() => { - const customEventListener = (event: any) => { - const newIsInEditMode = event.detail; - - setIsInEditMode(newIsInEditMode); - }; - - document.addEventListener( - `edit-mode-change-${cellPosition.row}:${cellPosition.column}`, - customEventListener, - ); - - return () => { - document.removeEventListener( - `edit-mode-change-${cellPosition.row}:${cellPosition.column}`, - customEventListener, - ); - }; - }, [cellPosition]); - return ( { closeCurrentTableCell(); - document.dispatchEvent( - new CustomEvent(`soft-focus-move-${row}:${column}`, { detail: false }), - ); }, });