Deprecate asExpression from field metadata search_vector (#22287)

## Summary

Fully deprecates the cached `asExpression` / `generatedType` settings on
`TS_VECTOR` (searchVector) fields. Previously the generated-column
expression was stored in `FieldMetadataSettings` and kept in sync via
imperative recompute side-effects. It is now **derived at DDL time**
from the `searchFieldMetadata` rows that describe which fields feed the
search vector, making `searchFieldMetadata` the single source of truth
and removing a whole class of cache-drift bugs.

This is delivered across the milestones tracked in #2587 and coordinates
with the frontend migration (#1428).

## Why

- The searchVector expression lived in two places (stored
`settings.asExpression` + the actual generated column), kept consistent
by bespoke side-effects (`recompute-search-vector-on-field-rename`,
label-identifier recompute, etc.).
- The frontend reconstructed the searchable-fields list by
**regex-parsing** the stored `asExpression`.
- Both are brittle. Deriving the expression from `searchFieldMetadata`
rows at build/run time removes the cache and the parsing.

## What changed

### Server - data model & derivation
- Introduce the `tsVectorFieldMetadata` relation on
`searchFieldMetadata` (`tsVectorFieldMetadataId` / universal identifier)
linking each searchable-field row to its target `TS_VECTOR` field.
- New runtime derivation
`deriveSearchVectorAsExpressionForTsVectorField`
(`flat-search-field-metadata/utils/...`) used by the create-object and
update-field handlers to generate the column expression from
`searchFieldMetadata` rows.
- Remove `asExpression` / `generatedType` from stored settings:
`FieldMetadataSettings.TS_VECTOR` is now `null`; the column builder
(`generate-column-definitions.util.ts`) hardcodes `generatedType:
'STORED'` and requires the derived expression.
- Delete the imperative recompute side-effects and the
`compute-search-vector-universal-settings-from-object-manifest` path;
drop the `settings` block from all 28 standard
`compute-*-standard-flat-field-metadata` utils.

### Server - migration runner
- New `rebuildSearchVector` marker on `update-field` actions: the
orchestrator synthesizes targeted column rebuilds
(`compute-search-vector-rebuild-target-universal-identifiers.util.ts` +
the deprioritize aggregator) only when a searchFieldMetadata change or
indexed-field rename actually requires it - instead of rebuilding on
every settings touch.
- Deferrable FKs + in-flight ID resolution so a `searchFieldMetadata`
row and its `TS_VECTOR` field can be created in the same transaction
(deterministic UUIDs).

### Frontend (contract change, #1428)
- New `SearchFieldMetadataDTO` + dataloader exposing
`searchFieldMetadataList` on object metadata.
- `SettingsObjectSearchSection` now reads
`objectMetadataItem.searchFieldMetadatas` instead of parsing
`asExpression`; new `SearchFieldMetadataItem` type, fragment, and
mapping updates.

### Upgrade commands (2.18)
-
`2-18-instance-command-fast-...-add-ts-vector-field-metadata-id-to-search-field-metadata`
-
`2-18-instance-command-fast-...-make-search-field-metadata-fks-deferrable`
-
`2-18-instance-command-slow-...-backfill-ts-vector-field-metadata-id-on-search-field-metadata`

(These were relocated from 2.16 to 2.18 and re-timestamped into an
ordered block - add column -> make FK deferrable -> backfill data -
since 2.16/2.17 are released.)

### Tests
- Updated search-vector side-effect integration specs to assert behavior
(search works) rather than the now-removed `asExpression`; removed the
obsolete expression-validation specs; refreshed the application-sync
snapshot (`universalSettings: null`).

## Upgrade / compatibility notes
- Existing workspaces keep their stored `settings` until a later
cleanup; nothing reads it anymore. The new derivation drives all DDL
going forward.
- Schema changes are gated behind the 2.18 instance commands above.

## Known follow-up (separate PR)
https://github.com/twentyhq/core-team-issues/issues/2620
- The column rebuild (`DROP`/`ADD` of the `searchVector` STORED column)
cascade-drops its GIN index and does not recreate it - a pre-existing
regression on `main` inherited here. A follow-up PR will fix the rebuild
handler to recreate the GIN index and add a 2.18 workspace command to
recompute every search vector + strip the deprecated settings.
(Planned.)

## Test plan
- [ ] `npx nx typecheck twenty-server` / `twenty-front`
- [ ] `npx nx lint:diff-with-main twenty-server` / `twenty-front`
- [ ] Server integration: create/update/delete field, rename indexed
field, update object - search returns expected records
- [ ] Run the 2.18 instance commands on a seeded DB; verify
`tsVectorFieldMetadataId` backfilled and FKs deferrable
- [ ] Frontend: object Search settings tab lists the correct searchable
fields (no `asExpression` parsing)

close https://github.com/twentyhq/core-team-issues/issues/2587

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22287?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-06-30 11:16:01 +02:00
committed by GitHub
parent 68c33a37ad
commit b0d7516951
144 changed files with 2030 additions and 1759 deletions
@@ -666,6 +666,7 @@ type Object {
duplicateCriteria: [[String!]!]
fieldsList: [Field!]!
indexMetadataList: [Index!]!
searchFieldMetadataList: [SearchField!]!
fields(
"""Limit or page results."""
paging: CursorPaging! = {first: 10}
@@ -2085,6 +2086,15 @@ type ObjectRecordCount {
totalCount: Int!
}
type SearchField {
id: UUID!
fieldMetadataId: UUID!
tsVectorFieldMetadataId: UUID!
position: Float!
createdAt: DateTime!
updatedAt: DateTime!
}
type ObjectConnection {
"""Paging information"""
pageInfo: PageInfo!
@@ -455,6 +455,7 @@ export interface Object {
duplicateCriteria?: Scalars['String'][][]
fieldsList: Field[]
indexMetadataList: Index[]
searchFieldMetadataList: SearchField[]
fields: ObjectFieldsConnection
indexMetadatas: ObjectIndexMetadatasConnection
__typename: 'Object'
@@ -1719,6 +1720,16 @@ export interface ObjectRecordCount {
__typename: 'ObjectRecordCount'
}
export interface SearchField {
id: Scalars['UUID']
fieldMetadataId: Scalars['UUID']
tsVectorFieldMetadataId: Scalars['UUID']
position: Scalars['Float']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
__typename: 'SearchField'
}
export interface ObjectConnection {
/** Paging information */
pageInfo: PageInfo
@@ -3487,6 +3498,7 @@ export interface ObjectGenqlSelection{
duplicateCriteria?: boolean | number
fieldsList?: FieldGenqlSelection
indexMetadataList?: IndexGenqlSelection
searchFieldMetadataList?: SearchFieldGenqlSelection
fields?: (ObjectFieldsConnectionGenqlSelection & { __args: {
/** Limit or page results. */
paging: CursorPaging,
@@ -4802,6 +4814,17 @@ export interface ObjectRecordCountGenqlSelection{
__scalar?: boolean | number
}
export interface SearchFieldGenqlSelection{
id?: boolean | number
fieldMetadataId?: boolean | number
tsVectorFieldMetadataId?: boolean | number
position?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ObjectConnectionGenqlSelection{
/** Paging information */
pageInfo?: PageInfoGenqlSelection
@@ -7771,6 +7794,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const SearchField_possibleTypes: string[] = ['SearchField']
export const isSearchField = (obj?: { __typename?: any } | null): obj is SearchField => {
if (!obj?.__typename) throw new Error('__typename is missing in "isSearchField"')
return SearchField_possibleTypes.includes(obj.__typename)
}
const ObjectConnection_possibleTypes: string[] = ['ObjectConnection']
export const isObjectConnection = (obj?: { __typename?: any } | null): obj is ObjectConnection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isObjectConnection"')
File diff suppressed because it is too large Load Diff
@@ -33,6 +33,15 @@ const METADATA_QUERY = `
isLabelSyncedWithName
isSearchable
duplicateCriteria
searchFieldMetadataList {
__typename
id
fieldMetadataId
tsVectorFieldMetadataId
position
createdAt
updatedAt
}
indexMetadataList {
__typename
id
File diff suppressed because one or more lines are too long
@@ -25,6 +25,14 @@ export const OBJECT_METADATA_FRAGMENT = gql`
isLabelSyncedWithName
isSearchable
duplicateCriteria
searchFieldMetadataList {
id
fieldMetadataId
tsVectorFieldMetadataId
position
createdAt
updatedAt
}
indexMetadataList {
id
createdAt
@@ -1,6 +1,7 @@
import { type Object as GeneratedObject } from '~/generated-metadata/graphql';
import { type IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
import { type SearchFieldMetadataItem } from '@/object-metadata/types/SearchFieldMetadataItem';
import { type FieldMetadataItem } from './FieldMetadataItem';
export type EnrichedObjectMetadataItem = Omit<
@@ -12,6 +13,7 @@ export type EnrichedObjectMetadataItem = Omit<
| 'labelIdentifierFieldMetadataId'
| 'fieldsList'
| 'indexMetadataList'
| 'searchFieldMetadataList'
// Deprecated GraphQL field kept server-side for one release; no longer queried
| 'isUIReadOnly'
> & {
@@ -22,4 +24,5 @@ export type EnrichedObjectMetadataItem = Omit<
updatableFields: FieldMetadataItem[];
labelIdentifierFieldMetadataId: string;
indexMetadatas: IndexMetadataItem[];
searchFieldMetadatas: SearchFieldMetadataItem[];
};
@@ -0,0 +1,8 @@
import { type SearchField as GeneratedSearchField } from '~/generated-metadata/graphql';
export type SearchFieldMetadataItem = Omit<
GeneratedSearchField,
'__typename'
> & {
__typename?: string;
};
@@ -1,5 +1,6 @@
import { type IndexFieldMetadataItem } from '@/object-metadata/types/IndexFieldMetadataItem';
import { type IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
import { type SearchFieldMetadataItem } from '@/object-metadata/types/SearchFieldMetadataItem';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { type ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
@@ -21,8 +22,12 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
object.node.labelIdentifierFieldMetadataId,
);
const { fieldsList, indexMetadataList, ...objectWithoutFieldsList } =
object.node;
const {
fieldsList,
indexMetadataList,
searchFieldMetadataList,
...objectWithoutFieldsList
} = object.node;
return {
...objectWithoutFieldsList,
@@ -30,6 +35,12 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
...field,
})),
labelIdentifierFieldMetadataId,
searchFieldMetadatas: searchFieldMetadataList.map(
(searchFieldMetadata) =>
({
...searchFieldMetadata,
}) satisfies SearchFieldMetadataItem,
),
indexMetadatas: indexMetadataList.map(
({ indexFieldMetadataList: indexFields, ...indexRest }) =>
({
@@ -3,6 +3,7 @@ import { z } from 'zod';
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
import { indexMetadataItemSchema } from '@/object-metadata/validation-schemas/indexMetadataItemSchema';
import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metadataLabelSchema';
import { searchFieldMetadataItemSchema } from '@/object-metadata/validation-schemas/searchFieldMetadataItemSchema';
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
export const objectMetadataItemSchema = z.object({
@@ -13,6 +14,7 @@ export const objectMetadataItemSchema = z.object({
readableFields: z.array(fieldMetadataItemSchema()),
updatableFields: z.array(fieldMetadataItemSchema()),
indexMetadatas: z.array(indexMetadataItemSchema),
searchFieldMetadatas: z.array(searchFieldMetadataItemSchema),
icon: z.string().startsWith('Icon').trim(),
applicationId: z.uuid(),
id: z.uuid(),
@@ -0,0 +1,13 @@
import { z } from 'zod';
import { type SearchFieldMetadataItem } from '@/object-metadata/types/SearchFieldMetadataItem';
export const searchFieldMetadataItemSchema = z.object({
__typename: z.literal('SearchField').optional(),
id: z.uuid(),
fieldMetadataId: z.uuid(),
tsVectorFieldMetadataId: z.uuid(),
position: z.number(),
createdAt: z.string(),
updatedAt: z.string(),
}) satisfies z.ZodType<SearchFieldMetadataItem>;
@@ -50,6 +50,7 @@ const mockObjectMetadataItem: EnrichedObjectMetadataItem = {
readableFields: fields,
updatableFields: fields,
indexMetadatas: [],
searchFieldMetadatas: [],
isLabelSyncedWithName: true,
isRemote: false,
isSystem: false,
@@ -29,6 +29,7 @@ const objectMetadataItemWithPositionField: EnrichedObjectMetadataItem = {
readableFields: fields,
updatableFields: fields,
indexMetadatas: [],
searchFieldMetadatas: [],
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
nameSingular: 'object1',
@@ -191,6 +192,7 @@ describe('turnSortsIntoOrderBy', () => {
readableFields: [],
updatableFields: [],
indexMetadatas: [],
searchFieldMetadatas: [],
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
nameSingular: 'company',
@@ -241,6 +243,7 @@ describe('turnSortsIntoOrderBy', () => {
readableFields: [],
updatableFields: [],
indexMetadatas: [],
searchFieldMetadatas: [],
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
nameSingular: 'person',
@@ -49,6 +49,7 @@ describe('buildRecordGqlFieldsAggregateForView', () => {
readableFields: fields,
updatableFields: fields,
indexMetadatas: [],
searchFieldMetadatas: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@@ -19,6 +19,7 @@ describe('generateAggregateQuery', () => {
readableFields: [],
updatableFields: [],
indexMetadatas: [],
searchFieldMetadatas: [],
isLabelSyncedWithName: true,
isRemote: false,
isSystem: false,
@@ -62,6 +63,7 @@ describe('generateAggregateQuery', () => {
readableFields: [],
updatableFields: [],
indexMetadatas: [],
searchFieldMetadatas: [],
isLabelSyncedWithName: true,
isRemote: false,
isSystem: false,
@@ -12,6 +12,7 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useContext, useMemo, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconEye, IconSearch, useIcons } from 'twenty-ui/icon';
import { Card } from 'twenty-ui/surfaces';
@@ -44,56 +45,34 @@ const StyledNameLabel = styled.div`
const INDEXED_FIELDS_GRID_TEMPLATE_COLUMNS = 'minmax(0, 1fr) 100px 148px';
// TODO: This is very DIRTY ; let's migrate searchVector to be proper tables
// Already tracked here: https://github.com/twentyhq/core-team-issues/issues/1428
const extractIndexedFields = (
objectMetadataItem: EnrichedObjectMetadataItem,
): IndexedFieldEntry[] => {
const searchVectorField = objectMetadataItem.fields.find(
(field) => field.name === SEARCH_VECTOR_FIELD_NAME,
const fieldById = new Map(
objectMetadataItem.fields.map((field) => [field.id, field]),
);
const asExpression = (
searchVectorField?.settings as { asExpression?: string } | null
)?.asExpression;
return [...objectMetadataItem.searchFieldMetadatas]
.sort(
(searchFieldMetadataA, searchFieldMetadataB) =>
searchFieldMetadataA.position - searchFieldMetadataB.position,
)
.map((searchFieldMetadata) => {
const field = fieldById.get(searchFieldMetadata.fieldMetadataId);
if (!asExpression) {
return [];
}
if (!isDefined(field) || field.name === SEARCH_VECTOR_FIELD_NAME) {
return undefined;
}
const columnNames = [
...new Set(
Array.from(asExpression.matchAll(/"([^"]+)"/g), (match) => match[1]),
),
];
const seenFieldIds = new Set<string>();
const entries: IndexedFieldEntry[] = [];
for (const columnName of columnNames) {
const field = objectMetadataItem.fields.find(
(fieldItem) =>
fieldItem.name === columnName || columnName.startsWith(fieldItem.name),
);
if (
field &&
field.name !== SEARCH_VECTOR_FIELD_NAME &&
!seenFieldIds.has(field.id)
) {
seenFieldIds.add(field.id);
entries.push({
return {
id: field.id,
label: field.label,
icon: field.icon,
weight: 1,
fieldType: field.type,
});
}
}
return entries;
} satisfies IndexedFieldEntry;
})
.filter(isDefined);
};
export const SettingsObjectSearchSection = ({
@@ -80,6 +80,7 @@ const mockObjectMetadataItem: EnrichedObjectMetadataItem = {
isSearchable: true,
labelIdentifierFieldMetadataId: '1',
indexMetadatas: [],
searchFieldMetadatas: [],
fields: fields,
readableFields: fields,
updatableFields: fields,
@@ -254,6 +254,7 @@ const buildObjectMetadataItemsFromMarketplaceApp = (
(field) => !nonUpdatableFieldIds.has(field.id),
),
indexMetadatas: [],
searchFieldMetadatas: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@@ -43,6 +43,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Calendar events",
"description": "Calendar events",
"icon": "IconCalendar",
"searchFieldMetadataList": [],
"indexMetadataList": [],
"fieldsList": [
{
@@ -1043,6 +1044,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -1488,6 +1490,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -2264,6 +2267,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -2378,6 +2382,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"labelPlural": "Message Threads",
"description": "Message Thread",
"icon": "IconMessage",
"searchFieldMetadataList": [],
"indexMetadataList": [],
"fieldsList": [
{
@@ -3572,6 +3577,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
]
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -4388,6 +4394,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -5184,6 +5191,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -5767,6 +5775,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
]
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -6366,6 +6375,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -7179,6 +7189,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
]
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -7694,6 +7705,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -8255,6 +8267,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -8842,6 +8855,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -9493,6 +9507,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -10144,6 +10159,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -11184,6 +11200,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -12432,6 +12449,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
]
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -13080,6 +13098,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
]
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -13492,6 +13511,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -14144,6 +14164,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -14722,6 +14743,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -15937,6 +15959,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -16541,6 +16564,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
]
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -17677,6 +17701,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -18195,6 +18220,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -18723,6 +18749,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -19072,6 +19099,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -19650,6 +19678,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"morphRelations": null
}
],
"searchFieldMetadataList": [],
"indexMetadataList": [
{
"__typename": "Index",
@@ -178,6 +178,8 @@ const buildLegacyCalendarEventRecordingPreferenceFieldMetadata = ({
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
viewSortIds: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataIds: [],
searchFieldMetadataUniversalIdentifiers: [],
universalSettings: null,
});
@@ -5,6 +5,7 @@ import { buildSearchFieldMetadataBackfillOperations } from 'src/database/command
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import {
getFlatObjectMetadataMock,
getStandardFlatObjectMetadataMock,
@@ -100,6 +101,8 @@ const buildSearchFieldMetadata = ({
fieldMetadataUniversalIdentifier,
applicationUniversalIdentifier,
position = 0,
tsVectorFieldMetadataId = 'ts-vector-field-metadata-id',
tsVectorFieldMetadataUniversalIdentifier = 'ts-vector-field-metadata-universal-identifier',
}: {
id: string;
universalIdentifier: string;
@@ -109,6 +112,8 @@ const buildSearchFieldMetadata = ({
fieldMetadataUniversalIdentifier: string;
applicationUniversalIdentifier: string;
position?: number;
tsVectorFieldMetadataId?: string;
tsVectorFieldMetadataUniversalIdentifier?: string;
}): FlatSearchFieldMetadata => {
const createdAt = '2024-01-01T00:00:00.000Z';
@@ -119,6 +124,8 @@ const buildSearchFieldMetadata = ({
fieldMetadataId,
objectMetadataUniversalIdentifier,
fieldMetadataUniversalIdentifier,
tsVectorFieldMetadataId,
tsVectorFieldMetadataUniversalIdentifier,
applicationId: 'unused-application-id',
applicationUniversalIdentifier,
position,
@@ -128,6 +135,25 @@ const buildSearchFieldMetadata = ({
};
};
const buildSearchVectorField = ({
objectMetadataId,
objectMetadataUniversalIdentifier,
applicationUniversalIdentifier,
}: {
objectMetadataId: string;
objectMetadataUniversalIdentifier: string;
applicationUniversalIdentifier: string;
}): FlatFieldMetadata =>
getFlatFieldMetadataMock({
id: `${objectMetadataId}-search-vector-field-id`,
universalIdentifier: `${objectMetadataUniversalIdentifier}-search-vector-field-uid`,
objectMetadataId,
objectMetadataUniversalIdentifier,
type: FieldMetadataType.TS_VECTOR,
name: SEARCH_VECTOR_FIELD.name,
applicationUniversalIdentifier,
});
// Custom object with a searchable `name` field plus a TEXT field whose name
// overlaps it by prefix (name / nameDescription). Only the `name` field should
// produce a row; the exact-name match makes the prefix overlap irrelevant. This
@@ -158,6 +184,12 @@ const buildCustomObjectFixture = () => {
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
});
const searchVectorField = buildSearchVectorField({
objectMetadataId: customObjectId,
objectMetadataUniversalIdentifier: customObjectUniversalIdentifier,
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
});
const customObject = getFlatObjectMetadataMock({
id: customObjectId,
universalIdentifier: customObjectUniversalIdentifier,
@@ -168,15 +200,16 @@ const buildCustomObjectFixture = () => {
fieldUniversalIdentifiers: [
nameField.universalIdentifier,
nameDescriptionField.universalIdentifier,
searchVectorField.universalIdentifier,
],
});
return { customObject, nameField, nameDescriptionField };
return { customObject, nameField, nameDescriptionField, searchVectorField };
};
describe('buildSearchFieldMetadataBackfillOperations', () => {
it('selects only the name field for a custom object, ignoring a prefix-overlapping field name', () => {
const { customObject, nameField, nameDescriptionField } =
const { customObject, nameField, nameDescriptionField, searchVectorField } =
buildCustomObjectFixture();
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
@@ -185,6 +218,7 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
nameField,
nameDescriptionField,
searchVectorField,
]),
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
standardFlatSearchFieldMetadataMaps:
@@ -204,6 +238,10 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
expect(customApplicationRows?.[0].objectMetadataUniversalIdentifier).toBe(
customObject.universalIdentifier,
);
// The row points at the object's searchVector field via the new FK.
expect(
customApplicationRows?.[0].tsVectorFieldMetadataUniversalIdentifier,
).toBe(searchVectorField.universalIdentifier);
// Custom object name field is seeded at position 0.
expect(customApplicationRows?.[0].position).toBe(0);
// No spurious row for the prefix-overlapping `nameDescription` field.
@@ -263,7 +301,7 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
});
it('groups rows per application: standard-object rows under the standard application, custom-object rows under the custom application', () => {
const { customObject, nameField, nameDescriptionField } =
const { customObject, nameField, nameDescriptionField, searchVectorField } =
buildCustomObjectFixture();
const standardObjectId = 'standard-object-id';
@@ -282,12 +320,23 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
});
const standardSearchVectorField = buildSearchVectorField({
objectMetadataId: standardObjectId,
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
});
const standardObject = getStandardFlatObjectMetadataMock({
id: standardObjectId,
universalIdentifier: standardObjectUniversalIdentifier,
isSearchable: true,
labelIdentifierFieldMetadataId: standardFieldId,
fieldIds: [standardFieldId],
fieldUniversalIdentifiers: [
standardFieldUniversalIdentifier,
standardSearchVectorField.universalIdentifier,
],
});
const standardSearchFieldMetadata = buildSearchFieldMetadata({
@@ -310,7 +359,9 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
nameField,
nameDescriptionField,
searchVectorField,
standardField,
standardSearchVectorField,
]),
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
@@ -339,6 +390,9 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
expect(standardApplicationRows?.[0].fieldMetadataUniversalIdentifier).toBe(
standardFieldUniversalIdentifier,
);
expect(
standardApplicationRows?.[0].tsVectorFieldMetadataUniversalIdentifier,
).toBe(standardSearchVectorField.universalIdentifier);
const customApplicationRows =
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier[
@@ -349,6 +403,9 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
expect(customApplicationRows?.[0].fieldMetadataUniversalIdentifier).toBe(
nameField.universalIdentifier,
);
expect(
customApplicationRows?.[0].tsVectorFieldMetadataUniversalIdentifier,
).toBe(searchVectorField.universalIdentifier);
});
it('selects only the standard-set field on a standard object, never a prefix-overlapping sibling (phone vs phoneNumber)', () => {
@@ -386,12 +443,24 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
});
const standardSearchVectorField = buildSearchVectorField({
objectMetadataId: standardObjectId,
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
});
const standardObject = getStandardFlatObjectMetadataMock({
id: standardObjectId,
universalIdentifier: standardObjectUniversalIdentifier,
isSearchable: true,
labelIdentifierFieldMetadataId: phoneFieldId,
fieldIds: [phoneFieldId, phoneNumberFieldId],
fieldUniversalIdentifiers: [
phoneFieldUniversalIdentifier,
phoneNumberFieldUniversalIdentifier,
standardSearchVectorField.universalIdentifier,
],
});
const phoneSearchFieldMetadata = buildSearchFieldMetadata({
@@ -411,6 +480,7 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
phoneField,
phoneNumberField,
standardSearchVectorField,
]),
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
@@ -455,12 +525,23 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
});
const standardSearchVectorField = buildSearchVectorField({
objectMetadataId: standardObjectId,
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
});
const standardObject = getStandardFlatObjectMetadataMock({
id: standardObjectId,
universalIdentifier: standardObjectUniversalIdentifier,
isSearchable: true,
labelIdentifierFieldMetadataId: emailsFieldId,
fieldIds: [emailsFieldId],
fieldUniversalIdentifiers: [
emailsFieldUniversalIdentifier,
standardSearchVectorField.universalIdentifier,
],
});
// The standard maps row sits at position 3 (e.g. SEARCH_FIELDS_FOR_PERSON order);
@@ -480,7 +561,10 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
buildSearchFieldMetadataBackfillOperations({
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([standardObject]),
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([emailsField]),
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
emailsField,
standardSearchVectorField,
]),
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
standardSearchFieldMetadata,
@@ -10,6 +10,7 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
import { buildFlatSearchFieldMetadataForField } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/build-flat-search-field-metadata-for-field.util';
import { findTsVectorFlatFieldMetadataForObject } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/find-ts-vector-flat-field-metadata-for-object.util';
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata.constants';
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
@@ -84,12 +85,22 @@ export const buildSearchFieldMetadataBackfillOperations = ({
return;
}
const tsVectorFlatFieldMetadata = findTsVectorFlatFieldMetadataForObject({
fieldUniversalIdentifiers: flatObjectMetadata.fieldUniversalIdentifiers,
flatFieldMetadataMaps,
});
if (!isDefined(tsVectorFlatFieldMetadata)) {
return;
}
candidateSearchFieldMetadataKeys.add(searchFieldMetadataKey);
flatSearchFieldMetadatasToCreate.push(
buildFlatSearchFieldMetadataForField({
flatObjectMetadata,
flatFieldMetadata,
tsVectorFlatFieldMetadata,
position,
}),
);
@@ -0,0 +1,53 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.18.0', 1810000001000)
export class AddTsVectorFieldMetadataIdToSearchFieldMetadataFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" ADD "tsVectorFieldMetadataId" uuid',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" ADD CONSTRAINT "FK_31102b31f7338b6a298fc22996a" FOREIGN KEY ("tsVectorFieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" DROP CONSTRAINT IF EXISTS "FK_1b78544eb06f82059a2a01013a3"',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" ADD CONSTRAINT "FK_1b78544eb06f82059a2a01013a3" FOREIGN KEY ("objectMetadataId") REFERENCES "core"."objectMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" DROP CONSTRAINT IF EXISTS "FK_6d5c6922bfd1578b1eff2abb9d6"',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" ADD CONSTRAINT "FK_6d5c6922bfd1578b1eff2abb9d6" FOREIGN KEY ("fieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" DROP CONSTRAINT IF EXISTS "FK_6d5c6922bfd1578b1eff2abb9d6"',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" ADD CONSTRAINT "FK_6d5c6922bfd1578b1eff2abb9d6" FOREIGN KEY ("fieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" DROP CONSTRAINT IF EXISTS "FK_1b78544eb06f82059a2a01013a3"',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" ADD CONSTRAINT "FK_1b78544eb06f82059a2a01013a3" FOREIGN KEY ("objectMetadataId") REFERENCES "core"."objectMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" DROP CONSTRAINT "FK_31102b31f7338b6a298fc22996a"',
);
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" DROP COLUMN "tsVectorFieldMetadataId"',
);
}
}
@@ -0,0 +1,51 @@
import { DataSource, QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
// Backfills the tsVectorFieldMetadataId FK by pointing every existing
// searchFieldMetadata row at its object's system searchVector (TS_VECTOR) field,
// then enforces NOT NULL. Runs after the fast command that adds the column (and makes
// the FKs deferrable). Any row left without a resolvable searchVector
// aborts the upgrade rather than silently relaxing the NOT NULL guarantee.
@RegisteredInstanceCommand('2.18.0', 1810000003000, { type: 'slow' })
export class BackfillTsVectorFieldMetadataIdOnSearchFieldMetadataSlowInstanceCommand
implements SlowInstanceCommand
{
async runDataMigration(dataSource: DataSource): Promise<void> {
await dataSource.query(
`UPDATE "core"."searchFieldMetadata" "searchFieldMetadata"
SET "tsVectorFieldMetadataId" = "fieldMetadata"."id"
FROM "core"."fieldMetadata" "fieldMetadata"
WHERE "fieldMetadata"."objectMetadataId" = "searchFieldMetadata"."objectMetadataId"
AND "fieldMetadata"."type" = 'TS_VECTOR'
AND "fieldMetadata"."name" = 'searchVector'
AND "searchFieldMetadata"."tsVectorFieldMetadataId" IS NULL`,
);
const unresolvedRows: { count: string }[] = await dataSource.query(
`SELECT COUNT(*) AS "count" FROM "core"."searchFieldMetadata"
WHERE "tsVectorFieldMetadataId" IS NULL`,
);
const unresolvedCount = Number(unresolvedRows[0]?.count ?? 0);
if (unresolvedCount > 0) {
throw new Error(
`Cannot enforce searchFieldMetadata.tsVectorFieldMetadataId NOT NULL: ${unresolvedCount} row(s) reference an object without a searchVector (TS_VECTOR) field`,
);
}
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" ALTER COLUMN "tsVectorFieldMetadataId" SET NOT NULL',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."searchFieldMetadata" ALTER COLUMN "tsVectorFieldMetadataId" DROP NOT NULL',
);
}
}
@@ -0,0 +1,2 @@
export const ADD_TS_VECTOR_FIELD_METADATA_ID_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME =
'2.18.0_AddTsVectorFieldMetadataIdToSearchFieldMetadataFastInstanceCommand_1810000001000';
@@ -34,6 +34,13 @@ import { AddIsSystemSideEffectFastInstanceCommand } from 'src/database/commands/
import { BackfillConnectionSecuritySlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-15/2-15-instance-command-slow-1781461753981-backfill-connection-security';
import { AddChannelWebhookSubscriptionFieldsFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782152096938-add-channel-webhook-subscription-fields';
import { AddUniversalIdentifierAndApplicationIdToSearchFieldMetadataFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782200000000-add-universal-identifier-and-application-id-to-search-field-metadata';
import { AddServerTriggerSettingsToLogicFunctionFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782211913427-add-server-trigger-settings-to-logic-function';
import { AddPrimaryPublicDomainToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782281874768-add-primary-public-domain-to-application';
import { MakePublicDomainApplicationIdNotNullSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-slow-1782281874769-make-public-domain-application-id-not-null';
import { CreateDpaAgreementCoreTableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-17/2-17-instance-command-fast-1801000020000-create-dpa-agreement-core-table';
import { CreateApplicationTranslationCoreTableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-17/2-17-instance-command-fast-1801000100000-create-application-translation-core-table';
import { AddTsVectorFieldMetadataIdToSearchFieldMetadataFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-18/2-18-instance-command-fast-1810000001000-add-ts-vector-field-metadata-id-to-search-field-metadata';
import { BackfillTsVectorFieldMetadataIdOnSearchFieldMetadataSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-18/2-18-instance-command-slow-1810000003000-backfill-ts-vector-field-metadata-id-on-search-field-metadata';
import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777455269302-add-cache-tokens-to-agent-chat-thread';
import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application';
import { AddSubFieldNameToViewSortEarlyFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1747234200000-add-sub-field-name-to-view-sort';
@@ -77,11 +84,6 @@ import { EncryptNonSecretApplicationVariableSlowInstanceCommand } from 'src/data
import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences';
import { AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action';
import { AddViewKanbanColumnWidthFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781900000000-add-view-kanban-column-width';
import { AddPrimaryPublicDomainToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782281874768-add-primary-public-domain-to-application';
import { MakePublicDomainApplicationIdNotNullSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-slow-1782281874769-make-public-domain-application-id-not-null';
import { AddServerTriggerSettingsToLogicFunctionFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782211913427-add-server-trigger-settings-to-logic-function';
import { CreateDpaAgreementCoreTableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-17/2-17-instance-command-fast-1801000020000-create-dpa-agreement-core-table';
import { CreateApplicationTranslationCoreTableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-17/2-17-instance-command-fast-1801000100000-create-application-translation-core-table';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -166,4 +168,6 @@ export const INSTANCE_COMMANDS = [
MakePublicDomainApplicationIdNotNullSlowInstanceCommand,
CreateDpaAgreementCoreTableFastInstanceCommand,
CreateApplicationTranslationCoreTableFastInstanceCommand,
AddTsVectorFieldMetadataIdToSearchFieldMetadataFastInstanceCommand,
BackfillTsVectorFieldMetadataIdOnSearchFieldMetadataSlowInstanceCommand,
];
@@ -0,0 +1,114 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
import { generateWorkspaceSchemaDdl } from 'src/database/commands/workspace-export/utils/generate-workspace-schema-ddl.util';
describe('generateWorkspaceSchemaDdl', () => {
const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const objectMetadataId = '20202020-object-id';
const nameFieldId = '20202020-name-field-id';
const searchVectorFieldId = '20202020-search-vector-field-id';
const objectMetadata = {
...getFlatObjectMetadataMock({
universalIdentifier: 'person',
id: objectMetadataId,
nameSingular: 'person',
namePlural: 'persons',
}),
isActive: true,
} as unknown as ObjectMetadataEntity;
const nameField = getFlatFieldMetadataMock({
universalIdentifier: 'name',
id: nameFieldId,
objectMetadataId,
type: FieldMetadataType.TEXT,
name: 'name',
}) as unknown as FieldMetadataEntity;
const searchVectorField = getFlatFieldMetadataMock({
universalIdentifier: 'searchVector',
id: searchVectorFieldId,
objectMetadataId,
type: FieldMetadataType.TS_VECTOR,
name: 'searchVector',
}) as unknown as FieldMetadataEntity;
const fieldsByObjectId = new Map<string, FieldMetadataEntity[]>([
[objectMetadataId, [nameField, searchVectorField]],
]);
const buildSearchFieldMetadata = (
overrides: Partial<SearchFieldMetadataEntity> = {},
): SearchFieldMetadataEntity =>
({
universalIdentifier: 'search-name',
objectMetadataId,
fieldMetadataId: nameFieldId,
tsVectorFieldMetadataId: searchVectorFieldId,
position: 0,
...overrides,
}) as SearchFieldMetadataEntity;
const generateSearchVectorColumnSql = (
searchFieldMetadatasByObjectId: Map<string, SearchFieldMetadataEntity[]>,
): string => {
const statements = generateWorkspaceSchemaDdl(
workspaceId,
'workspace_schema',
[objectMetadata],
fieldsByObjectId,
searchFieldMetadatasByObjectId,
);
const createTableStatement = statements.find((statement) =>
statement.includes('"searchVector"'),
);
expect(createTableStatement).toBeDefined();
return createTableStatement as string;
};
it('should emit the searchVector column as a STORED generated column derived from searchFieldMetadata', () => {
const createTableStatement = generateSearchVectorColumnSql(
new Map([[objectMetadataId, [buildSearchFieldMetadata()]]]),
);
expect(createTableStatement).toContain(
`"searchVector" tsvector GENERATED ALWAYS AS (to_tsvector('simple', COALESCE(public.unaccent_immutable("name"), ''))) STORED`,
);
});
it('should tolerate a legacy NULL tsVectorFieldMetadataId row (pre-2.18 backfill)', () => {
const createTableStatement = generateSearchVectorColumnSql(
new Map([
[
objectMetadataId,
[
buildSearchFieldMetadata({
tsVectorFieldMetadataId: null as never,
}),
],
],
]),
);
expect(createTableStatement).toContain(
`GENERATED ALWAYS AS (to_tsvector('simple', COALESCE(public.unaccent_immutable("name"), ''))) STORED`,
);
});
it('should still emit a valid STORED generated column when no searchFieldMetadata rows exist', () => {
const createTableStatement = generateSearchVectorColumnSql(new Map());
expect(createTableStatement).toContain(
`"searchVector" tsvector GENERATED ALWAYS AS (to_tsvector('simple', NULL)) STORED`,
);
});
});
@@ -1,26 +1,82 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
import { deriveSearchVectorAsExpressionForTsVectorField } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/derive-search-vector-as-expression-for-ts-vector-field.util';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { type SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
import { buildSqlColumnDefinition } from 'src/engine/twenty-orm/workspace-schema-manager/utils/build-sql-column-definition.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import {
escapeIdentifier,
escapeLiteral,
} from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/generate-column-definitions.util';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import {
type CreateEnumOperationSpec,
EnumOperation,
collectEnumOperationsForObject,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/workspace-schema-enum-operations.util';
const buildSearchFieldMetadataDerivationInputs = ({
fieldMetadatas,
objectSearchFieldMetadatas,
}: {
fieldMetadatas: FieldMetadataEntity[];
objectSearchFieldMetadatas: SearchFieldMetadataEntity[];
}): {
indexedFieldById: ReadonlyMap<
string,
{ name: string; type: FieldMetadataType }
>;
flatSearchFieldMetadataMaps: FlatEntityMaps<FlatSearchFieldMetadata>;
} => {
const indexedFieldById = new Map(
fieldMetadatas.map((fieldMetadata) => [
fieldMetadata.id,
{ name: fieldMetadata.name, type: fieldMetadata.type },
]),
);
const searchVectorFieldId = fieldMetadatas.find(
(fieldMetadata) =>
fieldMetadata.type === FieldMetadataType.TS_VECTOR &&
fieldMetadata.name === SEARCH_VECTOR_FIELD.name,
)?.id;
const byUniversalIdentifier = Object.fromEntries(
objectSearchFieldMetadatas.map((searchFieldMetadata) => [
searchFieldMetadata.universalIdentifier,
{
...searchFieldMetadata,
tsVectorFieldMetadataId:
searchFieldMetadata.tsVectorFieldMetadataId ?? searchVectorFieldId,
} as unknown as FlatSearchFieldMetadata,
]),
);
return {
indexedFieldById,
flatSearchFieldMetadataMaps: {
byUniversalIdentifier,
universalIdentifierById: {},
universalIdentifiersByApplicationId: {},
},
};
};
export const generateWorkspaceSchemaDdl = (
workspaceId: string,
schemaName: string,
objectMetadatas: ObjectMetadataEntity[],
fieldsByObjectId: Map<string, FieldMetadataEntity[]>,
searchFieldMetadatasByObjectId: Map<string, SearchFieldMetadataEntity[]>,
): string[] => {
const statements: string[] = [];
@@ -37,6 +93,13 @@ export const generateWorkspaceSchemaDdl = (
const flatFieldMetadatas = fieldMetadatas as unknown as FlatFieldMetadata[];
const flatObjectMetadata = objectMetadata as unknown as FlatObjectMetadata;
const { indexedFieldById, flatSearchFieldMetadataMaps } =
buildSearchFieldMetadataDerivationInputs({
fieldMetadatas,
objectSearchFieldMetadatas:
searchFieldMetadatasByObjectId.get(objectMetadata.id) ?? [],
});
const enumOperations = collectEnumOperationsForObject({
tableName,
operation: EnumOperation.CREATE,
@@ -57,6 +120,16 @@ export const generateWorkspaceSchemaDdl = (
flatFieldMetadata,
flatObjectMetadata,
workspaceId,
searchVectorAsExpression: isFlatFieldMetadataOfType(
flatFieldMetadata,
FieldMetadataType.TS_VECTOR,
)
? deriveSearchVectorAsExpressionForTsVectorField({
tsVectorFieldMetadataId: flatFieldMetadata.id,
flatSearchFieldMetadataMaps,
indexedFieldById,
})
: undefined,
}),
);
@@ -5,10 +5,15 @@ import { WorkspaceExportCommand } from 'src/database/commands/workspace-export/w
import { WorkspaceExportService } from 'src/database/commands/workspace-export/workspace-export.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
@Module({
imports: [
TypeOrmModule.forFeature([ObjectMetadataEntity, FieldMetadataEntity]),
TypeOrmModule.forFeature([
ObjectMetadataEntity,
FieldMetadataEntity,
SearchFieldMetadataEntity,
]),
],
providers: [WorkspaceExportCommand, WorkspaceExportService],
})
@@ -11,20 +11,21 @@ import {
Repository,
} from 'typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { escapeIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util';
import { getCoreEntityMetadatasWithWorkspaceId } from 'src/database/commands/workspace-export/utils/get-core-entity-metadatas-with-workspace-id.util';
import { generateWorkspaceSchemaDdl } from 'src/database/commands/workspace-export/utils/generate-workspace-schema-ddl.util';
import { buildInsertPrefix } from 'src/database/commands/workspace-export/utils/build-insert-prefix.util';
import { buildWorkspaceTableColumnSets } from 'src/database/commands/workspace-export/utils/build-workspace-table-column-sets.util';
import { formatSqlValue } from 'src/database/commands/workspace-export/utils/format-sql-value.util';
import { formatPgCopyField } from './utils/format-pg-copy-value.util';
import { generateWorkspaceSchemaDdl } from 'src/database/commands/workspace-export/utils/generate-workspace-schema-ddl.util';
import { getCoreEntityMetadatasWithWorkspaceId } from 'src/database/commands/workspace-export/utils/get-core-entity-metadatas-with-workspace-id.util';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import { escapeIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util';
import { isNonEmptyArray } from 'twenty-shared/utils';
import { formatPgCopyField } from './utils/format-pg-copy-value.util';
const BATCH_SIZE = 10_000;
@@ -57,6 +58,9 @@ export class WorkspaceExportService {
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
// eslint-disable-next-line twenty/prefer-workspace-scoped-repository -- Ignored
@InjectRepository(SearchFieldMetadataEntity)
private readonly searchFieldMetadataRepository: Repository<SearchFieldMetadataEntity>,
) {}
async exportWorkspace({
@@ -95,6 +99,28 @@ export class WorkspaceExportService {
fieldsByObjectId.set(fieldMetadata.objectMetadataId, objectFields);
}
const searchFieldMetadatas = await this.searchFieldMetadataRepository.find({
where: { workspaceId },
});
const searchFieldMetadatasByObjectId = new Map<
string,
SearchFieldMetadataEntity[]
>();
for (const searchFieldMetadata of searchFieldMetadatas) {
const objectSearchFieldMetadatas =
searchFieldMetadatasByObjectId.get(
searchFieldMetadata.objectMetadataId,
) ?? [];
objectSearchFieldMetadatas.push(searchFieldMetadata);
searchFieldMetadatasByObjectId.set(
searchFieldMetadata.objectMetadataId,
objectSearchFieldMetadatas,
);
}
mkdirSync(outputPath, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
@@ -117,6 +143,7 @@ export class WorkspaceExportService {
schemaName,
objectMetadatas,
fieldsByObjectId,
searchFieldMetadatasByObjectId,
stream,
);
@@ -327,6 +354,7 @@ export class WorkspaceExportService {
schemaName: string,
objectMetadatas: ObjectMetadataEntity[],
fieldsByObjectId: Map<string, FieldMetadataEntity[]>,
searchFieldMetadatasByObjectId: Map<string, SearchFieldMetadataEntity[]>,
stream: WriteStream,
): void {
this.logger.log('Generating workspace schema DDL from metadata...');
@@ -336,6 +364,7 @@ export class WorkspaceExportService {
schemaName,
objectMetadatas,
fieldsByObjectId,
searchFieldMetadatasByObjectId,
);
this.logger.log(` ${ddlStatements.length} DDL statements`);
@@ -114,6 +114,7 @@ export const fromFieldManifestToUniversalFlatFieldMetadata = ({
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
createdAt: now,
updatedAt: now,
};
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { type Manifest } from 'twenty-shared/application';
import { MAX_CUSTOM_INDEXES_PER_OBJECT } from 'twenty-shared/constants';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { fromApplicationVariableManifestToUniversalFlatApplicationVariable } from 'src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util';
@@ -25,7 +24,6 @@ import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/app
import { fromRowLevelPermissionPredicateGroupManifestToUniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-row-level-permission-predicate-group-manifest-to-universal-flat-row-level-permission-predicate-group.util';
import { fromRowLevelPermissionPredicateManifestToUniversalFlatRowLevelPermissionPredicate } from 'src/engine/core-modules/application/application-manifest/converters/from-row-level-permission-predicate-manifest-to-universal-flat-row-level-permission-predicate.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util';
import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util';
import { fromViewFieldGroupManifestToUniversalFlatViewFieldGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util';
import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util';
import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-group-manifest-to-universal-flat-view-filter-group.util';
@@ -95,21 +93,10 @@ export class ComputeApplicationManifestAllUniversalFlatEntityMapsService {
});
for (const fieldManifest of objectManifest.fields) {
const enrichedFieldManifest =
fieldManifest.type === FieldMetadataType.TS_VECTOR &&
!isDefined(fieldManifest.universalSettings)
? {
...fieldManifest,
objectUniversalIdentifier: objectManifest.universalIdentifier,
universalSettings:
computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest,
}),
}
: {
...fieldManifest,
objectUniversalIdentifier: objectManifest.universalIdentifier,
};
const enrichedFieldManifest = {
...fieldManifest,
objectUniversalIdentifier: objectManifest.universalIdentifier,
};
const flatFieldMetadata = fromFieldManifestToUniversalFlatFieldMetadata(
{
@@ -1,149 +0,0 @@
import { type ObjectManifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util';
const buildObjectManifest = (
overrides: Partial<ObjectManifest> & {
fields: ObjectManifest['fields'];
labelIdentifierFieldMetadataUniversalIdentifier: string;
},
): ObjectManifest => ({
universalIdentifier: 'obj-uuid-1',
nameSingular: 'testObject',
namePlural: 'testObjects',
labelSingular: 'Test Object',
labelPlural: 'Test Objects',
...overrides,
});
describe('computeSearchVectorUniversalSettingsFromObjectManifest', () => {
it('should return asExpression and generatedType for a TEXT label identifier field', () => {
const result = computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest: buildObjectManifest({
labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-name',
fields: [
{
universalIdentifier: 'field-uuid-name',
name: 'name',
label: 'Name',
type: FieldMetadataType.TEXT,
},
],
}),
});
expect(result?.generatedType).toBe('STORED');
expect(result?.asExpression).toContain("to_tsvector('simple'");
expect(result?.asExpression).toContain('"name"');
});
it('should return asExpression for a FULL_NAME label identifier field', () => {
const result = computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest: buildObjectManifest({
labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-name',
fields: [
{
universalIdentifier: 'field-uuid-name',
name: 'name',
label: 'Name',
type: FieldMetadataType.FULL_NAME,
},
],
}),
});
expect(result?.generatedType).toBe('STORED');
expect(result?.asExpression).toContain("to_tsvector('simple'");
expect(result?.asExpression).toContain('"nameFirstName"');
expect(result?.asExpression).toContain('"nameLastName"');
});
it('should return asExpression for an EMAILS label identifier field', () => {
const result = computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest: buildObjectManifest({
labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-email',
fields: [
{
universalIdentifier: 'field-uuid-email',
name: 'email',
label: 'Email',
type: FieldMetadataType.EMAILS,
},
],
}),
});
expect(result?.generatedType).toBe('STORED');
expect(result?.asExpression).toContain("to_tsvector('simple'");
expect(result?.asExpression).toContain('"emailPrimaryEmail"');
});
it('should return asExpression for a UUID label identifier field', () => {
const result = computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest: buildObjectManifest({
labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-id',
fields: [
{
universalIdentifier: 'field-uuid-id',
name: 'externalId',
label: 'External ID',
type: FieldMetadataType.UUID,
},
],
}),
});
expect(result?.generatedType).toBe('STORED');
expect(result?.asExpression).toContain("to_tsvector('simple'");
expect(result?.asExpression).toContain('"externalId"');
});
it('should return null when label identifier field is not found in fields', () => {
const result = computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest: buildObjectManifest({
labelIdentifierFieldMetadataUniversalIdentifier:
'non-existent-field-uuid',
fields: [
{
universalIdentifier: 'field-uuid-name',
name: 'name',
label: 'Name',
type: FieldMetadataType.TEXT,
},
],
}),
});
expect(result).toBeNull();
});
it('should return null when label identifier field has a non-searchable type', () => {
const result = computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest: buildObjectManifest({
labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-number',
fields: [
{
universalIdentifier: 'field-uuid-number',
name: 'amount',
label: 'Amount',
type: FieldMetadataType.NUMBER,
},
],
}),
});
expect(result).toBeNull();
});
it('should return null when fields array is empty', () => {
const result = computeSearchVectorUniversalSettingsFromObjectManifest({
objectManifest: buildObjectManifest({
labelIdentifierFieldMetadataUniversalIdentifier: 'field-uuid-name',
fields: [],
}),
});
expect(result).toBeNull();
});
});
@@ -1,37 +0,0 @@
import { type ObjectManifest } from 'twenty-shared/application';
import {
type FieldMetadataUniversalSettings,
FieldMetadataType,
} from 'twenty-shared/types';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { isDefined, isSearchableFieldType } from 'twenty-shared/utils';
export const computeSearchVectorUniversalSettingsFromObjectManifest = ({
objectManifest,
}: {
objectManifest: ObjectManifest;
}): FieldMetadataUniversalSettings<FieldMetadataType.TS_VECTOR> => {
const labelIdentifierField = objectManifest.fields.find(
(field) =>
field.universalIdentifier ===
objectManifest.labelIdentifierFieldMetadataUniversalIdentifier,
);
if (
!isDefined(labelIdentifierField) ||
!isSearchableFieldType(labelIdentifierField.type)
) {
return null;
}
return {
asExpression: getTsVectorColumnExpressionFromFields([
{
name: labelIdentifierField.name,
type: labelIdentifierField.type,
},
]),
generatedType: 'STORED',
};
};
@@ -5,6 +5,7 @@ import {
type FieldMetadataLoaderPayload,
type IndexFieldMetadataLoaderPayload,
type IndexMetadataLoaderPayload,
type SearchFieldMetadataLoaderPayload,
type IsConfiguredLoaderPayload,
type MorphRelationLoaderPayload,
type StandardApplicationIdLoaderPayload,
@@ -23,6 +24,7 @@ import { type RelationDTO } from 'src/engine/metadata-modules/field-metadata/dto
import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { type SearchFieldMetadataDTO } from 'src/engine/metadata-modules/search-field-metadata/dtos/search-field-metadata.dto';
import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
import { type ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import { type ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto';
@@ -53,6 +55,11 @@ export interface IDataloaders {
IndexFieldMetadataDTO[]
>;
searchFieldMetadataLoader: DataLoader<
SearchFieldMetadataLoaderPayload,
SearchFieldMetadataDTO[]
>;
objectMetadataLoader: DataLoader<
ObjectMetadataLoaderPayload,
ObjectMetadataDTO | null
@@ -31,6 +31,7 @@ import { resolveMorphRelationsFromFlatFieldMetadata } from 'src/engine/metadata-
import { resolveRelationFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-relation-from-flat-field-metadata.util';
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
import { fromFlatSearchFieldMetadataToSearchFieldMetadataDto } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/from-flat-search-field-metadata-to-search-field-metadata-dto.util';
import { fromFlatViewFieldGroupToViewFieldGroupDto } from 'src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util';
import { fromFlatViewFieldToViewFieldDto } from 'src/engine/metadata-modules/view-field/utils/from-flat-view-field-to-view-field-dto.util';
import { fromFlatViewFilterToViewFilterDto } from 'src/engine/metadata-modules/view-filter/utils/from-flat-view-filter-to-view-filter-dto.util';
@@ -41,6 +42,7 @@ import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-me
import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type SearchFieldMetadataDTO } from 'src/engine/metadata-modules/search-field-metadata/dtos/search-field-metadata.dto';
export type RelationMetadataLoaderPayload = {
workspaceId: string;
@@ -76,6 +78,11 @@ export type IndexFieldMetadataLoaderPayload = {
indexMetadata: Pick<IndexMetadataInterface, 'id'>;
};
export type SearchFieldMetadataLoaderPayload = {
workspaceId: string;
objectMetadata: Pick<ObjectMetadataEntity, 'id'>;
};
export type ObjectMetadataLoaderPayload = {
workspaceId: string;
objectMetadataId: string;
@@ -145,6 +152,7 @@ export class DataloaderService {
const fieldMetadataLoader = this.createFieldMetadataLoader();
const indexMetadataLoader = this.createIndexMetadataLoader();
const indexFieldMetadataLoader = this.createIndexFieldMetadataLoader();
const searchFieldMetadataLoader = this.createSearchFieldMetadataLoader();
const objectMetadataLoader = this.createObjectMetadataLoader();
const viewFieldGroupsByViewIdLoader =
this.createViewFieldGroupsByViewIdLoader();
@@ -168,6 +176,7 @@ export class DataloaderService {
fieldMetadataLoader,
indexMetadataLoader,
indexFieldMetadataLoader,
searchFieldMetadataLoader,
objectMetadataLoader,
viewFieldGroupsByViewIdLoader,
viewFieldsByViewFieldGroupIdLoader,
@@ -306,6 +315,46 @@ export class DataloaderService {
);
}
private createSearchFieldMetadataLoader() {
return new DataLoader<
SearchFieldMetadataLoaderPayload,
SearchFieldMetadataDTO[]
>(async (dataLoaderParams: SearchFieldMetadataLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const objectMetadataIds = dataLoaderParams.map(
(dataLoaderParam) => dataLoaderParam.objectMetadata.id,
);
const { flatSearchFieldMetadataMaps, flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatSearchFieldMetadataMaps',
'flatObjectMetadataMaps',
],
},
);
return objectMetadataIds.map((objectMetadataId) => {
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const searchFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: flatObjectMetadata.searchFieldMetadataIds,
flatEntityMaps: flatSearchFieldMetadataMaps,
});
return searchFieldMetadatas.map(
fromFlatSearchFieldMetadataToSearchFieldMetadataDto,
);
});
});
}
private createFieldMetadataLoader() {
return new DataLoader<FieldMetadataLoaderPayload, FieldMetadataDTO[]>(
async (dataLoaderParams: FieldMetadataLoaderPayload[]) => {
@@ -30,6 +30,7 @@ import { AssignTypeIfIsMorphOrRelationFieldMetadataType } from 'src/engine/metad
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 { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
@@ -238,4 +239,10 @@ export class FieldMetadataEntity<
@OneToMany(() => ViewSortEntity, (viewSort) => viewSort.fieldMetadata)
viewSorts: Relation<ViewSortEntity[]>;
@OneToMany(
() => SearchFieldMetadataEntity,
(searchFieldMetadata) => searchFieldMetadata.fieldMetadata,
)
searchFieldMetadatas: Relation<SearchFieldMetadataEntity[]>;
}
@@ -16,7 +16,6 @@ import {
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util';
@@ -114,7 +113,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
flatIndexesToDelete,
flatIndexesToUpdate,
searchFieldMetadatasToDelete,
flatSearchVectorFieldsToUpdate,
} = fromDeleteFieldInputToFlatFieldMetadatasToDelete({
deleteOneFieldInput,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
@@ -162,7 +160,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
fieldMetadata: {
flatEntityToCreate: [],
flatEntityToDelete: flatFieldMetadatasToDelete,
flatEntityToUpdate: flatSearchVectorFieldsToUpdate,
flatEntityToUpdate: [],
},
index: {
flatEntityToCreate: [],
@@ -228,7 +226,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
flatViewGroupMaps: existingFlatViewGroupMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
@@ -240,7 +237,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
'flatViewGroupMaps',
'flatViewMaps',
'flatViewFieldMaps',
'flatSearchFieldMetadataMaps',
],
},
);
@@ -254,7 +250,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
flatViewGroupMaps: existingFlatViewGroupMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
flatApplication: resolvedOwnerFlatApplication,
isSystemBuild,
});
@@ -354,8 +349,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatFieldMetadatasToUpdate[0].id,
return findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier: flatFieldMetadatasToUpdate[0].universalIdentifier,
flatEntityMaps: recomputedFlatFieldMetadataMaps,
});
}
@@ -1785,6 +1785,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
toStringify: false,
universalProperty: 'fieldMetadataUniversalIdentifier',
},
tsVectorFieldMetadataId: {
toCompare: false,
toStringify: false,
universalProperty: 'tsVectorFieldMetadataUniversalIdentifier',
},
position: {
toCompare: true,
toStringify: false,
@@ -312,6 +312,9 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
fieldMetadata: {
foreignKey: 'fieldMetadataId',
},
tsVectorFieldMetadata: {
foreignKey: 'tsVectorFieldMetadataId',
},
},
} as const satisfies ManyToOneMetadataRelationsProperties;
@@ -544,10 +544,17 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
fieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'fieldMetadataId',
inverseOneToManyProperty: null,
inverseOneToManyProperty: 'searchFieldMetadatas',
isNullable: false,
universalForeignKey: 'fieldMetadataUniversalIdentifier',
},
tsVectorFieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'tsVectorFieldMetadataId',
inverseOneToManyProperty: null,
isNullable: false,
universalForeignKey: 'tsVectorFieldMetadataUniversalIdentifier',
},
},
} as const satisfies ManyToOneMetadataRelationsProperties;
@@ -77,6 +77,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
universalFlatEntityForeignKeyAggregator:
'mainGroupByFieldMetadataViewUniversalIdentifiers',
},
searchFieldMetadatas: {
metadataName: 'searchFieldMetadata',
flatEntityForeignKeyAggregator: 'searchFieldMetadataIds',
universalFlatEntityForeignKeyAggregator:
'searchFieldMetadataUniversalIdentifiers',
},
},
objectMetadata: {
fields: {
@@ -17,7 +17,8 @@ type FieldMetadataOneToManySyncableRelations =
type Assertions = [
// FieldMetadataEntity OneToMany relations (all targets):
// - indexFieldMetadatas, fieldPermissions, viewFields, viewFilters,
// - kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews, viewSorts
// - kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews,
// - viewSorts, searchFieldMetadatas
Expect<
Equal<
FieldMetadataOneToManyRelations,
@@ -29,6 +30,7 @@ type Assertions = [
| 'calendarViews'
| 'mainGroupByFieldMetadataViews'
| 'viewSorts'
| 'searchFieldMetadatas'
>
>,
@@ -43,6 +45,7 @@ type Assertions = [
| 'calendarViews'
| 'mainGroupByFieldMetadataViews'
| 'viewSorts'
| 'searchFieldMetadatas'
>
>,
];
@@ -10,7 +10,7 @@ type FieldMetadataRelatedProperties =
type Assertions = [
// FieldMetadataEntity has both ManyToOne and OneToMany relations
// ManyToOne: object, workspace, application, relationTargetFieldMetadata, relationTargetObjectMetadata
// OneToMany: indexFieldMetadatas, fieldPermissions, viewFields, viewFilters, kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews, viewSorts
// OneToMany: indexFieldMetadatas, fieldPermissions, viewFields, viewFilters, kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews, viewSorts, searchFieldMetadatas
Expect<
Equal<
FieldMetadataRelatedProperties,
@@ -27,6 +27,7 @@ type Assertions = [
| 'calendarViews'
| 'mainGroupByFieldMetadataViews'
| 'viewSorts'
| 'searchFieldMetadatas'
>
>,
];
@@ -23,6 +23,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
"viewField",
"viewFilter",
"view",
"searchFieldMetadata",
]
`;
@@ -7,11 +7,11 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"viewFilter",
"commandMenuItem",
"fieldPermission",
"searchFieldMetadata",
"viewField",
"objectPermission",
"pageLayoutWidget",
"rolePermissionFlag",
"searchFieldMetadata",
"viewSort",
"index",
"pageLayout",
@@ -12,6 +12,7 @@ import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity
import { type MetadataRelatedFlatEntityMapsKeys } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-flat-entity-maps-keys.type';
import { type MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/replace-flat-entity-in-flat-entity-maps-through-mutation-or-throw.util';
@@ -22,6 +23,7 @@ type AddFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrowArgs<
metadataName: T;
flatEntity: MetadataFlatEntity<T>;
flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<T>;
skipMissingRelatedEntities?: boolean;
};
export const addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow =
@@ -29,6 +31,7 @@ export const addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow
metadataName,
flatEntity,
flatEntityAndRelatedMapsToMutate,
skipMissingRelatedEntities = false,
}: AddFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrowArgs<T>) => {
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
@@ -36,6 +39,7 @@ export const addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow
universalFlatEntity: flatEntity,
universalFlatEntityAndRelatedMapsToMutate:
flatEntityAndRelatedMapsToMutate,
skipMissingRelatedEntities,
},
);
@@ -101,6 +105,17 @@ export const addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow
continue;
}
if (skipMissingRelatedEntities) {
const maybeRelatedFlatEntity = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatEntityRelatedEntityForeignKeyValue,
flatEntityMaps: relatedFlatEntityMetadataMaps,
});
if (!isDefined(maybeRelatedFlatEntity)) {
continue;
}
}
const relatedFlatEntity = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatEntityRelatedEntityForeignKeyValue,
flatEntityMaps: relatedFlatEntityMetadataMaps,
@@ -57,6 +57,8 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
viewSortIds: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataIds: [],
searchFieldMetadataUniversalIdentifiers: [],
universalSettings: null,
...overrides,
};
@@ -77,6 +77,8 @@ export const getRelationTargetFlatFieldMetadataMock = ({
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
viewSortIds: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataIds: [],
searchFieldMetadataUniversalIdentifiers: [],
universalSettings: settings,
};
};
@@ -139,6 +139,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": Any<String>,
"relationTargetObjectMetadataUniversalIdentifier": Any<String>,
"searchFieldMetadataUniversalIdentifiers": [],
"standardOverrides": null,
"type": "MORPH_RELATION",
"universalIdentifier": Any<String>,
@@ -174,6 +175,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": Any<String>,
"relationTargetObjectMetadataUniversalIdentifier": Any<String>,
"searchFieldMetadataUniversalIdentifiers": [],
"standardOverrides": null,
"type": "RELATION",
"universalIdentifier": Any<String>,
@@ -211,6 +213,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": Any<String>,
"relationTargetObjectMetadataUniversalIdentifier": Any<String>,
"searchFieldMetadataUniversalIdentifiers": [],
"standardOverrides": null,
"type": "MORPH_RELATION",
"universalIdentifier": Any<String>,
@@ -246,6 +249,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": Any<String>,
"relationTargetObjectMetadataUniversalIdentifier": Any<String>,
"searchFieldMetadataUniversalIdentifiers": [],
"standardOverrides": null,
"type": "RELATION",
"universalIdentifier": Any<String>,
@@ -14,6 +14,7 @@ import { fromFieldMetadataEntityToFlatFieldMetadata } from 'src/engine/metadata-
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { computeUniqueFieldMetadataIdsFromIndexEntities } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-field-metadata-ids-from-index-entities.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
@@ -50,6 +51,8 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
private readonly viewSortRepository: WorkspaceScopedRepository<ViewSortEntity>,
@InjectWorkspaceScopedRepository(ViewEntity)
private readonly viewRepository: WorkspaceScopedRepository<ViewEntity>,
@InjectWorkspaceScopedRepository(SearchFieldMetadataEntity)
private readonly searchFieldMetadataRepository: WorkspaceScopedRepository<SearchFieldMetadataEntity>,
) {
super();
}
@@ -66,6 +69,7 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
viewFilters,
viewSorts,
views,
searchFieldMetadatas,
] = await Promise.all([
this.fieldMetadataRepository.find({
where: { workspaceId },
@@ -108,6 +112,9 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
],
withDeleted: true,
}),
this.searchFieldMetadataRepository.find(workspaceId, {
select: ['id', 'universalIdentifier', 'fieldMetadataId'],
}),
]);
const [
@@ -117,6 +124,7 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
kanbanViewsByFieldId,
mainGroupByFieldMetadataViewsByFieldId,
viewSortsByFieldId,
searchFieldMetadatasByFieldId,
] = (
[
{
@@ -143,6 +151,10 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
entities: viewSorts,
foreignKey: 'fieldMetadataId',
},
{
entities: searchFieldMetadatas,
foreignKey: 'fieldMetadataId',
},
] as const
).map(regroupEntitiesByRelatedEntityId);
@@ -173,6 +185,8 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
fieldMetadataEntity.id,
) || [],
viewSorts: viewSortsByFieldId.get(fieldMetadataEntity.id) || [],
searchFieldMetadatas:
searchFieldMetadatasByFieldId.get(fieldMetadataEntity.id) || [],
},
fieldMetadataIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
@@ -1,29 +1,23 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { recomputeSearchVectorFieldFromSearchFieldMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-from-search-field-metadatas.util';
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
export const computeSearchFieldMetadataDeletionForDeletedFields = ({
flatFieldMetadatasToDelete,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatSearchFieldMetadataMaps,
}: {
flatFieldMetadatasToDelete: FlatFieldMetadata[];
} & Pick<
AllFlatEntityMaps,
| 'flatObjectMetadataMaps'
| 'flatFieldMetadataMaps'
| 'flatSearchFieldMetadataMaps'
'flatObjectMetadataMaps' | 'flatSearchFieldMetadataMaps'
>): {
searchFieldMetadatasToDelete: UniversalFlatSearchFieldMetadata[];
flatSearchVectorFieldsToUpdate: FlatFieldMetadata<FieldMetadataType.TS_VECTOR>[];
} => {
const deletedFieldIds = new Set(
flatFieldMetadatasToDelete.map((flatFieldMetadata) => flatFieldMetadata.id),
@@ -35,8 +29,6 @@ export const computeSearchFieldMetadataDeletionForDeletedFields = ({
);
const searchFieldMetadatasToDelete: UniversalFlatSearchFieldMetadata[] = [];
const flatSearchVectorFieldsToUpdate: FlatFieldMetadata<FieldMetadataType.TS_VECTOR>[] =
[];
for (const objectMetadataId of affectedObjectMetadataIds) {
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
@@ -59,31 +51,10 @@ export const computeSearchFieldMetadataDeletionForDeletedFields = ({
deletedFieldIds.has(searchFieldMetadata.fieldMetadataId),
);
if (rowsToDelete.length === 0) {
continue;
}
searchFieldMetadatasToDelete.push(...rowsToDelete);
const remainingSearchFieldMetadatas = objectSearchFieldMetadatas.filter(
(searchFieldMetadata) =>
!deletedFieldIds.has(searchFieldMetadata.fieldMetadataId),
);
const flatSearchVectorFieldToUpdate =
recomputeSearchVectorFieldFromSearchFieldMetadatas({
flatObjectMetadata,
flatFieldMetadataMaps,
searchFieldMetadatas: remainingSearchFieldMetadatas,
});
if (isDefined(flatSearchVectorFieldToUpdate)) {
flatSearchVectorFieldsToUpdate.push(flatSearchVectorFieldToUpdate);
}
}
return {
searchFieldMetadatasToDelete,
flatSearchVectorFieldsToUpdate,
};
};
@@ -1,4 +1,3 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import {
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
@@ -13,7 +12,6 @@ import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
import { computeSearchFieldMetadataDeletionForDeletedFields } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-search-field-metadata-deletion-for-deleted-fields.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
@@ -44,7 +42,6 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
flatIndexesToUpdate: UniversalFlatIndexMetadata[];
flatIndexesToDelete: UniversalFlatIndexMetadata[];
searchFieldMetadatasToDelete: UniversalFlatSearchFieldMetadata[];
flatSearchVectorFieldsToUpdate: FlatFieldMetadata<FieldMetadataType.TS_VECTOR>[];
} => {
const { id: fieldMetadataToDeleteId } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
@@ -187,11 +184,10 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
},
);
const { searchFieldMetadatasToDelete, flatSearchVectorFieldsToUpdate } =
const { searchFieldMetadatasToDelete } =
computeSearchFieldMetadataDeletionForDeletedFields({
flatFieldMetadatasToDelete,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
});
@@ -200,6 +196,5 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
flatIndexesToDelete,
flatIndexesToUpdate,
searchFieldMetadatasToDelete,
flatSearchVectorFieldsToUpdate,
};
};
@@ -78,6 +78,12 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = (
fieldMetadataEntity.viewSorts?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
searchFieldMetadataIds:
fieldMetadataEntity.searchFieldMetadatas?.map(({ id }) => id) ?? [],
searchFieldMetadataUniversalIdentifiers:
fieldMetadataEntity.searchFieldMetadatas?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
fieldPermissionUniversalIdentifiers:
fieldMetadataEntity.fieldPermissions?.map(
({ universalIdentifier }) => universalIdentifier,
@@ -35,7 +35,6 @@ type FromUpdateFieldInputToFlatFieldMetadataArgs = {
| 'flatViewGroupMaps'
| 'flatViewMaps'
| 'flatViewFieldMaps'
| 'flatSearchFieldMetadataMaps'
>;
type FlatFieldMetadataAndIndexToUpdate = {
@@ -52,7 +51,6 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
flatViewGroupMaps,
flatViewMaps,
flatViewFieldMaps,
flatSearchFieldMetadataMaps,
isSystemBuild,
}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult<FlatFieldMetadataAndIndexToUpdate> => {
const updateFieldInputInformalProperties =
@@ -152,7 +150,6 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
toFlatFieldMetadata,
flatViewMaps,
flatViewFieldMaps,
flatSearchFieldMetadataMaps,
flatApplication,
});
@@ -175,14 +172,12 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewsToUpdate,
flatFieldMetadatasToUpdate: flatFieldMetadatasToUpdateFromSideEffect,
} = sideEffectResult.result;
return {
flatFieldMetadatasToUpdate: [
...accumulator.flatFieldMetadatasToUpdate,
toFlatFieldMetadata,
...flatFieldMetadatasToUpdateFromSideEffect,
],
flatIndexMetadatasToUpdate: [
...accumulator.flatIndexMetadatasToUpdate,
@@ -77,5 +77,6 @@ export const getDefaultFlatFieldMetadata = ({
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: settings ?? null,
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
} as const satisfies UniversalFlatFieldMetadata;
};
@@ -1,9 +1,7 @@
import { type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { handleEnumFlatFieldMetadataUpdateSideEffects } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util';
@@ -16,7 +14,6 @@ import {
handleIndexChangesDuringFieldUpdate,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { recomputeSearchVectorOnFieldRename } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-search-vector-on-field-rename.util';
import { type FlatViewFiltersToDeleteAndUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util';
import { type FlatViewGroupsToDeleteUpdateAndCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util';
@@ -24,9 +21,7 @@ export type FlatFieldMetadataUpdateSideEffects =
FlatViewFiltersToDeleteAndUpdate &
FlatViewGroupsToDeleteUpdateAndCreate &
FieldMetadataUpdateIndexSideEffect &
FieldMetadataDeactivationSideEffect & {
flatFieldMetadatasToUpdate: FlatFieldMetadata[];
};
FieldMetadataDeactivationSideEffect;
type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
FlatFieldMetadata,
@@ -41,7 +36,6 @@ type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
| 'flatViewGroupMaps'
| 'flatViewMaps'
| 'flatViewFieldMaps'
| 'flatSearchFieldMetadataMaps'
> & {
flatApplication: FlatApplication;
};
@@ -59,7 +53,6 @@ export const FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS: FlatFieldMetadataUpd
flatViewsToDelete: [],
flatViewFieldsToDelete: [],
flatViewsToUpdate: [],
flatFieldMetadatasToUpdate: [],
};
export const handleFlatFieldMetadataUpdateSideEffect = ({
@@ -72,7 +65,6 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
flatViewGroupMaps,
flatViewMaps,
flatViewFieldMaps,
flatSearchFieldMetadataMaps,
flatApplication,
}: HandleFlatFieldMetadataUpdateSideEffectArgs): FieldInputTranspilationResult<FlatFieldMetadataUpdateSideEffects> => {
const sideEffectResult = structuredClone(
@@ -140,25 +132,6 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
return indexChangesSideEffectResult;
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: fromFlatFieldMetadata.objectMetadataId,
});
const { flatSearchVectorFieldToUpdate } = recomputeSearchVectorOnFieldRename({
fromFlatFieldMetadata,
toFlatFieldMetadata,
flatObjectMetadata,
flatFieldMetadataMaps,
flatSearchFieldMetadataMaps,
});
if (isDefined(flatSearchVectorFieldToUpdate)) {
sideEffectResult.flatFieldMetadatasToUpdate.push(
flatSearchVectorFieldToUpdate,
);
}
const {
flatIndexMetadatasToUpdate,
flatIndexMetadatasToCreate,
@@ -1,68 +0,0 @@
import { type FieldMetadataType, type FromTo } from 'twenty-shared/types';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { recomputeSearchVectorFieldFromSearchFieldMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-from-search-field-metadatas.util';
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
export type FieldMetadataUpdateSearchVectorSideEffect = {
flatSearchVectorFieldToUpdate:
| FlatFieldMetadata<FieldMetadataType.TS_VECTOR>
| undefined;
};
type RecomputeSearchVectorOnFieldRenameArgs = {
flatObjectMetadata: FlatObjectMetadata;
} & FromTo<FlatFieldMetadata, 'flatFieldMetadata'> &
Pick<
AllFlatEntityMaps,
'flatFieldMetadataMaps' | 'flatSearchFieldMetadataMaps'
>;
const EMPTY_SIDE_EFFECT: FieldMetadataUpdateSearchVectorSideEffect = {
flatSearchVectorFieldToUpdate: undefined,
};
export const recomputeSearchVectorOnFieldRename = ({
fromFlatFieldMetadata,
toFlatFieldMetadata,
flatObjectMetadata,
flatFieldMetadataMaps,
flatSearchFieldMetadataMaps,
}: RecomputeSearchVectorOnFieldRenameArgs): FieldMetadataUpdateSearchVectorSideEffect => {
if (!flatObjectMetadata.isSearchable) {
return EMPTY_SIDE_EFFECT;
}
const objectSearchFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow<FlatSearchFieldMetadata>({
flatEntityMaps: flatSearchFieldMetadataMaps,
flatEntityIds: flatObjectMetadata.searchFieldMetadataIds,
});
const isIndexedField = objectSearchFieldMetadatas.some(
(searchFieldMetadata) =>
searchFieldMetadata.fieldMetadataId === toFlatFieldMetadata.id,
);
const hasNameChanged =
fromFlatFieldMetadata.name !== toFlatFieldMetadata.name;
if (!isIndexedField || !hasNameChanged) {
return EMPTY_SIDE_EFFECT;
}
return {
flatSearchVectorFieldToUpdate:
recomputeSearchVectorFieldFromSearchFieldMetadatas({
flatObjectMetadata,
flatFieldMetadataMaps,
searchFieldMetadatas: objectSearchFieldMetadatas,
overrideFlatFieldMetadataById: new Map([
[toFlatFieldMetadata.id, toFlatFieldMetadata],
]),
}),
};
};
@@ -1,11 +1,9 @@
import { msg } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { type FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type FlatFieldMetadataTypeValidationArgs } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type';
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
import { isSafeTsVectorExpression } from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util';
export const validateTsVectorFlatFieldMetadata = ({
flatEntityToValidate,
@@ -30,22 +28,5 @@ export const validateTsVectorFlatFieldMetadata = ({
});
}
const asExpression = flatEntityToValidate.universalSettings?.asExpression;
if (!isNonEmptyString(asExpression)) {
errors.push({
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message:
'Field type TS_VECTOR must have an expression. This may have failed to be built because record identifier field does not exist or is not of a searchable type.',
userFriendlyMessage: msg`Field type TS_VECTOR must have an expression. This may have failed to be built because record identifier field does not exist or is not of a searchable type.`,
});
} else if (!isSafeTsVectorExpression(asExpression)) {
errors.push({
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: 'Field type TS_VECTOR expression is invalid',
userFriendlyMessage: msg`The search field expression is invalid.`,
});
}
return errors;
};
@@ -1,24 +1,20 @@
import { type FieldMetadataType, type FromTo } from 'twenty-shared/types';
import { type FromTo } from 'twenty-shared/types';
import { isDefined, isSearchableFieldType } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { recomputeSearchVectorFieldFromSearchFieldMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-from-search-field-metadatas.util';
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
import { buildFlatSearchFieldMetadataForField } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/build-flat-search-field-metadata-for-field.util';
import { findTsVectorFlatFieldMetadataForObject } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/find-ts-vector-flat-field-metadata-for-object.util';
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
export type LabelIdentifierUpdateSearchVectorSideEffect = {
flatSearchVectorFieldToUpdate:
| FlatFieldMetadata<FieldMetadataType.TS_VECTOR>
| undefined;
export type LabelIdentifierUpdateSearchFieldMetadataSideEffect = {
searchFieldMetadatasToCreate: UniversalFlatSearchFieldMetadata[];
};
type RecomputeSearchVectorOnLabelIdentifierUpdateArgs = FromTo<
type ComputeSearchFieldMetadataToCreateOnLabelIdentifierUpdateArgs = FromTo<
FlatObjectMetadata,
'flatObjectMetadata'
> &
@@ -27,21 +23,20 @@ type RecomputeSearchVectorOnLabelIdentifierUpdateArgs = FromTo<
'flatFieldMetadataMaps' | 'flatSearchFieldMetadataMaps'
>;
const EMPTY_SIDE_EFFECT: LabelIdentifierUpdateSearchVectorSideEffect = {
flatSearchVectorFieldToUpdate: undefined,
const EMPTY_SIDE_EFFECT: LabelIdentifierUpdateSearchFieldMetadataSideEffect = {
searchFieldMetadatasToCreate: [],
};
// Relabeling is additive: it indexes the new label identifier field while preserving the
// existing search surface (e.g. the provisioned name row). We never drop a row here so the
// previous label identifier stays searchable. Removal of fields from the search surface is
// deferred to a follow-up PR.
export const recomputeSearchVectorOnLabelIdentifierUpdate = ({
// previous label identifier stays searchable. The searchVector column itself is rebuilt
// from the resulting rows by the rebuildSearchVector marker in the migration runner.
export const computeSearchFieldMetadataToCreateOnLabelIdentifierUpdate = ({
fromFlatObjectMetadata,
toFlatObjectMetadata,
flatFieldMetadataMaps,
flatSearchFieldMetadataMaps,
}: RecomputeSearchVectorOnLabelIdentifierUpdateArgs): LabelIdentifierUpdateSearchVectorSideEffect => {
}: ComputeSearchFieldMetadataToCreateOnLabelIdentifierUpdateArgs): LabelIdentifierUpdateSearchFieldMetadataSideEffect => {
const newLabelIdentifierFieldMetadataId =
toFlatObjectMetadata.labelIdentifierFieldMetadataId;
@@ -79,6 +74,15 @@ export const recomputeSearchVectorOnLabelIdentifierUpdate = ({
return EMPTY_SIDE_EFFECT;
}
const tsVectorFlatFieldMetadata = findTsVectorFlatFieldMetadataForObject({
fieldUniversalIdentifiers: fromFlatObjectMetadata.fieldUniversalIdentifiers,
flatFieldMetadataMaps,
});
if (!isDefined(tsVectorFlatFieldMetadata)) {
return EMPTY_SIDE_EFFECT;
}
const newLabelIdentifierPosition =
existingSearchFieldMetadatas.reduce(
(maxPosition, searchFieldMetadata) =>
@@ -89,25 +93,11 @@ export const recomputeSearchVectorOnLabelIdentifierUpdate = ({
const newSearchFieldMetadata = buildFlatSearchFieldMetadataForField({
flatObjectMetadata: fromFlatObjectMetadata,
flatFieldMetadata: newLabelIdentifierField,
tsVectorFlatFieldMetadata,
position: newLabelIdentifierPosition,
});
const nextSearchFieldMetadatas = [
...existingSearchFieldMetadatas,
{
fieldMetadataId: newLabelIdentifierFieldMetadataId,
position: newLabelIdentifierPosition,
universalIdentifier: newSearchFieldMetadata.universalIdentifier,
},
];
return {
flatSearchVectorFieldToUpdate:
recomputeSearchVectorFieldFromSearchFieldMetadatas({
flatObjectMetadata: fromFlatObjectMetadata,
flatFieldMetadataMaps,
searchFieldMetadatas: nextSearchFieldMetadatas,
}),
searchFieldMetadatasToCreate: [newSearchFieldMetadata],
};
};
@@ -102,7 +102,6 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities =
flatViewFieldsToCreate,
flatViewFieldsToUpdate,
otherObjectFlatFieldMetadatasToUpdate,
sameObjectFlatFieldMetadatasToUpdate,
searchFieldMetadatasToCreate,
} = handleFlatObjectMetadataUpdateSideEffect({
fromFlatObjectMetadata: existingFlatObjectMetadata,
@@ -121,7 +120,6 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities =
flatViewFieldsToCreate,
flatViewFieldsToUpdate,
otherObjectFlatFieldMetadatasToUpdate,
sameObjectFlatFieldMetadatasToUpdate,
searchFieldMetadatasToCreate,
};
};
@@ -2,10 +2,9 @@ import { type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { computeSearchFieldMetadataToCreateOnLabelIdentifierUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/compute-search-field-metadata-to-create-on-label-identifier-update.util';
import { recomputeIndexAfterFlatObjectMetadataSingularNameUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util';
import { recomputeSearchVectorOnLabelIdentifierUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-on-label-identifier-update.util';
import { recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util';
import { renameRelatedMorphFieldOnObjectNamesUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
@@ -15,7 +14,6 @@ import { type UniversalFlatViewField } from 'src/engine/workspace-manager/worksp
export type FlatObjectMetadataUpdateSideEffects = {
otherObjectFlatFieldMetadatasToUpdate: UniversalFlatFieldMetadata[];
sameObjectFlatFieldMetadatasToUpdate: UniversalFlatFieldMetadata[];
flatViewFieldsToUpdate: UniversalFlatViewField[];
flatViewFieldsToCreate: UniversalFlatViewField[];
flatIndexMetadatasToUpdate: UniversalFlatIndexMetadata[];
@@ -89,20 +87,14 @@ export const handleFlatObjectMetadataUpdateSideEffect = ({
flatViewFieldsToUpdate: [],
};
const sameObjectFlatFieldMetadatasToUpdate: FlatFieldMetadata[] = [];
const { flatSearchVectorFieldToUpdate, searchFieldMetadatasToCreate } =
recomputeSearchVectorOnLabelIdentifierUpdate({
const { searchFieldMetadatasToCreate } =
computeSearchFieldMetadataToCreateOnLabelIdentifierUpdate({
fromFlatObjectMetadata,
toFlatObjectMetadata,
flatFieldMetadataMaps,
flatSearchFieldMetadataMaps,
});
if (isDefined(flatSearchVectorFieldToUpdate)) {
sameObjectFlatFieldMetadatasToUpdate.push(flatSearchVectorFieldToUpdate);
}
return {
flatIndexMetadatasToUpdate: [
...morphRelatedFlatIndexesToUpdate,
@@ -111,7 +103,6 @@ export const handleFlatObjectMetadataUpdateSideEffect = ({
flatViewFieldsToCreate,
flatViewFieldsToUpdate,
otherObjectFlatFieldMetadatasToUpdate: morphFlatFieldMetadatasToUpdate,
sameObjectFlatFieldMetadatasToUpdate,
searchFieldMetadatasToCreate,
};
};
@@ -1,112 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import {
buildSearchVectorTargetField,
computeSearchVectorAsExpressionFromSearchFieldMetadatas,
} from 'src/engine/metadata-modules/flat-search-field-metadata/utils/compute-search-vector-as-expression-from-search-field-metadatas.util';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
type RecomputeSearchVectorFieldFromSearchFieldMetadatasArgs = {
flatObjectMetadata: FlatObjectMetadata;
// Post-change searchFieldMetadata rows for the object, carrying the field id and the
// per-object position that drives the deterministic asExpression order.
searchFieldMetadatas: Pick<
FlatSearchFieldMetadata,
'fieldMetadataId' | 'position' | 'universalIdentifier'
>[];
// Takes precedence over the flat maps when resolving an id — for a field created or
// renamed in the same migration, whose maps entry is absent or stale.
overrideFlatFieldMetadataById?: Map<string, FlatFieldMetadata>;
} & Pick<AllFlatEntityMaps, 'flatFieldMetadataMaps'>;
export const recomputeSearchVectorFieldFromSearchFieldMetadatas = ({
flatObjectMetadata,
flatFieldMetadataMaps,
searchFieldMetadatas,
overrideFlatFieldMetadataById,
}: RecomputeSearchVectorFieldFromSearchFieldMetadatasArgs):
| FlatFieldMetadata<FieldMetadataType.TS_VECTOR>
| undefined => {
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: flatObjectMetadata.fieldIds,
});
const searchVectorField = objectFlatFieldMetadatas.find(
(field) => field.name === SEARCH_VECTOR_FIELD.name,
);
if (
!isDefined(searchVectorField) ||
!isFlatFieldMetadataOfType(searchVectorField, FieldMetadataType.TS_VECTOR)
) {
throw new ObjectMetadataException(
`Search vector field not found for object metadata ${flatObjectMetadata.id}`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
);
}
const targetSearchableFields = searchFieldMetadatas.map(
(searchFieldMetadata) => {
const { fieldMetadataId, position, universalIdentifier } =
searchFieldMetadata;
const overriddenFlatFieldMetadata =
overrideFlatFieldMetadataById?.get(fieldMetadataId);
const flatFieldMetadata =
overriddenFlatFieldMetadata ??
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(flatFieldMetadata)) {
throw new ObjectMetadataException(
`Field metadata ${fieldMetadataId} referenced by searchFieldMetadata not found for object metadata ${flatObjectMetadata.id}`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
);
}
return buildSearchVectorTargetField(
flatFieldMetadata,
position,
universalIdentifier,
);
},
);
try {
const newAsExpression =
computeSearchVectorAsExpressionFromSearchFieldMetadatas(
targetSearchableFields,
);
return {
...searchVectorField,
universalSettings: {
...searchVectorField.universalSettings,
asExpression: newAsExpression,
generatedType: 'STORED',
},
};
} catch (error) {
throw new ObjectMetadataException(
`Failed to compute search vector column expression for object metadata ${flatObjectMetadata.id}: ${error instanceof Error ? error.message : String(error)}`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
);
}
};
@@ -1,6 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
@@ -11,6 +13,7 @@ import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-enti
import { FlatSearchFieldMetadataMaps } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata-maps.type';
import { fromSearchFieldMetadataEntityToFlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/from-search-field-metadata-entity-to-flat-search-field-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
@@ -56,7 +59,13 @@ export class WorkspaceFlatSearchFieldMetadataMapCacheService extends WorkspaceCa
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
select: [
'id',
'universalIdentifier',
'name',
'type',
'objectMetadataId',
],
withDeleted: true,
}),
]);
@@ -68,12 +77,42 @@ export class WorkspaceFlatSearchFieldMetadataMapCacheService extends WorkspaceCa
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fieldMetadatas);
// Each object's single system TS_VECTOR field, used to backfill the FK for
// legacy searchFieldMetadata rows still NULL before the 2.18 slow command runs.
// TODO: remove this fallback (and the searchVector map) once the minimum
// cross-upgrade supported version is past 2.18 — at which point no instance can
// still have NULL tsVectorFieldMetadataId rows.
const searchVectorFieldIdByObjectMetadataId = new Map<string, string>();
for (const fieldMetadata of fieldMetadatas) {
if (
fieldMetadata.type === FieldMetadataType.TS_VECTOR &&
fieldMetadata.name === SEARCH_VECTOR_FIELD.name
) {
searchVectorFieldIdByObjectMetadataId.set(
fieldMetadata.objectMetadataId,
fieldMetadata.id,
);
}
}
const flatSearchFieldMetadataMaps = createEmptyFlatEntityMaps();
for (const searchFieldMetadata of existingSearchFieldMetadatas) {
const resolvedTsVectorFieldMetadataId =
(searchFieldMetadata.tsVectorFieldMetadataId as string | null) ??
searchVectorFieldIdByObjectMetadataId.get(
searchFieldMetadata.objectMetadataId,
);
const flatSearchFieldMetadata =
fromSearchFieldMetadataEntityToFlatSearchFieldMetadata({
entity: searchFieldMetadata,
entity: isDefined(resolvedTsVectorFieldMetadataId)
? {
...searchFieldMetadata,
tsVectorFieldMetadataId: resolvedTsVectorFieldMetadataId,
}
: searchFieldMetadata,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
@@ -6,6 +6,7 @@ import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-mana
export const buildFlatSearchFieldMetadataForField = ({
flatObjectMetadata,
flatFieldMetadata,
tsVectorFlatFieldMetadata,
position,
}: {
flatObjectMetadata: Pick<
@@ -13,6 +14,7 @@ export const buildFlatSearchFieldMetadataForField = ({
'applicationUniversalIdentifier' | 'universalIdentifier'
>;
flatFieldMetadata: { universalIdentifier: string };
tsVectorFlatFieldMetadata: { universalIdentifier: string };
position: number;
}): UniversalFlatSearchFieldMetadata => {
const createdAt = new Date().toISOString();
@@ -26,5 +28,7 @@ export const buildFlatSearchFieldMetadataForField = ({
flatObjectMetadata.applicationUniversalIdentifier,
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
fieldMetadataUniversalIdentifier: flatFieldMetadata.universalIdentifier,
tsVectorFieldMetadataUniversalIdentifier:
tsVectorFlatFieldMetadata.universalIdentifier,
};
};
@@ -15,11 +15,15 @@ export type SearchVectorTargetField = {
sortKey: string;
};
export const buildSearchVectorTargetField = (
field: { name: string; type: FieldMetadataType },
position: number,
sortKey: string,
): SearchVectorTargetField => ({
export const buildSearchVectorTargetField = ({
field,
position,
sortKey,
}: {
field: { name: string; type: FieldMetadataType };
position: number;
sortKey: string;
}): SearchVectorTargetField => ({
name: field.name,
type: field.type,
position,
@@ -0,0 +1,53 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import {
buildSearchVectorTargetField,
computeSearchVectorAsExpressionFromSearchFieldMetadatas,
} from 'src/engine/metadata-modules/flat-search-field-metadata/utils/compute-search-vector-as-expression-from-search-field-metadatas.util';
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
export const deriveSearchVectorAsExpressionForTsVectorField = ({
tsVectorFieldMetadataId,
flatSearchFieldMetadataMaps,
indexedFieldById,
}: {
tsVectorFieldMetadataId: string;
flatSearchFieldMetadataMaps: FlatEntityMaps<FlatSearchFieldMetadata>;
indexedFieldById: ReadonlyMap<
string,
{ name: string; type: FieldMetadataType }
>;
}): string => {
const targetSearchableFields = Object.values(
flatSearchFieldMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(flatSearchFieldMetadata) =>
flatSearchFieldMetadata.tsVectorFieldMetadataId ===
tsVectorFieldMetadataId,
)
.flatMap((flatSearchFieldMetadata) => {
const indexedField = indexedFieldById.get(
flatSearchFieldMetadata.fieldMetadataId,
);
if (!isDefined(indexedField)) {
return [];
}
return [
buildSearchVectorTargetField({
field: indexedField,
position: flatSearchFieldMetadata.position,
sortKey: flatSearchFieldMetadata.universalIdentifier,
}),
];
});
return computeSearchVectorAsExpressionFromSearchFieldMetadatas(
targetSearchableFields,
);
};
@@ -0,0 +1,28 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
// Resolves the object's single system TS_VECTOR field (the 'searchVector' field) that
// every searchFieldMetadata row contributes to.
// Could be refactored once https://github.com/twentyhq/twenty/pull/21949 has been merged.
export const findTsVectorFlatFieldMetadataForObject = ({
fieldUniversalIdentifiers,
flatFieldMetadataMaps,
}: {
fieldUniversalIdentifiers: string[];
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}): FlatFieldMetadata | undefined =>
fieldUniversalIdentifiers
.map(
(fieldUniversalIdentifier) =>
flatFieldMetadataMaps.byUniversalIdentifier[fieldUniversalIdentifier],
)
.find(
(flatFieldMetadata) =>
isDefined(flatFieldMetadata) &&
flatFieldMetadata.type === FieldMetadataType.TS_VECTOR &&
flatFieldMetadata.name === SEARCH_VECTOR_FIELD.name,
);
@@ -0,0 +1,15 @@
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
import { type SearchFieldMetadataDTO } from 'src/engine/metadata-modules/search-field-metadata/dtos/search-field-metadata.dto';
export const fromFlatSearchFieldMetadataToSearchFieldMetadataDto = (
flatSearchFieldMetadata: FlatSearchFieldMetadata,
): SearchFieldMetadataDTO => ({
id: flatSearchFieldMetadata.id,
objectMetadataId: flatSearchFieldMetadata.objectMetadataId,
fieldMetadataId: flatSearchFieldMetadata.fieldMetadataId,
tsVectorFieldMetadataId: flatSearchFieldMetadata.tsVectorFieldMetadataId,
position: flatSearchFieldMetadata.position,
createdAt: new Date(flatSearchFieldMetadata.createdAt),
updatedAt: new Date(flatSearchFieldMetadata.updatedAt),
workspaceId: flatSearchFieldMetadata.workspaceId,
});
@@ -55,6 +55,18 @@ export const fromSearchFieldMetadataEntityToFlatSearchFieldMetadata = ({
);
}
const tsVectorFieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
searchFieldMetadataEntity.tsVectorFieldMetadataId,
);
if (!isDefined(tsVectorFieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`TS_VECTOR field metadata with id ${searchFieldMetadataEntity.tsVectorFieldMetadataId} not found for searchFieldMetadata ${searchFieldMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...searchFieldMetadataEntityWithoutRelations,
createdAt: searchFieldMetadataEntity.createdAt.toISOString(),
@@ -64,5 +76,6 @@ export const fromSearchFieldMetadataEntityToFlatSearchFieldMetadata = ({
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
fieldMetadataUniversalIdentifier,
tsVectorFieldMetadataUniversalIdentifier,
};
};
@@ -40,6 +40,7 @@ const PARTIAL_ID_FIELD = {
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.UUID>;
const PARTIAL_CREATED_AT_FIELD = {
@@ -69,6 +70,7 @@ const PARTIAL_CREATED_AT_FIELD = {
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
const PARTIAL_UPDATED_AT_FIELD = {
@@ -98,6 +100,7 @@ const PARTIAL_UPDATED_AT_FIELD = {
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
const PARTIAL_DELETED_AT_FIELD = {
@@ -127,6 +130,7 @@ const PARTIAL_DELETED_AT_FIELD = {
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
const PARTIAL_CREATED_BY_FIELD = {
@@ -156,6 +160,7 @@ const PARTIAL_CREATED_BY_FIELD = {
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.ACTOR>;
const PARTIAL_UPDATED_BY_FIELD = {
@@ -185,6 +190,7 @@ const PARTIAL_UPDATED_BY_FIELD = {
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.ACTOR>;
const PARTIAL_POSITION_FIELD = {
@@ -214,6 +220,7 @@ const PARTIAL_POSITION_FIELD = {
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.POSITION>;
const PARTIAL_SEARCH_VECTOR_FIELD = {
@@ -245,6 +252,7 @@ const PARTIAL_SEARCH_VECTOR_FIELD = {
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.TS_VECTOR>;
export const PARTIAL_SYSTEM_FLAT_FIELD_METADATAS = {
@@ -30,6 +30,7 @@ import { ObjectRecordCountDTO } from 'src/engine/metadata-modules/object-metadat
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { ObjectRecordCountService } from 'src/engine/metadata-modules/object-metadata/object-record-count.service';
import { SearchFieldMetadataDTO } from 'src/engine/metadata-modules/search-field-metadata/dtos/search-field-metadata.dto';
import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util';
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
@@ -273,4 +274,25 @@ export class ObjectMetadataResolver {
return [];
}
}
@ResolveField(() => [SearchFieldMetadataDTO], { nullable: false })
async searchFieldMetadataList(
@AuthWorkspace() workspace: WorkspaceEntity,
@Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: { loaders: IDataloaders },
): Promise<SearchFieldMetadataDTO[]> {
try {
const searchFieldMetadataItems =
await context.loaders.searchFieldMetadataLoader.load({
objectMetadata,
workspaceId: workspace.id,
});
return searchFieldMetadataItems;
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
return [];
}
}
}
@@ -107,7 +107,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const {
otherObjectFlatFieldMetadatasToUpdate,
sameObjectFlatFieldMetadatasToUpdate,
flatObjectMetadataToUpdate,
flatIndexMetadatasToUpdate,
flatViewFieldsToCreate,
@@ -171,10 +170,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
fieldMetadata: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [
...otherObjectFlatFieldMetadatasToUpdate,
...sameObjectFlatFieldMetadatasToUpdate,
],
flatEntityToUpdate: [...otherObjectFlatFieldMetadatasToUpdate],
},
viewField: {
flatEntityToCreate: flatViewFieldsToCreate,
@@ -2,7 +2,6 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
@@ -154,15 +153,11 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
fieldPermissionUniversalIdentifiers: [],
universalSettings: null,
viewSortUniversalIdentifiers: [],
searchFieldMetadataUniversalIdentifiers: [],
};
const searchVectorUniversalSettings: UniversalFlatFieldMetadata<FieldMetadataType.TS_VECTOR>['universalSettings'] =
{
asExpression: getTsVectorColumnExpressionFromFields(
nameField ? [nameField] : [],
),
generatedType: 'STORED',
};
null;
return {
fields: {
@@ -27,6 +27,8 @@ export const buildDefaultSearchFieldMetadatasForCustomObject = ({
const nameSearchFieldMetadata = buildFlatSearchFieldMetadataForField({
flatObjectMetadata,
flatFieldMetadata: nameField,
tsVectorFlatFieldMetadata:
defaultFlatFieldForCustomObjectMaps.fields.searchVector,
position: 0,
});
@@ -0,0 +1,57 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import {
Authorize,
IDField,
QueryOptions,
} from '@ptc-org/nestjs-query-graphql';
import { IsDateString, IsNotEmpty, IsNumber, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('SearchField')
@Authorize({
// oxlint-disable-next-line typescript/no-explicit-any
authorize: (context: any) => ({
workspaceId: { eq: context?.req?.workspace?.id },
}),
})
@QueryOptions({
defaultResultSize: 10,
disableSort: true,
maxResultsSize: 1000,
})
export class SearchFieldMetadataDTO {
@IsUUID()
@IsNotEmpty()
@IDField(() => UUIDScalarType)
id: string;
objectMetadataId: string;
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
fieldMetadataId: string;
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
tsVectorFieldMetadataId: string;
@IsNumber()
@IsNotEmpty()
@Field()
position: number;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
@HideField()
workspaceId: string;
}
@@ -3,4 +3,6 @@ export enum SearchFieldMetadataExceptionCode {
INVALID_SEARCH_FIELD_METADATA_DATA = 'INVALID_SEARCH_FIELD_METADATA_DATA',
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
FIELD_METADATA_NOT_FOUND = 'FIELD_METADATA_NOT_FOUND',
TS_VECTOR_FIELD_METADATA_NOT_FOUND = 'TS_VECTOR_FIELD_METADATA_NOT_FOUND',
INVALID_TS_VECTOR_FIELD_METADATA = 'INVALID_TS_VECTOR_FIELD_METADATA',
}
@@ -12,6 +12,7 @@ import {
} from 'typeorm';
import { ADD_UNIVERSAL_IDENTIFIER_AND_APPLICATION_ID_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-16/add-universal-identifier-and-application-id-to-search-field-metadata-upgrade-command-name.constant';
import { ADD_TS_VECTOR_FIELD_METADATA_ID_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-18/add-ts-vector-field-metadata-id-to-search-field-metadata-upgrade-command-name.constant';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -46,10 +47,25 @@ export class SearchFieldMetadataEntity extends SyncableEntity {
@Column({ nullable: false, type: 'uuid' })
fieldMetadataId: string;
@ManyToOne(() => FieldMetadataEntity, { onDelete: 'CASCADE' })
@ManyToOne(
() => FieldMetadataEntity,
(fieldMetadata) => fieldMetadata.searchFieldMetadatas,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'fieldMetadataId' })
fieldMetadata: Relation<FieldMetadataEntity>;
@WasIntroducedInUpgrade({
upgradeCommandName:
ADD_TS_VECTOR_FIELD_METADATA_ID_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME,
})
@Column({ nullable: false, type: 'uuid' })
tsVectorFieldMetadataId: string;
@ManyToOne(() => FieldMetadataEntity, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'tsVectorFieldMetadataId' })
tsVectorFieldMetadata: Relation<FieldMetadataEntity>;
@Column({ nullable: false, type: 'float' })
position: number;
@@ -186,6 +186,8 @@ describe('WorkspaceEntityManager', () => {
fieldPermissionUniversalIdentifiers: [],
viewSortIds: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataIds: [],
searchFieldMetadataUniversalIdentifiers: [],
universalSettings: null,
};
@@ -109,6 +109,8 @@ describe('WorkspaceRepository', () => {
fieldPermissionUniversalIdentifiers: [],
viewSortIds: [],
viewSortUniversalIdentifiers: [],
searchFieldMetadataIds: [],
searchFieldMetadataUniversalIdentifiers: [],
universalSettings: null,
};
@@ -15,9 +15,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildAttachmentStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -327,12 +324,6 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,9 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildBlocklistStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -190,12 +187,6 @@ export const buildBlocklistStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,9 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildCalendarChannelEventAssociationStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -188,12 +185,6 @@ export const buildCalendarChannelEventAssociationStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,9 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildCalendarEventParticipantStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -186,12 +183,6 @@ export const buildCalendarEventParticipantStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -13,9 +13,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildCalendarEventStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -182,12 +179,6 @@ export const buildCalendarEventStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,10 +14,8 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { CallRecordingRequestStatus } from 'src/modules/call-recording/common/enums/call-recording-request-status.enum';
import { CallRecordingStatus } from 'src/modules/call-recording/common/enums/call-recording-status.enum';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildCallRecordingStandardFlatFieldMetadatas = ({
now,
@@ -486,12 +484,6 @@ export const buildCallRecordingStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -15,9 +15,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildCompanyStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -279,12 +276,6 @@ export const buildCompanyStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,9 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildDashboardStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -219,12 +216,6 @@ export const buildDashboardStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,9 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildMessageCampaignStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -160,12 +157,6 @@ export const buildMessageCampaignStandardFlatFieldMetadatas = ({
icon: 'IconSend',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
}),
subject: createStandardFieldFlatMetadata({
@@ -14,9 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildMessageChannelMessageAssociationMessageFolderStandardFlatFieldMetadatas =
({
now,
@@ -192,12 +189,6 @@ export const buildMessageChannelMessageAssociationMessageFolderStandardFlatField
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,8 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
export const buildMessageChannelMessageAssociationStandardFlatFieldMetadatas =
@@ -193,12 +191,6 @@ export const buildMessageChannelMessageAssociationStandardFlatFieldMetadatas =
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,9 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildMessageListMemberStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -234,12 +231,6 @@ export const buildMessageListMemberStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -15,9 +15,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildMessageListStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -269,12 +266,6 @@ export const buildMessageListStandardFlatFieldMetadatas = ({
icon: 'IconUsersGroup',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -15,9 +15,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildMessageParticipantStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -187,12 +184,6 @@ export const buildMessageParticipantStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,9 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildMessageStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -183,12 +180,6 @@ export const buildMessageStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -13,9 +13,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildMessageThreadStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -182,12 +179,6 @@ export const buildMessageThreadStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -14,9 +14,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildNoteStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -224,12 +221,6 @@ export const buildNoteStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -15,9 +15,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildNoteTargetStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -191,12 +188,6 @@ export const buildNoteTargetStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -15,9 +15,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildOpportunityStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -295,12 +292,6 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -17,9 +17,6 @@ import {
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME } from 'src/engine/workspace-manager/twenty-standard-application/constants/search-fields-by-standard-object-name.constant';
export const buildPersonStandardFlatFieldMetadatas = ({
now,
objectName,
@@ -538,12 +535,6 @@ export const buildPersonStandardFlatFieldMetadatas = ({
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_BY_STANDARD_OBJECT_NAME[objectName],
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,

Some files were not shown because too many files have changed in this diff Show More