Search (#7237)
Steps to test 1. Run metadata migrations 2. Run sync-metadata on your workspace 3. Enable the following feature flags: IS_SEARCH_ENABLED IS_QUERY_RUNNER_TWENTY_ORM_ENABLED IS_WORKSPACE_MIGRATED_FOR_SEARCH 4. Type Cmd + K and search anything
This commit is contained in:
@ -369,6 +369,7 @@ export enum FieldMetadataType {
|
|||||||
RichText = 'RICH_TEXT',
|
RichText = 'RICH_TEXT',
|
||||||
Select = 'SELECT',
|
Select = 'SELECT',
|
||||||
Text = 'TEXT',
|
Text = 'TEXT',
|
||||||
|
TsVector = 'TS_VECTOR',
|
||||||
Uuid = 'UUID'
|
Uuid = 'UUID'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -280,6 +280,7 @@ export enum FieldMetadataType {
|
|||||||
RichText = 'RICH_TEXT',
|
RichText = 'RICH_TEXT',
|
||||||
Select = 'SELECT',
|
Select = 'SELECT',
|
||||||
Text = 'TEXT',
|
Text = 'TEXT',
|
||||||
|
TsVector = 'TS_VECTOR',
|
||||||
Uuid = 'UUID'
|
Uuid = 'UUID'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,9 @@ import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeybo
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
|
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
import { useSearchRecords } from '@/object-record/hooks/useSearchRecords';
|
||||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||||
|
import { Opportunity } from '@/opportunities/Opportunity';
|
||||||
import { Person } from '@/people/types/Person';
|
import { Person } from '@/people/types/Person';
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
@ -165,8 +167,21 @@ export const CommandMenu = () => {
|
|||||||
[closeCommandMenu],
|
[closeCommandMenu],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { records: people } = useFindManyRecords<Person>({
|
const isTwentyOrmEnabled = useIsFeatureEnabled(
|
||||||
skip: !isCommandMenuOpened,
|
'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED',
|
||||||
|
);
|
||||||
|
|
||||||
|
const isWorkspaceMigratedForSearch = useIsFeatureEnabled(
|
||||||
|
'IS_WORKSPACE_MIGRATED_FOR_SEARCH',
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSearchEnabled =
|
||||||
|
useIsFeatureEnabled('IS_SEARCH_ENABLED') &&
|
||||||
|
isTwentyOrmEnabled &&
|
||||||
|
isWorkspaceMigratedForSearch;
|
||||||
|
|
||||||
|
const { records: peopleFromFindMany } = useFindManyRecords<Person>({
|
||||||
|
skip: !isCommandMenuOpened || isSearchEnabled,
|
||||||
objectNameSingular: CoreObjectNameSingular.Person,
|
objectNameSingular: CoreObjectNameSingular.Person,
|
||||||
filter: commandMenuSearch
|
filter: commandMenuSearch
|
||||||
? makeOrFilterVariables([
|
? makeOrFilterVariables([
|
||||||
@ -183,9 +198,24 @@ export const CommandMenu = () => {
|
|||||||
: undefined,
|
: undefined,
|
||||||
limit: 3,
|
limit: 3,
|
||||||
});
|
});
|
||||||
|
const { records: peopleFromSearch } = useSearchRecords<Person>({
|
||||||
|
skip: !isCommandMenuOpened || !isSearchEnabled,
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Person,
|
||||||
|
limit: 3,
|
||||||
|
searchInput: commandMenuSearch ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const { records: companies } = useFindManyRecords<Company>({
|
const people = isSearchEnabled ? peopleFromSearch : peopleFromFindMany;
|
||||||
skip: !isCommandMenuOpened,
|
|
||||||
|
const { records: companiesFromSearch } = useSearchRecords<Company>({
|
||||||
|
skip: !isCommandMenuOpened || !isSearchEnabled,
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Company,
|
||||||
|
limit: 3,
|
||||||
|
searchInput: commandMenuSearch ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { records: companiesFromFindMany } = useFindManyRecords<Company>({
|
||||||
|
skip: !isCommandMenuOpened || isSearchEnabled,
|
||||||
objectNameSingular: CoreObjectNameSingular.Company,
|
objectNameSingular: CoreObjectNameSingular.Company,
|
||||||
filter: commandMenuSearch
|
filter: commandMenuSearch
|
||||||
? {
|
? {
|
||||||
@ -195,6 +225,10 @@ export const CommandMenu = () => {
|
|||||||
limit: 3,
|
limit: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const companies = isSearchEnabled
|
||||||
|
? companiesFromSearch
|
||||||
|
: companiesFromFindMany;
|
||||||
|
|
||||||
const { records: notes } = useFindManyRecords<Note>({
|
const { records: notes } = useFindManyRecords<Note>({
|
||||||
skip: !isCommandMenuOpened,
|
skip: !isCommandMenuOpened,
|
||||||
objectNameSingular: CoreObjectNameSingular.Note,
|
objectNameSingular: CoreObjectNameSingular.Note,
|
||||||
@ -207,8 +241,8 @@ export const CommandMenu = () => {
|
|||||||
limit: 3,
|
limit: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { records: opportunities } = useFindManyRecords({
|
const { records: opportunitiesFromFindMany } = useFindManyRecords({
|
||||||
skip: !isCommandMenuOpened,
|
skip: !isCommandMenuOpened || isSearchEnabled,
|
||||||
objectNameSingular: CoreObjectNameSingular.Opportunity,
|
objectNameSingular: CoreObjectNameSingular.Opportunity,
|
||||||
filter: commandMenuSearch
|
filter: commandMenuSearch
|
||||||
? {
|
? {
|
||||||
@ -218,6 +252,17 @@ export const CommandMenu = () => {
|
|||||||
limit: 3,
|
limit: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { records: opportunitiesFromSearch } = useSearchRecords<Opportunity>({
|
||||||
|
skip: !isCommandMenuOpened || !isSearchEnabled,
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Opportunity,
|
||||||
|
limit: 3,
|
||||||
|
searchInput: commandMenuSearch ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const opportunities = isSearchEnabled
|
||||||
|
? opportunitiesFromSearch
|
||||||
|
: opportunitiesFromFindMany;
|
||||||
|
|
||||||
const peopleCommands = useMemo(
|
const peopleCommands = useMemo(
|
||||||
() =>
|
() =>
|
||||||
people.map(({ id, name: { firstName, lastName } }) => ({
|
people.map(({ id, name: { firstName, lastName } }) => ({
|
||||||
|
|||||||
@ -1,16 +1,29 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
|
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { WorkspaceActivationStatus } from '~/generated/graphql';
|
import { WorkspaceActivationStatus } from '~/generated/graphql';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
|
const filterTsVectorFields = (
|
||||||
|
objectMetadataItems: ObjectMetadataItem[],
|
||||||
|
): ObjectMetadataItem[] => {
|
||||||
|
return objectMetadataItems.map((item) => ({
|
||||||
|
...item,
|
||||||
|
fields: item.fields.filter(
|
||||||
|
(field) => field.type !== FieldMetadataType.TsVector,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const ObjectMetadataItemsLoadEffect = () => {
|
export const ObjectMetadataItemsLoadEffect = () => {
|
||||||
const currentUser = useRecoilValue(currentUserState);
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
@ -21,26 +34,32 @@ export const ObjectMetadataItemsLoadEffect = () => {
|
|||||||
skip: !isLoggedIn,
|
skip: !isLoggedIn,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [objectMetadataItems, setObjectMetadataItems] = useRecoilState(
|
const updateObjectMetadataItems = useRecoilCallback(
|
||||||
objectMetadataItemsState,
|
({ set, snapshot }) =>
|
||||||
|
() => {
|
||||||
|
const filteredFields = filterTsVectorFields(newObjectMetadataItems);
|
||||||
|
const toSetObjectMetadataItems =
|
||||||
|
isUndefinedOrNull(currentUser) ||
|
||||||
|
currentWorkspace?.activationStatus !==
|
||||||
|
WorkspaceActivationStatus.Active
|
||||||
|
? generatedMockObjectMetadataItems
|
||||||
|
: filteredFields;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isDeeplyEqual(
|
||||||
|
snapshot.getLoadable(objectMetadataItemsState).getValue(),
|
||||||
|
toSetObjectMetadataItems,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
set(objectMetadataItemsState, toSetObjectMetadataItems);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentUser, currentWorkspace?.activationStatus, newObjectMetadataItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const toSetObjectMetadataItems =
|
updateObjectMetadataItems();
|
||||||
isUndefinedOrNull(currentUser) ||
|
}, [updateObjectMetadataItems]);
|
||||||
currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active
|
|
||||||
? generatedMockObjectMetadataItems
|
|
||||||
: newObjectMetadataItems;
|
|
||||||
if (!isDeeplyEqual(objectMetadataItems, toSetObjectMetadataItems)) {
|
|
||||||
setObjectMetadataItems(toSetObjectMetadataItems);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
currentUser,
|
|
||||||
currentWorkspace?.activationStatus,
|
|
||||||
newObjectMetadataItems,
|
|
||||||
objectMetadataItems,
|
|
||||||
setObjectMetadataItems,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||||
|
|
||||||
|
export type RecordGqlOperationSearchResult = {
|
||||||
|
[objectNamePlural: string]: RecordGqlConnection;
|
||||||
|
};
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
import { useQuery, WatchQueryFetchPolicy } from '@apollo/client';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||||
|
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||||
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
|
import { RecordGqlOperationSearchResult } from '@/object-record/graphql/types/RecordGqlOperationSearchResult';
|
||||||
|
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
||||||
|
import { useSearchRecordsQuery } from '@/object-record/hooks/useSearchRecordsQuery';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { logError } from '~/utils/logError';
|
||||||
|
|
||||||
|
export type UseSearchRecordsParams = ObjectMetadataItemIdentifier &
|
||||||
|
RecordGqlOperationVariables & {
|
||||||
|
onError?: (error?: Error) => void;
|
||||||
|
skip?: boolean;
|
||||||
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
|
fetchPolicy?: WatchQueryFetchPolicy;
|
||||||
|
searchInput?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSearchRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||||
|
objectNameSingular,
|
||||||
|
searchInput,
|
||||||
|
limit,
|
||||||
|
skip,
|
||||||
|
recordGqlFields,
|
||||||
|
fetchPolicy,
|
||||||
|
}: UseSearchRecordsParams) => {
|
||||||
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
const { searchRecordsQuery } = useSearchRecordsQuery({
|
||||||
|
objectNameSingular,
|
||||||
|
recordGqlFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const { data, loading, error } = useQuery<RecordGqlOperationSearchResult>(
|
||||||
|
searchRecordsQuery,
|
||||||
|
{
|
||||||
|
skip:
|
||||||
|
skip || !objectMetadataItem || !currentWorkspaceMember || !searchInput,
|
||||||
|
variables: {
|
||||||
|
search: searchInput,
|
||||||
|
limit: limit,
|
||||||
|
},
|
||||||
|
fetchPolicy: fetchPolicy,
|
||||||
|
onError: (error) => {
|
||||||
|
logError(
|
||||||
|
`useSearchRecords for "${objectMetadataItem.namePlural}" error : ` +
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
enqueueSnackBar(
|
||||||
|
`Error during useSearchRecords for "${objectMetadataItem.namePlural}", ${error.message}`,
|
||||||
|
{
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryResponseField = getSearchRecordsQueryResponseField(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = data?.[queryResponseField];
|
||||||
|
|
||||||
|
const records = useMemo(
|
||||||
|
() =>
|
||||||
|
result
|
||||||
|
? (getRecordsFromRecordConnection({
|
||||||
|
recordConnection: result,
|
||||||
|
}) as T[])
|
||||||
|
: [],
|
||||||
|
[result],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
objectMetadataItem,
|
||||||
|
records: records,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
|
import { generateSearchRecordsQuery } from '@/object-record/utils/generateSearchRecordsQuery';
|
||||||
|
|
||||||
|
export const useSearchRecordsQuery = ({
|
||||||
|
objectNameSingular,
|
||||||
|
recordGqlFields,
|
||||||
|
computeReferences,
|
||||||
|
}: {
|
||||||
|
objectNameSingular: string;
|
||||||
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
|
computeReferences?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
|
|
||||||
|
const searchRecordsQuery = generateSearchRecordsQuery({
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
recordGqlFields,
|
||||||
|
computeReferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchRecordsQuery,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||||
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
|
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export type QueryCursorDirection = 'before' | 'after';
|
||||||
|
|
||||||
|
export const generateSearchRecordsQuery = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
recordGqlFields,
|
||||||
|
computeReferences,
|
||||||
|
}: {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
objectMetadataItems: ObjectMetadataItem[]; // TODO - what is this used for?
|
||||||
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
|
computeReferences?: boolean;
|
||||||
|
}) => gql`
|
||||||
|
query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int) {
|
||||||
|
${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit){
|
||||||
|
edges {
|
||||||
|
node ${mapObjectMetadataToGraphQLQuery({
|
||||||
|
objectMetadataItems,
|
||||||
|
objectMetadataItem,
|
||||||
|
recordGqlFields,
|
||||||
|
computeReferences,
|
||||||
|
})}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const getSearchRecordsQueryResponseField = (objectNamePlural: string) =>
|
||||||
|
`search${capitalize(objectNamePlural)}`;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
export type Opportunity = {
|
||||||
|
__typename: 'Opportunity';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
deletedAt?: string | null;
|
||||||
|
name: string | null;
|
||||||
|
};
|
||||||
@ -2,5 +2,5 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|||||||
|
|
||||||
export type SettingsSupportedFieldType = Exclude<
|
export type SettingsSupportedFieldType = Exclude<
|
||||||
FieldMetadataType,
|
FieldMetadataType,
|
||||||
FieldMetadataType.Position
|
FieldMetadataType.Position | FieldMetadataType.TsVector
|
||||||
>;
|
>;
|
||||||
|
|||||||
@ -9,4 +9,7 @@ export type FeatureFlagKey =
|
|||||||
| 'IS_FREE_ACCESS_ENABLED'
|
| 'IS_FREE_ACCESS_ENABLED'
|
||||||
| 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED'
|
| 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED'
|
||||||
| 'IS_WORKFLOW_ENABLED'
|
| 'IS_WORKFLOW_ENABLED'
|
||||||
| 'IS_WORKSPACE_FAVORITE_ENABLED';
|
| 'IS_WORKSPACE_FAVORITE_ENABLED'
|
||||||
|
| 'IS_SEARCH_ENABLED'
|
||||||
|
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
|
||||||
|
| 'IS_WORKSPACE_MIGRATED_FOR_SEARCH';
|
||||||
|
|||||||
@ -60,6 +60,16 @@ export const seedFeatureFlags = async (
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: FeatureFlagKey.IsSearchEnabled,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: FeatureFlagKey.IsWorkspaceMigratedForSearch,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddIndexType1725893697807 implements MigrationInterface {
|
||||||
|
name = 'AddIndexType1725893697807';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TYPE metadata."indextype_enum" AS ENUM ('BTREE', 'GIN')`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE metadata."indexMetadata"
|
||||||
|
ADD COLUMN "indexType" metadata."indextype_enum" NOT NULL DEFAULT 'BTREE';
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE metadata."indexMetadata" DROP COLUMN "indexType"
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`DROP TYPE metadata."indextype_enum"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddTypeOrmMetadata1726848397026 implements MigrationInterface {
|
||||||
|
name = 'AddTypeOrmMetadata1726848397026';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE "core"."typeorm_metadata" (
|
||||||
|
"type" character varying NOT NULL,
|
||||||
|
"database" character varying,
|
||||||
|
"schema" character varying,
|
||||||
|
"table" character varying,
|
||||||
|
"name" character varying,
|
||||||
|
"value" text
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE "core"."typeorm_metadata"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddIsCustomColumnToIndexMetadata1727699709905
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddIsCustomColumnToIndexMetadata1727699709905';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "metadata"."indexMetadata"
|
||||||
|
ADD COLUMN "isCustom" BOOLEAN
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT FALSE;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "metadata"."indexMetadata"
|
||||||
|
DROP COLUMN "isCustom"
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,9 +3,13 @@ import { Module } from '@nestjs/common';
|
|||||||
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
|
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
|
||||||
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
||||||
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
||||||
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [WorkspaceQueryHookModule, WorkspaceQueryRunnerModule],
|
imports: [
|
||||||
|
WorkspaceQueryHookModule,
|
||||||
|
WorkspaceQueryRunnerModule,
|
||||||
|
FeatureFlagModule,
|
||||||
|
],
|
||||||
providers: [GraphqlQueryRunnerService],
|
providers: [GraphqlQueryRunnerService],
|
||||||
exports: [GraphqlQueryRunnerService],
|
exports: [GraphqlQueryRunnerService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
FindManyResolverArgs,
|
FindManyResolverArgs,
|
||||||
FindOneResolverArgs,
|
FindOneResolverArgs,
|
||||||
ResolverArgsType,
|
ResolverArgsType,
|
||||||
|
SearchResolverArgs,
|
||||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/gr
|
|||||||
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
|
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
|
||||||
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
|
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
|
||||||
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
|
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
|
||||||
|
import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service';
|
||||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||||
import {
|
import {
|
||||||
CallWebhookJobsJob,
|
CallWebhookJobsJob,
|
||||||
@ -36,6 +38,7 @@ import {
|
|||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||||
@ -48,6 +51,7 @@ import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/worksp
|
|||||||
export class GraphqlQueryRunnerService {
|
export class GraphqlQueryRunnerService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
private readonly workspaceQueryHookService: WorkspaceQueryHookService,
|
private readonly workspaceQueryHookService: WorkspaceQueryHookService,
|
||||||
private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory,
|
private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory,
|
||||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||||
@ -178,6 +182,20 @@ export class GraphqlQueryRunnerService {
|
|||||||
return results?.[0] as ObjectRecord;
|
return results?.[0] as ObjectRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@LogExecutionTime()
|
||||||
|
async search<ObjectRecord extends IRecord = IRecord>(
|
||||||
|
args: SearchResolverArgs,
|
||||||
|
options: WorkspaceQueryRunnerOptions,
|
||||||
|
): Promise<IConnection<ObjectRecord>> {
|
||||||
|
const graphqlQuerySearchResolverService =
|
||||||
|
new GraphqlQuerySearchResolverService(
|
||||||
|
this.twentyORMGlobalManager,
|
||||||
|
this.featureFlagService,
|
||||||
|
);
|
||||||
|
|
||||||
|
return graphqlQuerySearchResolverService.search(args, options);
|
||||||
|
}
|
||||||
|
|
||||||
@LogExecutionTime()
|
@LogExecutionTime()
|
||||||
async createMany<ObjectRecord extends IRecord = IRecord>(
|
async createMany<ObjectRecord extends IRecord = IRecord>(
|
||||||
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
|
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
|
||||||
|
|||||||
@ -0,0 +1,132 @@
|
|||||||
|
import {
|
||||||
|
Record as IRecord,
|
||||||
|
OrderByDirection,
|
||||||
|
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||||
|
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||||
|
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||||
|
import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
|
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
|
||||||
|
import {
|
||||||
|
GraphqlQueryRunnerException,
|
||||||
|
GraphqlQueryRunnerExceptionCode,
|
||||||
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
|
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
|
||||||
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
|
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||||
|
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
|
export class GraphqlQuerySearchResolverService {
|
||||||
|
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||||
|
private featureFlagService: FeatureFlagService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
featureFlagService: FeatureFlagService,
|
||||||
|
) {
|
||||||
|
this.twentyORMGlobalManager = twentyORMGlobalManager;
|
||||||
|
this.featureFlagService = featureFlagService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search<ObjectRecord extends IRecord = IRecord>(
|
||||||
|
args: SearchResolverArgs,
|
||||||
|
options: WorkspaceQueryRunnerOptions,
|
||||||
|
): Promise<IConnection<ObjectRecord>> {
|
||||||
|
const { authContext, objectMetadataItem, objectMetadataCollection } =
|
||||||
|
options;
|
||||||
|
|
||||||
|
const featureFlagsForWorkspace =
|
||||||
|
await this.featureFlagService.getWorkspaceFeatureFlags(
|
||||||
|
authContext.workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isQueryRunnerTwentyORMEnabled =
|
||||||
|
featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED;
|
||||||
|
|
||||||
|
const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED;
|
||||||
|
|
||||||
|
if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) {
|
||||||
|
throw new GraphqlQueryRunnerException(
|
||||||
|
'This endpoint is not available yet, please use findMany instead.',
|
||||||
|
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repository =
|
||||||
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||||
|
authContext.workspace.id,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectMetadataMap = generateObjectMetadataMap(
|
||||||
|
objectMetadataCollection,
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular];
|
||||||
|
|
||||||
|
if (!objectMetadata) {
|
||||||
|
throw new GraphqlQueryRunnerException(
|
||||||
|
`Object metadata not found for ${objectMetadataItem.nameSingular}`,
|
||||||
|
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeORMObjectRecordsParser =
|
||||||
|
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
|
||||||
|
|
||||||
|
if (!args.searchInput) {
|
||||||
|
return typeORMObjectRecordsParser.createConnection(
|
||||||
|
[],
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
[{ id: OrderByDirection.AscNullsFirst }],
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const searchTerms = this.formatSearchTerms(args.searchInput);
|
||||||
|
|
||||||
|
const limit = args?.limit ?? QUERY_MAX_RECORDS;
|
||||||
|
|
||||||
|
const resultsWithTsVector = (await repository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where(`"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, {
|
||||||
|
searchTerms,
|
||||||
|
})
|
||||||
|
.orderBy(
|
||||||
|
`ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`,
|
||||||
|
'DESC',
|
||||||
|
)
|
||||||
|
.setParameter('searchTerms', searchTerms)
|
||||||
|
.limit(limit)
|
||||||
|
.getMany()) as ObjectRecord[];
|
||||||
|
|
||||||
|
const objectRecords = await repository.formatResult(resultsWithTsVector);
|
||||||
|
|
||||||
|
const totalCount = await repository.count();
|
||||||
|
const order = undefined;
|
||||||
|
|
||||||
|
return typeORMObjectRecordsParser.createConnection(
|
||||||
|
objectRecords ?? [],
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
limit,
|
||||||
|
totalCount,
|
||||||
|
order,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatSearchTerms(searchTerm: string) {
|
||||||
|
const words = searchTerm.trim().split(/\s+/);
|
||||||
|
const formattedWords = words.map((word) => {
|
||||||
|
const escapedWord = word.replace(/[\\:'&|!()]/g, '\\$&');
|
||||||
|
|
||||||
|
return `${escapedWord}:*`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedWords.join(' | ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,18 +2,18 @@ import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/wor
|
|||||||
|
|
||||||
import { ArgsAliasFactory } from './args-alias.factory';
|
import { ArgsAliasFactory } from './args-alias.factory';
|
||||||
import { ArgsStringFactory } from './args-string.factory';
|
import { ArgsStringFactory } from './args-string.factory';
|
||||||
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
|
|
||||||
import { CreateManyQueryFactory } from './create-many-query.factory';
|
import { CreateManyQueryFactory } from './create-many-query.factory';
|
||||||
|
import { DeleteManyQueryFactory } from './delete-many-query.factory';
|
||||||
import { DeleteOneQueryFactory } from './delete-one-query.factory';
|
import { DeleteOneQueryFactory } from './delete-one-query.factory';
|
||||||
import { FieldAliasFactory } from './field-alias.factory';
|
import { FieldAliasFactory } from './field-alias.factory';
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
import { FieldsStringFactory } from './fields-string.factory';
|
||||||
|
import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory';
|
||||||
import { FindManyQueryFactory } from './find-many-query.factory';
|
import { FindManyQueryFactory } from './find-many-query.factory';
|
||||||
import { FindOneQueryFactory } from './find-one-query.factory';
|
import { FindOneQueryFactory } from './find-one-query.factory';
|
||||||
import { UpdateOneQueryFactory } from './update-one-query.factory';
|
|
||||||
import { UpdateManyQueryFactory } from './update-many-query.factory';
|
|
||||||
import { DeleteManyQueryFactory } from './delete-many-query.factory';
|
|
||||||
import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory';
|
|
||||||
import { RecordPositionQueryFactory } from './record-position-query.factory';
|
import { RecordPositionQueryFactory } from './record-position-query.factory';
|
||||||
|
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
|
||||||
|
import { UpdateManyQueryFactory } from './update-many-query.factory';
|
||||||
|
import { UpdateOneQueryFactory } from './update-one-query.factory';
|
||||||
|
|
||||||
export const workspaceQueryBuilderFactories = [
|
export const workspaceQueryBuilderFactories = [
|
||||||
ArgsAliasFactory,
|
ArgsAliasFactory,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||||
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
|
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
|
||||||
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
||||||
|
import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory';
|
||||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||||
|
|
||||||
import { CreateManyResolverFactory } from './create-many-resolver.factory';
|
import { CreateManyResolverFactory } from './create-many-resolver.factory';
|
||||||
@ -25,6 +26,7 @@ export const workspaceResolverBuilderFactories = [
|
|||||||
DestroyOneResolverFactory,
|
DestroyOneResolverFactory,
|
||||||
DestroyManyResolverFactory,
|
DestroyManyResolverFactory,
|
||||||
RestoreManyResolverFactory,
|
RestoreManyResolverFactory,
|
||||||
|
SearchResolverFactory,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const workspaceResolverBuilderMethodNames = {
|
export const workspaceResolverBuilderMethodNames = {
|
||||||
@ -32,6 +34,7 @@ export const workspaceResolverBuilderMethodNames = {
|
|||||||
FindManyResolverFactory.methodName,
|
FindManyResolverFactory.methodName,
|
||||||
FindOneResolverFactory.methodName,
|
FindOneResolverFactory.methodName,
|
||||||
FindDuplicatesResolverFactory.methodName,
|
FindDuplicatesResolverFactory.methodName,
|
||||||
|
SearchResolverFactory.methodName,
|
||||||
],
|
],
|
||||||
mutations: [
|
mutations: [
|
||||||
CreateManyResolverFactory.methodName,
|
CreateManyResolverFactory.methodName,
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||||
|
import {
|
||||||
|
Resolver,
|
||||||
|
SearchResolverArgs,
|
||||||
|
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||||
|
|
||||||
|
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
|
||||||
|
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SearchResolverFactory
|
||||||
|
implements WorkspaceResolverBuilderFactoryInterface
|
||||||
|
{
|
||||||
|
public static methodName = 'search' as const;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(context: WorkspaceSchemaBuilderContext): Resolver<SearchResolverArgs> {
|
||||||
|
const internalContext = context;
|
||||||
|
|
||||||
|
return async (_source, args, _context, info) => {
|
||||||
|
try {
|
||||||
|
return await this.graphqlQueryRunnerService.search(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
|
info,
|
||||||
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -48,6 +48,11 @@ export interface FindDuplicatesResolverArgs<
|
|||||||
data?: Data[];
|
data?: Data[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchResolverArgs {
|
||||||
|
searchInput?: string;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateOneResolverArgs<
|
export interface CreateOneResolverArgs<
|
||||||
Data extends Partial<Record> = Partial<Record>,
|
Data extends Partial<Record> = Partial<Record>,
|
||||||
> {
|
> {
|
||||||
@ -123,4 +128,5 @@ export type ResolverArgs =
|
|||||||
| UpdateManyResolverArgs
|
| UpdateManyResolverArgs
|
||||||
| UpdateOneResolverArgs
|
| UpdateOneResolverArgs
|
||||||
| DestroyManyResolverArgs
|
| DestroyManyResolverArgs
|
||||||
| RestoreManyResolverArgs;
|
| RestoreManyResolverArgs
|
||||||
|
| SearchResolverArgs;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-reso
|
|||||||
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||||
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
|
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
|
||||||
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
||||||
|
import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory';
|
||||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||||
@ -42,6 +43,7 @@ export class WorkspaceResolverFactory {
|
|||||||
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
|
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
|
||||||
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
|
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
|
||||||
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
|
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
|
||||||
|
private readonly searchResolverFactory: SearchResolverFactory,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
@ -65,6 +67,7 @@ export class WorkspaceResolverFactory {
|
|||||||
['deleteMany', this.deleteManyResolverFactory],
|
['deleteMany', this.deleteManyResolverFactory],
|
||||||
['restoreMany', this.restoreManyResolverFactory],
|
['restoreMany', this.restoreManyResolverFactory],
|
||||||
['destroyMany', this.destroyManyResolverFactory],
|
['destroyMany', this.destroyManyResolverFactory],
|
||||||
|
['search', this.searchResolverFactory],
|
||||||
]);
|
]);
|
||||||
const resolvers: IResolvers = {
|
const resolvers: IResolvers = {
|
||||||
Query: {},
|
Query: {},
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import { Inject, Injectable, forwardRef } from '@nestjs/common';
|
import { Inject, Injectable, forwardRef } from '@nestjs/common';
|
||||||
|
|
||||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
import { GraphQLInputObjectType } from 'graphql';
|
||||||
|
|
||||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { generateFields } from 'src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils';
|
||||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
|
||||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
|
||||||
import { pascalCase } from 'src/utils/pascal-case';
|
import { pascalCase } from 'src/utils/pascal-case';
|
||||||
|
|
||||||
import { InputTypeFactory } from './input-type.factory';
|
import { InputTypeFactory } from './input-type.factory';
|
||||||
@ -55,7 +53,12 @@ export class InputTypeDefinitionFactory {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...this.generateFields(objectMetadata, kind, options),
|
...generateFields(
|
||||||
|
objectMetadata,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
this.inputTypeFactory,
|
||||||
|
),
|
||||||
and: {
|
and: {
|
||||||
type: andOrType,
|
type: andOrType,
|
||||||
},
|
},
|
||||||
@ -73,7 +76,12 @@ export class InputTypeDefinitionFactory {
|
|||||||
* Other input types are generated with fields only
|
* Other input types are generated with fields only
|
||||||
*/
|
*/
|
||||||
default:
|
default:
|
||||||
return this.generateFields(objectMetadata, kind, options);
|
return generateFields(
|
||||||
|
objectMetadata,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
this.inputTypeFactory,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -84,46 +92,4 @@ export class InputTypeDefinitionFactory {
|
|||||||
type: inputType,
|
type: inputType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateFields(
|
|
||||||
objectMetadata: ObjectMetadataInterface,
|
|
||||||
kind: InputTypeDefinitionKind,
|
|
||||||
options: WorkspaceBuildSchemaOptions,
|
|
||||||
): GraphQLInputFieldConfigMap {
|
|
||||||
const fields: GraphQLInputFieldConfigMap = {};
|
|
||||||
|
|
||||||
for (const fieldMetadata of objectMetadata.fields) {
|
|
||||||
// Relation field types are generated during extension of object type definition
|
|
||||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
|
||||||
? fieldMetadata.type.toString()
|
|
||||||
: fieldMetadata.id;
|
|
||||||
|
|
||||||
const isIdField = fieldMetadata.name === 'id';
|
|
||||||
|
|
||||||
const type = this.inputTypeFactory.create(
|
|
||||||
target,
|
|
||||||
fieldMetadata.type,
|
|
||||||
kind,
|
|
||||||
options,
|
|
||||||
{
|
|
||||||
nullable: fieldMetadata.isNullable,
|
|
||||||
defaultValue: fieldMetadata.defaultValue,
|
|
||||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
|
||||||
settings: fieldMetadata.settings,
|
|
||||||
isIdField,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
fields[fieldMetadata.name] = {
|
|
||||||
type,
|
|
||||||
description: fieldMetadata.description,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
import { GraphQLObjectType } from 'graphql';
|
||||||
|
|
||||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
|
import { generateFields } from 'src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils';
|
||||||
import { pascalCase } from 'src/utils/pascal-case';
|
import { pascalCase } from 'src/utils/pascal-case';
|
||||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
||||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
|
||||||
|
|
||||||
import { OutputTypeFactory } from './output-type.factory';
|
import { OutputTypeFactory } from './output-type.factory';
|
||||||
|
|
||||||
@ -39,48 +37,13 @@ export class ObjectTypeDefinitionFactory {
|
|||||||
type: new GraphQLObjectType({
|
type: new GraphQLObjectType({
|
||||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||||
description: objectMetadata.description,
|
description: objectMetadata.description,
|
||||||
fields: this.generateFields(objectMetadata, kind, options),
|
fields: generateFields(
|
||||||
|
objectMetadata,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
this.outputTypeFactory,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateFields(
|
|
||||||
objectMetadata: ObjectMetadataInterface,
|
|
||||||
kind: ObjectTypeDefinitionKind,
|
|
||||||
options: WorkspaceBuildSchemaOptions,
|
|
||||||
): GraphQLFieldConfigMap<any, any> {
|
|
||||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
|
||||||
|
|
||||||
for (const fieldMetadata of objectMetadata.fields) {
|
|
||||||
// Relation field types are generated during extension of object type definition
|
|
||||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
|
||||||
? fieldMetadata.type.toString()
|
|
||||||
: fieldMetadata.id;
|
|
||||||
|
|
||||||
const type = this.outputTypeFactory.create(
|
|
||||||
target,
|
|
||||||
fieldMetadata.type,
|
|
||||||
kind,
|
|
||||||
options,
|
|
||||||
{
|
|
||||||
nullable: fieldMetadata.isNullable,
|
|
||||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
|
||||||
settings: fieldMetadata.settings,
|
|
||||||
// Scalar type is already defined in the entity itself.
|
|
||||||
isIdField: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
fields[fieldMetadata.name] = {
|
|
||||||
type,
|
|
||||||
description: fieldMetadata.description,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,9 +74,7 @@ export class RootTypeFactory {
|
|||||||
const args = getResolverArgs(methodName);
|
const args = getResolverArgs(methodName);
|
||||||
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||||
objectMetadata.id,
|
objectMetadata.id,
|
||||||
['findMany', 'findDuplicates'].includes(methodName)
|
this.getObjectTypeDefinitionKindByMethodName(methodName),
|
||||||
? ObjectTypeDefinitionKind.Connection
|
|
||||||
: ObjectTypeDefinitionKind.Plain,
|
|
||||||
);
|
);
|
||||||
const argsType = this.argsFactory.create(
|
const argsType = this.argsFactory.create(
|
||||||
{
|
{
|
||||||
@ -124,4 +122,17 @@ export class RootTypeFactory {
|
|||||||
|
|
||||||
return fieldConfigMap;
|
return fieldConfigMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getObjectTypeDefinitionKindByMethodName(
|
||||||
|
methodName: WorkspaceResolverBuilderMethodNames,
|
||||||
|
): ObjectTypeDefinitionKind {
|
||||||
|
switch (methodName) {
|
||||||
|
case 'findMany':
|
||||||
|
case 'findDuplicates':
|
||||||
|
case 'search':
|
||||||
|
return ObjectTypeDefinitionKind.Connection;
|
||||||
|
default:
|
||||||
|
return ObjectTypeDefinitionKind.Plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
import {
|
||||||
|
GraphQLFieldConfigMap,
|
||||||
|
GraphQLInputFieldConfigMap,
|
||||||
|
GraphQLInputType,
|
||||||
|
GraphQLOutputType,
|
||||||
|
} from 'graphql';
|
||||||
|
|
||||||
|
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||||
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
|
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
|
||||||
|
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||||
|
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||||
|
|
||||||
|
type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> =
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
target: string,
|
||||||
|
fieldType: FieldMetadataType,
|
||||||
|
kind: T,
|
||||||
|
options: WorkspaceBuildSchemaOptions,
|
||||||
|
additionalOptions: {
|
||||||
|
nullable?: boolean;
|
||||||
|
defaultValue?: any;
|
||||||
|
isArray: boolean;
|
||||||
|
settings: any;
|
||||||
|
isIdField: boolean;
|
||||||
|
},
|
||||||
|
) => T extends InputTypeDefinitionKind
|
||||||
|
? GraphQLInputType
|
||||||
|
: GraphQLOutputType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateFields = <
|
||||||
|
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
||||||
|
>(
|
||||||
|
objectMetadata: ObjectMetadataInterface,
|
||||||
|
kind: T,
|
||||||
|
options: WorkspaceBuildSchemaOptions,
|
||||||
|
typeFactory: TypeFactory<T>,
|
||||||
|
): T extends InputTypeDefinitionKind
|
||||||
|
? GraphQLInputFieldConfigMap
|
||||||
|
: GraphQLFieldConfigMap<any, any> => {
|
||||||
|
const fields = {};
|
||||||
|
|
||||||
|
for (const fieldMetadata of objectMetadata.fields) {
|
||||||
|
if (
|
||||||
|
isRelationFieldMetadataType(fieldMetadata.type) ||
|
||||||
|
fieldMetadata.type === FieldMetadataType.TS_VECTOR
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
||||||
|
? fieldMetadata.type.toString()
|
||||||
|
: fieldMetadata.id;
|
||||||
|
|
||||||
|
const typeFactoryOptions = isInputTypeDefinitionKind(kind)
|
||||||
|
? {
|
||||||
|
nullable: fieldMetadata.isNullable,
|
||||||
|
defaultValue: fieldMetadata.defaultValue,
|
||||||
|
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||||
|
settings: fieldMetadata.settings,
|
||||||
|
isIdField: fieldMetadata.name === 'id',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
nullable: fieldMetadata.isNullable,
|
||||||
|
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||||
|
settings: fieldMetadata.settings,
|
||||||
|
// Scalar type is already defined in the entity itself.
|
||||||
|
isIdField: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const type = typeFactory.create(
|
||||||
|
target,
|
||||||
|
fieldMetadata.type,
|
||||||
|
kind,
|
||||||
|
options,
|
||||||
|
typeFactoryOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
fields[fieldMetadata.name] = {
|
||||||
|
type,
|
||||||
|
description: fieldMetadata.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type guard
|
||||||
|
const isInputTypeDefinitionKind = (
|
||||||
|
kind: InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
||||||
|
): kind is InputTypeDefinitionKind => {
|
||||||
|
return Object.values(InputTypeDefinitionKind).includes(
|
||||||
|
kind as InputTypeDefinitionKind,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -137,6 +137,17 @@ export const getResolverArgs = (
|
|||||||
isNullable: false,
|
isNullable: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
case 'search':
|
||||||
|
return {
|
||||||
|
searchInput: {
|
||||||
|
type: GraphQLString,
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: GraphQLInt,
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown resolver type: ${type}`);
|
throw new Error(`Unknown resolver type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,4 +10,6 @@ export enum FeatureFlagKey {
|
|||||||
IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED',
|
IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED',
|
||||||
IsQueryRunnerTwentyORMEnabled = 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED',
|
IsQueryRunnerTwentyORMEnabled = 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED',
|
||||||
IsWorkspaceFavoriteEnabled = 'IS_WORKSPACE_FAVORITE_ENABLED',
|
IsWorkspaceFavoriteEnabled = 'IS_WORKSPACE_FAVORITE_ENABLED',
|
||||||
|
IsSearchEnabled = 'IS_SEARCH_ENABLED',
|
||||||
|
IsWorkspaceMigratedForSearch = 'IS_WORKSPACE_MIGRATED_FOR_SEARCH',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
export const SEARCH_VECTOR_FIELD = {
|
||||||
|
name: 'searchVector',
|
||||||
|
label: 'Search vector',
|
||||||
|
description: 'Field used for full-text search',
|
||||||
|
} as const;
|
||||||
@ -47,6 +47,7 @@ export enum FieldMetadataType {
|
|||||||
RICH_TEXT = 'RICH_TEXT',
|
RICH_TEXT = 'RICH_TEXT',
|
||||||
ACTOR = 'ACTOR',
|
ACTOR = 'ACTOR',
|
||||||
ARRAY = 'ARRAY',
|
ARRAY = 'ARRAY',
|
||||||
|
TS_VECTOR = 'TS_VECTOR',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity('fieldMetadata')
|
@Entity('fieldMetadata')
|
||||||
|
|||||||
@ -22,4 +22,6 @@ export interface FieldMetadataInterface<
|
|||||||
fromRelationMetadata?: RelationMetadataEntity;
|
fromRelationMetadata?: RelationMetadataEntity;
|
||||||
toRelationMetadata?: RelationMetadataEntity;
|
toRelationMetadata?: RelationMetadataEntity;
|
||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
|
generatedType?: 'STORED' | 'VIRTUAL';
|
||||||
|
asExpression?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,11 @@ import { pascalCase } from 'src/utils/pascal-case';
|
|||||||
|
|
||||||
type ComputeColumnNameOptions = { isForeignKey?: boolean };
|
type ComputeColumnNameOptions = { isForeignKey?: boolean };
|
||||||
|
|
||||||
|
export type FieldTypeAndNameMetadata = {
|
||||||
|
name: string;
|
||||||
|
type: FieldMetadataType;
|
||||||
|
};
|
||||||
|
|
||||||
export function computeColumnName(
|
export function computeColumnName(
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
options?: ComputeColumnNameOptions,
|
options?: ComputeColumnNameOptions,
|
||||||
@ -48,13 +53,16 @@ export function computeCompositeColumnName(
|
|||||||
export function computeCompositeColumnName<
|
export function computeCompositeColumnName<
|
||||||
T extends FieldMetadataType | 'default',
|
T extends FieldMetadataType | 'default',
|
||||||
>(
|
>(
|
||||||
fieldMetadata: FieldMetadataInterface<T>,
|
fieldMetadata: FieldTypeAndNameMetadata | FieldMetadataInterface<T>,
|
||||||
compositeProperty: CompositeProperty,
|
compositeProperty: CompositeProperty,
|
||||||
): string;
|
): string;
|
||||||
export function computeCompositeColumnName<
|
export function computeCompositeColumnName<
|
||||||
T extends FieldMetadataType | 'default',
|
T extends FieldMetadataType | 'default',
|
||||||
>(
|
>(
|
||||||
fieldMetadataOrFieldName: FieldMetadataInterface<T> | string,
|
fieldMetadataOrFieldName:
|
||||||
|
| FieldTypeAndNameMetadata
|
||||||
|
| FieldMetadataInterface<T>
|
||||||
|
| string,
|
||||||
compositeProperty: CompositeProperty,
|
compositeProperty: CompositeProperty,
|
||||||
): string {
|
): string {
|
||||||
const generateName = (name: string) => {
|
const generateName = (name: string) => {
|
||||||
|
|||||||
@ -13,6 +13,11 @@ import {
|
|||||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
|
||||||
|
export enum IndexType {
|
||||||
|
BTREE = 'BTREE',
|
||||||
|
GIN = 'GIN',
|
||||||
|
}
|
||||||
|
|
||||||
@Entity('indexMetadata')
|
@Entity('indexMetadata')
|
||||||
export class IndexMetadataEntity {
|
export class IndexMetadataEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@ -48,4 +53,15 @@ export class IndexMetadataEntity {
|
|||||||
|
|
||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
isCustom: boolean;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: IndexType,
|
||||||
|
nullable: true,
|
||||||
|
default: IndexType.BTREE,
|
||||||
|
})
|
||||||
|
indexType?: IndexType;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isDefined } from 'class-validator';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
import {
|
||||||
|
IndexMetadataEntity,
|
||||||
|
IndexType,
|
||||||
|
} from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||||
@ -28,6 +32,8 @@ export class IndexMetadataService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
objectMetadata: ObjectMetadataEntity,
|
objectMetadata: ObjectMetadataEntity,
|
||||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[],
|
fieldMetadataToIndex: Partial<FieldMetadataEntity>[],
|
||||||
|
isCustom: boolean,
|
||||||
|
indexType?: IndexType,
|
||||||
) {
|
) {
|
||||||
const tableName = computeObjectTargetTable(objectMetadata);
|
const tableName = computeObjectTargetTable(objectMetadata);
|
||||||
|
|
||||||
@ -53,6 +59,8 @@ export class IndexMetadataService {
|
|||||||
),
|
),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
objectMetadataId: objectMetadata.id,
|
objectMetadataId: objectMetadata.id,
|
||||||
|
...(isDefined(indexType) ? { indexType: indexType } : {}),
|
||||||
|
isCustom: isCustom,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -74,6 +82,7 @@ export class IndexMetadataService {
|
|||||||
action: WorkspaceMigrationIndexActionType.CREATE,
|
action: WorkspaceMigrationIndexActionType.CREATE,
|
||||||
columns: columnNames,
|
columns: columnNames,
|
||||||
name: indexName,
|
name: indexName,
|
||||||
|
type: indexType,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} satisfies WorkspaceMigrationTableAction;
|
} satisfies WorkspaceMigrationTableAction;
|
||||||
|
|||||||
@ -10,9 +10,11 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
|||||||
|
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
|
||||||
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
|
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
|
||||||
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
|
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
|
||||||
import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver';
|
import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver';
|
||||||
@ -44,6 +46,8 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
|
|||||||
WorkspaceMigrationRunnerModule,
|
WorkspaceMigrationRunnerModule,
|
||||||
WorkspaceMetadataVersionModule,
|
WorkspaceMetadataVersionModule,
|
||||||
RemoteTableRelationsModule,
|
RemoteTableRelationsModule,
|
||||||
|
IndexMetadataModule,
|
||||||
|
FeatureFlagModule,
|
||||||
],
|
],
|
||||||
services: [ObjectMetadataService],
|
services: [ObjectMetadataService],
|
||||||
resolvers: [
|
resolvers: [
|
||||||
|
|||||||
@ -5,19 +5,30 @@ import console from 'console';
|
|||||||
|
|
||||||
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
|
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
|
||||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||||
|
import { isDefined } from 'class-validator';
|
||||||
import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm';
|
import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||||
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
|
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import {
|
import {
|
||||||
FieldMetadataEntity,
|
FieldMetadataEntity,
|
||||||
FieldMetadataType,
|
FieldMetadataType,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
import {
|
||||||
|
computeColumnName,
|
||||||
|
FieldTypeAndNameMetadata,
|
||||||
|
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
|
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
||||||
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
||||||
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||||
|
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
|
||||||
import {
|
import {
|
||||||
ObjectMetadataException,
|
ObjectMetadataException,
|
||||||
ObjectMetadataExceptionCode,
|
ObjectMetadataExceptionCode,
|
||||||
@ -33,6 +44,7 @@ import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/
|
|||||||
import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service';
|
import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service';
|
||||||
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
|
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
|
||||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||||
|
import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory';
|
||||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||||
import {
|
import {
|
||||||
WorkspaceMigrationColumnActionType,
|
WorkspaceMigrationColumnActionType,
|
||||||
@ -58,6 +70,7 @@ import {
|
|||||||
createForeignKeyDeterministicUuid,
|
createForeignKeyDeterministicUuid,
|
||||||
createRelationDeterministicUuid,
|
createRelationDeterministicUuid,
|
||||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
|
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
|
||||||
|
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
|
|
||||||
@ -79,9 +92,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
|
|
||||||
private readonly remoteTableRelationsService: RemoteTableRelationsService,
|
private readonly remoteTableRelationsService: RemoteTableRelationsService,
|
||||||
|
|
||||||
|
private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory,
|
||||||
|
|
||||||
private readonly dataSourceService: DataSourceService,
|
private readonly dataSourceService: DataSourceService,
|
||||||
private readonly typeORMService: TypeORMService,
|
private readonly typeORMService: TypeORMService,
|
||||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||||
|
|
||||||
|
private readonly indexMetadataService: IndexMetadataService,
|
||||||
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
@ -350,6 +368,18 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
objectMetadataInput,
|
objectMetadataInput,
|
||||||
createdObjectMetadata,
|
createdObjectMetadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSearchEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||||
|
FeatureFlagKey.IsSearchEnabled,
|
||||||
|
objectMetadataInput.workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSearchEnabled) {
|
||||||
|
await this.createSearchVectorField(
|
||||||
|
objectMetadataInput,
|
||||||
|
createdObjectMetadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.remoteTableRelationsService.createForeignKeysMetadataAndMigrations(
|
await this.remoteTableRelationsService.createForeignKeysMetadataAndMigrations(
|
||||||
objectMetadataInput.workspaceId,
|
objectMetadataInput.workspaceId,
|
||||||
@ -548,6 +578,70 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async createSearchVectorField(
|
||||||
|
objectMetadataInput: CreateObjectInput,
|
||||||
|
createdObjectMetadata: ObjectMetadataEntity,
|
||||||
|
) {
|
||||||
|
const searchVectorFieldMetadata = await this.fieldMetadataRepository.save({
|
||||||
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
|
||||||
|
objectMetadataId: createdObjectMetadata.id,
|
||||||
|
workspaceId: objectMetadataInput.workspaceId,
|
||||||
|
isCustom: false,
|
||||||
|
isActive: false,
|
||||||
|
isSystem: true,
|
||||||
|
type: FieldMetadataType.TS_VECTOR,
|
||||||
|
name: SEARCH_VECTOR_FIELD.name,
|
||||||
|
label: SEARCH_VECTOR_FIELD.label,
|
||||||
|
description: SEARCH_VECTOR_FIELD.description,
|
||||||
|
isNullable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchableFieldForCustomObject =
|
||||||
|
createdObjectMetadata.labelIdentifierFieldMetadataId
|
||||||
|
? createdObjectMetadata.fields.find(
|
||||||
|
(field) =>
|
||||||
|
field.id === createdObjectMetadata.labelIdentifierFieldMetadataId,
|
||||||
|
)
|
||||||
|
: createdObjectMetadata.fields.find(
|
||||||
|
(field) => field.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDefined(searchableFieldForCustomObject)) {
|
||||||
|
throw new Error('No searchable field found for custom object');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.workspaceMigrationService.createCustomMigration(
|
||||||
|
generateMigrationName(`create-${createdObjectMetadata.nameSingular}`),
|
||||||
|
createdObjectMetadata.workspaceId,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: computeTableName(
|
||||||
|
createdObjectMetadata.nameSingular,
|
||||||
|
createdObjectMetadata.isCustom,
|
||||||
|
),
|
||||||
|
action: WorkspaceMigrationTableActionType.ALTER,
|
||||||
|
columns: this.tsVectorColumnActionFactory.handleCreateAction({
|
||||||
|
...searchVectorFieldMetadata,
|
||||||
|
defaultValue: undefined,
|
||||||
|
generatedType: 'STORED',
|
||||||
|
asExpression: getTsVectorColumnExpressionFromFields([
|
||||||
|
searchableFieldForCustomObject as FieldTypeAndNameMetadata,
|
||||||
|
]),
|
||||||
|
options: undefined,
|
||||||
|
} as FieldMetadataInterface<FieldMetadataType.TS_VECTOR>),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.indexMetadataService.createIndex(
|
||||||
|
objectMetadataInput.workspaceId,
|
||||||
|
createdObjectMetadata,
|
||||||
|
[searchVectorFieldMetadata],
|
||||||
|
false,
|
||||||
|
IndexType.GIN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async createActivityTargetRelation(
|
private async createActivityTargetRelation(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
createdObjectMetadata: ObjectMetadataEntity,
|
createdObjectMetadata: ObjectMetadataEntity,
|
||||||
|
|||||||
@ -153,6 +153,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
|
|||||||
relationMetadataInput.workspaceId,
|
relationMetadataInput.workspaceId,
|
||||||
toObjectMetadata,
|
toObjectMetadata,
|
||||||
[foreignKeyFieldMetadata, deletedFieldMetadata],
|
[foreignKeyFieldMetadata, deletedFieldMetadata],
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/metadata.constants';
|
import { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants';
|
||||||
|
|
||||||
export const exceedsDatabaseIdentifierMaximumLength = (string: string) => {
|
export const exceedsDatabaseIdentifierMaximumLength = (string: string) => {
|
||||||
return string.length > IDENTIFIER_MAX_CHAR_LENGTH;
|
return string.length > IDENTIFIER_MAX_CHAR_LENGTH;
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
|
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
|
||||||
import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||||
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
|
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
|
||||||
|
import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory';
|
||||||
|
|
||||||
export const workspaceColumnActionFactories = [
|
export const workspaceColumnActionFactories = [
|
||||||
|
TsVectorColumnActionFactory,
|
||||||
BasicColumnActionFactory,
|
BasicColumnActionFactory,
|
||||||
EnumColumnActionFactory,
|
EnumColumnActionFactory,
|
||||||
CompositeColumnActionFactory,
|
CompositeColumnActionFactory,
|
||||||
|
|||||||
@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||||
|
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
|
||||||
|
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||||
|
import {
|
||||||
|
WorkspaceMigrationColumnActionType,
|
||||||
|
WorkspaceMigrationColumnAlter,
|
||||||
|
WorkspaceMigrationColumnCreate,
|
||||||
|
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||||
|
import {
|
||||||
|
WorkspaceMigrationException,
|
||||||
|
WorkspaceMigrationExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
|
||||||
|
|
||||||
|
export type TsVectorFieldMetadataType = FieldMetadataType.TS_VECTOR;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TsVectorColumnActionFactory extends ColumnActionAbstractFactory<TsVectorFieldMetadataType> {
|
||||||
|
protected readonly logger = new Logger(TsVectorColumnActionFactory.name);
|
||||||
|
|
||||||
|
handleCreateAction(
|
||||||
|
fieldMetadata: FieldMetadataInterface<TsVectorFieldMetadataType>,
|
||||||
|
): WorkspaceMigrationColumnCreate[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
action: WorkspaceMigrationColumnActionType.CREATE,
|
||||||
|
columnName: computeColumnName(fieldMetadata),
|
||||||
|
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
|
||||||
|
isNullable: fieldMetadata.isNullable ?? true,
|
||||||
|
defaultValue: undefined,
|
||||||
|
generatedType: fieldMetadata.generatedType,
|
||||||
|
asExpression: fieldMetadata.asExpression,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleAlterAction(
|
||||||
|
_currentFieldMetadata: FieldMetadataInterface<TsVectorFieldMetadataType>,
|
||||||
|
_alteredFieldMetadata: FieldMetadataInterface<TsVectorFieldMetadataType>,
|
||||||
|
_options?: WorkspaceColumnActionOptions,
|
||||||
|
): WorkspaceMigrationColumnAlter[] {
|
||||||
|
throw new WorkspaceMigrationException(
|
||||||
|
`TsVectorColumnActionFactory.handleAlterAction has not been implemented yet.`,
|
||||||
|
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -38,6 +38,8 @@ export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
|
|||||||
return 'enum';
|
return 'enum';
|
||||||
case FieldMetadataType.RAW_JSON:
|
case FieldMetadataType.RAW_JSON:
|
||||||
return 'jsonb';
|
return 'jsonb';
|
||||||
|
case FieldMetadataType.TS_VECTOR:
|
||||||
|
return 'tsvector';
|
||||||
default:
|
default:
|
||||||
throw new WorkspaceMigrationException(
|
throw new WorkspaceMigrationException(
|
||||||
`Cannot convert ${fieldMetadataType} to column type.`,
|
`Cannot convert ${fieldMetadataType} to column type.`,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
|
|
||||||
export enum WorkspaceMigrationColumnActionType {
|
export enum WorkspaceMigrationColumnActionType {
|
||||||
@ -30,12 +31,15 @@ export interface WorkspaceMigrationColumnDefinition {
|
|||||||
isArray?: boolean;
|
isArray?: boolean;
|
||||||
isNullable: boolean;
|
isNullable: boolean;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
|
generatedType?: 'STORED' | 'VIRTUAL';
|
||||||
|
asExpression?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceMigrationIndexAction {
|
export interface WorkspaceMigrationIndexAction {
|
||||||
action: WorkspaceMigrationIndexActionType;
|
action: WorkspaceMigrationIndexActionType;
|
||||||
name: string;
|
name: string;
|
||||||
columns: string[];
|
columns: string[];
|
||||||
|
type?: IndexType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceMigrationColumnCreate
|
export interface WorkspaceMigrationColumnCreate
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/fi
|
|||||||
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
|
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
|
||||||
import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||||
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
|
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
|
||||||
|
import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory';
|
||||||
import {
|
import {
|
||||||
WorkspaceMigrationColumnAction,
|
WorkspaceMigrationColumnAction,
|
||||||
WorkspaceMigrationColumnActionType,
|
WorkspaceMigrationColumnActionType,
|
||||||
@ -30,6 +31,7 @@ export class WorkspaceMigrationFactory {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly basicColumnActionFactory: BasicColumnActionFactory,
|
private readonly basicColumnActionFactory: BasicColumnActionFactory,
|
||||||
|
private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory,
|
||||||
private readonly enumColumnActionFactory: EnumColumnActionFactory,
|
private readonly enumColumnActionFactory: EnumColumnActionFactory,
|
||||||
private readonly compositeColumnActionFactory: CompositeColumnActionFactory,
|
private readonly compositeColumnActionFactory: CompositeColumnActionFactory,
|
||||||
) {
|
) {
|
||||||
@ -106,6 +108,10 @@ export class WorkspaceMigrationFactory {
|
|||||||
FieldMetadataType.PHONES,
|
FieldMetadataType.PHONES,
|
||||||
{ factory: this.compositeColumnActionFactory },
|
{ factory: this.compositeColumnActionFactory },
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
FieldMetadataType.TS_VECTOR,
|
||||||
|
{ factory: this.tsVectorColumnActionFactory },
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { workspaceColumnActionFactories } from 'src/engine/metadata-modules/workspace-migration/factories/factories';
|
import { workspaceColumnActionFactories } from 'src/engine/metadata-modules/workspace-migration/factories/factories';
|
||||||
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
||||||
|
|
||||||
import { WorkspaceMigrationService } from './workspace-migration.service';
|
|
||||||
import { WorkspaceMigrationEntity } from './workspace-migration.entity';
|
import { WorkspaceMigrationEntity } from './workspace-migration.entity';
|
||||||
|
import { WorkspaceMigrationService } from './workspace-migration.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([WorkspaceMigrationEntity], 'metadata')],
|
imports: [TypeOrmModule.forFeature([WorkspaceMigrationEntity], 'metadata')],
|
||||||
@ -14,6 +14,10 @@ import { WorkspaceMigrationEntity } from './workspace-migration.entity';
|
|||||||
WorkspaceMigrationFactory,
|
WorkspaceMigrationFactory,
|
||||||
WorkspaceMigrationService,
|
WorkspaceMigrationService,
|
||||||
],
|
],
|
||||||
exports: [WorkspaceMigrationFactory, WorkspaceMigrationService],
|
exports: [
|
||||||
|
...workspaceColumnActionFactories,
|
||||||
|
WorkspaceMigrationFactory,
|
||||||
|
WorkspaceMigrationService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class WorkspaceMigrationModule {}
|
export class WorkspaceMigrationModule {}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||||
import {
|
import {
|
||||||
ActorMetadata,
|
ActorMetadata,
|
||||||
FieldActorSource,
|
FieldActorSource,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
|
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
|
||||||
import {
|
import {
|
||||||
RelationMetadataType,
|
RelationMetadataType,
|
||||||
RelationOnDeleteAction,
|
RelationOnDeleteAction,
|
||||||
@ -10,10 +13,12 @@ import {
|
|||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
|
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
|
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
|
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||||
import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity';
|
import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity';
|
||||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
@ -136,4 +141,22 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
timelineActivities: TimelineActivityWorkspaceEntity[];
|
timelineActivities: TimelineActivityWorkspaceEntity[];
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
|
||||||
|
type: FieldMetadataType.TS_VECTOR,
|
||||||
|
label: SEARCH_VECTOR_FIELD.label,
|
||||||
|
description: SEARCH_VECTOR_FIELD.description,
|
||||||
|
generatedType: 'STORED',
|
||||||
|
asExpression: getTsVectorColumnExpressionFromFields([
|
||||||
|
{
|
||||||
|
name: DEFAULT_LABEL_IDENTIFIER_FIELD_NAME,
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
@WorkspaceIsNullable()
|
||||||
|
@WorkspaceIsSystem()
|
||||||
|
@WorkspaceIndex({ indexType: IndexType.GIN })
|
||||||
|
[SEARCH_VECTOR_FIELD.name]: any;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ export interface WorkspaceFieldOptions<
|
|||||||
options?: FieldMetadataOptions<T>;
|
options?: FieldMetadataOptions<T>;
|
||||||
settings?: FieldMetadataSettings<T>;
|
settings?: FieldMetadataSettings<T>;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
generatedType?: 'STORED' | 'VIRTUAL';
|
||||||
|
asExpression?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceField<T extends FieldMetadataType>(
|
export function WorkspaceField<T extends FieldMetadataType>(
|
||||||
@ -76,6 +78,8 @@ export function WorkspaceField<T extends FieldMetadataType>(
|
|||||||
gate,
|
gate,
|
||||||
isDeprecated,
|
isDeprecated,
|
||||||
isActive: options.isActive,
|
isActive: options.isActive,
|
||||||
|
asExpression: options.asExpression,
|
||||||
|
generatedType: options.generatedType,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,36 @@
|
|||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
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';
|
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||||
|
import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util';
|
||||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||||
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
|
|
||||||
export function WorkspaceIndex(): PropertyDecorator;
|
export type WorkspaceIndexMetadata = {
|
||||||
export function WorkspaceIndex(columns: string[]): ClassDecorator;
|
columns?: string[];
|
||||||
|
indexType?: IndexType;
|
||||||
|
};
|
||||||
|
|
||||||
export function WorkspaceIndex(
|
export function WorkspaceIndex(
|
||||||
columns?: string[],
|
metadata?: WorkspaceIndexMetadata,
|
||||||
|
): PropertyDecorator;
|
||||||
|
export function WorkspaceIndex(
|
||||||
|
metadata: WorkspaceIndexMetadata,
|
||||||
|
): ClassDecorator;
|
||||||
|
export function WorkspaceIndex(
|
||||||
|
metadata?: WorkspaceIndexMetadata,
|
||||||
): PropertyDecorator | ClassDecorator {
|
): PropertyDecorator | ClassDecorator {
|
||||||
return (target: any, propertyKey: string | symbol) => {
|
return (target: any, propertyKey: string | symbol) => {
|
||||||
if (propertyKey === undefined && columns === undefined) {
|
if (propertyKey === undefined && metadata === undefined) {
|
||||||
throw new Error('Class level WorkspaceIndex should be used with columns');
|
throw new Error('Class level WorkspaceIndex should be used with columns');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (propertyKey !== undefined && metadata?.columns !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'Property level WorkspaceIndex should not be used with columns',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const gate = TypedReflect.getMetadata(
|
const gate = TypedReflect.getMetadata(
|
||||||
'workspace:gate-metadata-args',
|
'workspace:gate-metadata-args',
|
||||||
target,
|
target,
|
||||||
@ -20,29 +38,46 @@ export function WorkspaceIndex(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// TODO: handle composite field metadata types
|
// TODO: handle composite field metadata types
|
||||||
|
if (isDefined(metadata?.columns)) {
|
||||||
|
const columns = metadata.columns;
|
||||||
|
|
||||||
|
if (columns.length > 0) {
|
||||||
|
metadataArgsStorage.addIndexes({
|
||||||
|
name: `IDX_${generateDeterministicIndexName([
|
||||||
|
convertClassNameToObjectMetadataName(target.name),
|
||||||
|
...columns,
|
||||||
|
])}`,
|
||||||
|
columns,
|
||||||
|
target: target,
|
||||||
|
gate,
|
||||||
|
...(isDefined(metadata?.indexType)
|
||||||
|
? { type: metadata.indexType }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(propertyKey)) {
|
||||||
|
const additionalDefaultColumnsForIndex = getColumnsForIndex(
|
||||||
|
metadata?.indexType,
|
||||||
|
);
|
||||||
|
const columns = [
|
||||||
|
propertyKey.toString(),
|
||||||
|
...additionalDefaultColumnsForIndex,
|
||||||
|
];
|
||||||
|
|
||||||
if (Array.isArray(columns) && columns.length > 0) {
|
|
||||||
metadataArgsStorage.addIndexes({
|
metadataArgsStorage.addIndexes({
|
||||||
name: `IDX_${generateDeterministicIndexName([
|
name: `IDX_${generateDeterministicIndexName([
|
||||||
convertClassNameToObjectMetadataName(target.name),
|
convertClassNameToObjectMetadataName(target.constructor.name),
|
||||||
...columns,
|
...columns,
|
||||||
])}`,
|
])}`,
|
||||||
columns,
|
columns,
|
||||||
target: target,
|
target: target.constructor,
|
||||||
|
...(isDefined(metadata?.indexType) ? { type: metadata.indexType } : {}),
|
||||||
gate,
|
gate,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataArgsStorage.addIndexes({
|
|
||||||
name: `IDX_${generateDeterministicIndexName([
|
|
||||||
convertClassNameToObjectMetadataName(target.constructor.name),
|
|
||||||
...[propertyKey.toString(), 'deletedAt'],
|
|
||||||
])}`,
|
|
||||||
columns: [propertyKey.toString(), 'deletedAt'],
|
|
||||||
target: target.constructor,
|
|
||||||
gate,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,4 +89,14 @@ export interface WorkspaceFieldMetadataArgs {
|
|||||||
* Is active field.
|
* Is active field.
|
||||||
*/
|
*/
|
||||||
readonly isActive?: boolean;
|
readonly isActive?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is active field.
|
||||||
|
*/
|
||||||
|
readonly generatedType?: 'STORED' | 'VIRTUAL';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is active field.
|
||||||
|
*/
|
||||||
|
readonly asExpression?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
||||||
|
|
||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
|
|
||||||
export interface WorkspaceIndexMetadataArgs {
|
export interface WorkspaceIndexMetadataArgs {
|
||||||
/**
|
/**
|
||||||
* Class to which index is applied.
|
* Class to which index is applied.
|
||||||
@ -17,6 +19,11 @@ export interface WorkspaceIndexMetadataArgs {
|
|||||||
*/
|
*/
|
||||||
columns: string[];
|
columns: string[];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Index type. Defaults to Btree.
|
||||||
|
*/
|
||||||
|
type?: IndexType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field gate.
|
* Field gate.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -664,7 +664,7 @@ export class WorkspaceRepository<
|
|||||||
return formatData(data, objectMetadata) as T;
|
return formatData(data, objectMetadata) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async formatResult<T>(
|
async formatResult<T>(
|
||||||
data: T,
|
data: T,
|
||||||
objectMetadata?: ObjectMetadataMapItem,
|
objectMetadata?: ObjectMetadataMapItem,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
|
import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util';
|
||||||
|
|
||||||
|
describe('getColumnsForIndex', () => {
|
||||||
|
it('should return ["deletedAt"] when indexType is undefined', () => {
|
||||||
|
const result = getColumnsForIndex();
|
||||||
|
|
||||||
|
expect(result).toEqual(['deletedAt']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array when indexType is IndexType.GIN', () => {
|
||||||
|
const result = getColumnsForIndex(IndexType.GIN);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ["deletedAt"] when indexType is IndexType.BTREE', () => {
|
||||||
|
const result = getColumnsForIndex(IndexType.BTREE);
|
||||||
|
|
||||||
|
expect(result).toEqual(['deletedAt']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
|
|
||||||
|
export const getColumnsForIndex = (indexType?: IndexType) => {
|
||||||
|
switch (indexType) {
|
||||||
|
case IndexType.GIN:
|
||||||
|
return [];
|
||||||
|
default:
|
||||||
|
return ['deletedAt'];
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -33,6 +33,8 @@ export const getResolverName = (
|
|||||||
return `delete${pascalCase(objectMetadata.namePlural)}`;
|
return `delete${pascalCase(objectMetadata.namePlural)}`;
|
||||||
case 'destroyMany':
|
case 'destroyMany':
|
||||||
return `destroy${pascalCase(objectMetadata.namePlural)}`;
|
return `destroy${pascalCase(objectMetadata.namePlural)}`;
|
||||||
|
case 'search':
|
||||||
|
return `search${pascalCase(objectMetadata.namePlural)}`;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown resolver type: ${type}`);
|
throw new Error(`Unknown resolver type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,15 +2,15 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||||
|
|
||||||
|
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||||
import {
|
import {
|
||||||
WorkspaceMigrationEntity,
|
WorkspaceMigrationEntity,
|
||||||
WorkspaceMigrationIndexActionType,
|
WorkspaceMigrationIndexActionType,
|
||||||
WorkspaceMigrationTableActionType,
|
WorkspaceMigrationTableActionType,
|
||||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
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()
|
@Injectable()
|
||||||
export class WorkspaceMigrationIndexFactory {
|
export class WorkspaceMigrationIndexFactory {
|
||||||
@ -94,6 +94,7 @@ export class WorkspaceMigrationIndexFactory {
|
|||||||
|
|
||||||
return fieldMetadata.name;
|
return fieldMetadata.name;
|
||||||
}),
|
}),
|
||||||
|
type: indexMetadata.indexType,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
workspaceMigrations.push({
|
workspaceMigrations.push({
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace
|
|||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||||
import { WorkspaceMigrationEnumService } from 'src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service';
|
import { WorkspaceMigrationEnumService } from 'src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service';
|
||||||
import { convertOnDeleteActionToOnDelete } from 'src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util';
|
import { convertOnDeleteActionToOnDelete } from 'src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util';
|
||||||
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
|
||||||
import { WorkspaceMigrationTypeService } from './services/workspace-migration-type.service';
|
import { WorkspaceMigrationTypeService } from './services/workspace-migration-type.service';
|
||||||
import { customTableDefaultColumns } from './utils/custom-table-default-column.util';
|
import { customTableDefaultColumns } from './utils/custom-table-default-column.util';
|
||||||
@ -194,13 +195,21 @@ export class WorkspaceMigrationRunnerService {
|
|||||||
for (const index of indexes) {
|
for (const index of indexes) {
|
||||||
switch (index.action) {
|
switch (index.action) {
|
||||||
case WorkspaceMigrationIndexActionType.CREATE:
|
case WorkspaceMigrationIndexActionType.CREATE:
|
||||||
await queryRunner.createIndex(
|
if (isDefined(index.type)) {
|
||||||
`${schemaName}.${tableName}`,
|
const quotedColumns = index.columns.map((column) => `"${column}"`);
|
||||||
new TableIndex({
|
|
||||||
name: index.name,
|
await queryRunner.query(`
|
||||||
columnNames: index.columns,
|
CREATE INDEX "${index.name}" ON "${schemaName}"."${tableName}" USING ${index.type} (${quotedColumns.join(', ')})
|
||||||
}),
|
`);
|
||||||
);
|
} else {
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
`${schemaName}.${tableName}`,
|
||||||
|
new TableIndex({
|
||||||
|
name: index.name,
|
||||||
|
columnNames: index.columns,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case WorkspaceMigrationIndexActionType.DROP:
|
case WorkspaceMigrationIndexActionType.DROP:
|
||||||
try {
|
try {
|
||||||
@ -380,6 +389,8 @@ export class WorkspaceMigrationRunnerService {
|
|||||||
enumName: enumName,
|
enumName: enumName,
|
||||||
isArray: migrationColumn.isArray,
|
isArray: migrationColumn.isArray,
|
||||||
isNullable: migrationColumn.isNullable,
|
isNullable: migrationColumn.isNullable,
|
||||||
|
asExpression: migrationColumn.asExpression,
|
||||||
|
generatedType: migrationColumn.generatedType,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ const commonFieldPropertiesToIgnore = [
|
|||||||
'settings',
|
'settings',
|
||||||
'joinColumn',
|
'joinColumn',
|
||||||
'gate',
|
'gate',
|
||||||
|
'asExpression',
|
||||||
|
'generatedType',
|
||||||
];
|
];
|
||||||
|
|
||||||
const fieldPropertiesToStringify = ['defaultValue'] as const;
|
const fieldPropertiesToStringify = ['defaultValue'] as const;
|
||||||
|
|||||||
@ -135,6 +135,7 @@ export const COMPANY_STANDARD_FIELD_IDS = {
|
|||||||
favorites: '20202020-4d1d-41ac-b13b-621631298d55',
|
favorites: '20202020-4d1d-41ac-b13b-621631298d55',
|
||||||
attachments: '20202020-c1b5-4120-b0f0-987ca401ed53',
|
attachments: '20202020-c1b5-4120-b0f0-987ca401ed53',
|
||||||
timelineActivities: '20202020-0414-4daf-9c0d-64fe7b27f89f',
|
timelineActivities: '20202020-0414-4daf-9c0d-64fe7b27f89f',
|
||||||
|
searchVector: '85c71601-72f9-4b7b-b343-d46100b2c74d',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CONNECTED_ACCOUNT_STANDARD_FIELD_IDS = {
|
export const CONNECTED_ACCOUNT_STANDARD_FIELD_IDS = {
|
||||||
@ -300,6 +301,7 @@ export const OPPORTUNITY_STANDARD_FIELD_IDS = {
|
|||||||
noteTargets: '20202020-dd3f-42d5-a382-db58aabf43d3',
|
noteTargets: '20202020-dd3f-42d5-a382-db58aabf43d3',
|
||||||
attachments: '20202020-87c7-4118-83d6-2f4031005209',
|
attachments: '20202020-87c7-4118-83d6-2f4031005209',
|
||||||
timelineActivities: '20202020-30e2-421f-96c7-19c69d1cf631',
|
timelineActivities: '20202020-30e2-421f-96c7-19c69d1cf631',
|
||||||
|
searchVector: '428a0da5-4b2e-4ce3-b695-89a8b384e6e3',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PERSON_STANDARD_FIELD_IDS = {
|
export const PERSON_STANDARD_FIELD_IDS = {
|
||||||
@ -325,6 +327,7 @@ export const PERSON_STANDARD_FIELD_IDS = {
|
|||||||
messageParticipants: '20202020-498e-4c61-8158-fa04f0638334',
|
messageParticipants: '20202020-498e-4c61-8158-fa04f0638334',
|
||||||
calendarEventParticipants: '20202020-52ee-45e9-a702-b64b3753e3a9',
|
calendarEventParticipants: '20202020-52ee-45e9-a702-b64b3753e3a9',
|
||||||
timelineActivities: '20202020-a43e-4873-9c23-e522de906ce5',
|
timelineActivities: '20202020-a43e-4873-9c23-e522de906ce5',
|
||||||
|
searchVector: '57d1d7ad-fa10-44fc-82f3-ad0959ec2534',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TASK_STANDARD_FIELD_IDS = {
|
export const TASK_STANDARD_FIELD_IDS = {
|
||||||
@ -463,4 +466,5 @@ export const CUSTOM_OBJECT_STANDARD_FIELD_IDS = {
|
|||||||
favorites: '20202020-a4a7-4686-b296-1c6c3482ee21',
|
favorites: '20202020-a4a7-4686-b296-1c6c3482ee21',
|
||||||
attachments: '20202020-8d59-46ca-b7b2-73d167712134',
|
attachments: '20202020-8d59-46ca-b7b2-73d167712134',
|
||||||
timelineActivities: '20202020-f1ef-4ba4-8f33-1a4577afa477',
|
timelineActivities: '20202020-f1ef-4ba4-8f33-1a4577afa477',
|
||||||
|
searchVector: '70e56537-18ef-4811-b1c7-0a444006b815',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -166,6 +166,8 @@ export class StandardFieldFactory {
|
|||||||
isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false,
|
isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false,
|
||||||
isSystem: workspaceFieldMetadataArgs.isSystem ?? false,
|
isSystem: workspaceFieldMetadataArgs.isSystem ?? false,
|
||||||
isActive: workspaceFieldMetadataArgs.isActive ?? true,
|
isActive: workspaceFieldMetadataArgs.isActive ?? true,
|
||||||
|
asExpression: workspaceFieldMetadataArgs.asExpression,
|
||||||
|
generatedType: workspaceFieldMetadataArgs.generatedType,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,12 @@ import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-syn
|
|||||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||||
|
|
||||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
|
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
|
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||||
|
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||||
import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
|
import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -15,23 +18,37 @@ export class StandardIndexFactory {
|
|||||||
create(
|
create(
|
||||||
standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[],
|
standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[],
|
||||||
context: WorkspaceSyncContext,
|
context: WorkspaceSyncContext,
|
||||||
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
originalStandardObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||||
|
originalCustomObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||||
workspaceFeatureFlagsMap: FeatureFlagMap,
|
workspaceFeatureFlagsMap: FeatureFlagMap,
|
||||||
): Partial<IndexMetadataEntity>[] {
|
): Partial<IndexMetadataEntity>[] {
|
||||||
return standardObjectMetadataDefinitions.flatMap((standardObjectMetadata) =>
|
const standardIndexOnStandardObjects =
|
||||||
this.createIndexMetadata(
|
standardObjectMetadataDefinitions.flatMap((standardObjectMetadata) =>
|
||||||
standardObjectMetadata,
|
this.createStandardIndexMetadataForStandardObject(
|
||||||
|
standardObjectMetadata,
|
||||||
|
context,
|
||||||
|
originalStandardObjectMetadataMap,
|
||||||
|
workspaceFeatureFlagsMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const standardIndexesOnCustomObjects =
|
||||||
|
this.createStandardIndexMetadataForCustomObject(
|
||||||
context,
|
context,
|
||||||
originalObjectMetadataMap,
|
originalCustomObjectMetadataMap,
|
||||||
workspaceFeatureFlagsMap,
|
workspaceFeatureFlagsMap,
|
||||||
),
|
);
|
||||||
);
|
|
||||||
|
return [
|
||||||
|
standardIndexOnStandardObjects,
|
||||||
|
standardIndexesOnCustomObjects,
|
||||||
|
].flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
private createIndexMetadata(
|
private createStandardIndexMetadataForStandardObject(
|
||||||
target: typeof BaseWorkspaceEntity,
|
target: typeof BaseWorkspaceEntity,
|
||||||
context: WorkspaceSyncContext,
|
context: WorkspaceSyncContext,
|
||||||
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
originalStandardObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||||
workspaceFeatureFlagsMap: FeatureFlagMap,
|
workspaceFeatureFlagsMap: FeatureFlagMap,
|
||||||
): Partial<IndexMetadataEntity>[] {
|
): Partial<IndexMetadataEntity>[] {
|
||||||
const workspaceEntity = metadataArgsStorage.filterEntities(target);
|
const workspaceEntity = metadataArgsStorage.filterEntities(target);
|
||||||
@ -58,7 +75,7 @@ export class StandardIndexFactory {
|
|||||||
return workspaceIndexMetadataArgsCollection.map(
|
return workspaceIndexMetadataArgsCollection.map(
|
||||||
(workspaceIndexMetadataArgs) => {
|
(workspaceIndexMetadataArgs) => {
|
||||||
const objectMetadata =
|
const objectMetadata =
|
||||||
originalObjectMetadataMap[workspaceEntity.nameSingular];
|
originalStandardObjectMetadataMap[workspaceEntity.nameSingular];
|
||||||
|
|
||||||
if (!objectMetadata) {
|
if (!objectMetadata) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -71,10 +88,55 @@ export class StandardIndexFactory {
|
|||||||
objectMetadataId: objectMetadata.id,
|
objectMetadataId: objectMetadata.id,
|
||||||
name: workspaceIndexMetadataArgs.name,
|
name: workspaceIndexMetadataArgs.name,
|
||||||
columns: workspaceIndexMetadataArgs.columns,
|
columns: workspaceIndexMetadataArgs.columns,
|
||||||
|
isCustom: false,
|
||||||
|
indexType: workspaceIndexMetadataArgs.type,
|
||||||
};
|
};
|
||||||
|
|
||||||
return indexMetadata;
|
return indexMetadata;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createStandardIndexMetadataForCustomObject(
|
||||||
|
context: WorkspaceSyncContext,
|
||||||
|
originalCustomObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||||
|
workspaceFeatureFlagsMap: FeatureFlagMap,
|
||||||
|
): Partial<IndexMetadataEntity>[] {
|
||||||
|
const target = CustomWorkspaceEntity;
|
||||||
|
const workspaceEntity = metadataArgsStorage.filterExtendedEntities(target);
|
||||||
|
|
||||||
|
if (!workspaceEntity) {
|
||||||
|
throw new Error(
|
||||||
|
`Object metadata decorator not found, can't parse ${target.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceIndexMetadataArgsCollection = metadataArgsStorage
|
||||||
|
.filterIndexes(target)
|
||||||
|
.filter((workspaceIndexMetadataArgs) => {
|
||||||
|
return !isGatedAndNotEnabled(
|
||||||
|
workspaceIndexMetadataArgs.gate,
|
||||||
|
workspaceFeatureFlagsMap,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.entries(originalCustomObjectMetadataMap).flatMap(
|
||||||
|
([customObjectName, customObjectMetadata]) => {
|
||||||
|
return workspaceIndexMetadataArgsCollection.map(
|
||||||
|
(workspaceIndexMetadataArgs) => {
|
||||||
|
const indexMetadata: PartialIndexMetadata = {
|
||||||
|
workspaceId: context.workspaceId,
|
||||||
|
objectMetadataId: customObjectMetadata.id,
|
||||||
|
name: `IDX_${generateDeterministicIndexName([computeTableName(customObjectName, true), ...workspaceIndexMetadataArgs.columns])}`,
|
||||||
|
columns: workspaceIndexMetadataArgs.columns,
|
||||||
|
isCustom: false,
|
||||||
|
indexType: workspaceIndexMetadataArgs.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
return indexMetadata;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export type PartialFieldMetadata = Omit<
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
objectMetadataId?: string;
|
objectMetadataId?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
asExpression?: string;
|
||||||
|
generatedType?: 'STORED' | 'VIRTUAL';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PartialComputedFieldMetadata = {
|
export type PartialComputedFieldMetadata = {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||||
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||||
@ -143,13 +144,27 @@ export class WorkspaceSyncFieldMetadataService {
|
|||||||
] of standardObjectStandardFieldMetadataMap) {
|
] of standardObjectStandardFieldMetadataMap) {
|
||||||
const originalObjectMetadata =
|
const originalObjectMetadata =
|
||||||
originalObjectMetadataMap[standardObjectId];
|
originalObjectMetadataMap[standardObjectId];
|
||||||
const computedStandardFieldMetadataCollection = computeStandardFields(
|
|
||||||
|
let computedStandardFieldMetadataCollection = computeStandardFields(
|
||||||
standardFieldMetadataCollection,
|
standardFieldMetadataCollection,
|
||||||
originalObjectMetadata,
|
originalObjectMetadata,
|
||||||
// We need to provide this for generated relations with custom objects
|
// We need to provide this for generated relations with custom objects
|
||||||
customObjectMetadataCollection,
|
customObjectMetadataCollection,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let originalObjectMetadataFields = originalObjectMetadata.fields;
|
||||||
|
|
||||||
|
if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) {
|
||||||
|
computedStandardFieldMetadataCollection =
|
||||||
|
computedStandardFieldMetadataCollection.filter(
|
||||||
|
(field) => field.type !== FieldMetadataType.TS_VECTOR,
|
||||||
|
);
|
||||||
|
|
||||||
|
originalObjectMetadataFields = originalObjectMetadataFields.filter(
|
||||||
|
(field) => field.type !== FieldMetadataType.TS_VECTOR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const fieldComparatorResults = this.workspaceFieldComparator.compare(
|
const fieldComparatorResults = this.workspaceFieldComparator.compare(
|
||||||
originalObjectMetadata.id,
|
originalObjectMetadata.id,
|
||||||
originalObjectMetadata.fields,
|
originalObjectMetadata.fields,
|
||||||
@ -177,11 +192,24 @@ export class WorkspaceSyncFieldMetadataService {
|
|||||||
// Loop over all custom objects from the DB and compare their fields with standard fields
|
// Loop over all custom objects from the DB and compare their fields with standard fields
|
||||||
for (const customObjectMetadata of customObjectMetadataCollection) {
|
for (const customObjectMetadata of customObjectMetadataCollection) {
|
||||||
// Also, maybe it's better to refactor a bit and move generation part into a separate module ?
|
// Also, maybe it's better to refactor a bit and move generation part into a separate module ?
|
||||||
const standardFieldMetadataCollection = computeStandardFields(
|
let standardFieldMetadataCollection = computeStandardFields(
|
||||||
customObjectStandardFieldMetadataCollection,
|
customObjectStandardFieldMetadataCollection,
|
||||||
customObjectMetadata,
|
customObjectMetadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let customObjectMetadataFields = customObjectMetadata.fields;
|
||||||
|
|
||||||
|
if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) {
|
||||||
|
standardFieldMetadataCollection =
|
||||||
|
standardFieldMetadataCollection.filter(
|
||||||
|
(field) => field.type !== FieldMetadataType.TS_VECTOR,
|
||||||
|
);
|
||||||
|
|
||||||
|
customObjectMetadataFields = customObjectMetadataFields.filter(
|
||||||
|
(field) => field.type !== FieldMetadataType.TS_VECTOR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* COMPARE FIELD METADATA
|
* COMPARE FIELD METADATA
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,22 +1,25 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { EntityManager } from 'typeorm';
|
import { Any, EntityManager } from 'typeorm';
|
||||||
|
|
||||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
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 { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||||
|
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||||
|
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||||
|
|
||||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
import {
|
||||||
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
|
IndexMetadataEntity,
|
||||||
import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory';
|
IndexType,
|
||||||
|
} from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
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 { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||||
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';
|
import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory';
|
||||||
|
import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator';
|
||||||
|
import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory';
|
||||||
|
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
|
||||||
|
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
||||||
|
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
|
||||||
|
import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceSyncIndexMetadataService {
|
export class WorkspaceSyncIndexMetadataService {
|
||||||
@ -47,35 +50,60 @@ export class WorkspaceSyncIndexMetadataService {
|
|||||||
workspaceId: context.workspaceId,
|
workspaceId: context.workspaceId,
|
||||||
// We're only interested in standard fields
|
// We're only interested in standard fields
|
||||||
fields: { isCustom: false },
|
fields: { isCustom: false },
|
||||||
isCustom: false,
|
|
||||||
},
|
},
|
||||||
relations: ['dataSource', 'fields', 'indexes'],
|
relations: ['dataSource', 'fields', 'indexes'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create map of object metadata & field metadata by unique identifier
|
// Create map of object metadata & field metadata by unique identifier
|
||||||
const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier(
|
const originalStandardObjectMetadataMap =
|
||||||
originalObjectMetadataCollection,
|
mapObjectMetadataByUniqueIdentifier(
|
||||||
// Relation are based on the singular name
|
originalObjectMetadataCollection.filter(
|
||||||
|
(objectMetadata) => !objectMetadata.isCustom,
|
||||||
|
),
|
||||||
|
// Relation are based on the singular name
|
||||||
|
(objectMetadata) => objectMetadata.nameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
const originalCustomObjectMetadataMap = mapObjectMetadataByUniqueIdentifier(
|
||||||
|
originalObjectMetadataCollection.filter(
|
||||||
|
(objectMetadata) => objectMetadata.isCustom,
|
||||||
|
),
|
||||||
(objectMetadata) => objectMetadata.nameSingular,
|
(objectMetadata) => objectMetadata.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
const indexMetadataRepository = manager.getRepository(IndexMetadataEntity);
|
const indexMetadataRepository = manager.getRepository(IndexMetadataEntity);
|
||||||
|
|
||||||
const originalIndexMetadataCollection = await indexMetadataRepository.find({
|
let originalIndexMetadataCollection = await indexMetadataRepository.find({
|
||||||
where: {
|
where: {
|
||||||
workspaceId: context.workspaceId,
|
workspaceId: context.workspaceId,
|
||||||
|
objectMetadataId: Any(
|
||||||
|
Object.values(originalObjectMetadataCollection).map(
|
||||||
|
(object) => object.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isCustom: false,
|
||||||
},
|
},
|
||||||
relations: ['indexFieldMetadatas.fieldMetadata'],
|
relations: ['indexFieldMetadatas.fieldMetadata'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate index metadata from models
|
// Generate index metadata from models
|
||||||
const standardIndexMetadataCollection = this.standardIndexFactory.create(
|
let standardIndexMetadataCollection = this.standardIndexFactory.create(
|
||||||
standardObjectMetadataDefinitions,
|
standardObjectMetadataDefinitions,
|
||||||
context,
|
context,
|
||||||
originalObjectMetadataMap,
|
originalStandardObjectMetadataMap,
|
||||||
|
originalCustomObjectMetadataMap,
|
||||||
workspaceFeatureFlagsMap,
|
workspaceFeatureFlagsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) {
|
||||||
|
originalIndexMetadataCollection = originalIndexMetadataCollection.filter(
|
||||||
|
(index) => index.indexType !== IndexType.GIN,
|
||||||
|
);
|
||||||
|
|
||||||
|
standardIndexMetadataCollection = standardIndexMetadataCollection.filter(
|
||||||
|
(index) => index.indexType !== IndexType.GIN,
|
||||||
|
);
|
||||||
|
}
|
||||||
const indexComparatorResults = this.workspaceIndexComparator.compare(
|
const indexComparatorResults = this.workspaceIndexComparator.compare(
|
||||||
originalIndexMetadataCollection,
|
originalIndexMetadataCollection,
|
||||||
standardIndexMetadataCollection,
|
standardIndexMetadataCollection,
|
||||||
|
|||||||
@ -54,8 +54,6 @@ export const standardObjectMetadataDefinitions = [
|
|||||||
CompanyWorkspaceEntity,
|
CompanyWorkspaceEntity,
|
||||||
ConnectedAccountWorkspaceEntity,
|
ConnectedAccountWorkspaceEntity,
|
||||||
FavoriteWorkspaceEntity,
|
FavoriteWorkspaceEntity,
|
||||||
OpportunityWorkspaceEntity,
|
|
||||||
PersonWorkspaceEntity,
|
|
||||||
TimelineActivityWorkspaceEntity,
|
TimelineActivityWorkspaceEntity,
|
||||||
ViewFieldWorkspaceEntity,
|
ViewFieldWorkspaceEntity,
|
||||||
ViewFilterWorkspaceEntity,
|
ViewFilterWorkspaceEntity,
|
||||||
@ -79,10 +77,4 @@ export const standardObjectMetadataDefinitions = [
|
|||||||
PersonWorkspaceEntity,
|
PersonWorkspaceEntity,
|
||||||
TaskWorkspaceEntity,
|
TaskWorkspaceEntity,
|
||||||
TaskTargetWorkspaceEntity,
|
TaskTargetWorkspaceEntity,
|
||||||
TimelineActivityWorkspaceEntity,
|
|
||||||
ViewFieldWorkspaceEntity,
|
|
||||||
ViewFilterWorkspaceEntity,
|
|
||||||
ViewSortWorkspaceEntity,
|
|
||||||
ViewWorkspaceEntity,
|
|
||||||
WebhookWorkspaceEntity,
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +1,103 @@
|
|||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||||
|
|
||||||
|
const nameTextField = { name: 'name', type: FieldMetadataType.TEXT };
|
||||||
|
const nameFullNameField = {
|
||||||
|
name: 'name',
|
||||||
|
type: FieldMetadataType.FULL_NAME,
|
||||||
|
};
|
||||||
|
const jobTitleTextField = { name: 'jobTitle', type: FieldMetadataType.TEXT };
|
||||||
|
const emailsEmailsField = { name: 'emails', type: FieldMetadataType.EMAILS };
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util',
|
||||||
|
() => ({
|
||||||
|
computeColumnName: jest.fn((name) => {
|
||||||
|
if (name === 'name') {
|
||||||
|
return 'name';
|
||||||
|
}
|
||||||
|
if (name === 'jobTitle') {
|
||||||
|
return 'jobTitle';
|
||||||
|
}
|
||||||
|
if (name === 'emailsPrimaryEmail') {
|
||||||
|
return 'emailsPrimaryEmail';
|
||||||
|
}
|
||||||
|
if (name === 'emailsAdditionalEmails') {
|
||||||
|
return 'emailsAdditionalEmails';
|
||||||
|
}
|
||||||
|
if (name === 'nameFirstName') {
|
||||||
|
return 'nameFirstName';
|
||||||
|
}
|
||||||
|
if (name === 'nameLastName') {
|
||||||
|
return 'nameLastName';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
computeCompositeColumnName: jest.fn((field, property) => {
|
||||||
|
if (
|
||||||
|
field.name === emailsEmailsField.name &&
|
||||||
|
property.name === 'primaryEmail'
|
||||||
|
) {
|
||||||
|
return 'emailsPrimaryEmail';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
field.name === emailsEmailsField.name &&
|
||||||
|
property.name === 'additionalEmails'
|
||||||
|
) {
|
||||||
|
return 'emailsAdditionalEmails';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
field.name === nameFullNameField.name &&
|
||||||
|
property.name === 'firstName'
|
||||||
|
) {
|
||||||
|
return 'nameFirstName';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
field.name === nameFullNameField.name &&
|
||||||
|
property.name === 'lastName'
|
||||||
|
) {
|
||||||
|
return 'nameLastName';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('getTsVectorColumnExpressionFromFields', () => {
|
||||||
|
it('should generate correct expression for simple text field', () => {
|
||||||
|
const fields = [nameTextField];
|
||||||
|
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||||
|
|
||||||
|
expect(result).toContain("to_tsvector('simple', COALESCE(\"name\", ''))");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple fields', () => {
|
||||||
|
const fields = [nameFullNameField, jobTitleTextField, emailsEmailsField];
|
||||||
|
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||||
|
const expected = `
|
||||||
|
CASE
|
||||||
|
WHEN "deletedAt" IS NULL THEN
|
||||||
|
to_tsvector('simple', COALESCE("nameFirstName", '') || ' ' || COALESCE("nameLastName", '') || ' ' || COALESCE("jobTitle", '') || ' ' ||
|
||||||
|
COALESCE(
|
||||||
|
replace(
|
||||||
|
"emailsPrimaryEmail",
|
||||||
|
'@',
|
||||||
|
' '
|
||||||
|
),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
expect(result.trim()).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include CASE statement for handling deletedAt', () => {
|
||||||
|
const fields = [nameTextField];
|
||||||
|
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||||
|
|
||||||
|
expect(result).toContain('CASE');
|
||||||
|
expect(result).toContain('WHEN "deletedAt" IS NULL THEN');
|
||||||
|
expect(result).toContain('ELSE NULL');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import {
|
||||||
|
computeColumnName,
|
||||||
|
computeCompositeColumnName,
|
||||||
|
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||||
|
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||||
|
import {
|
||||||
|
WorkspaceMigrationException,
|
||||||
|
WorkspaceMigrationExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
|
||||||
|
|
||||||
|
type FieldTypeAndNameMetadata = {
|
||||||
|
name: string;
|
||||||
|
type: FieldMetadataType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTsVectorColumnExpressionFromFields = (
|
||||||
|
fieldsUsedForSearch: FieldTypeAndNameMetadata[],
|
||||||
|
): string => {
|
||||||
|
const columnExpressions = fieldsUsedForSearch.flatMap(
|
||||||
|
getColumnExpressionsFromField,
|
||||||
|
);
|
||||||
|
const concatenatedExpression = columnExpressions.join(" || ' ' || ");
|
||||||
|
|
||||||
|
const tsVectorExpression = `to_tsvector('simple', ${concatenatedExpression})`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
CASE
|
||||||
|
WHEN "deletedAt" IS NULL THEN
|
||||||
|
${tsVectorExpression}
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColumnExpressionsFromField = (
|
||||||
|
fieldMetadataTypeAndName: FieldTypeAndNameMetadata,
|
||||||
|
): string[] => {
|
||||||
|
if (isCompositeFieldMetadataType(fieldMetadataTypeAndName.type)) {
|
||||||
|
const compositeType = compositeTypeDefinitions.get(
|
||||||
|
fieldMetadataTypeAndName.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!compositeType) {
|
||||||
|
throw new WorkspaceMigrationException(
|
||||||
|
`Composite type not found for field metadata type: ${fieldMetadataTypeAndName.type}`,
|
||||||
|
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return compositeType.properties
|
||||||
|
.filter((property) => property.type === FieldMetadataType.TEXT)
|
||||||
|
.map((property) => {
|
||||||
|
const columnName = computeCompositeColumnName(
|
||||||
|
fieldMetadataTypeAndName,
|
||||||
|
property,
|
||||||
|
);
|
||||||
|
|
||||||
|
return getColumnExpression(columnName, fieldMetadataTypeAndName.type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const columnName = computeColumnName(fieldMetadataTypeAndName.name);
|
||||||
|
|
||||||
|
return [getColumnExpression(columnName, fieldMetadataTypeAndName.type)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColumnExpression = (
|
||||||
|
columnName: string,
|
||||||
|
fieldType: FieldMetadataType,
|
||||||
|
): string => {
|
||||||
|
const quotedColumnName = `"${columnName}"`;
|
||||||
|
|
||||||
|
if (fieldType === FieldMetadataType.EMAILS) {
|
||||||
|
return `
|
||||||
|
COALESCE(
|
||||||
|
replace(
|
||||||
|
${quotedColumnName},
|
||||||
|
'@',
|
||||||
|
' '
|
||||||
|
),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return `COALESCE(${quotedColumnName}, '')`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
|
|
||||||
|
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||||
import {
|
import {
|
||||||
ActorMetadata,
|
ActorMetadata,
|
||||||
FieldActorSource,
|
FieldActorSource,
|
||||||
@ -8,6 +9,7 @@ import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/comp
|
|||||||
import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||||
import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
import {
|
import {
|
||||||
RelationMetadataType,
|
RelationMetadataType,
|
||||||
RelationOnDeleteAction,
|
RelationOnDeleteAction,
|
||||||
@ -15,6 +17,7 @@ import {
|
|||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
|
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||||
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
|
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
@ -22,6 +25,7 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-
|
|||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
import { COMPANY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { COMPANY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
|
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||||
import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity';
|
import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity';
|
||||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
@ -32,6 +36,9 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
|
|||||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
|
const NAME_FIELD_NAME = 'name';
|
||||||
|
const DOMAIN_NAME_FIELD_NAME = 'domainName';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.company,
|
standardId: STANDARD_OBJECT_IDS.company,
|
||||||
namePlural: 'companies',
|
namePlural: 'companies',
|
||||||
@ -49,7 +56,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
description: 'The company name',
|
description: 'The company name',
|
||||||
icon: 'IconBuildingSkyscraper',
|
icon: 'IconBuildingSkyscraper',
|
||||||
})
|
})
|
||||||
name: string;
|
[NAME_FIELD_NAME]: string;
|
||||||
|
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: COMPANY_STANDARD_FIELD_IDS.domainName,
|
standardId: COMPANY_STANDARD_FIELD_IDS.domainName,
|
||||||
@ -59,7 +66,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
'The company website URL. We use this url to fetch the company icon',
|
'The company website URL. We use this url to fetch the company icon',
|
||||||
icon: 'IconLink',
|
icon: 'IconLink',
|
||||||
})
|
})
|
||||||
domainName?: LinksMetadata;
|
[DOMAIN_NAME_FIELD_NAME]?: LinksMetadata;
|
||||||
|
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: COMPANY_STANDARD_FIELD_IDS.employees,
|
standardId: COMPANY_STANDARD_FIELD_IDS.employees,
|
||||||
@ -273,4 +280,21 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
@WorkspaceIsDeprecated()
|
@WorkspaceIsDeprecated()
|
||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
addressOld: string;
|
addressOld: string;
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: COMPANY_STANDARD_FIELD_IDS.searchVector,
|
||||||
|
type: FieldMetadataType.TS_VECTOR,
|
||||||
|
label: SEARCH_VECTOR_FIELD.label,
|
||||||
|
description: SEARCH_VECTOR_FIELD.description,
|
||||||
|
icon: 'IconUser',
|
||||||
|
generatedType: 'STORED',
|
||||||
|
asExpression: getTsVectorColumnExpressionFromFields([
|
||||||
|
{ name: NAME_FIELD_NAME, type: FieldMetadataType.TEXT },
|
||||||
|
{ name: DOMAIN_NAME_FIELD_NAME, type: FieldMetadataType.LINKS },
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
@WorkspaceIsNullable()
|
||||||
|
@WorkspaceIsSystem()
|
||||||
|
@WorkspaceIndex({ indexType: IndexType.GIN })
|
||||||
|
[SEARCH_VECTOR_FIELD.name]: any;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
|
|
||||||
|
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||||
import {
|
import {
|
||||||
ActorMetadata,
|
ActorMetadata,
|
||||||
FieldActorSource,
|
FieldActorSource,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||||
import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
import {
|
import {
|
||||||
RelationMetadataType,
|
RelationMetadataType,
|
||||||
RelationOnDeleteAction,
|
RelationOnDeleteAction,
|
||||||
@ -22,6 +24,7 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-
|
|||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
import { OPPORTUNITY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { OPPORTUNITY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
|
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||||
import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity';
|
import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity';
|
||||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||||
@ -31,6 +34,8 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
|
|||||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
|
|
||||||
|
const NAME_FIELD_NAME = 'name';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.opportunity,
|
standardId: STANDARD_OBJECT_IDS.opportunity,
|
||||||
namePlural: 'opportunities',
|
namePlural: 'opportunities',
|
||||||
@ -232,4 +237,20 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
})
|
})
|
||||||
@WorkspaceIsDeprecated()
|
@WorkspaceIsDeprecated()
|
||||||
probability: string;
|
probability: string;
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: OPPORTUNITY_STANDARD_FIELD_IDS.searchVector,
|
||||||
|
type: FieldMetadataType.TS_VECTOR,
|
||||||
|
label: SEARCH_VECTOR_FIELD.label,
|
||||||
|
description: SEARCH_VECTOR_FIELD.description,
|
||||||
|
icon: 'IconUser',
|
||||||
|
generatedType: 'STORED',
|
||||||
|
asExpression: getTsVectorColumnExpressionFromFields([
|
||||||
|
{ name: NAME_FIELD_NAME, type: FieldMetadataType.TEXT },
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
@WorkspaceIsNullable()
|
||||||
|
@WorkspaceIsSystem()
|
||||||
|
@WorkspaceIndex({ indexType: IndexType.GIN })
|
||||||
|
[SEARCH_VECTOR_FIELD.name]: any;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
|
|
||||||
|
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||||
import {
|
import {
|
||||||
ActorMetadata,
|
ActorMetadata,
|
||||||
FieldActorSource,
|
FieldActorSource,
|
||||||
@ -9,6 +10,7 @@ import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/com
|
|||||||
import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||||
import { PhonesMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
|
import { PhonesMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
import {
|
import {
|
||||||
RelationMetadataType,
|
RelationMetadataType,
|
||||||
RelationOnDeleteAction,
|
RelationOnDeleteAction,
|
||||||
@ -16,6 +18,7 @@ import {
|
|||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
|
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||||
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
|
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
@ -23,6 +26,7 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-
|
|||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
|
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||||
import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity';
|
import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity';
|
||||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||||
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||||
@ -34,6 +38,10 @@ import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-obj
|
|||||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
|
|
||||||
|
const NAME_FIELD_NAME = 'name';
|
||||||
|
const EMAILS_FIELD_NAME = 'emails';
|
||||||
|
const JOB_TITLE_FIELD_NAME = 'jobTitle';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.person,
|
standardId: STANDARD_OBJECT_IDS.person,
|
||||||
namePlural: 'people',
|
namePlural: 'people',
|
||||||
@ -53,7 +61,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
icon: 'IconUser',
|
icon: 'IconUser',
|
||||||
})
|
})
|
||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
name: FullNameMetadata | null;
|
[NAME_FIELD_NAME]: FullNameMetadata | null;
|
||||||
|
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: PERSON_STANDARD_FIELD_IDS.email,
|
standardId: PERSON_STANDARD_FIELD_IDS.email,
|
||||||
@ -72,7 +80,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
description: 'Contact’s Emails',
|
description: 'Contact’s Emails',
|
||||||
icon: 'IconMail',
|
icon: 'IconMail',
|
||||||
})
|
})
|
||||||
emails: EmailsMetadata;
|
[EMAILS_FIELD_NAME]: EmailsMetadata;
|
||||||
|
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: PERSON_STANDARD_FIELD_IDS.linkedinLink,
|
standardId: PERSON_STANDARD_FIELD_IDS.linkedinLink,
|
||||||
@ -101,7 +109,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
description: 'Contact’s job title',
|
description: 'Contact’s job title',
|
||||||
icon: 'IconBriefcase',
|
icon: 'IconBriefcase',
|
||||||
})
|
})
|
||||||
jobTitle: string;
|
[JOB_TITLE_FIELD_NAME]: string;
|
||||||
|
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: PERSON_STANDARD_FIELD_IDS.phone,
|
standardId: PERSON_STANDARD_FIELD_IDS.phone,
|
||||||
@ -290,4 +298,22 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>;
|
timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>;
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: PERSON_STANDARD_FIELD_IDS.searchVector,
|
||||||
|
type: FieldMetadataType.TS_VECTOR,
|
||||||
|
label: SEARCH_VECTOR_FIELD.label,
|
||||||
|
description: SEARCH_VECTOR_FIELD.description,
|
||||||
|
icon: 'IconUser',
|
||||||
|
generatedType: 'STORED',
|
||||||
|
asExpression: getTsVectorColumnExpressionFromFields([
|
||||||
|
{ name: NAME_FIELD_NAME, type: FieldMetadataType.FULL_NAME },
|
||||||
|
{ name: EMAILS_FIELD_NAME, type: FieldMetadataType.EMAILS },
|
||||||
|
{ name: JOB_TITLE_FIELD_NAME, type: FieldMetadataType.TEXT },
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
@WorkspaceIsNullable()
|
||||||
|
@WorkspaceIsSystem()
|
||||||
|
@WorkspaceIndex({ indexType: IndexType.GIN })
|
||||||
|
[SEARCH_VECTOR_FIELD.name]: any;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user